From 2ee074a16b99b2961902c8e9abe20c24976bfaa4 Mon Sep 17 00:00:00 2001 From: Stephan Cilliers <5469870+stephancill@users.noreply.github.com> Date: Thu, 11 Jan 2024 17:06:38 +0200 Subject: [PATCH 01/26] mod: erc-20 --- examples/api/package.json | 6 +- examples/api/src/app/api/erc-20/route.ts | 340 +++++++++++++++ examples/nextjs-shadcn/src/app/dummy-casts.ts | 15 + examples/nextjs-shadcn/src/app/embeds.tsx | 7 +- examples/nextjs-shadcn/src/app/page.tsx | 2 + mods/erc-20/index.ts | 1 + mods/erc-20/package.json | 10 + mods/erc-20/src/manifest.ts | 33 ++ mods/erc-20/src/view.ts | 98 +++++ mods/erc-20/tsconfig.json | 5 + packages/mod-registry/src/index.ts | 2 + turbo.json | 4 +- yarn.lock | 388 +++++++++++++++++- 13 files changed, 897 insertions(+), 14 deletions(-) create mode 100644 examples/api/src/app/api/erc-20/route.ts create mode 100644 mods/erc-20/index.ts create mode 100644 mods/erc-20/package.json create mode 100644 mods/erc-20/src/manifest.ts create mode 100644 mods/erc-20/src/view.ts create mode 100644 mods/erc-20/tsconfig.json diff --git a/examples/api/package.json b/examples/api/package.json index 3dcf32b7..b7cc8d47 100644 --- a/examples/api/package.json +++ b/examples/api/package.json @@ -16,10 +16,12 @@ "@lit-protocol/types": "^2.2.61", "@mod-protocol/core": "^0.1.1", "@reservoir0x/reservoir-sdk": "^1.8.4", + "@uniswap/smart-order-router": "^3.20.1", "@vercel/postgres-kysely": "^0.6.0", "@zoralabs/protocol-sdk": "^0.5.0", "chatgpt": "^5.2.5", "cheerio": "^1.0.0-rc.12", + "ethers": "^5.7.2", "kysely": "^0.26.3", "next": "^13.5.6", "nft.storage": "^7.1.1", @@ -27,9 +29,11 @@ "pg": "^8.11.3", "react": "^18.2.0", "react-dom": "^18.2.0", + "reverse-mirage": "^1.0.3", "siwe": "^1.1.6", "uint8arrays": "^3.0.0", - "viem": "1.20.1" + "viem": "1.20.1", + "viem2": "npm:viem@^2.0.6" }, "devDependencies": { "@types/node": "^17.0.12", diff --git a/examples/api/src/app/api/erc-20/route.ts b/examples/api/src/app/api/erc-20/route.ts new file mode 100644 index 00000000..7478d61b --- /dev/null +++ b/examples/api/src/app/api/erc-20/route.ts @@ -0,0 +1,340 @@ +import { FarcasterUser } from "@mod-protocol/core"; +import { Token } from "@uniswap/sdk-core"; +import * as smartOrderRouter from "@uniswap/smart-order-router"; +import { USDC_BASE } from "@uniswap/smart-order-router"; +import { NextRequest, NextResponse } from "next/server"; +import { publicActionReverseMirage, priceQuote } from "reverse-mirage"; +import { PublicClient, createClient, http, parseUnits } from "viem2"; +import * as chains from "viem2/chains"; + +const { AIRSTACK_API_KEY } = process.env; +const AIRSTACK_API_URL = "https://api.airstack.xyz/gql"; + +const chainByName: { [key: string]: chains.Chain } = Object.entries( + chains +).reduce( + (acc: { [key: string]: chains.Chain }, [key, chain]) => { + acc[key] = chain; + return acc; + }, + { ethereum: chains.mainnet } // Convenience for ethereum, which is 'homestead' otherwise +); + +const chainById = Object.values(chains).reduce( + (acc: { [key: number]: chains.Chain }, cur) => { + if (cur.id) acc[cur.id] = cur; + return acc; + }, + {} +); + +function numberWithCommas(x: string | number) { + var parts = x.toString().split("."); + parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ","); + return parts.join("."); +} + +const query = ` +query MyQuery($identity: Identity!, $token_address: Address!, $blockchain: TokenBlockchain, $cursor: String) { + SocialFollowings( + input: { + filter: { + identity: {_eq: $identity}, + dappName: {_eq: farcaster} + }, + blockchain: ALL, + limit: 200, + cursor: $cursor + } + ) { + pageInfo { + hasNextPage + nextCursor + } + Following { + followingProfileId, + followingAddress { + socials { + profileDisplayName + profileName + profileImage + profileBio + } + tokenBalances( + input: { + filter: { + tokenAddress: {_eq: $token_address}, + formattedAmount: {_gt: 0} + }, + blockchain: $blockchain + } + ) { + owner { + identity + } + formattedAmount + } + } + } + } +} +`; + +async function getFollowingHolderInfo({ + fid, + tokenAddress, + blockchain, +}: { + fid: string; + tokenAddress: string; + blockchain: string; +}): Promise<{ user: FarcasterUser; amount: number }[]> { + const acc: any[] = []; + + let hasNextPage = true; + let cursor = ""; + + try { + while (hasNextPage) { + hasNextPage = false; + const res = await fetch(AIRSTACK_API_URL, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: AIRSTACK_API_KEY, // Add API key to Authorization header + }, + body: JSON.stringify({ + query, + variables: { + identity: `fc_fid:${fid}`, + token_address: tokenAddress, + blockchain, + cursor, + }, + }), + }); + const json = await res?.json(); + const result = json?.data.SocialFollowings.Following.filter( + (item) => item.followingAddress.tokenBalances.length > 0 + ); + acc.push(...result); + + hasNextPage = json?.data.SocialFollowings.pageInfo.hasNextPage; + cursor = json?.data.SocialFollowings.pageInfo.nextCursor; + } + } catch (error) { + console.error(error); + } + + const result = acc + .map((item) => { + const socialData = item.followingAddress.socials[0]; + return { + user: { + displayName: socialData.profileDisplayName, + username: socialData.profileName, + fid: item.followingProfileId, + pfp: socialData.profileImage, + } as FarcasterUser, + amount: item.followingAddress.tokenBalances[0].formattedAmount, + }; + }) + .sort((a, b) => Number(b.amount) - Number(a.amount)); + + return result; +} + +async function getPriceData({ + tokenAddress, + blockchain, +}: { + tokenAddress: string; + blockchain: string; +}): Promise<{ + unitPriceUsd: string; + marketCapUsd?: string; + volume24hUsd?: string; + change24h?: string; +}> { + // https://api.coingecko.com/api/v3/simple/token_price/ethereum?contract_addresses=0xd7c1eb0fe4a30d3b2a846c04aa6300888f087a5f&vs_currencies=usd&points&vs_currencies=usd&include_market_cap=true&include_24hr_vol=true&include_24hr_change=true&include_last_updated_at=true + const params = new URLSearchParams({ + contract_addresses: tokenAddress, + vs_currencies: "usd", + include_market_cap: "true", + include_24hr_vol: "true", + include_24hr_change: "true", + include_last_updated_at: "true", + }); + const coingecko = await fetch( + `https://api.coingecko.com/api/v3/simple/token_price/${blockchain}?${params.toString()}` + ); + const coingeckoJson = await coingecko.json(); + + if (coingeckoJson[tokenAddress]) { + const { + usd: unitPriceUsd, + usd_market_cap: marketCapUsd, + usd_24h_vol: volume24hUsd, + usd_24h_change: change24h, + } = coingeckoJson[tokenAddress]; + + const unitPriceUsdFormatted = `${numberWithCommas( + parseFloat(unitPriceUsd).toPrecision(4) + )}`; + const marketCapUsdFormatted = `${parseFloat( + parseFloat(marketCapUsd).toFixed(0) + ).toLocaleString()}`; + const volume24hUsdFormatted = `${parseFloat( + parseFloat(volume24hUsd).toFixed(0) + ).toLocaleString()}`; + + const change24hNumber = parseFloat(change24h); + const change24hPartial = parseFloat( + change24hNumber.toFixed(2) + ).toLocaleString(); + const change24hFormatted = + change24hNumber > 0 ? `+${change24hPartial}%` : `-${change24hPartial}%`; + + return { + unitPriceUsd: unitPriceUsdFormatted, + marketCapUsd: marketCapUsdFormatted, + volume24hUsd: volume24hUsdFormatted, + change24h: change24hFormatted, + }; + } + + // Use on-chain data as fallback + const chain = chainByName[blockchain.toLowerCase()]; + const url = `https://api.1inch.dev/price/v1.1/${chain.id}/${tokenAddress}?currency=USD`; + const res = await fetch(url, { + headers: { + Authorization: `Bearer ${process.env["1INCH_API_KEY"]}`, + }, + }); + const resJson = await res.json(); + + return { + unitPriceUsd: parseFloat(resJson[tokenAddress]).toPrecision(4), + }; +} + +async function tokenInfo({ + tokenAddress, + blockchain, +}: { + tokenAddress: string; + blockchain: string; +}): Promise<{ + symbol: string; + name: string; + image?: string; +}> { + //0x4ed4e862860bed51a9570b96d89af5e1b0efefed + // https://api.coingecko.com/api/v3/coins/0x4ed4e862860bed51a9570b96d89af5e1b0efefed/market_chart?vs_currency=usd&days=1 + // https://api.coingecko.com/api/v3/simple/token_price/ethereum?contract_addresses=0xd7c1eb0fe4a30d3b2a846c04aa6300888f087a5f&vs_currencies=usd&points&vs_currencies=usd&include_market_cap=true&include_24hr_vol=true&include_24hr_change=true&include_last_updated_at=true + // https://api.coingecko.com/api/v3/coins/ethereum/contract/0xd7c1eb0fe4a30d3b2a846c04aa6300888f087a5f + // https://api.coingecko.com/api/v3/coins/base/contract/0x27d2decb4bfc9c76f0309b8e88dec3a601fe25a8 + const res = await fetch( + `https://api.coingecko.com/api/v3/coins/${blockchain}/contract/${tokenAddress}` + ); + + if (res.ok) { + const json = await res?.json(); + return { + symbol: json.symbol, + name: json.name, + image: json.image?.thumb, + }; + } + + // Use on-chain data as fallback + const chain = chainByName[blockchain]; + const client = ( + createClient({ + transport: http(), + chain, + }) as PublicClient + ).extend(publicActionReverseMirage); + + const token = await client.getERC20({ + erc20: { + address: tokenAddress as `0x${string}`, + chainID: chain.id, + }, + }); + + return { + symbol: token.symbol, + name: token.name, + }; +} + +export async function GET(request: NextRequest) { + const fid = request.nextUrl.searchParams.get("fid")?.toLowerCase(); + const token = request.nextUrl.searchParams.get("token")?.toLowerCase(); + let tokenAddress = request.nextUrl.searchParams + .get("tokenAddress") + ?.toLowerCase(); + let blockchain = request.nextUrl.searchParams + .get("blockchain") + ?.toLowerCase(); + + if (token) { + // Splitting the string at '/erc20:' + const parts = token.split("/erc20:"); + + // Extracting the chain ID + const chainIdPart = parts[0]; + const chainId = chainIdPart.split(":")[1]; + + // The token address is the second part of the split, but without '0x' if present + tokenAddress = parts[1]; + + const [blockchainName] = Object.entries(chainByName).find( + ([, value]) => value.id.toString() == chainId + ); + blockchain = blockchainName; + } + + if (!tokenAddress) { + return NextResponse.json({ + error: "Missing tokenAddress", + }); + } + + if (!blockchain) { + return NextResponse.json({ + error: "Missing or invalid blockchain (ethereum, polygon, base)", + }); + } + + const [holderData, priceData, tokenData] = await Promise.all([ + getFollowingHolderInfo({ + blockchain: blockchain, + tokenAddress: tokenAddress, + fid: fid, + }), + getPriceData({ + blockchain: blockchain, + tokenAddress: tokenAddress, + }), + tokenInfo({ + tokenAddress, + blockchain, + }), + ]); + + return NextResponse.json({ + holderData: { + holders: [...(holderData || [])], + holdersCount: holderData?.length || 0, + }, + priceData, + tokenData, + }); +} + +// needed for preflight requests to succeed +export const OPTIONS = async (request: NextRequest) => { + return NextResponse.json({}); +}; diff --git a/examples/nextjs-shadcn/src/app/dummy-casts.ts b/examples/nextjs-shadcn/src/app/dummy-casts.ts index 3968f8f1..6937b2ae 100644 --- a/examples/nextjs-shadcn/src/app/dummy-casts.ts +++ b/examples/nextjs-shadcn/src/app/dummy-casts.ts @@ -165,4 +165,19 @@ export const dummyCastData: Array<{ }, ], }, + { + avatar_url: + "https://res.cloudinary.com/merkle-manufactory/image/fetch/c_fill,f_png,w_144/https%3A%2F%2Flh3.googleusercontent.com%2F-S5cdhOpZtJ_Qzg9iPWELEsRTkIsZ7qGYmVlwEORgFB00WWAtZGefRnS4Bjcz5ah40WVOOWeYfU5pP9Eekikb3cLMW2mZQOMQHlWhg", + display_name: "David Furlong", + username: "df", + timestamp: "2023-08-17 09:16:52.293739", + text: "Just bought this token 🚀🚀🚀", + embeds: [ + { + url: "eip155:1/erc20:0xd7c1eb0fe4a30d3b2a846c04aa6300888f087a5f", + status: "loaded", + metadata: {}, + }, + ], + }, ]; diff --git a/examples/nextjs-shadcn/src/app/embeds.tsx b/examples/nextjs-shadcn/src/app/embeds.tsx index c753175b..4fd11d1b 100644 --- a/examples/nextjs-shadcn/src/app/embeds.tsx +++ b/examples/nextjs-shadcn/src/app/embeds.tsx @@ -7,11 +7,12 @@ import { SendEthTransactionActionResolverInit, } from "@mod-protocol/core"; import { - richEmbedMods, defaultRichEmbedMod, + richEmbedMods, richEmbedModsExperimental, } from "@mod-protocol/mod-registry"; import { RichEmbed } from "@mod-protocol/react"; +import "@mod-protocol/react-ui-shadcn/dist/public/video-js.css"; import { renderers } from "@mod-protocol/react-ui-shadcn/dist/renderers"; import { sendTransaction, @@ -21,7 +22,6 @@ import { import { useMemo } from "react"; import { useAccount } from "wagmi"; import { useExperimentalMods } from "./use-experimental-mods"; -import "@mod-protocol/react-ui-shadcn/dist/public/video-js.css"; export function Embeds(props: { embeds: Array }) { const experimentalMods = useExperimentalMods(); @@ -71,6 +71,9 @@ export function Embeds(props: { embeds: Array }) { wallet: { address, }, + farcaster: { + fid: "1214", + }, }, }; }, [address]); diff --git a/examples/nextjs-shadcn/src/app/page.tsx b/examples/nextjs-shadcn/src/app/page.tsx index 27a6ed38..eb65a709 100644 --- a/examples/nextjs-shadcn/src/app/page.tsx +++ b/examples/nextjs-shadcn/src/app/page.tsx @@ -101,6 +101,8 @@ export default function Page() {

Embed renderers

+

ERC-20 Mod

+

NFT Mod with native minting

Video Mod

diff --git a/mods/erc-20/index.ts b/mods/erc-20/index.ts new file mode 100644 index 00000000..8e71bc78 --- /dev/null +++ b/mods/erc-20/index.ts @@ -0,0 +1 @@ +export { default as default } from "./src/manifest"; diff --git a/mods/erc-20/package.json b/mods/erc-20/package.json new file mode 100644 index 00000000..06db49fc --- /dev/null +++ b/mods/erc-20/package.json @@ -0,0 +1,10 @@ +{ + "name": "@mods/erc-20", + "main": "./index.ts", + "types": "./index.ts", + "version": "0.1.0", + "private": true, + "dependencies": { + "@mod-protocol/core": "^0.1.0" + } +} \ No newline at end of file diff --git a/mods/erc-20/src/manifest.ts b/mods/erc-20/src/manifest.ts new file mode 100644 index 00000000..b639b0a8 --- /dev/null +++ b/mods/erc-20/src/manifest.ts @@ -0,0 +1,33 @@ +import { ModManifest } from "@mod-protocol/core"; +import view from "./view"; + +const manifest: ModManifest = { + slug: "erc-20", + name: "ERC-20", + custodyAddress: "stephancill.eth", + version: "0.0.1", + logo: "", + custodyGithubUsername: "stephancill", + richEmbedEntrypoints: [ + { + if: { + value: "{{embed.url}}", + match: { + OR: [ + { startsWith: "https://app.uniswap.org/tokens/" }, + { + regex: "eip155:(\\d+)/erc20:0x([0-9a-fA-F]+)", + }, + ], + }, + }, + element: view, + }, + ], + elements: { + "#view": view, + }, + permissions: ["user.wallet.address"], // "user.farcaster.fid" +}; + +export default manifest; diff --git a/mods/erc-20/src/view.ts b/mods/erc-20/src/view.ts new file mode 100644 index 00000000..c2b9d3ca --- /dev/null +++ b/mods/erc-20/src/view.ts @@ -0,0 +1,98 @@ +import { ModElement } from "@mod-protocol/core"; + +const view: ModElement[] = [ + { + type: "horizontal-layout", + onload: { + type: "GET", + url: "{{api}}/erc-20?fid={{user.farcaster.fid}}&token={{embed.url}}", + ref: "tokenReq", + }, + elements: [ + { + if: { + value: "{{refs.tokenReq.response.data}}", + match: { + NOT: { + equals: "", + }, + }, + }, + then: { + type: "vertical-layout", + elements: [ + { + type: "horizontal-layout", + elements: [ + { + if: { + value: "{{refs.tokenReq.response.data.tokenData.image}}", + match: { + NOT: { + equals: "", + }, + }, + }, + then: { + type: "avatar", + src: "{{refs.tokenReq.response.data.tokenData.image}}", + }, + }, + { + type: "link", + label: "{{refs.tokenReq.response.data.tokenData.name}}", + url: "https://coingecko.com/en/coins/points", + }, + { + type: "text", + variant: "secondary", + label: + "${{refs.tokenReq.response.data.priceData.unitPriceUsd}}", + }, + { + if: { + value: + "{{refs.tokenReq.response.data.priceData.change24h}}", + match: { + NOT: { + equals: "", + }, + }, + }, + then: { + type: "horizontal-layout", + elements: [ + { + type: "text", + variant: "secondary", + label: "24h:", + }, + { + type: "text", + variant: "secondary", + label: + "{{refs.tokenReq.response.data.priceData.change24h}}", + }, + ], + }, + }, + ], + }, + // Holders you know + { + type: "text", + variant: "secondary", + label: + "{{refs.tokenReq.response.data.holderData.holdersCount}} holders you know", + }, + ], + }, + else: { + type: "circular-progress", + }, + }, + ], + }, +]; + +export default view; diff --git a/mods/erc-20/tsconfig.json b/mods/erc-20/tsconfig.json new file mode 100644 index 00000000..37906aab --- /dev/null +++ b/mods/erc-20/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "tsconfig/base.json", + "include": ["."], + "exclude": ["dist", "build", "node_modules"] +} diff --git a/packages/mod-registry/src/index.ts b/packages/mod-registry/src/index.ts index 1b13ac01..63de2ddb 100644 --- a/packages/mod-registry/src/index.ts +++ b/packages/mod-registry/src/index.ts @@ -13,6 +13,7 @@ import ZoraNftMinter from "@mods/zora-nft-minter"; import ImgurUpload from "@mods/imgur-upload"; import DALLE from "@mods/dall-e"; import ZoraCreate from "@mods/zora-create"; +import ERC20 from "@mods/erc-20"; /** All - Stable, suitable for use */ @@ -54,6 +55,7 @@ export const allModsExperimental = [ ChatGPTShorten, ChatGPT, DALLE, + ERC20, ]; export const creationModsExperimental: ModManifest[] = diff --git a/turbo.json b/turbo.json index 4d0611b9..8a657af9 100644 --- a/turbo.json +++ b/turbo.json @@ -39,6 +39,8 @@ "IMGUR_CLIENT_ID", "NEXT_PUBLIC_EXPERIMENTAL_MODS", "ZORA_ADMIN_PRIVATE_KEY", - "NFT_STORAGE_API_KEY" + "NFT_STORAGE_API_KEY", + "AIRSTACK_API_KEY", + "1INCH_API_KEY" ] } \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 8423fbd0..e554f415 100644 --- a/yarn.lock +++ b/yarn.lock @@ -970,7 +970,7 @@ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.56.0.tgz#ef20350fec605a7f7035a01764731b2de0f3782b" integrity sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A== -"@ethersproject/abi@5.7.0", "@ethersproject/abi@^5.7.0": +"@ethersproject/abi@5.7.0", "@ethersproject/abi@^5.0.12", "@ethersproject/abi@^5.5.0", "@ethersproject/abi@^5.7.0": version "5.7.0" resolved "https://registry.yarnpkg.com/@ethersproject/abi/-/abi-5.7.0.tgz#b3f3e045bbbeed1af3947335c247ad625a44e449" integrity sha512-351ktp42TiRcYB3H1OP8yajPeAQstMW/yCFokj/AthP9bLHzQFPlOrxOcwYEDkUAICmOHljvN4K39OMTMUa9RA== @@ -1009,7 +1009,7 @@ "@ethersproject/logger" "^5.7.0" "@ethersproject/properties" "^5.7.0" -"@ethersproject/address@5.7.0", "@ethersproject/address@^5.7.0": +"@ethersproject/address@5.7.0", "@ethersproject/address@^5.0.0", "@ethersproject/address@^5.0.2", "@ethersproject/address@^5.7.0": version "5.7.0" resolved "https://registry.yarnpkg.com/@ethersproject/address/-/address-5.7.0.tgz#19b56c4d74a3b0a46bfdbb6cfcc0a153fc697f37" integrity sha512-9wYhYt7aghVGo758POM5nqcOMaE168Q6aRLJZwUmiqSrAungkG74gSSeKEIR7ukixesdRZGPgVqme6vmxs1fkA== @@ -1224,7 +1224,7 @@ elliptic "6.5.4" hash.js "1.1.7" -"@ethersproject/solidity@5.7.0": +"@ethersproject/solidity@5.7.0", "@ethersproject/solidity@^5.0.0", "@ethersproject/solidity@^5.0.9": version "5.7.0" resolved "https://registry.yarnpkg.com/@ethersproject/solidity/-/solidity-5.7.0.tgz#5e9c911d8a2acce2a5ebb48a5e2e0af20b631cb8" integrity sha512-HmabMd2Dt/raavyaGukF4XxizWKhKQ24DoLtdNbBmNKUOPqwjsKQSdV9GQtj9CBEea9DlzETlVER1gYeXXBGaA== @@ -4541,6 +4541,184 @@ resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== +"@uniswap/default-token-list@^11.2.0": + version "11.11.0" + resolved "https://registry.yarnpkg.com/@uniswap/default-token-list/-/default-token-list-11.11.0.tgz#270c3cd817275b0c46258bc6a4630207fa9e75e4" + integrity sha512-UngPIUcycnKUkFUskRe2SLxAz5Gt/5i3G8XW8m9NJWTXAzfDMJD6wvOsk+gXnSfSBwpGlVPgiJy5f5FRWloTUA== + +"@uniswap/lib@^4.0.1-alpha": + version "4.0.1-alpha" + resolved "https://registry.yarnpkg.com/@uniswap/lib/-/lib-4.0.1-alpha.tgz#2881008e55f075344675b3bca93f020b028fbd02" + integrity sha512-f6UIliwBbRsgVLxIaBANF6w09tYqc6Y/qXdsrbEmXHyFA7ILiKrIwRFXe1yOg8M3cksgVsO9N7yuL2DdCGQKBA== + +"@uniswap/permit2-sdk@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@uniswap/permit2-sdk/-/permit2-sdk-1.2.0.tgz#ed86440a87a6c318169c8e6f161fc263ad040891" + integrity sha512-Ietv3FxN7+RCXcPSED/i/8b0a2GUZrMdyX05k3FsSztvYKyPFAMS/hBXojF0NZqYB1bHecqYc7Ej+7tV/rdYXg== + dependencies: + ethers "^5.3.1" + tiny-invariant "^1.3.1" + +"@uniswap/router-sdk@^1.6.0": + version "1.7.1" + resolved "https://registry.yarnpkg.com/@uniswap/router-sdk/-/router-sdk-1.7.1.tgz#642d5804299cd50b1a3ba2fa0a87963320fb7f93" + integrity sha512-uBN9QX3t5lPLkxlkPoQPZpd0eN+GA0Ab9nq1pcPk/XDFuRnRxxVF629Ecz2SfTVm0gooOPO3aU3ETgyB3vuhYA== + dependencies: + "@ethersproject/abi" "^5.5.0" + "@uniswap/sdk-core" "^4.0.7" + "@uniswap/swap-router-contracts" "^1.1.0" + "@uniswap/v2-sdk" "^3.2.0" + "@uniswap/v3-sdk" "^3.10.0" + +"@uniswap/sdk-core@^4", "@uniswap/sdk-core@^4.0.0", "@uniswap/sdk-core@^4.0.7": + version "4.0.10" + resolved "https://registry.yarnpkg.com/@uniswap/sdk-core/-/sdk-core-4.0.10.tgz#6173cc39d4e6b5ed679775447bb96a5b3c3fb2f2" + integrity sha512-RiobXJKXvVVb+wfNM09Ik8djOMOuRQGfyRP5pHgUjojicK/7nscZILjZ87DjVCGjXEoD8yTSIps0UAQuz6pJIw== + dependencies: + "@ethersproject/address" "^5.0.2" + big.js "^5.2.2" + decimal.js-light "^2.5.0" + jsbi "^3.1.4" + tiny-invariant "^1.1.0" + toformat "^2.0.0" + +"@uniswap/smart-order-router@^3.20.1": + version "3.20.1" + resolved "https://registry.yarnpkg.com/@uniswap/smart-order-router/-/smart-order-router-3.20.1.tgz#6e5343d56caac487d0eb022fd889111958463bd6" + integrity sha512-T+lKPthApVpWA2cV9svJRiIeqjtJSNq6OSA2J0fQjViQT1bYhwfBk6S+FaEHU780oRYpiRb7lBxhnRsSU6k99g== + dependencies: + "@uniswap/default-token-list" "^11.2.0" + "@uniswap/permit2-sdk" "^1.2.0" + "@uniswap/router-sdk" "^1.6.0" + "@uniswap/sdk-core" "^4.0.7" + "@uniswap/swap-router-contracts" "^1.3.0" + "@uniswap/token-lists" "^1.0.0-beta.31" + "@uniswap/universal-router" "^1.0.1" + "@uniswap/universal-router-sdk" "^1.5.8" + "@uniswap/v2-sdk" "^3.2.3" + "@uniswap/v3-sdk" "^3.10.0" + async-retry "^1.3.1" + await-timeout "^1.1.1" + axios "^0.21.1" + bunyan "^1.8.15" + bunyan-blackhole "^1.1.1" + ethers "^5.7.2" + graphql "^15.5.0" + graphql-request "^3.4.0" + lodash "^4.17.21" + mnemonist "^0.38.3" + node-cache "^5.1.2" + stats-lite "^2.2.0" + +"@uniswap/swap-router-contracts@^1.1.0", "@uniswap/swap-router-contracts@^1.2.1", "@uniswap/swap-router-contracts@^1.3.0": + version "1.3.1" + resolved "https://registry.yarnpkg.com/@uniswap/swap-router-contracts/-/swap-router-contracts-1.3.1.tgz#0ebbb93eb578625618ed9489872de381f9c66fb4" + integrity sha512-mh/YNbwKb7Mut96VuEtL+Z5bRe0xVIbjjiryn+iMMrK2sFKhR4duk/86mEz0UO5gSx4pQIw9G5276P5heY/7Rg== + dependencies: + "@openzeppelin/contracts" "3.4.2-solc-0.7" + "@uniswap/v2-core" "^1.0.1" + "@uniswap/v3-core" "^1.0.0" + "@uniswap/v3-periphery" "^1.4.4" + dotenv "^14.2.0" + hardhat-watcher "^2.1.1" + +"@uniswap/token-lists@^1.0.0-beta.31": + version "1.0.0-beta.33" + resolved "https://registry.yarnpkg.com/@uniswap/token-lists/-/token-lists-1.0.0-beta.33.tgz#966ba96c9ccc8f0e9e09809890b438203f2b1911" + integrity sha512-JQkXcpRI3jFG8y3/CGC4TS8NkDgcxXaOQuYW8Qdvd6DcDiIyg2vVYCG9igFEzF0G6UvxgHkBKC7cWCgzZNYvQg== + +"@uniswap/universal-router-sdk@^1.5.8": + version "1.5.8" + resolved "https://registry.yarnpkg.com/@uniswap/universal-router-sdk/-/universal-router-sdk-1.5.8.tgz#16c62c3883e99073ba8b6e19188cf418b6551847" + integrity sha512-9tDDBTXarpdRfJStF5mDCNmsQrCfiIT6HCQN1EPq0tAm2b+JzjRkUzsLpbNpVef066FETc3YjPH6JDPB3CMyyA== + dependencies: + "@uniswap/permit2-sdk" "^1.2.0" + "@uniswap/router-sdk" "^1.6.0" + "@uniswap/sdk-core" "^4.0.0" + "@uniswap/universal-router" "1.4.3" + "@uniswap/v2-sdk" "^3.2.0" + "@uniswap/v3-sdk" "^3.10.0" + bignumber.js "^9.0.2" + ethers "^5.3.1" + +"@uniswap/universal-router@1.4.3": + version "1.4.3" + resolved "https://registry.yarnpkg.com/@uniswap/universal-router/-/universal-router-1.4.3.tgz#7736cf7f8dc99435a6be87c2e80b5c5d4589d641" + integrity sha512-SZmYfhYZtsuxrTMCitcA39iJuG9sbe2nvm9iQfd70WjMpbB0+GuEs5OqSHc5tB/ujrVKzPJ1LOoNNGOs0xPEeA== + dependencies: + "@openzeppelin/contracts" "4.7.0" + "@uniswap/v2-core" "1.0.1" + "@uniswap/v3-core" "1.0.0" + +"@uniswap/universal-router@^1.0.1": + version "1.5.1" + resolved "https://registry.yarnpkg.com/@uniswap/universal-router/-/universal-router-1.5.1.tgz#2ce832485eb85093b0cb94a53be20661e1aece70" + integrity sha512-+htTC/nHQXKfY/c+9C1XHMRs7Jz0bX9LQfYn9Hb7WZKZ/YHWhOsCZQylYhksieLYTRam5sQheow747hOZ+QpZQ== + dependencies: + "@openzeppelin/contracts" "4.7.0" + "@uniswap/v2-core" "1.0.1" + "@uniswap/v3-core" "1.0.0" + +"@uniswap/v2-core@1.0.1", "@uniswap/v2-core@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@uniswap/v2-core/-/v2-core-1.0.1.tgz#af8f508bf183204779938969e2e54043e147d425" + integrity sha512-MtybtkUPSyysqLY2U210NBDeCHX+ltHt3oADGdjqoThZaFRDKwM6k1Nb3F0A3hk5hwuQvytFWhrWHOEq6nVJ8Q== + +"@uniswap/v2-sdk@^3.2.0", "@uniswap/v2-sdk@^3.2.3": + version "3.3.0" + resolved "https://registry.yarnpkg.com/@uniswap/v2-sdk/-/v2-sdk-3.3.0.tgz#76c95d234fe73ca6ad34ba9509f7451955ee0ce7" + integrity sha512-cf5PjoNQN5tNELIOVJsqV4/VeuDtxFw6Zl8oFmFJ6PNoQ8sx+XnGoO0aGKTB/o5II3oQ7820xtY3k47UsXgd6A== + dependencies: + "@ethersproject/address" "^5.0.0" + "@ethersproject/solidity" "^5.0.0" + "@uniswap/sdk-core" "^4.0.7" + tiny-invariant "^1.1.0" + tiny-warning "^1.0.3" + +"@uniswap/v3-core@1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@uniswap/v3-core/-/v3-core-1.0.0.tgz#6c24adacc4c25dceee0ba3ca142b35adbd7e359d" + integrity sha512-kSC4djMGKMHj7sLMYVnn61k9nu+lHjMIxgg9CDQT+s2QYLoA56GbSK9Oxr+qJXzzygbkrmuY6cwgP6cW2JXPFA== + +"@uniswap/v3-core@^1.0.0": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@uniswap/v3-core/-/v3-core-1.0.1.tgz#b6d2bdc6ba3c3fbd610bdc502395d86cd35264a0" + integrity sha512-7pVk4hEm00j9tc71Y9+ssYpO6ytkeI0y7WE9P6UcmNzhxPePwyAxImuhVsTqWK9YFvzgtvzJHi64pBl4jUzKMQ== + +"@uniswap/v3-periphery@^1.0.1", "@uniswap/v3-periphery@^1.1.1", "@uniswap/v3-periphery@^1.4.4": + version "1.4.4" + resolved "https://registry.yarnpkg.com/@uniswap/v3-periphery/-/v3-periphery-1.4.4.tgz#d2756c23b69718173c5874f37fd4ad57d2f021b7" + integrity sha512-S4+m+wh8HbWSO3DKk4LwUCPZJTpCugIsHrWR86m/OrUyvSqGDTXKFfc2sMuGXCZrD1ZqO3rhQsKgdWg3Hbb2Kw== + dependencies: + "@openzeppelin/contracts" "3.4.2-solc-0.7" + "@uniswap/lib" "^4.0.1-alpha" + "@uniswap/v2-core" "^1.0.1" + "@uniswap/v3-core" "^1.0.0" + base64-sol "1.0.1" + +"@uniswap/v3-sdk@^3.10.0": + version "3.10.0" + resolved "https://registry.yarnpkg.com/@uniswap/v3-sdk/-/v3-sdk-3.10.0.tgz#962c9e598250ced00702d944783c2d9ee3fa12f6" + integrity sha512-sbmSA1O+Ct960r66Ie/c1rOmVadpwRu8nQ79pGICv0pZJdnFIQ/SReG3F+iC2C2UNNjNP6aC2WDUggXrjyrgnA== + dependencies: + "@ethersproject/abi" "^5.0.12" + "@ethersproject/solidity" "^5.0.9" + "@uniswap/sdk-core" "^4" + "@uniswap/swap-router-contracts" "^1.2.1" + "@uniswap/v3-periphery" "^1.1.1" + "@uniswap/v3-staker" "1.0.0" + tiny-invariant "^1.1.0" + tiny-warning "^1.0.3" + +"@uniswap/v3-staker@1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@uniswap/v3-staker/-/v3-staker-1.0.0.tgz#9a6915ec980852479dfc903f50baf822ff8fa66e" + integrity sha512-JV0Qc46Px5alvg6YWd+UIaGH9lDuYG/Js7ngxPit1SPaIP30AlVer1UYB7BRYeUVVxE+byUyIeN5jeQ7LLDjIw== + dependencies: + "@openzeppelin/contracts" "3.4.1-solc-0.7-2" + "@uniswap/v3-core" "1.0.0" + "@uniswap/v3-periphery" "^1.0.1" + "@vanilla-extract/css@1.9.1": version "1.9.1" resolved "https://registry.yarnpkg.com/@vanilla-extract/css/-/css-1.9.1.tgz#337b79faa5f8f98915a90c3fe3c30b54be746c09" @@ -5300,6 +5478,11 @@ JSONStream@^1.3.5: jsonparse "^1.2.0" through ">=2.2.7 <3" +abitype@0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/abitype/-/abitype-0.10.0.tgz#d3504747cc81df2acaa6c460250ef7bc9219a77c" + integrity sha512-QvMHEUzgI9nPj9TWtUGnS2scas80/qaL5PBxGdwWhhvzqXfOph+IEiiiWrzuisu3U3JgDQVruW9oLbJoQ3oZ3A== + abitype@0.8.7: version "0.8.7" resolved "https://registry.yarnpkg.com/abitype/-/abitype-0.8.7.tgz#e4b3f051febd08111f486c0cc6a98fa72d033622" @@ -5720,7 +5903,7 @@ async-mutex@^0.2.6: dependencies: tslib "^2.0.0" -async-retry@^1.3.3: +async-retry@^1.3.1, async-retry@^1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/async-retry/-/async-retry-1.3.3.tgz#0e7f36c04d8478e7a58bdbed80cedf977785f280" integrity sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw== @@ -5774,6 +5957,11 @@ available-typed-arrays@^1.0.5: resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7" integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw== +await-timeout@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/await-timeout/-/await-timeout-1.1.1.tgz#d42062ee6bc4eb271fe4d4f851eb658dae7e3906" + integrity sha512-gsDXAS6XVc4Jt+7S92MPX6Noq69bdeXUPEaXd8dk3+yVr629LTDLxNt4j1ycBbrU+AStK2PhKIyNIM+xzWMVOQ== + axe-core@=4.7.0: version "4.7.0" resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.7.0.tgz#34ba5a48a8b564f67e103f0aa5768d76e15bbbbf" @@ -5787,7 +5975,7 @@ axios@0.27.2, axios@^0.27.2: follow-redirects "^1.14.9" form-data "^4.0.0" -axios@^0.21.2: +axios@^0.21.1, axios@^0.21.2: version "0.21.4" resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575" integrity sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg== @@ -5897,6 +6085,11 @@ base64-js@^1.3.0, base64-js@^1.3.1, base64-js@^1.5.1: resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== +base64-sol@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/base64-sol/-/base64-sol-1.0.1.tgz#91317aa341f0bc763811783c5729f1c2574600f6" + integrity sha512-ld3cCNMeXt4uJXmLZBHFGMvVpK9KsLVEhPpFRXnvSVAqABKbuNZg/+dsq3NuM+wxFLb/UrVkz7m1ciWmkMfTbg== + base64url@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/base64url/-/base64url-3.0.1.tgz#6399d572e2bc3f90a9a8b22d5dbb0a32d33f788d" @@ -5919,6 +6112,11 @@ better-path-resolve@1.0.0: dependencies: is-windows "^1.0.0" +big.js@^5.2.2: + version "5.2.2" + resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" + integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== + bigint-buffer@^1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/bigint-buffer/-/bigint-buffer-1.1.5.tgz#d038f31c8e4534c1f8d0015209bf34b4fa6dd442" @@ -5926,7 +6124,7 @@ bigint-buffer@^1.1.5: dependencies: bindings "^1.3.0" -bignumber.js@*, bignumber.js@^9.0.0, bignumber.js@^9.0.1, bignumber.js@^9.1.1: +bignumber.js@*, bignumber.js@^9.0.0, bignumber.js@^9.0.1, bignumber.js@^9.0.2, bignumber.js@^9.1.1: version "9.1.2" resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.1.2.tgz#b7c4242259c008903b13707983b5f4bbd31eda0c" integrity sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug== @@ -6185,6 +6383,23 @@ bundle-require@^4.0.0: dependencies: load-tsconfig "^0.2.3" +bunyan-blackhole@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/bunyan-blackhole/-/bunyan-blackhole-1.1.1.tgz#b9208586dc0b4e47f4f713215b1bddd65e4f6257" + integrity sha512-UwzNPhbbSqbzeJhCbygqjlAY7p0ZUdv1ADXPQvDh3CA7VW3C/rCc1gaQO/8j9QL4vsKQCQZQSQIEwX+lxioPAQ== + dependencies: + stream-blackhole "^1.0.3" + +bunyan@^1.8.15: + version "1.8.15" + resolved "https://registry.yarnpkg.com/bunyan/-/bunyan-1.8.15.tgz#8ce34ca908a17d0776576ca1b2f6cbd916e93b46" + integrity sha512-0tECWShh6wUysgucJcBAoYegf3JJoZWibxdqhTm7OHPeT42qdjkZ29QCMcKwbgU1kiH+auSIasNRXMLWXafXig== + optionalDependencies: + dtrace-provider "~0.8" + moment "^2.19.3" + mv "~2" + safe-json-stringify "~1" + busboy@1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893" @@ -6545,6 +6760,11 @@ clone-response@^1.0.2: dependencies: mimic-response "^1.0.0" +clone@2.x: + version "2.1.2" + resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f" + integrity sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w== + clone@^1.0.2: version "1.0.4" resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" @@ -6798,7 +7018,7 @@ crelt@^1.0.0: resolved "https://registry.yarnpkg.com/crelt/-/crelt-1.0.6.tgz#7cc898ea74e190fb6ef9dae57f8f81cf7302df72" integrity sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g== -cross-fetch@^3.1.4, cross-fetch@^3.1.5: +cross-fetch@^3.0.6, cross-fetch@^3.1.4, cross-fetch@^3.1.5: version "3.1.8" resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.8.tgz#0327eba65fd68a7d119f8fb2bf9334a1a7956f82" integrity sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg== @@ -7252,6 +7472,11 @@ decamelize@^1.1.0, decamelize@^1.2.0: resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA== +decimal.js-light@^2.5.0: + version "2.5.1" + resolved "https://registry.yarnpkg.com/decimal.js-light/-/decimal.js-light-2.5.1.tgz#134fd32508f19e208f4fb2f8dac0d2626a867934" + integrity sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg== + decode-named-character-reference@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz#daabac9690874c394c81e4162a0304b35d824f0e" @@ -7552,6 +7777,11 @@ dotenv@16.0.3: resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.3.tgz#115aec42bac5053db3c456db30cc243a5a836a07" integrity sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ== +dotenv@^14.2.0: + version "14.3.2" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-14.3.2.tgz#7c30b3a5f777c79a3429cb2db358eef6751e8369" + integrity sha512-vwEppIphpFdvaMCaHfCEv9IgwcxMljMw2TnAQBB4VWPvzXQLTb82jwmdOKzlEVUL3gNFT4l4TPKO+Bn+sqcrVQ== + dotenv@^16.0.3, dotenv@^16.3.1: version "16.3.1" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.3.1.tgz#369034de7d7e5b120972693352a3bf112172cc3e" @@ -8297,7 +8527,7 @@ eth-rpc-errors@^4.0.2: dependencies: fast-safe-stringify "^2.0.6" -ethers@^5.7.1: +ethers@^5.3.1, ethers@^5.7.1, ethers@^5.7.2: version "5.7.2" resolved "https://registry.yarnpkg.com/ethers/-/ethers-5.7.2.tgz#3a7deeabbb8c030d4126b24f84e525466145872e" integrity sha512-wswUsmWo1aOK8rR7DIKiWSw9DbLWe6x98Jrn8wcTflTVvaXhAMaB5zGAXy0GYQEQp9iO1iSHWVyARQm11zUtyg== @@ -8463,6 +8693,11 @@ external-editor@^3.0.3, external-editor@^3.1.0: iconv-lite "^0.4.24" tmp "^0.0.33" +extract-files@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/extract-files/-/extract-files-9.0.0.tgz#8a7744f2437f81f5ed3250ed9f1550de902fe54a" + integrity sha512-CvdFfHkC95B4bBBk36hcEmvdR2awOdhhVUYH6S/zrVj3477zven/fJMYg7121h4T1xHZC+tetUpubpAhxwI7hQ== + eyes@^0.1.8: version "0.1.8" resolved "https://registry.yarnpkg.com/eyes/-/eyes-0.1.8.tgz#62cf120234c683785d902348a800ef3e0cc20bc0" @@ -8694,6 +8929,15 @@ form-data@4.0.0, form-data@^4.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" +form-data@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.1.tgz#ebd53791b78356a99af9a300d4282c4d5eb9755f" + integrity sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + fraction.js@^4.3.6: version "4.3.7" resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7" @@ -8939,6 +9183,17 @@ glob@7.1.7: once "^1.3.0" path-is-absolute "^1.0.0" +glob@^6.0.1: + version "6.0.4" + resolved "https://registry.yarnpkg.com/glob/-/glob-6.0.4.tgz#0f08860f6a155127b2fadd4f9ce24b1aab6e4d22" + integrity sha512-MKZeRNyYZAVVVG1oZeLaWie1uweH40m9AZwIwxyPbTSX4hHrVYSzLg0Ro5Z5R7XKkIX+Cc6oD1rqeDJnwsB8/A== + dependencies: + inflight "^1.0.4" + inherits "2" + minimatch "2 || 3" + once "^1.3.0" + path-is-absolute "^1.0.0" + glob@^7.0.0, glob@^7.1.3, glob@^7.1.4: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" @@ -9051,6 +9306,20 @@ graphemer@^1.4.0: resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== +graphql-request@^3.4.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/graphql-request/-/graphql-request-3.7.0.tgz#c7406e537084f8b9788541e3e6704340ca13055b" + integrity sha512-dw5PxHCgBneN2DDNqpWu8QkbbJ07oOziy8z+bK/TAXufsOLaETuVO4GkXrbs0WjhdKhBMN3BkpN/RIvUHkmNUQ== + dependencies: + cross-fetch "^3.0.6" + extract-files "^9.0.0" + form-data "^3.0.0" + +graphql@^15.5.0: + version "15.8.0" + resolved "https://registry.yarnpkg.com/graphql/-/graphql-15.8.0.tgz#33410e96b012fa3bdb1091cc99a94769db212b38" + integrity sha512-5gghUc24tP9HRznNpV2+FIoq3xKkj5dTQqf4v0CpdPbFVwFkWoxOM+o+2OC9ZSvjEMTjfmG9QT+gcvggTwW1zw== + gray-matter@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/gray-matter/-/gray-matter-4.0.3.tgz#e893c064825de73ea1f5f7d88c7a9f7274288798" @@ -9108,6 +9377,13 @@ hard-rejection@^2.1.0: resolved "https://registry.yarnpkg.com/hard-rejection/-/hard-rejection-2.1.0.tgz#1c6eda5c1685c63942766d79bb40ae773cecd883" integrity sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA== +hardhat-watcher@^2.1.1: + version "2.5.0" + resolved "https://registry.yarnpkg.com/hardhat-watcher/-/hardhat-watcher-2.5.0.tgz#3ee76c3cb5b99f2875b78d176207745aa484ed4a" + integrity sha512-Su2qcSMIo2YO2PrmJ0/tdkf+6pSt8zf9+4URR5edMVti6+ShI8T3xhPrwugdyTOFuyj8lKHrcTZNKUFYowYiyA== + dependencies: + chokidar "^3.5.3" + has-bigints@^1.0.1, has-bigints@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" @@ -10318,6 +10594,11 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== +isnumber@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isnumber/-/isnumber-1.0.0.tgz#0e3f9759b581d99dd85086f0ec2a74909cfadd01" + integrity sha512-JLiSz/zsZcGFXPrB4I/AGBvtStkt+8QmksyZBZnVXnnK9XdTEyz0tX8CRYljtwYDuIuZzih6DpHQdi+3Q6zHPw== + iso-url@^1.1.5: version "1.2.1" resolved "https://registry.yarnpkg.com/iso-url/-/iso-url-1.2.1.tgz#db96a49d8d9a64a1c889fc07cc525d093afb1811" @@ -10944,6 +11225,11 @@ js-yaml@^4.0.0, js-yaml@^4.1.0: dependencies: argparse "^2.0.1" +jsbi@^3.1.4: + version "3.2.5" + resolved "https://registry.yarnpkg.com/jsbi/-/jsbi-3.2.5.tgz#b37bb90e0e5c2814c1c2a1bcd8c729888a2e37d6" + integrity sha512-aBE4n43IPvjaddScbvWRA2YlTzKEynHzu7MqOyTipdHucf/VxS63ViCjxYRg86M8Rxwbt/GfzHl1kKERkt45fQ== + jsesc@^2.5.1: version "2.5.2" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" @@ -12399,7 +12685,7 @@ minimalistic-crypto-utils@^1.0.1: resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" integrity sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg== -minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: +"minimatch@2 || 3", minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== @@ -12432,7 +12718,7 @@ mixme@^0.5.1: resolved "https://registry.yarnpkg.com/mixme/-/mixme-0.5.10.tgz#d653b2984b75d9018828f1ea333e51717ead5f51" integrity sha512-5H76ANWinB1H3twpJ6JY8uvAtpmFvHNArpilJAjXRKXSDDLPIMoZArw5SH0q9z+lLs8IrMw7Q2VWpWimFKFT1Q== -mkdirp@^0.5.1: +mkdirp@^0.5.1, mkdirp@~0.5.1: version "0.5.6" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== @@ -12449,6 +12735,18 @@ mlly@^1.2.0, mlly@^1.4.2: pkg-types "^1.0.3" ufo "^1.3.0" +mnemonist@^0.38.3: + version "0.38.5" + resolved "https://registry.yarnpkg.com/mnemonist/-/mnemonist-0.38.5.tgz#4adc7f4200491237fe0fa689ac0b86539685cade" + integrity sha512-bZTFT5rrPKtPJxj8KSV0WkPyNxl72vQepqqVUAW2ARUpUSF2qXMB6jZj7hW5/k7C1rtpzqbD/IIbJwLXUjCHeg== + dependencies: + obliterator "^2.0.0" + +moment@^2.19.3: + version "2.30.1" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.30.1.tgz#f8c91c07b7a786e30c59926df530b4eac96974ae" + integrity sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how== + motion@10.16.2: version "10.16.2" resolved "https://registry.yarnpkg.com/motion/-/motion-10.16.2.tgz#7dc173c6ad62210a7e9916caeeaf22c51e598d21" @@ -12571,6 +12869,15 @@ mux.js@^7.0.1: "@babel/runtime" "^7.11.2" global "^4.4.0" +mv@~2: + version "2.1.1" + resolved "https://registry.yarnpkg.com/mv/-/mv-2.1.1.tgz#ae6ce0d6f6d5e0a4f7d893798d03c1ea9559b6a2" + integrity sha512-at/ZndSy3xEGJ8i0ygALh8ru9qy7gWW1cmkaqBN29JmMlIvM//MEO9y1sk/avxuwnPcfhkejkLsuPxH81BrkSg== + dependencies: + mkdirp "~0.5.1" + ncp "~2.0.0" + rimraf "~2.4.0" + mz@^2.7.0: version "2.7.0" resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32" @@ -12580,6 +12887,11 @@ mz@^2.7.0: object-assign "^4.0.1" thenify-all "^1.0.0" +nan@^2.14.0: + version "2.18.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.18.0.tgz#26a6faae7ffbeb293a39660e88a76b82e30b7554" + integrity sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w== + nanoid@^3.0.2, nanoid@^3.1.20, nanoid@^3.1.23, nanoid@^3.3.6, nanoid@^3.3.7: version "3.3.7" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" @@ -12605,6 +12917,11 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== +ncp@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ncp/-/ncp-2.0.0.tgz#195a21d6c46e361d2fb1281ba38b91e9df7bdbb3" + integrity sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA== + near-hd-key@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/near-hd-key/-/near-hd-key-1.2.1.tgz#f508ff15436cf8a439b543220f3cc72188a46756" @@ -12779,6 +13096,13 @@ node-addon-api@^7.0.0: resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-7.0.0.tgz#8136add2f510997b3b94814f4af1cce0b0e3962e" integrity sha512-vgbBJTS4m5/KkE16t5Ly0WW9hz46swAstv0hYYwMtbG7AznRhNyfLRe8HZAiWIpcHzoO7HxhLuBQj9rJ/Ho0ZA== +node-cache@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/node-cache/-/node-cache-5.1.2.tgz#f264dc2ccad0a780e76253a694e9fd0ed19c398d" + integrity sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg== + dependencies: + clone "2.x" + node-fetch-native@^1.4.0, node-fetch-native@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/node-fetch-native/-/node-fetch-native-1.4.1.tgz#5a336e55b4e1b1e72b9927da09fecd2b374c9be5" @@ -12988,6 +13312,11 @@ object.values@^1.1.5, object.values@^1.1.6, object.values@^1.1.7: define-properties "^1.2.0" es-abstract "^1.22.1" +obliterator@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/obliterator/-/obliterator-2.0.4.tgz#fa650e019b2d075d745e44f1effeb13a2adbe816" + integrity sha512-lgHwxlxV1qIg1Eap7LgIeoBWIMFibOjbrYPIPJZcI1mmGAI2m3lNYpK12Y+GBdPQ0U1hRwSord7GIaawz962qQ== + obuf@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" @@ -14715,6 +15044,13 @@ rimraf@^3.0.0, rimraf@^3.0.2: dependencies: glob "^7.1.3" +rimraf@~2.4.0: + version "2.4.5" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.4.5.tgz#ee710ce5d93a8fdb856fb5ea8ff0e2d75934b2da" + integrity sha512-J5xnxTyqaiw06JjMftq7L9ouA448dw/E7dKghkP9WpKNuwmARNNg+Gk8/u5ryb9N/Yo2+z3MCwuqFK/+qPOPfQ== + dependencies: + glob "^6.0.1" + ripemd160@^2.0.0, ripemd160@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c" @@ -14845,6 +15181,11 @@ safe-json-parse@4.0.0: dependencies: rust-result "^1.0.0" +safe-json-stringify@~1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/safe-json-stringify/-/safe-json-stringify-1.2.0.tgz#356e44bc98f1f93ce45df14bcd7c01cda86e0afd" + integrity sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg== + safe-regex-test@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.0.tgz#793b874d524eb3640d1873aad03596db2d4f2295" @@ -15298,6 +15639,13 @@ standard-as-callback@^2.1.0: resolved "https://registry.yarnpkg.com/standard-as-callback/-/standard-as-callback-2.1.0.tgz#8953fc05359868a77b5b9739a665c5977bb7df45" integrity sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A== +stats-lite@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/stats-lite/-/stats-lite-2.2.0.tgz#278a5571fa1d2e8b1691295dccc0235282393bbf" + integrity sha512-/Kz55rgUIv2KP2MKphwYT/NCuSfAlbbMRv2ZWw7wyXayu230zdtzhxxuXXcvsc6EmmhS8bSJl3uS1wmMHFumbA== + dependencies: + isnumber "~1.0.0" + "statuses@>= 1.5.0 < 2": version "1.5.0" resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" @@ -15308,6 +15656,11 @@ std-env@^3.4.3: resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.6.0.tgz#94807562bddc68fa90f2e02c5fd5b6865bb4e98e" integrity sha512-aFZ19IgVmhdB2uX599ve2kE6BIE3YMnQ6Gp6BURhW/oIzpXGKr878TQfAQZn1+i0Flcc/UKUy1gOlcfaUBCryg== +stream-blackhole@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/stream-blackhole/-/stream-blackhole-1.0.3.tgz#6fc2e2c2e9d9fde6be8c68d3db88de09802e4d63" + integrity sha512-7NWl3dkmCd12mPkEwTbBPGxwvxj7L4O9DTjJudn02Fmk9K+RuPaDF8zeGo3kmjbsffU5E1aGpZ1dTR9AaRg6AQ== + stream-browserify@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-3.0.0.tgz#22b0a2850cdf6503e73085da1fc7b7d0c2122f2f" @@ -15722,11 +16075,21 @@ timeout-abort-controller@^3.0.0: dependencies: retimer "^3.0.0" +tiny-invariant@^1.1.0, tiny-invariant@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.1.tgz#8560808c916ef02ecfd55e66090df23a4b7aa642" + integrity sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw== + tiny-typed-emitter@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz#b3b027fdd389ff81a152c8e847ee2f5be9fad7b5" integrity sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA== +tiny-warning@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" + integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== + tinycolor2@^1.0.0: version "1.6.0" resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.6.0.tgz#f98007460169b0263b97072c5ae92484ce02d09e" @@ -15808,6 +16171,11 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" +toformat@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/toformat/-/toformat-2.0.0.tgz#7a043fd2dfbe9021a4e36e508835ba32056739d8" + integrity sha512-03SWBVop6nU8bpyZCx7SodpYznbZF5R4ljwNLBcTQzKOD9xuihRo/psX58llS1BMFhhAI08H3luot5GoXJz2pQ== + toggle-selection@^1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/toggle-selection/-/toggle-selection-1.0.6.tgz#6e45b1263f2017fa0acc7d89d78b15b8bf77da32" From 8141c10b78a964f7509c8009800632d775dec20d Mon Sep 17 00:00:00 2001 From: Stephan Cilliers <5469870+stephancill@users.noreply.github.com> Date: Mon, 15 Jan 2024 09:52:21 +0200 Subject: [PATCH 02/26] feat(core): add setstate action --- .changeset/orange-spies-teach.md | 5 +++++ packages/core/src/manifest.ts | 6 ++++++ packages/core/src/renderer.ts | 11 +++++++++++ 3 files changed, 22 insertions(+) create mode 100644 .changeset/orange-spies-teach.md diff --git a/.changeset/orange-spies-teach.md b/.changeset/orange-spies-teach.md new file mode 100644 index 00000000..ecbf2bee --- /dev/null +++ b/.changeset/orange-spies-teach.md @@ -0,0 +1,5 @@ +--- +"@mod-protocol/core": patch +--- + +feat: add `SETSTATE` action diff --git a/packages/core/src/manifest.ts b/packages/core/src/manifest.ts index 3e47b32c..422c3957 100644 --- a/packages/core/src/manifest.ts +++ b/packages/core/src/manifest.ts @@ -130,6 +130,11 @@ type OpenLinkAction = BaseAction & { url: string; }; +type SetStateAction = BaseAction & { + type: "SETSTATE"; + state: { [key: string]: string }; +}; + export type EthPersonalSignData = { statement: string; version: string; @@ -171,6 +176,7 @@ export type ModAction = | AddEmbedAction | SetInputAction | OpenLinkAction + | SetStateAction | EthPersonalSignAction | SendEthTransactionAction | ExitAction; diff --git a/packages/core/src/renderer.ts b/packages/core/src/renderer.ts index adce7deb..e2339bac 100644 --- a/packages/core/src/renderer.ts +++ b/packages/core/src/renderer.ts @@ -274,6 +274,10 @@ export interface OpenLinkActionResolver { events: OpenLinkActionResolverEvents ): void; } +export interface SetStateActionResolver { + ref: string; + value: string; +} export type EthPersonalSignActionResolverInit = { data: EthPersonalSignData; @@ -863,6 +867,13 @@ export class Renderer { }; break; } + case "SETSTATE": { + Object.entries(action.state).map(([key, value]) => { + set(this.refs, key, this.replaceInlineContext(value)); + }); + this.onTreeChange(); + break; + } case "web3.eth.personal.sign": { const promise = new Promise((resolve) => { setTimeout(() => { From 3ef9a8ef3e2ff7e0f2c6e64718db3b6c830ab594 Mon Sep 17 00:00:00 2001 From: Stephan Cilliers <5469870+stephancill@users.noreply.github.com> Date: Mon, 15 Jan 2024 09:53:26 +0200 Subject: [PATCH 03/26] fix: use larger logo image --- examples/api/src/app/api/erc-20/route.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/api/src/app/api/erc-20/route.ts b/examples/api/src/app/api/erc-20/route.ts index 7478d61b..107e3ef1 100644 --- a/examples/api/src/app/api/erc-20/route.ts +++ b/examples/api/src/app/api/erc-20/route.ts @@ -243,7 +243,7 @@ async function tokenInfo({ return { symbol: json.symbol, name: json.name, - image: json.image?.thumb, + image: json.image?.small, }; } From 327e50a66f605593cfb8b4c51c4685140a89cb24 Mon Sep 17 00:00:00 2001 From: Stephan Cilliers <5469870+stephancill@users.noreply.github.com> Date: Mon, 15 Jan 2024 17:18:01 +0200 Subject: [PATCH 04/26] chore: refactor util functions --- examples/api/src/app/api/erc-20/lib/utils.ts | 315 +++++++++++++++++++ examples/api/src/app/api/erc-20/route.ts | 294 +---------------- 2 files changed, 325 insertions(+), 284 deletions(-) create mode 100644 examples/api/src/app/api/erc-20/lib/utils.ts diff --git a/examples/api/src/app/api/erc-20/lib/utils.ts b/examples/api/src/app/api/erc-20/lib/utils.ts new file mode 100644 index 00000000..6da4beca --- /dev/null +++ b/examples/api/src/app/api/erc-20/lib/utils.ts @@ -0,0 +1,315 @@ +import { FarcasterUser } from "@mod-protocol/core"; +import { publicActionReverseMirage } from "reverse-mirage"; +import { createPublicClient, http } from "viem2"; +import * as chains from "viem2/chains"; + +export function numberWithCommas(x: string | number) { + var parts = x.toString().split("."); + parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ","); + return parts.join("."); +} + +const { ERC_20_AIRSTACK_API_KEY } = process.env; +const AIRSTACK_API_URL = "https://api.airstack.xyz/gql"; +const airstackQuery = ` +query MyQuery($identity: Identity!, $token_address: Address!, $blockchain: TokenBlockchain, $cursor: String) { + SocialFollowings( + input: { + filter: { + identity: {_eq: $identity}, + dappName: {_eq: farcaster} + }, + blockchain: ALL, + limit: 200, + cursor: $cursor + } + ) { + pageInfo { + hasNextPage + nextCursor + } + Following { + followingProfileId, + followingAddress { + socials { + profileDisplayName + profileName + profileImage + profileBio + } + tokenBalances( + input: { + filter: { + tokenAddress: {_eq: $token_address}, + formattedAmount: {_gt: 0} + }, + blockchain: $blockchain + } + ) { + owner { + identity + } + formattedAmount + } + } + } + } +} +`; + +export async function getFollowingHolderInfo({ + fid, + tokenAddress, + blockchain, +}: { + fid: string; + tokenAddress: string; + blockchain: string; +}): Promise<{ user: FarcasterUser; amount: number }[]> { + const acc: any[] = []; + + let hasNextPage = true; + let cursor = ""; + + try { + while (hasNextPage) { + hasNextPage = false; + const res = await fetch(AIRSTACK_API_URL, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: ERC_20_AIRSTACK_API_KEY, // Add API key to Authorization header + }, + body: JSON.stringify({ + query: airstackQuery, + variables: { + identity: `fc_fid:${fid}`, + token_address: tokenAddress, + blockchain, + cursor, + }, + }), + }); + const json = await res?.json(); + const result = json?.data.SocialFollowings.Following.filter( + (item) => item.followingAddress.tokenBalances?.length > 0 + ); + acc.push(...result); + + hasNextPage = json?.data.SocialFollowings.pageInfo.hasNextPage; + cursor = json?.data.SocialFollowings.pageInfo.nextCursor; + } + } catch (error) { + console.error(error); + } + + const result = acc + .map((item) => { + const socialData = item.followingAddress.socials[0]; + return { + user: { + displayName: socialData.profileDisplayName, + username: socialData.profileName, + fid: item.followingProfileId, + pfp: socialData.profileImage, + } as FarcasterUser, + amount: item.followingAddress.tokenBalances[0].formattedAmount, + }; + }) + .sort((a, b) => Number(b.amount) - Number(a.amount)); + + return result; +} + +export async function getPriceData({ + tokenAddress, + blockchain, +}: { + tokenAddress: string; + blockchain: string; +}): Promise<{ + unitPriceUsd: string; + marketCapUsd?: string; + volume24hUsd?: string; + change24h?: string; +}> { + // https://api.coingecko.com/api/v3/simple/token_price/ethereum?contract_addresses=0xd7c1eb0fe4a30d3b2a846c04aa6300888f087a5f&vs_currencies=usd&points&vs_currencies=usd&include_market_cap=true&include_24hr_vol=true&include_24hr_change=true&include_last_updated_at=true + const params = new URLSearchParams({ + contract_addresses: tokenAddress, + vs_currencies: "usd", + include_market_cap: "true", + include_24hr_vol: "true", + include_24hr_change: "true", + include_last_updated_at: "true", + }); + const coingecko = await fetch( + `https://api.coingecko.com/api/v3/simple/token_price/${blockchain}?${params.toString()}` + ); + const coingeckoJson = await coingecko.json(); + + if (coingeckoJson[tokenAddress]) { + const { + usd: unitPriceUsd, + usd_market_cap: marketCapUsd, + usd_24h_vol: volume24hUsd, + usd_24h_change: change24h, + } = coingeckoJson[tokenAddress]; + + const unitPriceUsdFormatted = `${numberWithCommas( + parseFloat(unitPriceUsd).toPrecision(4) + )}`; + const marketCapUsdFormatted = `${parseFloat( + parseFloat(marketCapUsd).toFixed(0) + ).toLocaleString()}`; + const volume24hUsdFormatted = `${parseFloat( + parseFloat(volume24hUsd).toFixed(0) + ).toLocaleString()}`; + + const change24hNumber = parseFloat(change24h); + const change24hPartial = parseFloat( + change24hNumber.toFixed(2) + ).toLocaleString(); + const change24hFormatted = + change24hNumber > 0 ? `+${change24hPartial}%` : `-${change24hPartial}%`; + + return { + unitPriceUsd: unitPriceUsdFormatted, + marketCapUsd: marketCapUsdFormatted, + volume24hUsd: volume24hUsdFormatted, + change24h: change24hFormatted, + }; + } + + // Use on-chain data as fallback + const chain = chainByName[blockchain.toLowerCase()]; + const url = `https://api.1inch.dev/price/v1.1/${chain.id}/${tokenAddress}?currency=USD`; + const res = await fetch(url, { + headers: { + Authorization: `Bearer ${process.env["ERC_20_1INCH_API_KEY"]}`, + }, + }); + const resJson = await res.json(); + + return { + unitPriceUsd: parseFloat(resJson[tokenAddress]).toPrecision(4), + }; +} + +export async function tokenInfo({ + tokenAddress, + blockchain, +}: { + tokenAddress: string; + blockchain: string; +}): Promise<{ + symbol: string; + name: string; + url: string; + image?: string; +}> { + //0x4ed4e862860bed51a9570b96d89af5e1b0efefed + // https://api.coingecko.com/api/v3/coins/0x4ed4e862860bed51a9570b96d89af5e1b0efefed/market_chart?vs_currency=usd&days=1 + // https://api.coingecko.com/api/v3/simple/token_price/ethereum?contract_addresses=0xd7c1eb0fe4a30d3b2a846c04aa6300888f087a5f&vs_currencies=usd&points&vs_currencies=usd&include_market_cap=true&include_24hr_vol=true&include_24hr_change=true&include_last_updated_at=true + // https://api.coingecko.com/api/v3/coins/ethereum/contract/0xd7c1eb0fe4a30d3b2a846c04aa6300888f087a5f + // https://api.coingecko.com/api/v3/coins/base/contract/0x27d2decb4bfc9c76f0309b8e88dec3a601fe25a8 + const res = await fetch( + `https://api.coingecko.com/api/v3/coins/${blockchain}/contract/${tokenAddress}` + ); + + if (res.ok) { + const json = await res?.json(); + return { + symbol: json.symbol, + name: json.name, + image: json.image?.small, + url: `https://www.coingecko.com/en/coins/${json.id}`, + }; + } + + // Use on-chain data as fallback + const chain = chainByName[blockchain]; + const client = createPublicClient({ + transport: http(), + chain, + }).extend(publicActionReverseMirage); + + const token = await client.getERC20({ + erc20: { + address: tokenAddress as `0x${string}`, + chainID: chain.id, + }, + }); + + return { + symbol: token.symbol, + name: token.name, + url: `https://app.uniswap.org/tokens/${blockchain}/${tokenAddress}`, + }; +} +export const chainByName: { [key: string]: chains.Chain } = Object.entries( + chains +).reduce( + (acc: { [key: string]: chains.Chain }, [key, chain]) => { + acc[key] = chain; + return acc; + }, + { ethereum: chains.mainnet } // Convenience for ethereum, which is 'homestead' otherwise +); + +export function parseTokenParam(tokenParam: string) { + let tokenAddress: string; + let blockchain: string; + + // Splitting the string at '/erc20:' + const parts = tokenParam.split("/erc20:"); + + // Extracting the chain ID + const chainIdPart = parts[0]; + const chainId = chainIdPart.split(":")[1]; + + // The token address is the second part of the split, but without '0x' if present + tokenAddress = parts[1]; + + const [blockchainName] = Object.entries(chainByName).find( + ([, value]) => value.id.toString() == chainId + ); + blockchain = blockchainName; + + return { + tokenAddress, + blockchain, + }; +} + +export async function getEthUsdPrice(): Promise { + const client = createPublicClient({ + transport: http(), + chain: chains.mainnet, + }); + + // roundId uint80, answer int256, startedAt uint256, updatedAt uint256, answeredInRound uint80 + const [, answer] = await client.readContract({ + abi: [ + { + inputs: [], + name: "latestRoundData", + outputs: [ + { internalType: "uint80", name: "roundId", type: "uint80" }, + { internalType: "int256", name: "answer", type: "int256" }, + { internalType: "uint256", name: "startedAt", type: "uint256" }, + { internalType: "uint256", name: "updatedAt", type: "uint256" }, + { internalType: "uint80", name: "answeredInRound", type: "uint80" }, + ], + stateMutability: "view", + type: "function", + }, + ], + functionName: "latestRoundData", + // https://docs.chain.link/data-feeds/price-feeds/addresses?network=ethereum&page=1&search=usdc#ethereum-mainnet + address: "0x986b5E1e1755e3C2440e960477f25201B0a8bbD4", + }); + + const ethPriceUsd = (1 / Number(answer)) * 1e18; + + return ethPriceUsd; +} diff --git a/examples/api/src/app/api/erc-20/route.ts b/examples/api/src/app/api/erc-20/route.ts index 107e3ef1..65add4c8 100644 --- a/examples/api/src/app/api/erc-20/route.ts +++ b/examples/api/src/app/api/erc-20/route.ts @@ -1,273 +1,10 @@ -import { FarcasterUser } from "@mod-protocol/core"; -import { Token } from "@uniswap/sdk-core"; -import * as smartOrderRouter from "@uniswap/smart-order-router"; -import { USDC_BASE } from "@uniswap/smart-order-router"; import { NextRequest, NextResponse } from "next/server"; -import { publicActionReverseMirage, priceQuote } from "reverse-mirage"; -import { PublicClient, createClient, http, parseUnits } from "viem2"; -import * as chains from "viem2/chains"; - -const { AIRSTACK_API_KEY } = process.env; -const AIRSTACK_API_URL = "https://api.airstack.xyz/gql"; - -const chainByName: { [key: string]: chains.Chain } = Object.entries( - chains -).reduce( - (acc: { [key: string]: chains.Chain }, [key, chain]) => { - acc[key] = chain; - return acc; - }, - { ethereum: chains.mainnet } // Convenience for ethereum, which is 'homestead' otherwise -); - -const chainById = Object.values(chains).reduce( - (acc: { [key: number]: chains.Chain }, cur) => { - if (cur.id) acc[cur.id] = cur; - return acc; - }, - {} -); - -function numberWithCommas(x: string | number) { - var parts = x.toString().split("."); - parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ","); - return parts.join("."); -} - -const query = ` -query MyQuery($identity: Identity!, $token_address: Address!, $blockchain: TokenBlockchain, $cursor: String) { - SocialFollowings( - input: { - filter: { - identity: {_eq: $identity}, - dappName: {_eq: farcaster} - }, - blockchain: ALL, - limit: 200, - cursor: $cursor - } - ) { - pageInfo { - hasNextPage - nextCursor - } - Following { - followingProfileId, - followingAddress { - socials { - profileDisplayName - profileName - profileImage - profileBio - } - tokenBalances( - input: { - filter: { - tokenAddress: {_eq: $token_address}, - formattedAmount: {_gt: 0} - }, - blockchain: $blockchain - } - ) { - owner { - identity - } - formattedAmount - } - } - } - } -} -`; - -async function getFollowingHolderInfo({ - fid, - tokenAddress, - blockchain, -}: { - fid: string; - tokenAddress: string; - blockchain: string; -}): Promise<{ user: FarcasterUser; amount: number }[]> { - const acc: any[] = []; - - let hasNextPage = true; - let cursor = ""; - - try { - while (hasNextPage) { - hasNextPage = false; - const res = await fetch(AIRSTACK_API_URL, { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: AIRSTACK_API_KEY, // Add API key to Authorization header - }, - body: JSON.stringify({ - query, - variables: { - identity: `fc_fid:${fid}`, - token_address: tokenAddress, - blockchain, - cursor, - }, - }), - }); - const json = await res?.json(); - const result = json?.data.SocialFollowings.Following.filter( - (item) => item.followingAddress.tokenBalances.length > 0 - ); - acc.push(...result); - - hasNextPage = json?.data.SocialFollowings.pageInfo.hasNextPage; - cursor = json?.data.SocialFollowings.pageInfo.nextCursor; - } - } catch (error) { - console.error(error); - } - - const result = acc - .map((item) => { - const socialData = item.followingAddress.socials[0]; - return { - user: { - displayName: socialData.profileDisplayName, - username: socialData.profileName, - fid: item.followingProfileId, - pfp: socialData.profileImage, - } as FarcasterUser, - amount: item.followingAddress.tokenBalances[0].formattedAmount, - }; - }) - .sort((a, b) => Number(b.amount) - Number(a.amount)); - - return result; -} - -async function getPriceData({ - tokenAddress, - blockchain, -}: { - tokenAddress: string; - blockchain: string; -}): Promise<{ - unitPriceUsd: string; - marketCapUsd?: string; - volume24hUsd?: string; - change24h?: string; -}> { - // https://api.coingecko.com/api/v3/simple/token_price/ethereum?contract_addresses=0xd7c1eb0fe4a30d3b2a846c04aa6300888f087a5f&vs_currencies=usd&points&vs_currencies=usd&include_market_cap=true&include_24hr_vol=true&include_24hr_change=true&include_last_updated_at=true - const params = new URLSearchParams({ - contract_addresses: tokenAddress, - vs_currencies: "usd", - include_market_cap: "true", - include_24hr_vol: "true", - include_24hr_change: "true", - include_last_updated_at: "true", - }); - const coingecko = await fetch( - `https://api.coingecko.com/api/v3/simple/token_price/${blockchain}?${params.toString()}` - ); - const coingeckoJson = await coingecko.json(); - - if (coingeckoJson[tokenAddress]) { - const { - usd: unitPriceUsd, - usd_market_cap: marketCapUsd, - usd_24h_vol: volume24hUsd, - usd_24h_change: change24h, - } = coingeckoJson[tokenAddress]; - - const unitPriceUsdFormatted = `${numberWithCommas( - parseFloat(unitPriceUsd).toPrecision(4) - )}`; - const marketCapUsdFormatted = `${parseFloat( - parseFloat(marketCapUsd).toFixed(0) - ).toLocaleString()}`; - const volume24hUsdFormatted = `${parseFloat( - parseFloat(volume24hUsd).toFixed(0) - ).toLocaleString()}`; - - const change24hNumber = parseFloat(change24h); - const change24hPartial = parseFloat( - change24hNumber.toFixed(2) - ).toLocaleString(); - const change24hFormatted = - change24hNumber > 0 ? `+${change24hPartial}%` : `-${change24hPartial}%`; - - return { - unitPriceUsd: unitPriceUsdFormatted, - marketCapUsd: marketCapUsdFormatted, - volume24hUsd: volume24hUsdFormatted, - change24h: change24hFormatted, - }; - } - - // Use on-chain data as fallback - const chain = chainByName[blockchain.toLowerCase()]; - const url = `https://api.1inch.dev/price/v1.1/${chain.id}/${tokenAddress}?currency=USD`; - const res = await fetch(url, { - headers: { - Authorization: `Bearer ${process.env["1INCH_API_KEY"]}`, - }, - }); - const resJson = await res.json(); - - return { - unitPriceUsd: parseFloat(resJson[tokenAddress]).toPrecision(4), - }; -} - -async function tokenInfo({ - tokenAddress, - blockchain, -}: { - tokenAddress: string; - blockchain: string; -}): Promise<{ - symbol: string; - name: string; - image?: string; -}> { - //0x4ed4e862860bed51a9570b96d89af5e1b0efefed - // https://api.coingecko.com/api/v3/coins/0x4ed4e862860bed51a9570b96d89af5e1b0efefed/market_chart?vs_currency=usd&days=1 - // https://api.coingecko.com/api/v3/simple/token_price/ethereum?contract_addresses=0xd7c1eb0fe4a30d3b2a846c04aa6300888f087a5f&vs_currencies=usd&points&vs_currencies=usd&include_market_cap=true&include_24hr_vol=true&include_24hr_change=true&include_last_updated_at=true - // https://api.coingecko.com/api/v3/coins/ethereum/contract/0xd7c1eb0fe4a30d3b2a846c04aa6300888f087a5f - // https://api.coingecko.com/api/v3/coins/base/contract/0x27d2decb4bfc9c76f0309b8e88dec3a601fe25a8 - const res = await fetch( - `https://api.coingecko.com/api/v3/coins/${blockchain}/contract/${tokenAddress}` - ); - - if (res.ok) { - const json = await res?.json(); - return { - symbol: json.symbol, - name: json.name, - image: json.image?.small, - }; - } - - // Use on-chain data as fallback - const chain = chainByName[blockchain]; - const client = ( - createClient({ - transport: http(), - chain, - }) as PublicClient - ).extend(publicActionReverseMirage); - - const token = await client.getERC20({ - erc20: { - address: tokenAddress as `0x${string}`, - chainID: chain.id, - }, - }); - - return { - symbol: token.symbol, - name: token.name, - }; -} +import { + getFollowingHolderInfo, + getPriceData, + parseTokenParam, + tokenInfo, +} from "./lib/utils"; export async function GET(request: NextRequest) { const fid = request.nextUrl.searchParams.get("fid")?.toLowerCase(); @@ -280,20 +17,9 @@ export async function GET(request: NextRequest) { ?.toLowerCase(); if (token) { - // Splitting the string at '/erc20:' - const parts = token.split("/erc20:"); - - // Extracting the chain ID - const chainIdPart = parts[0]; - const chainId = chainIdPart.split(":")[1]; - - // The token address is the second part of the split, but without '0x' if present - tokenAddress = parts[1]; - - const [blockchainName] = Object.entries(chainByName).find( - ([, value]) => value.id.toString() == chainId - ); - blockchain = blockchainName; + const parsedToken = parseTokenParam(token); + tokenAddress = parsedToken.tokenAddress; + blockchain = parsedToken.blockchain; } if (!tokenAddress) { @@ -304,7 +30,7 @@ export async function GET(request: NextRequest) { if (!blockchain) { return NextResponse.json({ - error: "Missing or invalid blockchain (ethereum, polygon, base)", + error: "Missing or invalid blockchain", }); } From 1fcd9dc9d8a5f6c22057984ca221c6c25f1b90b3 Mon Sep 17 00:00:00 2001 From: Stephan Cilliers <5469870+stephancill@users.noreply.github.com> Date: Mon, 15 Jan 2024 17:18:53 +0200 Subject: [PATCH 05/26] feat: buy tokens --- .../api/src/app/api/erc-20/balances/route.ts | 66 ++++ examples/api/src/app/api/erc-20/buy/route.ts | 86 +++++ mods/erc-20/src/buying.ts | 72 +++++ mods/erc-20/src/manifest.ts | 2 + mods/erc-20/src/view.ts | 293 ++++++++++++++---- packages/core/src/manifest.ts | 11 +- 6 files changed, 471 insertions(+), 59 deletions(-) create mode 100644 examples/api/src/app/api/erc-20/balances/route.ts create mode 100644 examples/api/src/app/api/erc-20/buy/route.ts create mode 100644 mods/erc-20/src/buying.ts diff --git a/examples/api/src/app/api/erc-20/balances/route.ts b/examples/api/src/app/api/erc-20/balances/route.ts new file mode 100644 index 00000000..97b7ca08 --- /dev/null +++ b/examples/api/src/app/api/erc-20/balances/route.ts @@ -0,0 +1,66 @@ +import { NextRequest, NextResponse } from "next/server"; +import { createPublicClient, formatEther, http } from "viem2"; +import { + chainByName, + getEthUsdPrice, + numberWithCommas, + parseTokenParam, +} from "../lib/utils"; + +export async function GET(request: NextRequest) { + const userAddress = request.nextUrl.searchParams + .get("walletAddress") + ?.toLowerCase(); + const token = request.nextUrl.searchParams.get("token")?.toLowerCase(); + let tokenAddress = request.nextUrl.searchParams + .get("tokenAddress") + ?.toLowerCase(); + let blockchain = request.nextUrl.searchParams + .get("blockchain") + ?.toLowerCase(); + const buyOptionsUsd = request.nextUrl.searchParams + .get("buyOptionsUsd") + .split(",") + .map((x) => parseFloat(x)); + + if (token) { + const parsedToken = parseTokenParam(token); + tokenAddress = parsedToken.tokenAddress; + blockchain = parsedToken.blockchain; + } + + if (!tokenAddress || !blockchain) { + return new Response("Missing tokenAddress or blockchain", { + status: 400, + }); + } + + // Get eth balance on blockchain + const chain = chainByName[blockchain]; + const client = createPublicClient({ + transport: http(), + chain, + }); + + const [balance, ethPriceUsd] = await Promise.all([ + client.getBalance({ + address: userAddress as `0x${string}`, + }), + getEthUsdPrice(), + ]); + + const ethBalanceUsd = parseFloat(formatEther(balance)) * Number(ethPriceUsd); + + return NextResponse.json({ + ethBalance: numberWithCommas( + parseFloat(formatEther(balance)).toPrecision(4) + ), + ethPriceUsd, + ethBalanceUsd: numberWithCommas(ethBalanceUsd.toFixed(2)), + chain: { + id: chain.id, + name: chain.name, + }, + buyOptionsUsd: buyOptionsUsd.map((x) => ethBalanceUsd > x), + }); +} diff --git a/examples/api/src/app/api/erc-20/buy/route.ts b/examples/api/src/app/api/erc-20/buy/route.ts new file mode 100644 index 00000000..e6448db5 --- /dev/null +++ b/examples/api/src/app/api/erc-20/buy/route.ts @@ -0,0 +1,86 @@ +import { NextRequest, NextResponse } from "next/server"; +import { createPublicClient, http, parseEther } from "viem2"; +import { chainByName, parseTokenParam } from "../lib/utils"; +import { getEthUsdPrice } from "../lib/utils"; + +export async function POST(request: NextRequest) { + // TODO: Expose separate execution/receiver addresses + const userAddress = request.nextUrl.searchParams + .get("walletAddress") + ?.toLowerCase(); + const token = request.nextUrl.searchParams.get("token")?.toLowerCase(); + let tokenAddress = request.nextUrl.searchParams + .get("tokenAddress") + ?.toLowerCase(); + let blockchain = request.nextUrl.searchParams + .get("blockchain") + ?.toLowerCase(); + const buyAmountUsd = parseFloat( + request.nextUrl.searchParams.get("buyAmountUsd") + ); + + if (token) { + const parsedToken = parseTokenParam(token); + tokenAddress = parsedToken.tokenAddress; + blockchain = parsedToken.blockchain; + } + + if (!tokenAddress || !blockchain) { + return new Response("Missing tokenAddress or blockchain", { + status: 400, + }); + } + + // Get eth balance on blockchain + const chain = chainByName[blockchain]; + const client = createPublicClient({ + transport: http(), + chain, + }); + + const ethPriceUsd = await getEthUsdPrice(); + + const ethInputAmount = parseEther((buyAmountUsd / ethPriceUsd).toString()); + + const swapCalldataParams: { + src: string; + dst: string; + amount: string; + from: string; + slippage: string; + receiver: string; + fee?: string; + referrer?: string; + } = { + src: "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", // Native ETH + dst: tokenAddress, + amount: ethInputAmount.toString(), + from: userAddress, + slippage: "5", + receiver: userAddress, + }; + + // TODO: Use Uniswap to take advantage of configurable fee + // The referrer here is the 1inch referral program recipient (20% of surplus from trade) + // See https://blog.1inch.io/why-should-you-integrate-1inch-apis-into-your-service/ + if (process.env.ERC_20_FEE_RECIPIENT) { + swapCalldataParams.referrer = process.env.ERC_20_FEE_RECIPIENT; + } + + const swapCalldataRes = await fetch( + `https://api.1inch.dev/swap/v5.2/${chain.id}/swap?${new URLSearchParams( + swapCalldataParams + ).toString()}`, + { + headers: { + Authorization: `Bearer ${process.env["ERC_20_1INCH_API_KEY"]}`, + }, + } + ); + + const swapCalldataJson = await swapCalldataRes.json(); + + return NextResponse.json({ + transaction: swapCalldataJson.tx, + }); +} diff --git a/mods/erc-20/src/buying.ts b/mods/erc-20/src/buying.ts new file mode 100644 index 00000000..3b174d92 --- /dev/null +++ b/mods/erc-20/src/buying.ts @@ -0,0 +1,72 @@ +import { ModElement } from "@mod-protocol/core"; + +const buy: ModElement[] = [ + { + type: "padding", + elements: [ + { + type: "horizontal-layout", + elements: [ + { + type: "circular-progress", + }, + { + type: "text", + label: + "Buying ~${{refs.buyAmountUsd}} of {{refs.tokenReq.response.data.tokenData.name}}...", + }, + ], + }, + + { + type: "horizontal-layout", + elements: [ + // If there is no buyAmountUsd value, go to #view + { + if: { + value: "{{refs.buyAmountUsd}}", + match: { + equals: "", + }, + }, + then: { + type: "horizontal-layout", + onload: "#view", + }, + }, + ], + }, + { + type: "horizontal-layout", + onload: { + type: "POST", + url: "{{api}}/erc-20/buy?walletAddress={{user.wallet.address}}&token={{embed.url}}&buyAmountUsd={{refs.buyAmountUsd}}", + ref: "swapTxDataReq", + onsuccess: { + type: "SENDETHTRANSACTION", + ref: "swapTxReq", + txData: { + from: "{{refs.swapTxDataReq.response.data.transaction.from}}", + to: "{{refs.swapTxDataReq.response.data.transaction.to}}", + value: "{{refs.swapTxDataReq.response.data.transaction.value}}", + data: "{{refs.swapTxDataReq.response.data.transaction.data}}", + }, + chainId: "{{refs.balancesReq.response.data.chain.id}}", + onerror: { + type: "SETSTATE", + ref: "buyAmountUsd", + value: "", + }, + }, + onerror: { + type: "SETSTATE", + ref: "buyAmountUsd", + value: "", + }, + }, + }, + ], + }, +]; + +export default buy; diff --git a/mods/erc-20/src/manifest.ts b/mods/erc-20/src/manifest.ts index b639b0a8..85b7d8a2 100644 --- a/mods/erc-20/src/manifest.ts +++ b/mods/erc-20/src/manifest.ts @@ -1,5 +1,6 @@ import { ModManifest } from "@mod-protocol/core"; import view from "./view"; +import buying from "./buying"; const manifest: ModManifest = { slug: "erc-20", @@ -26,6 +27,7 @@ const manifest: ModManifest = { ], elements: { "#view": view, + "#buying": buying, }, permissions: ["user.wallet.address"], // "user.farcaster.fid" }; diff --git a/mods/erc-20/src/view.ts b/mods/erc-20/src/view.ts index c2b9d3ca..532c65a4 100644 --- a/mods/erc-20/src/view.ts +++ b/mods/erc-20/src/view.ts @@ -10,86 +10,265 @@ const view: ModElement[] = [ }, elements: [ { - if: { - value: "{{refs.tokenReq.response.data}}", - match: { - NOT: { - equals: "", + type: "padding", + elements: [ + // Go to #buying to execute order if there is a buyAmountUsd value + { + if: { + value: "{{refs.buyAmountUsd}}", + match: { + NOT: { + equals: "", + }, + }, }, - }, - }, - then: { - type: "vertical-layout", - elements: [ - { + then: { type: "horizontal-layout", + onload: "#buying", + }, + }, + { + if: { + value: "{{refs.tokenReq.response.data}}", + match: { + NOT: { + equals: "", + }, + }, + }, + then: { + type: "vertical-layout", elements: [ { - if: { - value: "{{refs.tokenReq.response.data.tokenData.image}}", - match: { - NOT: { - equals: "", + type: "horizontal-layout", + elements: [ + { + if: { + value: + "{{refs.tokenReq.response.data.tokenData.image}}", + match: { + NOT: { + equals: "", + }, + }, + }, + then: { + type: "avatar", + src: "{{refs.tokenReq.response.data.tokenData.image}}", }, }, - }, - then: { - type: "avatar", - src: "{{refs.tokenReq.response.data.tokenData.image}}", - }, - }, - { - type: "link", - label: "{{refs.tokenReq.response.data.tokenData.name}}", - url: "https://coingecko.com/en/coins/points", - }, - { - type: "text", - variant: "secondary", - label: - "${{refs.tokenReq.response.data.priceData.unitPriceUsd}}", + { + type: "link", + label: "{{refs.tokenReq.response.data.tokenData.name}}", + variant: "secondary", + url: "{{refs.tokenReq.response.data.tokenData.url}}", + }, + { + type: "text", + variant: "secondary", + label: + "${{refs.tokenReq.response.data.priceData.unitPriceUsd}}", + }, + { + if: { + value: + "{{refs.tokenReq.response.data.priceData.change24h}}", + match: { + NOT: { + equals: "", + }, + }, + }, + then: { + type: "horizontal-layout", + elements: [ + { + type: "text", + variant: "secondary", + label: "24h:", + }, + { + type: "text", + variant: "secondary", + label: + "{{refs.tokenReq.response.data.priceData.change24h}}", + }, + ], + }, + }, + { + if: { + value: "{{user.wallet.address}}", + match: { + NOT: { + equals: "", + }, + }, + }, + then: { + if: { + value: "{{refs.isBuying}}", + match: { + equals: "true", + }, + }, + then: { + type: "button", + label: "Cancel", + variant: "secondary", + onclick: { + type: "SETSTATE", + ref: "isBuying", + value: "false", + }, + }, + else: { + type: "button", + label: "Buy", + onclick: { + type: "SETSTATE", + ref: "isBuying", + value: "true", + }, + }, + }, + }, + ], }, + { if: { - value: - "{{refs.tokenReq.response.data.priceData.change24h}}", + value: "{{refs.isBuying}}", match: { - NOT: { - equals: "", - }, + equals: "true", }, }, then: { type: "horizontal-layout", + onload: { + type: "GET", + url: "{{api}}/erc-20/balances?walletAddress={{user.wallet.address}}&token={{embed.url}}&buyOptionsUsd=5,50,500", + ref: "balancesReq", + }, elements: [ { - type: "text", - variant: "secondary", - label: "24h:", - }, - { - type: "text", - variant: "secondary", - label: - "{{refs.tokenReq.response.data.priceData.change24h}}", + if: { + value: "{{refs.balancesReq.response.data}}", + match: { + NOT: { + equals: "", + }, + }, + }, + then: { + type: "vertical-layout", + elements: [ + { + if: { + value: + "{{refs.balancesReq.response.data.buyOptionsUsd[0]}}", + match: { + equals: "true", + }, + }, + then: { + type: "horizontal-layout", + elements: [ + { + type: "text", + label: "Buy amount:", + }, + { + if: { + value: + "{{refs.balancesReq.response.data.buyOptionsUsd[0]}}", + match: { + equals: "true", + }, + }, + then: { + type: "button", + label: "$5.00", + onclick: { + type: "SETSTATE", + ref: "buyAmountUsd", + value: "5.00", + }, + }, + }, + { + if: { + value: + "{{refs.balancesReq.response.data.buyOptionsUsd[1]}}", + match: { + equals: "true", + }, + }, + then: { + type: "button", + label: "$50.00", + onclick: { + type: "SETSTATE", + ref: "buyAmountUsd", + value: "50.00", + }, + }, + }, + { + if: { + value: + "{{refs.balancesReq.response.data.buyOptionsUsd[2]}}", + match: { + equals: "true", + }, + }, + then: { + type: "button", + label: "$500.00", + onclick: { + type: "SETSTATE", + ref: "buyAmountUsd", + value: "500.00", + }, + }, + }, + ], + }, + else: { + type: "text", + label: + "Not enough ETH on {{refs.balancesReq.response.data.chain.name}}", + }, + }, + { + type: "text", + label: + "{{refs.balancesReq.response.data.chain.name}} balance: {{refs.balancesReq.response.data.ethBalance}} ETH ({{refs.balancesReq.response.data.ethBalanceUsd}} USD)", + variant: "secondary", + }, + ], + }, + else: { + type: "circular-progress", + }, }, ], }, + // Holders you know + else: { + type: "text", + variant: "secondary", + label: + "{{refs.tokenReq.response.data.holderData.holdersCount}} holders you know", + }, }, ], }, - // Holders you know - { - type: "text", - variant: "secondary", - label: - "{{refs.tokenReq.response.data.holderData.holdersCount}} holders you know", + else: { + type: "circular-progress", }, - ], - }, - else: { - type: "circular-progress", - }, + }, + ], }, ], }, diff --git a/packages/core/src/manifest.ts b/packages/core/src/manifest.ts index 422c3957..deabb67b 100644 --- a/packages/core/src/manifest.ts +++ b/packages/core/src/manifest.ts @@ -30,7 +30,9 @@ export type ModManifest = { /** A definition map of reusable elements, using their id as the key */ elements?: Record; /** Permissions requested by the Mod */ - permissions?: Array<"user.wallet.address" | "web3.eth.personal.sign">; + permissions?: Array< + "user.wallet.address" | "web3.eth.personal.sign" | "user.farcaster.fid" + >; }; export type ModEvent = @@ -311,4 +313,9 @@ export type ModElement = bottomLeftBadge?: never; bottomRightBadge?: never; } - )); + )) + | { + type: "container"; + elements?: string | ElementOrConditionalFlow[]; + onclick?: ModEvent; + }; From f8b2bbe5301ede57f5f985519bd00f93ad963247 Mon Sep 17 00:00:00 2001 From: Stephan Cilliers <5469870+stephancill@users.noreply.github.com> Date: Mon, 15 Jan 2024 17:33:06 +0200 Subject: [PATCH 06/26] feat(core+react-ui-shadcn): add padding element --- .changeset/slimy-starfishes-march.md | 6 ++++++ packages/core/src/manifest.ts | 2 +- packages/core/src/renderer.ts | 17 +++++++++++++++++ .../react-ui-shadcn/src/renderers/index.tsx | 2 ++ .../react-ui-shadcn/src/renderers/padding.tsx | 8 ++++++++ packages/react/src/index.tsx | 19 +++++++++++++++++++ 6 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 .changeset/slimy-starfishes-march.md create mode 100644 packages/react-ui-shadcn/src/renderers/padding.tsx diff --git a/.changeset/slimy-starfishes-march.md b/.changeset/slimy-starfishes-march.md new file mode 100644 index 00000000..deedfb12 --- /dev/null +++ b/.changeset/slimy-starfishes-march.md @@ -0,0 +1,6 @@ +--- +"@mod-protocol/react-ui-shadcn": patch +"@mod-protocol/core": patch +--- + +feat: add `Padding` element diff --git a/packages/core/src/manifest.ts b/packages/core/src/manifest.ts index deabb67b..3ac54b7a 100644 --- a/packages/core/src/manifest.ts +++ b/packages/core/src/manifest.ts @@ -315,7 +315,7 @@ export type ModElement = } )) | { - type: "container"; + type: "padding"; elements?: string | ElementOrConditionalFlow[]; onclick?: ModEvent; }; diff --git a/packages/core/src/renderer.ts b/packages/core/src/renderer.ts index e2339bac..3e267f4e 100644 --- a/packages/core/src/renderer.ts +++ b/packages/core/src/renderer.ts @@ -156,6 +156,10 @@ export type ModElementRef = events: { onClick: () => void; }; + } + | { + type: "padding"; + elements?: T[]; }; export type BaseContext = { @@ -274,6 +278,7 @@ export interface OpenLinkActionResolver { events: OpenLinkActionResolverEvents ): void; } + export interface SetStateActionResolver { ref: string; value: string; @@ -1504,6 +1509,18 @@ export class Renderer { key ); } + case "padding": { + return fn( + { + type: el.type, + elements: + el.elements && isArray(el.elements) + ? el.elements.map(mapper).filter(nonNullable) + : undefined, // TODO reference + }, + key + ); + } } }; return this.currentTree.map(mapper).filter(nonNullable); diff --git a/packages/react-ui-shadcn/src/renderers/index.tsx b/packages/react-ui-shadcn/src/renderers/index.tsx index 18921377..4718122f 100644 --- a/packages/react-ui-shadcn/src/renderers/index.tsx +++ b/packages/react-ui-shadcn/src/renderers/index.tsx @@ -18,6 +18,7 @@ import { ContainerRenderer } from "./container"; import { SelectRenderer } from "./select"; import { TextareaRenderer } from "./textarea"; import { ComboboxRenderer } from "./combobox"; +import { PaddingRenderer } from "./padding"; export const renderers: Renderers = { Select: SelectRenderer, @@ -25,6 +26,7 @@ export const renderers: Renderers = { Combobox: ComboboxRenderer, Textarea: TextareaRenderer, Container: ContainerRenderer, + Padding: PaddingRenderer, Text: TextRenderer, Image: ImageRenderer, Card: CardRenderer, diff --git a/packages/react-ui-shadcn/src/renderers/padding.tsx b/packages/react-ui-shadcn/src/renderers/padding.tsx new file mode 100644 index 00000000..bb4e9dcd --- /dev/null +++ b/packages/react-ui-shadcn/src/renderers/padding.tsx @@ -0,0 +1,8 @@ +import React from "react"; +import { Renderers } from "@mod-protocol/react"; + +export const PaddingRenderer = ( + props: React.ComponentProps +) => { + return
{props.children}
; +}; diff --git a/packages/react/src/index.tsx b/packages/react/src/index.tsx index 7a24b53f..4d83485c 100644 --- a/packages/react/src/index.tsx +++ b/packages/react/src/index.tsx @@ -28,6 +28,7 @@ export * from "./render-embed"; export type Renderers = { Container: React.ComponentType<{ children: React.ReactNode }>; + Padding: React.ComponentType<{ children: React.ReactNode }>; Video: React.ComponentType<{ videoSrc: string; }>; @@ -410,6 +411,16 @@ const WrappedCardRenderer = (props: { ); }; +const WrappedPaddingRenderer = (props: { + component: Renderers["Padding"]; + element: Extract, { type: "padding" }>; +}) => { + const { component: Component, element } = props; + const { type, elements, ...rest } = element; + + return {elements}; +}; + const useForceRerender = () => { const [, setValue] = React.useState(0); return React.useCallback(() => { @@ -726,6 +737,14 @@ export const Mod = (props: Props & { renderer: Renderer }) => { element={el} /> ); + case "padding": + return ( + + ); } })} From 4f635f4296440af26fdb1b99a31ca3093aff43cc Mon Sep 17 00:00:00 2001 From: Stephan Cilliers <5469870+stephancill@users.noreply.github.com> Date: Mon, 15 Jan 2024 17:36:52 +0200 Subject: [PATCH 07/26] fix: add env vars to turbo.json --- turbo.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/turbo.json b/turbo.json index 8a657af9..ccedb4b1 100644 --- a/turbo.json +++ b/turbo.json @@ -40,7 +40,8 @@ "NEXT_PUBLIC_EXPERIMENTAL_MODS", "ZORA_ADMIN_PRIVATE_KEY", "NFT_STORAGE_API_KEY", - "AIRSTACK_API_KEY", - "1INCH_API_KEY" + "ERC_20_AIRSTACK_API_KEY", + "ERC_20_1INCH_API_KEY", + "ERC_20_FEE_RECIPIENT" ] } \ No newline at end of file From d1913621e0c99f9be16520e3887da598061e796c Mon Sep 17 00:00:00 2001 From: Stephan Cilliers <5469870+stephancill@users.noreply.github.com> Date: Mon, 15 Jan 2024 17:37:31 +0200 Subject: [PATCH 08/26] feat: add second token to demo --- examples/nextjs-shadcn/src/app/dummy-casts.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/examples/nextjs-shadcn/src/app/dummy-casts.ts b/examples/nextjs-shadcn/src/app/dummy-casts.ts index 6937b2ae..9694d3b4 100644 --- a/examples/nextjs-shadcn/src/app/dummy-casts.ts +++ b/examples/nextjs-shadcn/src/app/dummy-casts.ts @@ -178,6 +178,11 @@ export const dummyCastData: Array<{ status: "loaded", metadata: {}, }, + { + url: "eip155:8453/erc20:0x4ed4e862860bed51a9570b96d89af5e1b0efefed", + status: "loaded", + metadata: {}, + }, ], }, ]; From 545be9819ee83097dafdc6e7680681efdfd1bc82 Mon Sep 17 00:00:00 2001 From: Stephan Cilliers <5469870+stephancill@users.noreply.github.com> Date: Thu, 18 Jan 2024 16:58:27 +0200 Subject: [PATCH 09/26] feat(core): support multiple async actions --- .changeset/silly-students-compete.md | 5 ++ packages/core/src/renderer.ts | 112 +++++++++++++++------------ 2 files changed, 69 insertions(+), 48 deletions(-) create mode 100644 .changeset/silly-students-compete.md diff --git a/.changeset/silly-students-compete.md b/.changeset/silly-students-compete.md new file mode 100644 index 00000000..b3357d5b --- /dev/null +++ b/.changeset/silly-students-compete.md @@ -0,0 +1,5 @@ +--- +"@mod-protocol/core": patch +--- + +feat: support multiple async actions diff --git a/packages/core/src/renderer.ts b/packages/core/src/renderer.ts index 3e267f4e..ec4a56ca 100644 --- a/packages/core/src/renderer.ts +++ b/packages/core/src/renderer.ts @@ -453,13 +453,17 @@ export type RendererOptions = { } ); +const DEFAULT_ASYNC_ACTION_KEY = "__default"; + export class Renderer { private interrupted: boolean = false; private currentTree: ModElement[] = []; - private asyncAction: { - promise: Promise; - ref: ModAction; - } | null = null; + private asyncActions: { + [key: string]: { + promise: Promise; + ref: ModAction; + } | null; + } = {}; private refs: Record = {}; private context: Readonly; private manifestContext: Record = {}; @@ -608,6 +612,11 @@ export class Renderer { return options; } private executeAction(action: ModAction) { + const actionRef = + "ref" in action + ? action.ref || DEFAULT_ASYNC_ACTION_KEY + : DEFAULT_ASYNC_ACTION_KEY; + switch (action.type) { case "GET": case "POST": @@ -626,18 +635,18 @@ export class Renderer { onAbort: () => { // TODO: we should probably support this resolve(); - if (this.asyncAction?.promise !== promise) { + if (this.asyncActions[actionRef]?.promise !== promise) { return; } }, onSuccess: (response) => { resolve(); - if (this.asyncAction?.promise !== promise) { + if (this.asyncActions[actionRef]?.promise !== promise) { return; } - this.asyncAction = null; + this.asyncActions[actionRef] = null; if (action.ref) { set(this.refs, action.ref, { response, progress: 100 }); @@ -649,7 +658,10 @@ export class Renderer { } }, onUploadProgress: (progress) => { - if (action.ref && this.asyncAction?.promise === promise) { + if ( + action.ref && + this.asyncActions[action.ref]?.promise === promise + ) { set(this.refs, action.ref, { progress }); this.onTreeChange(); } @@ -657,7 +669,7 @@ export class Renderer { onError: (error) => { resolve(); - if (this.asyncAction?.promise !== promise) { + if (this.asyncActions[actionRef]?.promise !== promise) { return; } @@ -666,7 +678,7 @@ export class Renderer { this.onTreeChange(); } - this.asyncAction = null; + this.asyncActions[actionRef] = null; if (action.onerror) { this.stepIntoOrTriggerAction(action.onerror); @@ -676,7 +688,7 @@ export class Renderer { }, 1); }); - this.asyncAction = { + this.asyncActions[actionRef] = { promise, ref: action, }; @@ -695,11 +707,11 @@ export class Renderer { onAbort: () => { resolve(); - if (this.asyncAction?.promise !== promise) { + if (this.asyncActions[actionRef]?.promise !== promise) { return; } - this.asyncAction = null; + this.asyncActions[actionRef] = null; if (action.oncancel) { this.stepIntoOrTriggerAction(action.oncancel); @@ -710,11 +722,11 @@ export class Renderer { onSuccess: (files) => { resolve(); - if (this.asyncAction?.promise !== promise) { + if (this.asyncActions[actionRef]?.promise !== promise) { return; } - this.asyncAction = null; + this.asyncActions[actionRef] = null; if (!files.length) { if (action.oncancel) { @@ -737,7 +749,7 @@ export class Renderer { onError: (error) => { resolve(); - if (this.asyncAction?.promise !== promise) { + if (this.asyncActions[actionRef]?.promise !== promise) { return; } @@ -745,7 +757,7 @@ export class Renderer { set(this.refs, action.ref, { error }); } - this.asyncAction = null; + this.asyncActions[actionRef] = null; if (action.onerror) { this.stepIntoOrTriggerAction(action.onerror); @@ -758,7 +770,7 @@ export class Renderer { }, 1); }); - this.asyncAction = { + this.asyncActions[actionRef] = { promise, ref: action, }; @@ -775,11 +787,11 @@ export class Renderer { onSuccess: (input) => { resolve(); - if (this.asyncAction?.promise !== promise) { + if (this.asyncActions[actionRef]?.promise !== promise) { return; } - this.asyncAction = null; + this.asyncActions[actionRef] = null; if (action.ref) { set(this.refs, action.ref, input); @@ -794,7 +806,7 @@ export class Renderer { }, 1); }); - this.asyncAction = { + this.asyncActions[actionRef] = { promise, ref: action, }; @@ -813,11 +825,11 @@ export class Renderer { onSuccess: () => { resolve(); - if (this.asyncAction?.promise !== promise) { + if (this.asyncActions[actionRef]?.promise !== promise) { return; } - this.asyncAction = null; + this.asyncActions[actionRef] = null; if (action.onsuccess) { this.stepIntoOrTriggerAction(action.onsuccess); @@ -828,7 +840,7 @@ export class Renderer { }, 1); }); - this.asyncAction = { + this.asyncActions[actionRef] = { promise, ref: action, }; @@ -851,11 +863,11 @@ export class Renderer { onSuccess: () => { resolve(); - if (this.asyncAction?.promise !== promise) { + if (this.asyncActions[actionRef]?.promise !== promise) { return; } - this.asyncAction = null; + this.asyncActions[actionRef] = null; if (action.onsuccess) { this.stepIntoOrTriggerAction(action.onsuccess); @@ -866,7 +878,7 @@ export class Renderer { }, 1); }); - this.asyncAction = { + this.asyncActions[actionRef] = { promise, ref: action, }; @@ -897,11 +909,11 @@ export class Renderer { onSuccess: ({ signature, signedMessage, address }) => { resolve(); - if (this.asyncAction?.promise !== promise) { + if (this.asyncActions[actionRef]?.promise !== promise) { return; } - this.asyncAction = null; + this.asyncActions[actionRef] = null; if (action.ref) { set(this.refs, action.ref, { @@ -918,7 +930,7 @@ export class Renderer { onError: (error) => { resolve(); - if (this.asyncAction?.promise !== promise) { + if (this.asyncActions[actionRef]?.promise !== promise) { return; } @@ -926,7 +938,7 @@ export class Renderer { set(this.refs, action.ref, { error }); } - this.asyncAction = null; + this.asyncActions[actionRef] = null; if (action.onerror) { this.stepIntoOrTriggerAction(action.onerror); @@ -939,7 +951,7 @@ export class Renderer { }, 1); }); - this.asyncAction = { + this.asyncActions[actionRef] = { promise, ref: action, }; @@ -979,11 +991,11 @@ export class Renderer { onConfirmed: (txHash, isSuccess) => { resolve(); - if (this.asyncAction?.promise !== promise) { + if (this.asyncActions[actionRef]?.promise !== promise) { return; } - this.asyncAction = null; + this.asyncActions[actionRef] = null; if (action.ref) { set(this.refs, action.ref, { @@ -1002,7 +1014,7 @@ export class Renderer { onError: (error) => { resolve(); - if (this.asyncAction?.promise !== promise) { + if (this.asyncActions[actionRef]?.promise !== promise) { return; } @@ -1010,7 +1022,7 @@ export class Renderer { set(this.refs, action.ref, { error }); } - this.asyncAction = null; + this.asyncActions[actionRef] = null; if (action.onerror) { this.stepIntoOrTriggerAction(action.onerror); @@ -1023,7 +1035,7 @@ export class Renderer { }, 1); }); - this.asyncAction = { + this.asyncActions[actionRef] = { promise, ref: action, }; @@ -1037,12 +1049,10 @@ export class Renderer { } } - if (this.asyncAction) { - if ( - "onloading" in this.asyncAction.ref && - this.asyncAction.ref.onloading - ) { - this.stepIntoOrTriggerAction(this.asyncAction.ref.onloading); + const asyncAction = this.asyncActions[actionRef]; + if (asyncAction) { + if ("onloading" in asyncAction.ref && asyncAction.ref.onloading) { + this.stepIntoOrTriggerAction(asyncAction.ref.onloading); } else { // Maybe we need to re-render this.onTreeChange(); @@ -1073,15 +1083,20 @@ export class Renderer { } if ("type" in maybeElementTreeOrAction) { - if (this.asyncAction) { + const actionRef = + "ref" in maybeElementTreeOrAction + ? maybeElementTreeOrAction.ref || DEFAULT_ASYNC_ACTION_KEY + : DEFAULT_ASYNC_ACTION_KEY; + const asyncAction = this.asyncActions[actionRef]; + if (asyncAction) { // eslint-disable-next-line no-console console.warn( - `Aborted in-flight action with type '${this.asyncAction.ref.type}' in favor of ` + + `Aborted in-flight action with type '${asyncAction.ref.type}' in favor of ` + `action with type '${maybeElementTreeOrAction.type}'` ); } - this.asyncAction = null; + this.asyncActions[actionRef] = null; return this.executeAction(maybeElementTreeOrAction); } @@ -1179,8 +1194,9 @@ export class Renderer { type: "button", loadingLabel: this.replaceInlineContext(el.loadingLabel ?? ""), label: this.replaceInlineContext(el.label), - isLoading: this.asyncAction?.ref === el.onclick, - isDisabled: Boolean(this.asyncAction), + isLoading: + this.asyncActions[DEFAULT_ASYNC_ACTION_KEY]?.ref === el.onclick, + isDisabled: Boolean(this.asyncActions[DEFAULT_ASYNC_ACTION_KEY]), variant: el.variant, events: { onClick: () => { From 56fb3444e5ce2500078cd334d3e8c9c8808cbdcb Mon Sep 17 00:00:00 2001 From: Stephan Cilliers <5469870+stephancill@users.noreply.github.com> Date: Thu, 18 Jan 2024 17:01:21 +0200 Subject: [PATCH 10/26] feat(erc-20): separate and cache initial load --- examples/api/src/app/api/erc-20/lib/utils.ts | 12 +- examples/api/src/app/api/erc-20/route.ts | 77 ++-- mods/erc-20/src/buying.ts | 5 +- mods/erc-20/src/view.ts | 356 +++++++++++-------- 4 files changed, 259 insertions(+), 191 deletions(-) diff --git a/examples/api/src/app/api/erc-20/lib/utils.ts b/examples/api/src/app/api/erc-20/lib/utils.ts index 6da4beca..27492762 100644 --- a/examples/api/src/app/api/erc-20/lib/utils.ts +++ b/examples/api/src/app/api/erc-20/lib/utils.ts @@ -65,7 +65,10 @@ export async function getFollowingHolderInfo({ fid: string; tokenAddress: string; blockchain: string; -}): Promise<{ user: FarcasterUser; amount: number }[]> { +}): Promise<{ + holders: { user: FarcasterUser; amount: number }[]; + holdersCount: number; +}> { const acc: any[] = []; let hasNextPage = true; @@ -118,7 +121,7 @@ export async function getFollowingHolderInfo({ }) .sort((a, b) => Number(b.amount) - Number(a.amount)); - return result; + return { holders: result, holdersCount: result.length }; } export async function getPriceData({ @@ -170,7 +173,7 @@ export async function getPriceData({ change24hNumber.toFixed(2) ).toLocaleString(); const change24hFormatted = - change24hNumber > 0 ? `+${change24hPartial}%` : `-${change24hPartial}%`; + change24hNumber > 0 ? `+${change24hPartial}%` : `-${-change24hPartial}%`; return { unitPriceUsd: unitPriceUsdFormatted, @@ -181,6 +184,7 @@ export async function getPriceData({ } // Use on-chain data as fallback + // TODO: Query uniswap contracts directly const chain = chainByName[blockchain.toLowerCase()]; const url = `https://api.1inch.dev/price/v1.1/${chain.id}/${tokenAddress}?currency=USD`; const res = await fetch(url, { @@ -195,7 +199,7 @@ export async function getPriceData({ }; } -export async function tokenInfo({ +export async function getTokenInfo({ tokenAddress, blockchain, }: { diff --git a/examples/api/src/app/api/erc-20/route.ts b/examples/api/src/app/api/erc-20/route.ts index 65add4c8..84f08076 100644 --- a/examples/api/src/app/api/erc-20/route.ts +++ b/examples/api/src/app/api/erc-20/route.ts @@ -1,13 +1,13 @@ -import { NextRequest, NextResponse } from "next/server"; +import { NextRequest } from "next/server"; import { getFollowingHolderInfo, getPriceData, parseTokenParam, - tokenInfo, + getTokenInfo, } from "./lib/utils"; export async function GET(request: NextRequest) { - const fid = request.nextUrl.searchParams.get("fid")?.toLowerCase(); + const fid = request.nextUrl.searchParams.get("fid"); const token = request.nextUrl.searchParams.get("token")?.toLowerCase(); let tokenAddress = request.nextUrl.searchParams .get("tokenAddress") @@ -15,6 +15,9 @@ export async function GET(request: NextRequest) { let blockchain = request.nextUrl.searchParams .get("blockchain") ?.toLowerCase(); + const methodName = request.nextUrl.searchParams + .get("function") + ?.toLowerCase(); if (token) { const parsedToken = parseTokenParam(token); @@ -22,45 +25,51 @@ export async function GET(request: NextRequest) { blockchain = parsedToken.blockchain; } - if (!tokenAddress) { - return NextResponse.json({ - error: "Missing tokenAddress", - }); - } + const { fn, options } = { + holders: { + fn: getFollowingHolderInfo, + options: { + headers: new Headers({ + "Cache-Control": "public, max-age=3600, immutable", + }), + }, + }, + price: { + fn: getPriceData, + options: { + headers: new Headers({ + "Cache-Control": "public, max-age=3600, immutable", + }), + }, + }, + token: { + fn: getTokenInfo, + options: { + headers: new Headers({ + "Cache-Control": "public, max-age=3600, immutable", + }), + }, + }, + }[methodName]; - if (!blockchain) { - return NextResponse.json({ - error: "Missing or invalid blockchain", + if (!fn) { + return Response.json({ + error: "Invalid function", }); } - const [holderData, priceData, tokenData] = await Promise.all([ - getFollowingHolderInfo({ - blockchain: blockchain, - tokenAddress: tokenAddress, - fid: fid, - }), - getPriceData({ - blockchain: blockchain, - tokenAddress: tokenAddress, - }), - tokenInfo({ - tokenAddress, - blockchain, - }), - ]); + const result = await fn({ + fid, + tokenAddress, + blockchain, + }); - return NextResponse.json({ - holderData: { - holders: [...(holderData || [])], - holdersCount: holderData?.length || 0, - }, - priceData, - tokenData, + return Response.json(result, { + ...options, }); } // needed for preflight requests to succeed export const OPTIONS = async (request: NextRequest) => { - return NextResponse.json({}); + return Response.json({}); }; diff --git a/mods/erc-20/src/buying.ts b/mods/erc-20/src/buying.ts index 3b174d92..d1f90931 100644 --- a/mods/erc-20/src/buying.ts +++ b/mods/erc-20/src/buying.ts @@ -13,7 +13,8 @@ const buy: ModElement[] = [ { type: "text", label: - "Buying ~${{refs.buyAmountUsd}} of {{refs.tokenReq.response.data.tokenData.name}}...", + "Buying ~${{refs.buyAmountUsd}} of {{refs.tokenReq.response.data.name}}...", + variant: "secondary", }, ], }, @@ -37,7 +38,7 @@ const buy: ModElement[] = [ ], }, { - type: "horizontal-layout", + type: "vertical-layout", onload: { type: "POST", url: "{{api}}/erc-20/buy?walletAddress={{user.wallet.address}}&token={{embed.url}}&buyAmountUsd={{refs.buyAmountUsd}}", diff --git a/mods/erc-20/src/view.ts b/mods/erc-20/src/view.ts index 532c65a4..0bfd75e7 100644 --- a/mods/erc-20/src/view.ts +++ b/mods/erc-20/src/view.ts @@ -3,11 +3,6 @@ import { ModElement } from "@mod-protocol/core"; const view: ModElement[] = [ { type: "horizontal-layout", - onload: { - type: "GET", - url: "{{api}}/erc-20?fid={{user.farcaster.fid}}&token={{embed.url}}", - ref: "tokenReq", - }, elements: [ { type: "padding", @@ -28,14 +23,32 @@ const view: ModElement[] = [ }, }, { - if: { - value: "{{refs.tokenReq.response.data}}", - match: { - NOT: { - equals: "", + if: [ + { + value: "{{refs.tokenReq.response.data}}", + match: { + NOT: { + equals: "", + }, }, }, - }, + { + value: "{{refs.holdersReq.response.data}}", + match: { + NOT: { + equals: "", + }, + }, + }, + { + value: "{{refs.priceReq.response.data}}", + match: { + NOT: { + equals: "", + }, + }, + }, + ], then: { type: "vertical-layout", elements: [ @@ -44,8 +57,7 @@ const view: ModElement[] = [ elements: [ { if: { - value: - "{{refs.tokenReq.response.data.tokenData.image}}", + value: "{{refs.tokenReq.response.data.image}}", match: { NOT: { equals: "", @@ -54,48 +66,168 @@ const view: ModElement[] = [ }, then: { type: "avatar", - src: "{{refs.tokenReq.response.data.tokenData.image}}", + src: "{{refs.tokenReq.response.data.image}}", }, }, - { - type: "link", - label: "{{refs.tokenReq.response.data.tokenData.name}}", - variant: "secondary", - url: "{{refs.tokenReq.response.data.tokenData.url}}", - }, - { - type: "text", - variant: "secondary", - label: - "${{refs.tokenReq.response.data.priceData.unitPriceUsd}}", - }, { if: { - value: - "{{refs.tokenReq.response.data.priceData.change24h}}", + value: "{{refs.isBuying}}", match: { - NOT: { - equals: "", - }, + equals: "true", }, }, then: { + type: "vertical-layout", + onload: { + type: "GET", + url: "{{api}}/erc-20/balances?walletAddress={{user.wallet.address}}&token={{embed.url}}&buyOptionsUsd=5,50,500", + ref: "balancesReq", + }, + elements: [ + { + type: "horizontal-layout", + elements: [ + { + if: { + value: "{{refs.balancesReq.response.data}}", + match: { + NOT: { + equals: "", + }, + }, + }, + then: { + type: "vertical-layout", + elements: [ + { + if: { + value: + "{{refs.balancesReq.response.data.buyOptionsUsd[0]}}", + match: { + equals: "true", + }, + }, + then: { + type: "horizontal-layout", + elements: [ + { + if: { + value: + "{{refs.balancesReq.response.data.buyOptionsUsd[0]}}", + match: { + equals: "true", + }, + }, + then: { + type: "button", + label: "$5.00", + onclick: { + type: "SETSTATE", + ref: "buyAmountUsd", + value: "5.00", + }, + }, + }, + { + if: { + value: + "{{refs.balancesReq.response.data.buyOptionsUsd[1]}}", + match: { + equals: "true", + }, + }, + then: { + type: "button", + label: "$50.00", + onclick: { + type: "SETSTATE", + ref: "buyAmountUsd", + value: "50.00", + }, + }, + }, + { + if: { + value: + "{{refs.balancesReq.response.data.buyOptionsUsd[2]}}", + match: { + equals: "true", + }, + }, + then: { + type: "button", + label: "$500.00", + onclick: { + type: "SETSTATE", + ref: "buyAmountUsd", + value: "500.00", + }, + }, + }, + ], + }, + else: { + type: "text", + label: + "Not enough ETH on {{refs.balancesReq.response.data.chain.name}}", + }, + }, + ], + }, + else: { + type: "circular-progress", + }, + }, + ], + }, + ], + }, + else: { type: "horizontal-layout", elements: [ { - type: "text", + type: "link", + label: "{{refs.tokenReq.response.data.name}}", variant: "secondary", - label: "24h:", + url: "{{refs.tokenReq.response.data.url}}", }, { type: "text", variant: "secondary", label: - "{{refs.tokenReq.response.data.priceData.change24h}}", + "${{refs.priceReq.response.data.unitPriceUsd}}", + }, + { + if: { + value: + "{{refs.priceReq.response.data.change24h}}", + match: { + NOT: { + equals: "", + }, + }, + }, + then: { + type: "horizontal-layout", + elements: [ + { + type: "text", + variant: "secondary", + label: "24h:", + }, + { + type: "text", + variant: "secondary", + label: + "{{refs.priceReq.response.data.change24h}}", + }, + ], + }, }, ], }, }, + { if: { value: "{{user.wallet.address}}", @@ -114,7 +246,7 @@ const view: ModElement[] = [ }, then: { type: "button", - label: "Cancel", + label: "Back", variant: "secondary", onclick: { type: "SETSTATE", @@ -135,137 +267,59 @@ const view: ModElement[] = [ }, ], }, - { if: { - value: "{{refs.isBuying}}", + value: "{{refs.balancesReq.response.data.ethBalance}}", match: { - equals: "true", + NOT: { + equals: "", + }, }, }, then: { - type: "horizontal-layout", - onload: { - type: "GET", - url: "{{api}}/erc-20/balances?walletAddress={{user.wallet.address}}&token={{embed.url}}&buyOptionsUsd=5,50,500", - ref: "balancesReq", - }, - elements: [ - { - if: { - value: "{{refs.balancesReq.response.data}}", - match: { - NOT: { - equals: "", - }, - }, - }, - then: { - type: "vertical-layout", - elements: [ - { - if: { - value: - "{{refs.balancesReq.response.data.buyOptionsUsd[0]}}", - match: { - equals: "true", - }, - }, - then: { - type: "horizontal-layout", - elements: [ - { - type: "text", - label: "Buy amount:", - }, - { - if: { - value: - "{{refs.balancesReq.response.data.buyOptionsUsd[0]}}", - match: { - equals: "true", - }, - }, - then: { - type: "button", - label: "$5.00", - onclick: { - type: "SETSTATE", - ref: "buyAmountUsd", - value: "5.00", - }, - }, - }, - { - if: { - value: - "{{refs.balancesReq.response.data.buyOptionsUsd[1]}}", - match: { - equals: "true", - }, - }, - then: { - type: "button", - label: "$50.00", - onclick: { - type: "SETSTATE", - ref: "buyAmountUsd", - value: "50.00", - }, - }, - }, - { - if: { - value: - "{{refs.balancesReq.response.data.buyOptionsUsd[2]}}", - match: { - equals: "true", - }, - }, - then: { - type: "button", - label: "$500.00", - onclick: { - type: "SETSTATE", - ref: "buyAmountUsd", - value: "500.00", - }, - }, - }, - ], - }, - else: { - type: "text", - label: - "Not enough ETH on {{refs.balancesReq.response.data.chain.name}}", - }, - }, - { - type: "text", - label: - "{{refs.balancesReq.response.data.chain.name}} balance: {{refs.balancesReq.response.data.ethBalance}} ETH ({{refs.balancesReq.response.data.ethBalanceUsd}} USD)", - variant: "secondary", - }, - ], - }, - else: { - type: "circular-progress", - }, - }, - ], + type: "text", + label: + "{{refs.balancesReq.response.data.chain.name}} balance: {{refs.balancesReq.response.data.ethBalance}} ETH ({{refs.balancesReq.response.data.ethBalanceUsd}} USD)", + variant: "secondary", }, - // Holders you know else: { type: "text", variant: "secondary", label: - "{{refs.tokenReq.response.data.holderData.holdersCount}} holders you know", + "{{refs.holdersReq.response.data.holdersCount}} holders you know", }, }, ], }, - else: { - type: "circular-progress", + else: { type: "circular-progress" }, + }, + ], + }, + { + type: "horizontal-layout", + elements: [ + { + type: "horizontal-layout", + onload: { + type: "GET", + url: "{{api}}/erc-20?fid={{user.farcaster.fid}}&token={{embed.url}}&function=token", + ref: "tokenReq", + }, + }, + { + type: "horizontal-layout", + onload: { + type: "GET", + url: "{{api}}/erc-20?fid={{user.farcaster.fid}}&token={{embed.url}}&function=holders", + ref: "holdersReq", + }, + }, + { + type: "horizontal-layout", + onload: { + type: "GET", + url: "{{api}}/erc-20?fid={{user.farcaster.fid}}&token={{embed.url}}&function=price", + ref: "priceReq", }, }, ], From ef6e22897a2647a8d0569631cca2b5b6fd6f31f4 Mon Sep 17 00:00:00 2001 From: Stephan Cilliers <5469870+stephancill@users.noreply.github.com> Date: Thu, 18 Jan 2024 17:05:58 +0200 Subject: [PATCH 11/26] fix(nextjs-shadcn): check network before switching --- examples/nextjs-shadcn/src/app/embeds.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/examples/nextjs-shadcn/src/app/embeds.tsx b/examples/nextjs-shadcn/src/app/embeds.tsx index 4fd11d1b..81b5f05f 100644 --- a/examples/nextjs-shadcn/src/app/embeds.tsx +++ b/examples/nextjs-shadcn/src/app/embeds.tsx @@ -18,6 +18,7 @@ import { sendTransaction, switchNetwork, waitForTransaction, + getNetwork, } from "@wagmi/core"; import { useMemo } from "react"; import { useAccount } from "wagmi"; @@ -41,7 +42,10 @@ export function Embeds(props: { embeds: Array }) { const parsedChainId = parseInt(chainId); // Switch chains if the user is not on the right one - await switchNetwork({ chainId: parsedChainId }); + // Note: silently fails if switching to the same chain + const network = getNetwork(); + if (network.chain.id !== parsedChainId) + await switchNetwork({ chainId: parsedChainId }); // Send the transaction const { hash } = await sendTransaction({ @@ -58,6 +62,7 @@ export function Embeds(props: { embeds: Array }) { onConfirmed(hash, status === "success"); } catch (e) { + console.error(e); onError(e); } }, From 5d4939d5e612146849dffe0d8bd5fb9baf41089a Mon Sep 17 00:00:00 2001 From: Stephan Cilliers <5469870+stephancill@users.noreply.github.com> Date: Fri, 19 Jan 2024 15:36:09 +0200 Subject: [PATCH 12/26] feat(mod/erc-20): add tx states, clean up --- examples/api/src/app/api/erc-20/buy/route.ts | 12 +- examples/api/src/app/api/erc-20/route.ts | 7 +- mods/erc-20/src/buying.ts | 129 +++++++++++++++---- mods/erc-20/src/view.ts | 25 ++-- 4 files changed, 130 insertions(+), 43 deletions(-) diff --git a/examples/api/src/app/api/erc-20/buy/route.ts b/examples/api/src/app/api/erc-20/buy/route.ts index e6448db5..62bb48f9 100644 --- a/examples/api/src/app/api/erc-20/buy/route.ts +++ b/examples/api/src/app/api/erc-20/buy/route.ts @@ -1,7 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; -import { createPublicClient, http, parseEther } from "viem2"; -import { chainByName, parseTokenParam } from "../lib/utils"; -import { getEthUsdPrice } from "../lib/utils"; +import { parseEther } from "viem2"; +import { chainByName, getEthUsdPrice, parseTokenParam } from "../lib/utils"; export async function POST(request: NextRequest) { // TODO: Expose separate execution/receiver addresses @@ -31,15 +30,9 @@ export async function POST(request: NextRequest) { }); } - // Get eth balance on blockchain const chain = chainByName[blockchain]; - const client = createPublicClient({ - transport: http(), - chain, - }); const ethPriceUsd = await getEthUsdPrice(); - const ethInputAmount = parseEther((buyAmountUsd / ethPriceUsd).toString()); const swapCalldataParams: { @@ -82,5 +75,6 @@ export async function POST(request: NextRequest) { return NextResponse.json({ transaction: swapCalldataJson.tx, + explorer: chain.blockExplorers.default, }); } diff --git a/examples/api/src/app/api/erc-20/route.ts b/examples/api/src/app/api/erc-20/route.ts index 84f08076..67e69966 100644 --- a/examples/api/src/app/api/erc-20/route.ts +++ b/examples/api/src/app/api/erc-20/route.ts @@ -30,7 +30,8 @@ export async function GET(request: NextRequest) { fn: getFollowingHolderInfo, options: { headers: new Headers({ - "Cache-Control": "public, max-age=3600, immutable", + // Cache for 1 day + "Cache-Control": "public, max-age=86400, immutable", }), }, }, @@ -38,6 +39,7 @@ export async function GET(request: NextRequest) { fn: getPriceData, options: { headers: new Headers({ + // Cache for 1 hour "Cache-Control": "public, max-age=3600, immutable", }), }, @@ -46,7 +48,8 @@ export async function GET(request: NextRequest) { fn: getTokenInfo, options: { headers: new Headers({ - "Cache-Control": "public, max-age=3600, immutable", + // Cache for 1 month + "Cache-Control": "public, max-age=2592000, immutable", }), }, }, diff --git a/mods/erc-20/src/buying.ts b/mods/erc-20/src/buying.ts index d1f90931..053695ad 100644 --- a/mods/erc-20/src/buying.ts +++ b/mods/erc-20/src/buying.ts @@ -4,25 +4,10 @@ const buy: ModElement[] = [ { type: "padding", elements: [ + // If no buy amount, go back to #view { type: "horizontal-layout", elements: [ - { - type: "circular-progress", - }, - { - type: "text", - label: - "Buying ~${{refs.buyAmountUsd}} of {{refs.tokenReq.response.data.name}}...", - variant: "secondary", - }, - ], - }, - - { - type: "horizontal-layout", - elements: [ - // If there is no buyAmountUsd value, go to #view { if: { value: "{{refs.buyAmountUsd}}", @@ -31,12 +16,40 @@ const buy: ModElement[] = [ }, }, then: { - type: "horizontal-layout", + type: "vertical-layout", onload: "#view", }, }, ], }, + // + { + if: [ + { + value: "{{refs.swapTx.isSuccess}}", + match: { + NOT: { + equals: "true", + }, + }, + }, + ], + then: { + type: "horizontal-layout", + elements: [ + { + type: "circular-progress", + }, + { + type: "text", + label: + "Buying ~${{refs.buyAmountUsd}} of {{refs.tokenReq.response.data.name}}...", + variant: "secondary", + }, + ], + }, + }, + { type: "vertical-layout", onload: { @@ -45,7 +58,7 @@ const buy: ModElement[] = [ ref: "swapTxDataReq", onsuccess: { type: "SENDETHTRANSACTION", - ref: "swapTxReq", + ref: "swapTx", txData: { from: "{{refs.swapTxDataReq.response.data.transaction.from}}", to: "{{refs.swapTxDataReq.response.data.transaction.to}}", @@ -55,14 +68,86 @@ const buy: ModElement[] = [ chainId: "{{refs.balancesReq.response.data.chain.id}}", onerror: { type: "SETSTATE", - ref: "buyAmountUsd", - value: "", + state: { + buyAmountUsd: "", + }, }, }, onerror: { type: "SETSTATE", - ref: "buyAmountUsd", - value: "", + state: { + buyAmountUsd: "", + }, + }, + }, + }, + { + if: { + value: "{{refs.swapTx.hash}}", + match: { + NOT: { + equals: "", + }, + }, + }, + then: { + if: { + value: "{{refs.swapTx.isSuccess}}", + match: { + equals: "true", + }, + }, + then: { + type: "horizontal-layout", + elements: [ + { + type: "button", + label: "Back", + variant: "secondary", + onclick: { + type: "SETSTATE", + state: { + isBuying: "false", + buyAmountUsd: "", + }, + }, + }, + { + type: "text", + label: "Transaction successful", + variant: "secondary", + }, + { + type: "link", + label: "Explorer", + url: "{{refs.swapTxDataReq.response.data.explorer.url}}/tx/{{refs.swapTx.hash}}", + }, + ], + }, + else: { + if: { + value: "{{refs.swapTx.isSuccess}}", + match: { + equals: "false", + }, + }, + then: { + type: "link", + label: "Failed", + variant: "link", + url: "{{refs.swapTxDataReq.response.data.explorer.url}}/tx/{{refs.swapTx.hash}}", + }, + else: { + type: "horizontal-layout", + elements: [ + { + type: "link", + label: "Confirming...", + variant: "link", + url: "{{refs.swapTxDataReq.response.data.explorer.url}}/tx/{{refs.swapTx.hash}}", + }, + ], + }, }, }, }, diff --git a/mods/erc-20/src/view.ts b/mods/erc-20/src/view.ts index 0bfd75e7..52cab16b 100644 --- a/mods/erc-20/src/view.ts +++ b/mods/erc-20/src/view.ts @@ -123,8 +123,9 @@ const view: ModElement[] = [ label: "$5.00", onclick: { type: "SETSTATE", - ref: "buyAmountUsd", - value: "5.00", + state: { + buyAmountUsd: "5.00", + }, }, }, }, @@ -141,8 +142,9 @@ const view: ModElement[] = [ label: "$50.00", onclick: { type: "SETSTATE", - ref: "buyAmountUsd", - value: "50.00", + state: { + buyAmountUsd: "50.00", + }, }, }, }, @@ -159,8 +161,9 @@ const view: ModElement[] = [ label: "$500.00", onclick: { type: "SETSTATE", - ref: "buyAmountUsd", - value: "500.00", + state: { + buyAmountUsd: "500.00", + }, }, }, }, @@ -250,8 +253,9 @@ const view: ModElement[] = [ variant: "secondary", onclick: { type: "SETSTATE", - ref: "isBuying", - value: "false", + state: { + isBuying: "false", + }, }, }, else: { @@ -259,8 +263,9 @@ const view: ModElement[] = [ label: "Buy", onclick: { type: "SETSTATE", - ref: "isBuying", - value: "true", + state: { + isBuying: "true", + }, }, }, }, From 8b339ac535780d8dd57013a262f4c0729ec491f1 Mon Sep 17 00:00:00 2001 From: Stephan Cilliers <5469870+stephancill@users.noreply.github.com> Date: Fri, 19 Jan 2024 16:22:00 +0200 Subject: [PATCH 13/26] fix: yarn.lock --- yarn.lock | 53 ++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 48 insertions(+), 5 deletions(-) diff --git a/yarn.lock b/yarn.lock index e554f415..14adf0db 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2597,6 +2597,21 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@openzeppelin/contracts@3.4.1-solc-0.7-2": + version "3.4.1-solc-0.7-2" + resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-3.4.1-solc-0.7-2.tgz#371c67ebffe50f551c3146a9eec5fe6ffe862e92" + integrity sha512-tAG9LWg8+M2CMu7hIsqHPaTyG4uDzjr6mhvH96LvOpLZZj6tgzTluBt+LsCf1/QaYrlis6pITvpIaIhE+iZB+Q== + +"@openzeppelin/contracts@3.4.2-solc-0.7": + version "3.4.2-solc-0.7" + resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-3.4.2-solc-0.7.tgz#38f4dbab672631034076ccdf2f3201fab1726635" + integrity sha512-W6QmqgkADuFcTLzHL8vVoNBtkwjvQRpYIAom7KiUNoLKghyx3FgH0GBjt8NRvigV1ZmMOBllvE1By1C+bi8WpA== + +"@openzeppelin/contracts@4.7.0": + version "4.7.0" + resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-4.7.0.tgz#3092d70ea60e3d1835466266b1d68ad47035a2d5" + integrity sha512-52Qb+A1DdOss8QvJrijYYPSf32GUg2pGaG/yCxtaA3cu4jduouTdg4XZSMLW9op54m1jH7J8hoajhHKOPsoJFw== + "@openzeppelin/contracts@4.9.2": version "4.9.2" resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-4.9.2.tgz#1cb2d5e4d3360141a17dbc45094a8cad6aac16c1" @@ -5478,11 +5493,6 @@ JSONStream@^1.3.5: jsonparse "^1.2.0" through ">=2.2.7 <3" -abitype@0.10.0: - version "0.10.0" - resolved "https://registry.yarnpkg.com/abitype/-/abitype-0.10.0.tgz#d3504747cc81df2acaa6c460250ef7bc9219a77c" - integrity sha512-QvMHEUzgI9nPj9TWtUGnS2scas80/qaL5PBxGdwWhhvzqXfOph+IEiiiWrzuisu3U3JgDQVruW9oLbJoQ3oZ3A== - abitype@0.8.7: version "0.8.7" resolved "https://registry.yarnpkg.com/abitype/-/abitype-0.8.7.tgz#e4b3f051febd08111f486c0cc6a98fa72d033622" @@ -5493,6 +5503,11 @@ abitype@0.9.8: resolved "https://registry.yarnpkg.com/abitype/-/abitype-0.9.8.tgz#1f120b6b717459deafd213dfbf3a3dd1bf10ae8c" integrity sha512-puLifILdm+8sjyss4S+fsUN09obiT1g2YW6CtcQF+QDzxR0euzgEB29MZujC6zMk2a6SVmtttq1fc6+YFA7WYQ== +abitype@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/abitype/-/abitype-1.0.0.tgz#237176dace81d90d018bebf3a45cb42f2a2d9e97" + integrity sha512-NMeMah//6bJ56H5XRj8QCV4AwuW6hB6zqz2LnhhLdcWVQOsXki6/Pn3APeqxCma62nXIcmZWdu1DlHWS74umVQ== + abitype@^0.10.3: version "0.10.3" resolved "https://registry.yarnpkg.com/abitype/-/abitype-0.10.3.tgz#27ce7a7cdb9a80ccd732a3f3cf1ce6ff05266fce" @@ -7791,6 +7806,13 @@ dotenv@^16.0.3, dotenv@^16.3.1: version "1.0.0" resolved "https://github.com/dapphub/ds-test#cd98eff28324bfac652e63a239a60632a761790b" +dtrace-provider@~0.8: + version "0.8.8" + resolved "https://registry.yarnpkg.com/dtrace-provider/-/dtrace-provider-0.8.8.tgz#2996d5490c37e1347be263b423ed7b297fb0d97e" + integrity sha512-b7Z7cNtHPhH9EJhNNbbeqTcXB8LGFFZhq1PGgEvpeHlzd36bhbdTWoE/Ba/YguqpBSlAPKnARWhVlhunCMwfxg== + dependencies: + nan "^2.14.0" + duplexify@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-4.1.2.tgz#18b4f8d28289132fa0b9573c898d9f903f81c7b0" @@ -15030,6 +15052,13 @@ reusify@^1.0.4: resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== +reverse-mirage@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/reverse-mirage/-/reverse-mirage-1.0.3.tgz#3294f6030217b80a76c6f50d8ae10b78c413b489" + integrity sha512-mxMLfwKG18DKH2gcBBJhjRTgSQsB0yFy/k/XjiKOikDHdAZiMZ8srW7i0fcjIj5W/hlYh4m1KY3saG+q16uNew== + dependencies: + tiny-invariant "^1.3.1" + rimraf@^2.6.3: version "2.7.1" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" @@ -17028,6 +17057,20 @@ videojs-vtt.js@0.15.5: dependencies: global "^4.3.1" +"viem2@npm:viem@^2.0.6": + version "2.4.0" + resolved "https://registry.yarnpkg.com/viem/-/viem-2.4.0.tgz#977e6bf4ee67c0e616f853961fbb699cca19700d" + integrity sha512-CPP4ZBy0vKqJE1L2Dzrw/am3vD9p42H3nQwqNBk3o3R8jnM4vwncHjdu+V8tWdk3ZyM8now6Bdlqv76WPpZQhg== + dependencies: + "@adraffy/ens-normalize" "1.10.0" + "@noble/curves" "1.2.0" + "@noble/hashes" "1.3.2" + "@scure/bip32" "1.3.2" + "@scure/bip39" "1.2.1" + abitype "1.0.0" + isows "1.0.3" + ws "8.13.0" + viem@1.20.1, viem@^1.0.0, viem@^1.12.2, viem@^1.19.0: version "1.20.1" resolved "https://registry.yarnpkg.com/viem/-/viem-1.20.1.tgz#ea92f9bab2fded4be556be4d4be724805d11780e" From 61988f6b2ff2e8e769c086f9a0aa9c48eb217614 Mon Sep 17 00:00:00 2001 From: Stephan Cilliers <5469870+stephancill@users.noreply.github.com> Date: Mon, 22 Jan 2024 16:58:57 +0200 Subject: [PATCH 14/26] feat(core+react+react-ui-shadcn): add skeleton loading state --- packages/core/src/manifest.ts | 2 ++ packages/core/src/renderer.ts | 3 +++ .../src/renderers/horizontal-layout.tsx | 9 +++++++-- .../react-ui-shadcn/src/renderers/vertical-layout.tsx | 11 ++++++++++- packages/react/src/index.tsx | 10 ++++++++-- 5 files changed, 30 insertions(+), 5 deletions(-) diff --git a/packages/core/src/manifest.ts b/packages/core/src/manifest.ts index 3ac54b7a..472d227a 100644 --- a/packages/core/src/manifest.ts +++ b/packages/core/src/manifest.ts @@ -216,11 +216,13 @@ export type ModElement = type: "horizontal-layout"; elements?: string | ElementOrConditionalFlow[]; onload?: ModEvent; + loading?: boolean; } | { type: "vertical-layout"; elements?: string | ElementOrConditionalFlow[]; onload?: ModEvent; + loading?: boolean; } | { type: "textarea"; diff --git a/packages/core/src/renderer.ts b/packages/core/src/renderer.ts index ec4a56ca..bfd9b6d0 100644 --- a/packages/core/src/renderer.ts +++ b/packages/core/src/renderer.ts @@ -62,6 +62,7 @@ export type ModElementRef = onLoad: () => void; }; elements?: T[]; + isLoading?: boolean; } | { type: "vertical-layout"; @@ -69,6 +70,7 @@ export type ModElementRef = onLoad: () => void; }; elements?: T[]; + isLoading?: boolean; } | { type: "combobox"; @@ -1249,6 +1251,7 @@ export class Renderer { } }, }, + isLoading: el.loading, }, key ); diff --git a/packages/react-ui-shadcn/src/renderers/horizontal-layout.tsx b/packages/react-ui-shadcn/src/renderers/horizontal-layout.tsx index 8e305fd4..e5c5d5b5 100644 --- a/packages/react-ui-shadcn/src/renderers/horizontal-layout.tsx +++ b/packages/react-ui-shadcn/src/renderers/horizontal-layout.tsx @@ -1,12 +1,17 @@ import React from "react"; import { Renderers } from "@mod-protocol/react"; +import { Skeleton } from "../components/ui/skeleton"; export const HorizontalLayoutRenderer = ( props: React.ComponentProps ) => { return ( -
- {props.children} +
+ {props.isLoading ? ( + + ) : ( + props.children + )}
); }; diff --git a/packages/react-ui-shadcn/src/renderers/vertical-layout.tsx b/packages/react-ui-shadcn/src/renderers/vertical-layout.tsx index 22d7ea09..2c812f92 100644 --- a/packages/react-ui-shadcn/src/renderers/vertical-layout.tsx +++ b/packages/react-ui-shadcn/src/renderers/vertical-layout.tsx @@ -1,8 +1,17 @@ import React from "react"; import { Renderers } from "@mod-protocol/react"; +import { Skeleton } from "../components/ui/skeleton"; export const VerticalLayoutRenderer = ( props: React.ComponentProps ) => { - return
{props.children}
; + return ( +
+ {props.isLoading ? ( + + ) : ( + props.children + )} +
+ ); }; diff --git a/packages/react/src/index.tsx b/packages/react/src/index.tsx index 4d83485c..d3b2670f 100644 --- a/packages/react/src/index.tsx +++ b/packages/react/src/index.tsx @@ -66,8 +66,14 @@ export type Renderers = { onClick: () => void; }>; CircularProgress: React.ComponentType<{}>; - HorizontalLayout: React.ComponentType<{ children: React.ReactNode }>; - VerticalLayout: React.ComponentType<{ children: React.ReactNode }>; + HorizontalLayout: React.ComponentType<{ + children: React.ReactNode; + isLoading?: boolean; + }>; + VerticalLayout: React.ComponentType<{ + children: React.ReactNode; + isLoading?: boolean; + }>; Input: React.ComponentType<{ isClearable: boolean; placeholder?: string; From 940e8d28aef55987a0c73f0c3ad9b53d190e5588 Mon Sep 17 00:00:00 2001 From: Stephan Cilliers <5469870+stephancill@users.noreply.github.com> Date: Mon, 22 Jan 2024 17:01:06 +0200 Subject: [PATCH 15/26] fix(react-ui-shadcn): padding on link element --- packages/react-ui-shadcn/src/components/ui/button.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-ui-shadcn/src/components/ui/button.tsx b/packages/react-ui-shadcn/src/components/ui/button.tsx index c979a7b4..05a6a5e2 100644 --- a/packages/react-ui-shadcn/src/components/ui/button.tsx +++ b/packages/react-ui-shadcn/src/components/ui/button.tsx @@ -20,7 +20,7 @@ const buttonVariants = cva( secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", ghost: "hover:bg-accent hover:text-accent-foreground", - link: "text-primary underline-offset-4 hover:underline", + link: "text-primary underline-offset-4 hover:underline !px-0", }, size: { default: "h-9 px-4 py-2", From 5c1fade8d1a39ab6ab515c8bdcadb5a2333a4172 Mon Sep 17 00:00:00 2001 From: Stephan Cilliers <5469870+stephancill@users.noreply.github.com> Date: Mon, 22 Jan 2024 17:08:49 +0200 Subject: [PATCH 16/26] fix(react-ui-shadcn): padding element width --- packages/react-ui-shadcn/src/renderers/padding.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-ui-shadcn/src/renderers/padding.tsx b/packages/react-ui-shadcn/src/renderers/padding.tsx index bb4e9dcd..c7872fc7 100644 --- a/packages/react-ui-shadcn/src/renderers/padding.tsx +++ b/packages/react-ui-shadcn/src/renderers/padding.tsx @@ -4,5 +4,5 @@ import { Renderers } from "@mod-protocol/react"; export const PaddingRenderer = ( props: React.ComponentProps ) => { - return
{props.children}
; + return
{props.children}
; }; From f7417fc4f828b34d634bac3a595808ab9a0625c7 Mon Sep 17 00:00:00 2001 From: Stephan Cilliers <5469870+stephancill@users.noreply.github.com> Date: Mon, 22 Jan 2024 17:09:19 +0200 Subject: [PATCH 17/26] fix(nextjs-shadcn): text element width --- packages/react-ui-shadcn/src/renderers/text.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-ui-shadcn/src/renderers/text.tsx b/packages/react-ui-shadcn/src/renderers/text.tsx index 73dc7ce5..81e7e931 100644 --- a/packages/react-ui-shadcn/src/renderers/text.tsx +++ b/packages/react-ui-shadcn/src/renderers/text.tsx @@ -3,7 +3,7 @@ import { Renderers } from "@mod-protocol/react"; import { cva } from "class-variance-authority"; import { cn } from "../lib/utils"; -const textVariants = cva("my-0 flex-1 text-sm break-words", { +const textVariants = cva("my-0 text-sm break-words", { variants: { variant: { bold: "font-bold", From d0201c37045148d77db85abcf5ca3d11f7d24f72 Mon Sep 17 00:00:00 2001 From: Stephan Cilliers <5469870+stephancill@users.noreply.github.com> Date: Mon, 22 Jan 2024 17:09:55 +0200 Subject: [PATCH 18/26] fix(erc-20): uppercase $symbol instead of token name --- examples/api/src/app/api/erc-20/lib/utils.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/examples/api/src/app/api/erc-20/lib/utils.ts b/examples/api/src/app/api/erc-20/lib/utils.ts index 27492762..41ea1fcc 100644 --- a/examples/api/src/app/api/erc-20/lib/utils.ts +++ b/examples/api/src/app/api/erc-20/lib/utils.ts @@ -211,11 +211,6 @@ export async function getTokenInfo({ url: string; image?: string; }> { - //0x4ed4e862860bed51a9570b96d89af5e1b0efefed - // https://api.coingecko.com/api/v3/coins/0x4ed4e862860bed51a9570b96d89af5e1b0efefed/market_chart?vs_currency=usd&days=1 - // https://api.coingecko.com/api/v3/simple/token_price/ethereum?contract_addresses=0xd7c1eb0fe4a30d3b2a846c04aa6300888f087a5f&vs_currencies=usd&points&vs_currencies=usd&include_market_cap=true&include_24hr_vol=true&include_24hr_change=true&include_last_updated_at=true - // https://api.coingecko.com/api/v3/coins/ethereum/contract/0xd7c1eb0fe4a30d3b2a846c04aa6300888f087a5f - // https://api.coingecko.com/api/v3/coins/base/contract/0x27d2decb4bfc9c76f0309b8e88dec3a601fe25a8 const res = await fetch( `https://api.coingecko.com/api/v3/coins/${blockchain}/contract/${tokenAddress}` ); @@ -223,7 +218,7 @@ export async function getTokenInfo({ if (res.ok) { const json = await res?.json(); return { - symbol: json.symbol, + symbol: json.symbol.toUpperCase(), name: json.name, image: json.image?.small, url: `https://www.coingecko.com/en/coins/${json.id}`, @@ -245,7 +240,7 @@ export async function getTokenInfo({ }); return { - symbol: token.symbol, + symbol: token.symbol.toUpperCase(), name: token.name, url: `https://app.uniswap.org/tokens/${blockchain}/${tokenAddress}`, }; From 12a4c30a16c2efc98552874e34e290e00bee8e5b Mon Sep 17 00:00:00 2001 From: Stephan Cilliers <5469870+stephancill@users.noreply.github.com> Date: Mon, 22 Jan 2024 17:11:02 +0200 Subject: [PATCH 19/26] fix(erc-20): pr feedback --- mods/erc-20/src/view.ts | 402 +++++++++++++++++++++------------------- 1 file changed, 208 insertions(+), 194 deletions(-) diff --git a/mods/erc-20/src/view.ts b/mods/erc-20/src/view.ts index 52cab16b..4f27726b 100644 --- a/mods/erc-20/src/view.ts +++ b/mods/erc-20/src/view.ts @@ -2,7 +2,7 @@ import { ModElement } from "@mod-protocol/core"; const view: ModElement[] = [ { - type: "horizontal-layout", + type: "vertical-layout", elements: [ { type: "padding", @@ -32,14 +32,6 @@ const view: ModElement[] = [ }, }, }, - { - value: "{{refs.holdersReq.response.data}}", - match: { - NOT: { - equals: "", - }, - }, - }, { value: "{{refs.priceReq.response.data}}", match: { @@ -56,181 +48,52 @@ const view: ModElement[] = [ type: "horizontal-layout", elements: [ { - if: { - value: "{{refs.tokenReq.response.data.image}}", - match: { - NOT: { - equals: "", + type: "horizontal-layout", + elements: [ + { + if: { + value: "{{refs.tokenReq.response.data.image}}", + match: { + NOT: { + equals: "", + }, + }, + }, + then: { + type: "avatar", + src: "{{refs.tokenReq.response.data.image}}", }, }, - }, - then: { - type: "avatar", - src: "{{refs.tokenReq.response.data.image}}", - }, - }, - { - if: { - value: "{{refs.isBuying}}", - match: { - equals: "true", + { + type: "link", + label: "${{refs.tokenReq.response.data.symbol}}", + variant: "link", + url: "{{refs.tokenReq.response.data.url}}", }, - }, - then: { - type: "vertical-layout", - onload: { - type: "GET", - url: "{{api}}/erc-20/balances?walletAddress={{user.wallet.address}}&token={{embed.url}}&buyOptionsUsd=5,50,500", - ref: "balancesReq", + { + type: "text", + variant: "secondary", + label: + "${{refs.priceReq.response.data.unitPriceUsd}}", }, - elements: [ - { - type: "horizontal-layout", - elements: [ - { - if: { - value: "{{refs.balancesReq.response.data}}", - match: { - NOT: { - equals: "", - }, - }, - }, - then: { - type: "vertical-layout", - elements: [ - { - if: { - value: - "{{refs.balancesReq.response.data.buyOptionsUsd[0]}}", - match: { - equals: "true", - }, - }, - then: { - type: "horizontal-layout", - elements: [ - { - if: { - value: - "{{refs.balancesReq.response.data.buyOptionsUsd[0]}}", - match: { - equals: "true", - }, - }, - then: { - type: "button", - label: "$5.00", - onclick: { - type: "SETSTATE", - state: { - buyAmountUsd: "5.00", - }, - }, - }, - }, - { - if: { - value: - "{{refs.balancesReq.response.data.buyOptionsUsd[1]}}", - match: { - equals: "true", - }, - }, - then: { - type: "button", - label: "$50.00", - onclick: { - type: "SETSTATE", - state: { - buyAmountUsd: "50.00", - }, - }, - }, - }, - { - if: { - value: - "{{refs.balancesReq.response.data.buyOptionsUsd[2]}}", - match: { - equals: "true", - }, - }, - then: { - type: "button", - label: "$500.00", - onclick: { - type: "SETSTATE", - state: { - buyAmountUsd: "500.00", - }, - }, - }, - }, - ], - }, - else: { - type: "text", - label: - "Not enough ETH on {{refs.balancesReq.response.data.chain.name}}", - }, - }, - ], - }, - else: { - type: "circular-progress", - }, + { + if: { + value: "{{refs.priceReq.response.data.change24h}}", + match: { + NOT: { + equals: "", }, - ], - }, - ], - }, - else: { - type: "horizontal-layout", - elements: [ - { - type: "link", - label: "{{refs.tokenReq.response.data.name}}", - variant: "secondary", - url: "{{refs.tokenReq.response.data.url}}", + }, }, - { + then: { type: "text", variant: "secondary", label: - "${{refs.priceReq.response.data.unitPriceUsd}}", + "24h: {{refs.priceReq.response.data.change24h}}", }, - { - if: { - value: - "{{refs.priceReq.response.data.change24h}}", - match: { - NOT: { - equals: "", - }, - }, - }, - then: { - type: "horizontal-layout", - elements: [ - { - type: "text", - variant: "secondary", - label: "24h:", - }, - { - type: "text", - variant: "secondary", - label: - "{{refs.priceReq.response.data.change24h}}", - }, - ], - }, - }, - ], - }, + }, + ], }, - { if: { value: "{{user.wallet.address}}", @@ -244,23 +107,15 @@ const view: ModElement[] = [ if: { value: "{{refs.isBuying}}", match: { - equals: "true", - }, - }, - then: { - type: "button", - label: "Back", - variant: "secondary", - onclick: { - type: "SETSTATE", - state: { - isBuying: "false", + NOT: { + equals: "true", }, }, }, - else: { + then: { type: "button", label: "Buy", + variant: "secondary", onclick: { type: "SETSTATE", state: { @@ -274,13 +129,158 @@ const view: ModElement[] = [ }, { if: { - value: "{{refs.balancesReq.response.data.ethBalance}}", + value: "{{refs.isBuying}}", match: { - NOT: { - equals: "", - }, + equals: "true", + }, + }, + then: { + type: "vertical-layout", + onload: { + type: "GET", + url: "{{api}}/erc-20/balances?walletAddress={{user.wallet.address}}&token={{embed.url}}&buyOptionsUsd=5,50,500", + ref: "balancesReq", }, + elements: [ + { + type: "horizontal-layout", + elements: [ + { + if: { + value: "{{refs.balancesReq.response.data}}", + match: { + NOT: { + equals: "", + }, + }, + }, + then: { + type: "vertical-layout", + elements: [ + { + if: { + value: + "{{refs.balancesReq.response.data.buyOptionsUsd[0]}}", + match: { + equals: "true", + }, + }, + then: { + type: "horizontal-layout", + elements: [ + { + if: { + value: + "{{refs.balancesReq.response.data.buyOptionsUsd[0]}}", + match: { + equals: "true", + }, + }, + then: { + type: "button", + label: "$5.00", + onclick: { + type: "SETSTATE", + state: { + buyAmountUsd: "5.00", + }, + }, + }, + }, + { + if: { + value: + "{{refs.balancesReq.response.data.buyOptionsUsd[1]}}", + match: { + equals: "true", + }, + }, + then: { + type: "button", + label: "$50.00", + onclick: { + type: "SETSTATE", + state: { + buyAmountUsd: "50.00", + }, + }, + }, + }, + { + if: { + value: + "{{refs.balancesReq.response.data.buyOptionsUsd[2]}}", + match: { + equals: "true", + }, + }, + then: { + type: "button", + label: "$500.00", + onclick: { + type: "SETSTATE", + state: { + buyAmountUsd: "500.00", + }, + }, + }, + }, + { + if: { + value: "{{refs.isBuying}}", + match: { + equals: "true", + }, + }, + then: { + type: "button", + label: "Cancel", + variant: "secondary", + onclick: { + type: "SETSTATE", + state: { + isBuying: "false", + }, + }, + }, + }, + ], + }, + else: { + type: "text", + label: + "Not enough ETH on {{refs.balancesReq.response.data.chain.name}}", + }, + }, + ], + }, + else: { + type: "horizontal-layout", + loading: true, + }, + }, + ], + }, + ], }, + }, + { + if: [ + { + value: "{{refs.isBuying}}", + match: { + equals: "true", + }, + }, + { + value: "{{refs.balancesReq.response.data.ethBalance}}", + match: { + NOT: { + equals: "", + }, + }, + }, + ], then: { type: "text", label: @@ -288,15 +288,29 @@ const view: ModElement[] = [ variant: "secondary", }, else: { - type: "text", - variant: "secondary", - label: - "{{refs.holdersReq.response.data.holdersCount}} holders you know", + if: { + value: "{{refs.holdersReq.response.data}}", + match: { + NOT: { + equals: "", + }, + }, + }, + then: { + type: "text", + variant: "secondary", + label: + "{{refs.holdersReq.response.data.holdersCount}} holders you follow", + }, + else: { + type: "horizontal-layout", + loading: true, + }, }, }, ], }, - else: { type: "circular-progress" }, + else: { type: "horizontal-layout", loading: true }, }, ], }, From 6aeaa27d357fa14b48912813783a44b3e5ef91c6 Mon Sep 17 00:00:00 2001 From: Stephan Cilliers <5469870+stephancill@users.noreply.github.com> Date: Mon, 22 Jan 2024 17:16:12 +0200 Subject: [PATCH 20/26] chore: add changesets --- .changeset/purple-zebras-stare.md | 7 +++++++ .changeset/wicked-rabbits-beg.md | 5 +++++ 2 files changed, 12 insertions(+) create mode 100644 .changeset/purple-zebras-stare.md create mode 100644 .changeset/wicked-rabbits-beg.md diff --git a/.changeset/purple-zebras-stare.md b/.changeset/purple-zebras-stare.md new file mode 100644 index 00000000..f362b353 --- /dev/null +++ b/.changeset/purple-zebras-stare.md @@ -0,0 +1,7 @@ +--- +"@mod-protocol/react-ui-shadcn": patch +"@mod-protocol/react": patch +"@mod-protocol/core": patch +--- + +feat: skeleton loading state on layouts diff --git a/.changeset/wicked-rabbits-beg.md b/.changeset/wicked-rabbits-beg.md new file mode 100644 index 00000000..574dc96f --- /dev/null +++ b/.changeset/wicked-rabbits-beg.md @@ -0,0 +1,5 @@ +--- +"@mod-protocol/react-ui-shadcn": patch +--- + +fix: padding on link element From 7c5ee4767b48313b419973e930c8351f979f0fd8 Mon Sep 17 00:00:00 2001 From: Stephan Cilliers <5469870+stephancill@users.noreply.github.com> Date: Tue, 23 Jan 2024 12:28:22 +0200 Subject: [PATCH 21/26] fix: refactor info endpoint --- .../api/src/app/api/erc-20/balances/route.ts | 15 +--- examples/api/src/app/api/erc-20/buy/route.ts | 21 ++--- .../src/app/api/erc-20/info/holders/route.ts | 27 +++++++ .../src/app/api/erc-20/info/price/route.ts | 23 ++++++ .../src/app/api/erc-20/info/token/route.ts | 23 ++++++ examples/api/src/app/api/erc-20/lib/utils.ts | 24 ++++++ examples/api/src/app/api/erc-20/route.ts | 78 ------------------- mods/erc-20/src/view.ts | 6 +- 8 files changed, 109 insertions(+), 108 deletions(-) create mode 100644 examples/api/src/app/api/erc-20/info/holders/route.ts create mode 100644 examples/api/src/app/api/erc-20/info/price/route.ts create mode 100644 examples/api/src/app/api/erc-20/info/token/route.ts delete mode 100644 examples/api/src/app/api/erc-20/route.ts diff --git a/examples/api/src/app/api/erc-20/balances/route.ts b/examples/api/src/app/api/erc-20/balances/route.ts index 97b7ca08..ab6bc573 100644 --- a/examples/api/src/app/api/erc-20/balances/route.ts +++ b/examples/api/src/app/api/erc-20/balances/route.ts @@ -5,30 +5,19 @@ import { getEthUsdPrice, numberWithCommas, parseTokenParam, + parseInfoRequestParams, } from "../lib/utils"; export async function GET(request: NextRequest) { + const { blockchain, tokenAddress } = parseInfoRequestParams(request); const userAddress = request.nextUrl.searchParams .get("walletAddress") ?.toLowerCase(); - const token = request.nextUrl.searchParams.get("token")?.toLowerCase(); - let tokenAddress = request.nextUrl.searchParams - .get("tokenAddress") - ?.toLowerCase(); - let blockchain = request.nextUrl.searchParams - .get("blockchain") - ?.toLowerCase(); const buyOptionsUsd = request.nextUrl.searchParams .get("buyOptionsUsd") .split(",") .map((x) => parseFloat(x)); - if (token) { - const parsedToken = parseTokenParam(token); - tokenAddress = parsedToken.tokenAddress; - blockchain = parsedToken.blockchain; - } - if (!tokenAddress || !blockchain) { return new Response("Missing tokenAddress or blockchain", { status: 400, diff --git a/examples/api/src/app/api/erc-20/buy/route.ts b/examples/api/src/app/api/erc-20/buy/route.ts index 62bb48f9..fd138998 100644 --- a/examples/api/src/app/api/erc-20/buy/route.ts +++ b/examples/api/src/app/api/erc-20/buy/route.ts @@ -1,29 +1,22 @@ import { NextRequest, NextResponse } from "next/server"; import { parseEther } from "viem2"; -import { chainByName, getEthUsdPrice, parseTokenParam } from "../lib/utils"; +import { + chainByName, + getEthUsdPrice, + parseInfoRequestParams, +} from "../lib/utils"; export async function POST(request: NextRequest) { + const { blockchain, tokenAddress } = parseInfoRequestParams(request); + // TODO: Expose separate execution/receiver addresses const userAddress = request.nextUrl.searchParams .get("walletAddress") ?.toLowerCase(); - const token = request.nextUrl.searchParams.get("token")?.toLowerCase(); - let tokenAddress = request.nextUrl.searchParams - .get("tokenAddress") - ?.toLowerCase(); - let blockchain = request.nextUrl.searchParams - .get("blockchain") - ?.toLowerCase(); const buyAmountUsd = parseFloat( request.nextUrl.searchParams.get("buyAmountUsd") ); - if (token) { - const parsedToken = parseTokenParam(token); - tokenAddress = parsedToken.tokenAddress; - blockchain = parsedToken.blockchain; - } - if (!tokenAddress || !blockchain) { return new Response("Missing tokenAddress or blockchain", { status: 400, diff --git a/examples/api/src/app/api/erc-20/info/holders/route.ts b/examples/api/src/app/api/erc-20/info/holders/route.ts new file mode 100644 index 00000000..759eeb39 --- /dev/null +++ b/examples/api/src/app/api/erc-20/info/holders/route.ts @@ -0,0 +1,27 @@ +import { NextRequest } from "next/server"; +import { + getFollowingHolderInfo, + parseInfoRequestParams, +} from "../../lib/utils"; + +export async function GET(request: NextRequest) { + const { fid, tokenAddress, blockchain } = parseInfoRequestParams(request); + const result = await getFollowingHolderInfo({ + fid, + blockchain, + tokenAddress, + }); + + if (!tokenAddress || !blockchain) { + return new Response("Missing tokenAddress or blockchain", { + status: 400, + }); + } + + return Response.json(result, { + headers: new Headers({ + // Cache for 1 day + "Cache-Control": "public, max-age=86400, immutable", + }), + }); +} diff --git a/examples/api/src/app/api/erc-20/info/price/route.ts b/examples/api/src/app/api/erc-20/info/price/route.ts new file mode 100644 index 00000000..c00f3847 --- /dev/null +++ b/examples/api/src/app/api/erc-20/info/price/route.ts @@ -0,0 +1,23 @@ +import { NextRequest } from "next/server"; +import { getPriceData, parseInfoRequestParams } from "../../lib/utils"; + +export async function GET(request: NextRequest) { + const { tokenAddress, blockchain } = parseInfoRequestParams(request); + const result = await getPriceData({ + blockchain, + tokenAddress, + }); + + if (!tokenAddress || !blockchain) { + return new Response("Missing tokenAddress or blockchain", { + status: 400, + }); + } + + return Response.json(result, { + headers: new Headers({ + // Cache for 1 hour + "Cache-Control": "public, max-age=3600, immutable", + }), + }); +} diff --git a/examples/api/src/app/api/erc-20/info/token/route.ts b/examples/api/src/app/api/erc-20/info/token/route.ts new file mode 100644 index 00000000..46feb05c --- /dev/null +++ b/examples/api/src/app/api/erc-20/info/token/route.ts @@ -0,0 +1,23 @@ +import { NextRequest } from "next/server"; +import { getTokenInfo, parseInfoRequestParams } from "../../lib/utils"; + +export async function GET(request: NextRequest) { + const { tokenAddress, blockchain } = parseInfoRequestParams(request); + const result = await getTokenInfo({ + blockchain, + tokenAddress, + }); + + if (!tokenAddress || !blockchain) { + return new Response("Missing tokenAddress or blockchain", { + status: 400, + }); + } + + return Response.json(result, { + headers: new Headers({ + // Cache for 1 month + "Cache-Control": "public, max-age=2592000, immutable", + }), + }); +} diff --git a/examples/api/src/app/api/erc-20/lib/utils.ts b/examples/api/src/app/api/erc-20/lib/utils.ts index 41ea1fcc..80bb4ace 100644 --- a/examples/api/src/app/api/erc-20/lib/utils.ts +++ b/examples/api/src/app/api/erc-20/lib/utils.ts @@ -1,4 +1,5 @@ import { FarcasterUser } from "@mod-protocol/core"; +import { NextRequest } from "next/server"; import { publicActionReverseMirage } from "reverse-mirage"; import { createPublicClient, http } from "viem2"; import * as chains from "viem2/chains"; @@ -312,3 +313,26 @@ export async function getEthUsdPrice(): Promise { return ethPriceUsd; } + +export function parseInfoRequestParams(request: NextRequest) { + const fid = request.nextUrl.searchParams.get("fid"); + const token = request.nextUrl.searchParams.get("token")?.toLowerCase(); + let tokenAddress = request.nextUrl.searchParams + .get("tokenAddress") + ?.toLowerCase(); + let blockchain = request.nextUrl.searchParams + .get("blockchain") + ?.toLowerCase(); + + if (token) { + const parsedToken = parseTokenParam(token); + tokenAddress = parsedToken.tokenAddress; + blockchain = parsedToken.blockchain; + } + + return { + fid, + tokenAddress, + blockchain, + }; +} diff --git a/examples/api/src/app/api/erc-20/route.ts b/examples/api/src/app/api/erc-20/route.ts deleted file mode 100644 index 67e69966..00000000 --- a/examples/api/src/app/api/erc-20/route.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { NextRequest } from "next/server"; -import { - getFollowingHolderInfo, - getPriceData, - parseTokenParam, - getTokenInfo, -} from "./lib/utils"; - -export async function GET(request: NextRequest) { - const fid = request.nextUrl.searchParams.get("fid"); - const token = request.nextUrl.searchParams.get("token")?.toLowerCase(); - let tokenAddress = request.nextUrl.searchParams - .get("tokenAddress") - ?.toLowerCase(); - let blockchain = request.nextUrl.searchParams - .get("blockchain") - ?.toLowerCase(); - const methodName = request.nextUrl.searchParams - .get("function") - ?.toLowerCase(); - - if (token) { - const parsedToken = parseTokenParam(token); - tokenAddress = parsedToken.tokenAddress; - blockchain = parsedToken.blockchain; - } - - const { fn, options } = { - holders: { - fn: getFollowingHolderInfo, - options: { - headers: new Headers({ - // Cache for 1 day - "Cache-Control": "public, max-age=86400, immutable", - }), - }, - }, - price: { - fn: getPriceData, - options: { - headers: new Headers({ - // Cache for 1 hour - "Cache-Control": "public, max-age=3600, immutable", - }), - }, - }, - token: { - fn: getTokenInfo, - options: { - headers: new Headers({ - // Cache for 1 month - "Cache-Control": "public, max-age=2592000, immutable", - }), - }, - }, - }[methodName]; - - if (!fn) { - return Response.json({ - error: "Invalid function", - }); - } - - const result = await fn({ - fid, - tokenAddress, - blockchain, - }); - - return Response.json(result, { - ...options, - }); -} - -// needed for preflight requests to succeed -export const OPTIONS = async (request: NextRequest) => { - return Response.json({}); -}; diff --git a/mods/erc-20/src/view.ts b/mods/erc-20/src/view.ts index 4f27726b..0d8b3e48 100644 --- a/mods/erc-20/src/view.ts +++ b/mods/erc-20/src/view.ts @@ -321,7 +321,7 @@ const view: ModElement[] = [ type: "horizontal-layout", onload: { type: "GET", - url: "{{api}}/erc-20?fid={{user.farcaster.fid}}&token={{embed.url}}&function=token", + url: "{{api}}/erc-20/info/token?fid={{user.farcaster.fid}}&token={{embed.url}}", ref: "tokenReq", }, }, @@ -329,7 +329,7 @@ const view: ModElement[] = [ type: "horizontal-layout", onload: { type: "GET", - url: "{{api}}/erc-20?fid={{user.farcaster.fid}}&token={{embed.url}}&function=holders", + url: "{{api}}/erc-20/info/holders?fid={{user.farcaster.fid}}&token={{embed.url}}", ref: "holdersReq", }, }, @@ -337,7 +337,7 @@ const view: ModElement[] = [ type: "horizontal-layout", onload: { type: "GET", - url: "{{api}}/erc-20?fid={{user.farcaster.fid}}&token={{embed.url}}&function=price", + url: "{{api}}/erc-20/info/price?fid={{user.farcaster.fid}}&token={{embed.url}}", ref: "priceReq", }, }, From de7c32c63d083b2ac6014322c4bf4fa08352950a Mon Sep 17 00:00:00 2001 From: Stephan Cilliers <5469870+stephancill@users.noreply.github.com> Date: Wed, 24 Jan 2024 15:47:18 +0200 Subject: [PATCH 22/26] fix(mod/zora-nft-minter): fix button layout --- .changeset/eleven-phones-notice.md | 5 + mods/zora-nft-minter/src/view.ts | 159 +++++++++++++++-------------- 2 files changed, 90 insertions(+), 74 deletions(-) create mode 100644 .changeset/eleven-phones-notice.md diff --git a/.changeset/eleven-phones-notice.md b/.changeset/eleven-phones-notice.md new file mode 100644 index 00000000..530ec23a --- /dev/null +++ b/.changeset/eleven-phones-notice.md @@ -0,0 +1,5 @@ +--- +"@mods/zora-nft-minter": minor +--- + +fix: button layout diff --git a/mods/zora-nft-minter/src/view.ts b/mods/zora-nft-minter/src/view.ts index 35ce74d1..5aea0543 100644 --- a/mods/zora-nft-minter/src/view.ts +++ b/mods/zora-nft-minter/src/view.ts @@ -8,104 +8,115 @@ const view: ModElement[] = [ topLeftBadge: "{{embed.metadata.nft.collection.creator.username}}", elements: [ { - type: "horizontal-layout", + type: "vertical-layout", elements: [ { - type: "avatar", - src: "{{api}}/nft-chain-logo?chain={{embed.metadata.nft.collection.chain}}", - }, - { - type: "text", - label: "{{embed.metadata.nft.collection.name}}", - }, - { - if: { - value: "{{user.wallet.address}}", - match: { - NOT: { - equals: "", - }, - }, - }, - then: { - if: { - value: "{{refs.mintTx.hash}}", - match: { - NOT: { - equals: "", + type: "horizontal-layout", + elements: [ + { + type: "horizontal-layout", + elements: [ + { + type: "avatar", + src: "{{api}}/nft-chain-logo?chain={{embed.metadata.nft.collection.chain}}", }, - }, + { + type: "text", + label: "{{embed.metadata.nft.collection.name}}", + }, + ], }, - then: { + { if: { - value: "{{refs.mintTx.isSuccess}}", + value: "{{user.wallet.address}}", match: { - equals: "true", + NOT: { + equals: "", + }, }, }, then: { - type: "link", - label: "View NFT", - url: "{{refs.txDataRequest.response.data.explorer.url}}/tx/{{refs.mintTx.hash}}", - }, - else: { if: { - value: "{{refs.mintTx.isSuccess}}", + value: "{{refs.mintTx.hash}}", match: { - equals: "false", + NOT: { + equals: "", + }, }, }, then: { - type: "link", - label: "Failed", - variant: "link", - url: "{{refs.txDataRequest.response.data.explorer.url}}/tx/{{refs.mintTx.hash}}", - }, - else: { - type: "horizontal-layout", - elements: [ - { + if: { + value: "{{refs.mintTx.isSuccess}}", + match: { + equals: "true", + }, + }, + then: { + type: "link", + label: "View NFT", + url: "{{refs.txDataRequest.response.data.explorer.url}}/tx/{{refs.mintTx.hash}}", + }, + else: { + if: { + value: "{{refs.mintTx.isSuccess}}", + match: { + equals: "false", + }, + }, + then: { type: "link", - label: "Confirming...", + label: "Failed", variant: "link", url: "{{refs.txDataRequest.response.data.explorer.url}}/tx/{{refs.mintTx.hash}}", }, - { - type: "circular-progress", + else: { + type: "horizontal-layout", + elements: [ + { + type: "link", + label: "Confirming...", + variant: "link", + url: "{{refs.txDataRequest.response.data.explorer.url}}/tx/{{refs.mintTx.hash}}", + }, + { + type: "circular-progress", + }, + ], }, - ], + }, }, - }, - }, - else: { - type: "button", - label: "Mint", - onclick: { - type: "GET", - ref: "txDataRequest", - url: "{{api}}/nft-minter?taker={{user.wallet.address}}&itemId={{embed.metadata.nft.collection.id}}/{{embed.metadata.nft.tokenId}}", - onsuccess: { - type: "SENDETHTRANSACTION", - ref: "mintTx", - txData: { - from: "{{refs.txDataRequest.response.data.data.from}}", - to: "{{refs.txDataRequest.response.data.data.to}}", - value: "{{refs.txDataRequest.response.data.data.value}}", - data: "{{refs.txDataRequest.response.data.data.data}}", + else: { + type: "button", + label: "Mint", + onclick: { + type: "GET", + ref: "txDataRequest", + url: "{{api}}/nft-minter?taker={{user.wallet.address}}&itemId={{embed.metadata.nft.collection.id}}/{{embed.metadata.nft.tokenId}}", + onsuccess: { + type: "SENDETHTRANSACTION", + ref: "mintTx", + txData: { + from: "{{refs.txDataRequest.response.data.data.from}}", + to: "{{refs.txDataRequest.response.data.data.to}}", + value: + "{{refs.txDataRequest.response.data.data.value}}", + data: "{{refs.txDataRequest.response.data.data.data}}", + }, + chainId: "{{refs.txDataRequest.response.data.chainId}}", + }, }, - chainId: "{{refs.txDataRequest.response.data.chainId}}", + }, + }, + else: { + type: "button", + label: "Mint", + onclick: { + type: "OPENLINK", + url: "{{embed.metadata.nft.mintUrl}}", }, }, }, - }, - else: { - type: "button", - label: "Mint", - onclick: { - type: "OPENLINK", - url: "{{embed.metadata.nft.mintUrl}}", - }, - }, + ], }, ], }, From a19f7afc1fe8df01f882a4094ff383fe0eb628dd Mon Sep 17 00:00:00 2001 From: Stephan Cilliers <5469870+stephancill@users.noreply.github.com> Date: Thu, 25 Jan 2024 12:25:28 +0200 Subject: [PATCH 23/26] feat(mod/erc-20): add uniswap fee --- .../api/src/app/api/erc-20/balances/route.ts | 1 - examples/api/src/app/api/erc-20/buy/route.ts | 62 +++----- examples/api/src/app/api/erc-20/lib/utils.ts | 144 ++++++++++++++++-- mods/erc-20/src/buying.ts | 24 ++- 4 files changed, 170 insertions(+), 61 deletions(-) diff --git a/examples/api/src/app/api/erc-20/balances/route.ts b/examples/api/src/app/api/erc-20/balances/route.ts index ab6bc573..c7705ad0 100644 --- a/examples/api/src/app/api/erc-20/balances/route.ts +++ b/examples/api/src/app/api/erc-20/balances/route.ts @@ -4,7 +4,6 @@ import { chainByName, getEthUsdPrice, numberWithCommas, - parseTokenParam, parseInfoRequestParams, } from "../lib/utils"; diff --git a/examples/api/src/app/api/erc-20/buy/route.ts b/examples/api/src/app/api/erc-20/buy/route.ts index fd138998..b7159d7f 100644 --- a/examples/api/src/app/api/erc-20/buy/route.ts +++ b/examples/api/src/app/api/erc-20/buy/route.ts @@ -1,15 +1,15 @@ import { NextRequest, NextResponse } from "next/server"; -import { parseEther } from "viem2"; +import { fromHex } from "viem2"; import { chainByName, getEthUsdPrice, + getSwapTransaction, parseInfoRequestParams, } from "../lib/utils"; export async function POST(request: NextRequest) { const { blockchain, tokenAddress } = parseInfoRequestParams(request); - // TODO: Expose separate execution/receiver addresses const userAddress = request.nextUrl.searchParams .get("walletAddress") ?.toLowerCase(); @@ -26,48 +26,28 @@ export async function POST(request: NextRequest) { const chain = chainByName[blockchain]; const ethPriceUsd = await getEthUsdPrice(); - const ethInputAmount = parseEther((buyAmountUsd / ethPriceUsd).toString()); - - const swapCalldataParams: { - src: string; - dst: string; - amount: string; - from: string; - slippage: string; - receiver: string; - fee?: string; - referrer?: string; - } = { - src: "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", // Native ETH - dst: tokenAddress, - amount: ethInputAmount.toString(), - from: userAddress, - slippage: "5", - receiver: userAddress, - }; - - // TODO: Use Uniswap to take advantage of configurable fee - // The referrer here is the 1inch referral program recipient (20% of surplus from trade) - // See https://blog.1inch.io/why-should-you-integrate-1inch-apis-into-your-service/ - if (process.env.ERC_20_FEE_RECIPIENT) { - swapCalldataParams.referrer = process.env.ERC_20_FEE_RECIPIENT; - } - - const swapCalldataRes = await fetch( - `https://api.1inch.dev/swap/v5.2/${chain.id}/swap?${new URLSearchParams( - swapCalldataParams - ).toString()}`, - { - headers: { - Authorization: `Bearer ${process.env["ERC_20_1INCH_API_KEY"]}`, - }, - } - ); + const ethInputAmount = (buyAmountUsd / ethPriceUsd).toString(); + + const swapRoute = await getSwapTransaction({ + blockchain, + ethInputAmountFormatted: ethInputAmount, + outTokenAddress: tokenAddress, + recipientAddress: userAddress, + feePercentageInt: 5, + feeRecipientAddress: process.env.ERC_20_FEE_RECIPIENT, + }); - const swapCalldataJson = await swapCalldataRes.json(); + const tx = swapRoute.methodParameters; return NextResponse.json({ - transaction: swapCalldataJson.tx, + transaction: { + from: userAddress, + to: tx.to, + value: fromHex(tx.value as `0x${string}`, { + to: "bigint", + }).toString(), + data: tx.calldata, + }, explorer: chain.blockExplorers.default, }); } diff --git a/examples/api/src/app/api/erc-20/lib/utils.ts b/examples/api/src/app/api/erc-20/lib/utils.ts index 80bb4ace..623d0d7e 100644 --- a/examples/api/src/app/api/erc-20/lib/utils.ts +++ b/examples/api/src/app/api/erc-20/lib/utils.ts @@ -1,4 +1,16 @@ import { FarcasterUser } from "@mod-protocol/core"; +import { Protocol } from "@uniswap/router-sdk"; +import { Percent, Token, TradeType } from "@uniswap/sdk-core"; +import { + AlphaRouter, + AlphaRouterConfig, + CurrencyAmount, + SwapOptions, + SwapType, + nativeOnChain, +} from "@uniswap/smart-order-router"; +import { ethers } from "ethers"; +import JSBI from "jsbi"; import { NextRequest } from "next/server"; import { publicActionReverseMirage } from "reverse-mirage"; import { createPublicClient, http } from "viem2"; @@ -10,9 +22,21 @@ export function numberWithCommas(x: string | number) { return parts.join("."); } -const { ERC_20_AIRSTACK_API_KEY } = process.env; -const AIRSTACK_API_URL = "https://api.airstack.xyz/gql"; -const airstackQuery = ` +export async function getFollowingHolderInfo({ + fid, + tokenAddress, + blockchain, +}: { + fid: string; + tokenAddress: string; + blockchain: string; +}): Promise<{ + holders: { user: FarcasterUser; amount: number }[]; + holdersCount: number; +}> { + const { ERC_20_AIRSTACK_API_KEY } = process.env; + const AIRSTACK_API_URL = "https://api.airstack.xyz/gql"; + const airstackQuery = ` query MyQuery($identity: Identity!, $token_address: Address!, $blockchain: TokenBlockchain, $cursor: String) { SocialFollowings( input: { @@ -58,18 +82,6 @@ query MyQuery($identity: Identity!, $token_address: Address!, $blockchain: Token } `; -export async function getFollowingHolderInfo({ - fid, - tokenAddress, - blockchain, -}: { - fid: string; - tokenAddress: string; - blockchain: string; -}): Promise<{ - holders: { user: FarcasterUser; amount: number }[]; - holdersCount: number; -}> { const acc: any[] = []; let hasNextPage = true; @@ -314,6 +326,108 @@ export async function getEthUsdPrice(): Promise { return ethPriceUsd; } +export async function getSwapTransaction({ + outTokenAddress, + blockchain, + ethInputAmountFormatted, + recipientAddress, + feeRecipientAddress, + feePercentageInt, +}: { + outTokenAddress: string; + blockchain: string; + ethInputAmountFormatted: string; + recipientAddress: string; + feePercentageInt?: number; + feeRecipientAddress?: string; +}) { + const tokenOut = await getUniswapToken({ + tokenAddress: outTokenAddress, + blockchain, + }); + const chain = chainByName[blockchain]; + const provider = new ethers.providers.JsonRpcProvider( + chain.rpcUrls.default.http[0] + ); + + const router = new AlphaRouter({ + chainId: chain.id, + provider, + }); + + const tokenIn = nativeOnChain(chain.id); + const amountIn = CurrencyAmount.fromRawAmount( + tokenIn, + JSBI.BigInt( + ethers.utils.parseUnits(ethInputAmountFormatted, tokenIn.decimals) + ) + ); + + let swapOptions: SwapOptions = { + type: SwapType.UNIVERSAL_ROUTER, + recipient: recipientAddress, + slippageTolerance: new Percent(5, 100), + deadlineOrPreviousBlockhash: parseDeadline("360"), + fee: + feeRecipientAddress && feePercentageInt + ? { + fee: new Percent(feePercentageInt, 100), + recipient: feeRecipientAddress, + } + : undefined, + }; + + const partialRoutingConfig: Partial = { + protocols: [Protocol.V2, Protocol.V3], + }; + + const quote = await router.route( + amountIn, + tokenOut, + TradeType.EXACT_INPUT, + swapOptions, + partialRoutingConfig + ); + + if (!quote) return; + return quote; +} + +async function getUniswapToken({ + tokenAddress, + blockchain, +}: { + tokenAddress: string; + blockchain: string; +}): Promise { + const chain = chainByName[blockchain]; + const client = createPublicClient({ + transport: http(), + chain, + }).extend(publicActionReverseMirage); + + const token = await client.getERC20({ + erc20: { + address: tokenAddress as `0x${string}`, + chainID: chain.id, + }, + }); + + const uniswapToken = new Token( + chain.id, + tokenAddress, + token.decimals, + token.symbol, + token.name + ); + + return uniswapToken; +} + +function parseDeadline(deadline: string): number { + return Math.floor(Date.now() / 1000) + parseInt(deadline); +} + export function parseInfoRequestParams(request: NextRequest) { const fid = request.nextUrl.searchParams.get("fid"); const token = request.nextUrl.searchParams.get("token")?.toLowerCase(); diff --git a/mods/erc-20/src/buying.ts b/mods/erc-20/src/buying.ts index 053695ad..4e5437b3 100644 --- a/mods/erc-20/src/buying.ts +++ b/mods/erc-20/src/buying.ts @@ -41,10 +41,26 @@ const buy: ModElement[] = [ type: "circular-progress", }, { - type: "text", - label: - "Buying ~${{refs.buyAmountUsd}} of {{refs.tokenReq.response.data.name}}...", - variant: "secondary", + if: { + value: "{{refs.swapTxDataReq.response.data}}", + match: { + NOT: { + equals: "", + }, + }, + }, + then: { + type: "text", + label: + "Buying ${{refs.buyAmountUsd}} of ${{refs.tokenReq.response.data.symbol}}...", + variant: "secondary", + }, + else: { + type: "text", + label: + "Calculating best swap route for ${{refs.tokenReq.response.data.symbol}}...", + variant: "secondary", + }, }, ], }, From 2d8a4b06064bca76361844a30f9c71136b0c4f55 Mon Sep 17 00:00:00 2001 From: Stephan Cilliers <5469870+stephancill@users.noreply.github.com> Date: Fri, 26 Jan 2024 11:51:29 +0200 Subject: [PATCH 24/26] feat(core+react+react-ui-shadcn): add href to avatar element --- .changeset/lemon-spoons-raise.md | 7 +++++++ packages/core/src/manifest.ts | 1 + packages/core/src/renderer.ts | 2 ++ packages/react-ui-shadcn/src/renderers/avatar.tsx | 9 ++++++++- packages/react/src/index.tsx | 1 + 5 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 .changeset/lemon-spoons-raise.md diff --git a/.changeset/lemon-spoons-raise.md b/.changeset/lemon-spoons-raise.md new file mode 100644 index 00000000..2266e3ab --- /dev/null +++ b/.changeset/lemon-spoons-raise.md @@ -0,0 +1,7 @@ +--- +"@mod-protocol/react-ui-shadcn": patch +"@mod-protocol/react": patch +"@mod-protocol/core": patch +--- + +feat: add optional `href` to avatar component diff --git a/packages/core/src/manifest.ts b/packages/core/src/manifest.ts index 472d227a..12fcf04c 100644 --- a/packages/core/src/manifest.ts +++ b/packages/core/src/manifest.ts @@ -293,6 +293,7 @@ export type ModElement = | { type: "avatar"; src: string; + href?: string; } | ({ type: "card"; diff --git a/packages/core/src/renderer.ts b/packages/core/src/renderer.ts index bfd9b6d0..848cdeeb 100644 --- a/packages/core/src/renderer.ts +++ b/packages/core/src/renderer.ts @@ -145,6 +145,7 @@ export type ModElementRef = | { type: "avatar"; src: string; + href?: string; } | { type: "card"; @@ -1489,6 +1490,7 @@ export class Renderer { { type: "avatar", src: this.replaceInlineContext(el.src), + href: el.href ? this.replaceInlineContext(el.href) : undefined, }, key ); diff --git a/packages/react-ui-shadcn/src/renderers/avatar.tsx b/packages/react-ui-shadcn/src/renderers/avatar.tsx index 1e6e21f4..e9b34dce 100644 --- a/packages/react-ui-shadcn/src/renderers/avatar.tsx +++ b/packages/react-ui-shadcn/src/renderers/avatar.tsx @@ -7,7 +7,14 @@ export const AvatarRenderer = ( ) => { return ( - + {props.href ? ( + + + + ) : ( + + )} + ); diff --git a/packages/react/src/index.tsx b/packages/react/src/index.tsx index d3b2670f..873715af 100644 --- a/packages/react/src/index.tsx +++ b/packages/react/src/index.tsx @@ -108,6 +108,7 @@ export type Renderers = { Avatar: React.ComponentType<{ src: string; size?: "sm" | "md" | "lg"; + href?: string; }>; Card: React.ComponentType<{ imageSrc?: string; From e2fb675d99c50f52258dda11b0efb054f8438291 Mon Sep 17 00:00:00 2001 From: Stephan Cilliers <5469870+stephancill@users.noreply.github.com> Date: Fri, 26 Jan 2024 11:53:02 +0200 Subject: [PATCH 25/26] fix(mod/erc-20): token logo links to token url --- mods/erc-20/src/view.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/mods/erc-20/src/view.ts b/mods/erc-20/src/view.ts index 0d8b3e48..8ea272bb 100644 --- a/mods/erc-20/src/view.ts +++ b/mods/erc-20/src/view.ts @@ -62,6 +62,7 @@ const view: ModElement[] = [ then: { type: "avatar", src: "{{refs.tokenReq.response.data.image}}", + href: "{{refs.tokenReq.response.data.url}}", }, }, { From 869c9d003e74b2780e1feda5a4214e63d1aa0c15 Mon Sep 17 00:00:00 2001 From: Stephan Cilliers <5469870+stephancill@users.noreply.github.com> Date: Fri, 26 Jan 2024 16:25:36 +0200 Subject: [PATCH 26/26] chore(mod/erc-20): update custody address --- mods/erc-20/src/manifest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mods/erc-20/src/manifest.ts b/mods/erc-20/src/manifest.ts index 85b7d8a2..41eaa29c 100644 --- a/mods/erc-20/src/manifest.ts +++ b/mods/erc-20/src/manifest.ts @@ -5,7 +5,7 @@ import buying from "./buying"; const manifest: ModManifest = { slug: "erc-20", name: "ERC-20", - custodyAddress: "stephancill.eth", + custodyAddress: "0xdcC59cF0Adf4175973D4abc8c0715f83f90d2f1d", version: "0.0.1", logo: "", custodyGithubUsername: "stephancill",