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 ab6bc57..c7705ad 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 fd13899..b7159d7 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 80bb4ac..623d0d7 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 053695a..4e5437b 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", + }, }, ], },