Skip to content

Commit a19f7af

Browse files
committed
feat(mod/erc-20): add uniswap fee
1 parent de7c32c commit a19f7af

File tree

4 files changed

+170
-61
lines changed

4 files changed

+170
-61
lines changed

examples/api/src/app/api/erc-20/balances/route.ts

-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import {
44
chainByName,
55
getEthUsdPrice,
66
numberWithCommas,
7-
parseTokenParam,
87
parseInfoRequestParams,
98
} from "../lib/utils";
109

+21-41
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import { NextRequest, NextResponse } from "next/server";
2-
import { parseEther } from "viem2";
2+
import { fromHex } from "viem2";
33
import {
44
chainByName,
55
getEthUsdPrice,
6+
getSwapTransaction,
67
parseInfoRequestParams,
78
} from "../lib/utils";
89

910
export async function POST(request: NextRequest) {
1011
const { blockchain, tokenAddress } = parseInfoRequestParams(request);
1112

12-
// TODO: Expose separate execution/receiver addresses
1313
const userAddress = request.nextUrl.searchParams
1414
.get("walletAddress")
1515
?.toLowerCase();
@@ -26,48 +26,28 @@ export async function POST(request: NextRequest) {
2626
const chain = chainByName[blockchain];
2727

2828
const ethPriceUsd = await getEthUsdPrice();
29-
const ethInputAmount = parseEther((buyAmountUsd / ethPriceUsd).toString());
30-
31-
const swapCalldataParams: {
32-
src: string;
33-
dst: string;
34-
amount: string;
35-
from: string;
36-
slippage: string;
37-
receiver: string;
38-
fee?: string;
39-
referrer?: string;
40-
} = {
41-
src: "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", // Native ETH
42-
dst: tokenAddress,
43-
amount: ethInputAmount.toString(),
44-
from: userAddress,
45-
slippage: "5",
46-
receiver: userAddress,
47-
};
48-
49-
// TODO: Use Uniswap to take advantage of configurable fee
50-
// The referrer here is the 1inch referral program recipient (20% of surplus from trade)
51-
// See https://blog.1inch.io/why-should-you-integrate-1inch-apis-into-your-service/
52-
if (process.env.ERC_20_FEE_RECIPIENT) {
53-
swapCalldataParams.referrer = process.env.ERC_20_FEE_RECIPIENT;
54-
}
55-
56-
const swapCalldataRes = await fetch(
57-
`https://api.1inch.dev/swap/v5.2/${chain.id}/swap?${new URLSearchParams(
58-
swapCalldataParams
59-
).toString()}`,
60-
{
61-
headers: {
62-
Authorization: `Bearer ${process.env["ERC_20_1INCH_API_KEY"]}`,
63-
},
64-
}
65-
);
29+
const ethInputAmount = (buyAmountUsd / ethPriceUsd).toString();
30+
31+
const swapRoute = await getSwapTransaction({
32+
blockchain,
33+
ethInputAmountFormatted: ethInputAmount,
34+
outTokenAddress: tokenAddress,
35+
recipientAddress: userAddress,
36+
feePercentageInt: 5,
37+
feeRecipientAddress: process.env.ERC_20_FEE_RECIPIENT,
38+
});
6639

67-
const swapCalldataJson = await swapCalldataRes.json();
40+
const tx = swapRoute.methodParameters;
6841

6942
return NextResponse.json({
70-
transaction: swapCalldataJson.tx,
43+
transaction: {
44+
from: userAddress,
45+
to: tx.to,
46+
value: fromHex(tx.value as `0x${string}`, {
47+
to: "bigint",
48+
}).toString(),
49+
data: tx.calldata,
50+
},
7151
explorer: chain.blockExplorers.default,
7252
});
7353
}

examples/api/src/app/api/erc-20/lib/utils.ts

+129-15
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,16 @@
11
import { FarcasterUser } from "@mod-protocol/core";
2+
import { Protocol } from "@uniswap/router-sdk";
3+
import { Percent, Token, TradeType } from "@uniswap/sdk-core";
4+
import {
5+
AlphaRouter,
6+
AlphaRouterConfig,
7+
CurrencyAmount,
8+
SwapOptions,
9+
SwapType,
10+
nativeOnChain,
11+
} from "@uniswap/smart-order-router";
12+
import { ethers } from "ethers";
13+
import JSBI from "jsbi";
214
import { NextRequest } from "next/server";
315
import { publicActionReverseMirage } from "reverse-mirage";
416
import { createPublicClient, http } from "viem2";
@@ -10,9 +22,21 @@ export function numberWithCommas(x: string | number) {
1022
return parts.join(".");
1123
}
1224

13-
const { ERC_20_AIRSTACK_API_KEY } = process.env;
14-
const AIRSTACK_API_URL = "https://api.airstack.xyz/gql";
15-
const airstackQuery = `
25+
export async function getFollowingHolderInfo({
26+
fid,
27+
tokenAddress,
28+
blockchain,
29+
}: {
30+
fid: string;
31+
tokenAddress: string;
32+
blockchain: string;
33+
}): Promise<{
34+
holders: { user: FarcasterUser; amount: number }[];
35+
holdersCount: number;
36+
}> {
37+
const { ERC_20_AIRSTACK_API_KEY } = process.env;
38+
const AIRSTACK_API_URL = "https://api.airstack.xyz/gql";
39+
const airstackQuery = `
1640
query MyQuery($identity: Identity!, $token_address: Address!, $blockchain: TokenBlockchain, $cursor: String) {
1741
SocialFollowings(
1842
input: {
@@ -58,18 +82,6 @@ query MyQuery($identity: Identity!, $token_address: Address!, $blockchain: Token
5882
}
5983
`;
6084

61-
export async function getFollowingHolderInfo({
62-
fid,
63-
tokenAddress,
64-
blockchain,
65-
}: {
66-
fid: string;
67-
tokenAddress: string;
68-
blockchain: string;
69-
}): Promise<{
70-
holders: { user: FarcasterUser; amount: number }[];
71-
holdersCount: number;
72-
}> {
7385
const acc: any[] = [];
7486

7587
let hasNextPage = true;
@@ -314,6 +326,108 @@ export async function getEthUsdPrice(): Promise<number> {
314326
return ethPriceUsd;
315327
}
316328

329+
export async function getSwapTransaction({
330+
outTokenAddress,
331+
blockchain,
332+
ethInputAmountFormatted,
333+
recipientAddress,
334+
feeRecipientAddress,
335+
feePercentageInt,
336+
}: {
337+
outTokenAddress: string;
338+
blockchain: string;
339+
ethInputAmountFormatted: string;
340+
recipientAddress: string;
341+
feePercentageInt?: number;
342+
feeRecipientAddress?: string;
343+
}) {
344+
const tokenOut = await getUniswapToken({
345+
tokenAddress: outTokenAddress,
346+
blockchain,
347+
});
348+
const chain = chainByName[blockchain];
349+
const provider = new ethers.providers.JsonRpcProvider(
350+
chain.rpcUrls.default.http[0]
351+
);
352+
353+
const router = new AlphaRouter({
354+
chainId: chain.id,
355+
provider,
356+
});
357+
358+
const tokenIn = nativeOnChain(chain.id);
359+
const amountIn = CurrencyAmount.fromRawAmount(
360+
tokenIn,
361+
JSBI.BigInt(
362+
ethers.utils.parseUnits(ethInputAmountFormatted, tokenIn.decimals)
363+
)
364+
);
365+
366+
let swapOptions: SwapOptions = {
367+
type: SwapType.UNIVERSAL_ROUTER,
368+
recipient: recipientAddress,
369+
slippageTolerance: new Percent(5, 100),
370+
deadlineOrPreviousBlockhash: parseDeadline("360"),
371+
fee:
372+
feeRecipientAddress && feePercentageInt
373+
? {
374+
fee: new Percent(feePercentageInt, 100),
375+
recipient: feeRecipientAddress,
376+
}
377+
: undefined,
378+
};
379+
380+
const partialRoutingConfig: Partial<AlphaRouterConfig> = {
381+
protocols: [Protocol.V2, Protocol.V3],
382+
};
383+
384+
const quote = await router.route(
385+
amountIn,
386+
tokenOut,
387+
TradeType.EXACT_INPUT,
388+
swapOptions,
389+
partialRoutingConfig
390+
);
391+
392+
if (!quote) return;
393+
return quote;
394+
}
395+
396+
async function getUniswapToken({
397+
tokenAddress,
398+
blockchain,
399+
}: {
400+
tokenAddress: string;
401+
blockchain: string;
402+
}): Promise<Token> {
403+
const chain = chainByName[blockchain];
404+
const client = createPublicClient({
405+
transport: http(),
406+
chain,
407+
}).extend(publicActionReverseMirage);
408+
409+
const token = await client.getERC20({
410+
erc20: {
411+
address: tokenAddress as `0x${string}`,
412+
chainID: chain.id,
413+
},
414+
});
415+
416+
const uniswapToken = new Token(
417+
chain.id,
418+
tokenAddress,
419+
token.decimals,
420+
token.symbol,
421+
token.name
422+
);
423+
424+
return uniswapToken;
425+
}
426+
427+
function parseDeadline(deadline: string): number {
428+
return Math.floor(Date.now() / 1000) + parseInt(deadline);
429+
}
430+
317431
export function parseInfoRequestParams(request: NextRequest) {
318432
const fid = request.nextUrl.searchParams.get("fid");
319433
const token = request.nextUrl.searchParams.get("token")?.toLowerCase();

mods/erc-20/src/buying.ts

+20-4
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,26 @@ const buy: ModElement[] = [
4141
type: "circular-progress",
4242
},
4343
{
44-
type: "text",
45-
label:
46-
"Buying ~${{refs.buyAmountUsd}} of {{refs.tokenReq.response.data.name}}...",
47-
variant: "secondary",
44+
if: {
45+
value: "{{refs.swapTxDataReq.response.data}}",
46+
match: {
47+
NOT: {
48+
equals: "",
49+
},
50+
},
51+
},
52+
then: {
53+
type: "text",
54+
label:
55+
"Buying ${{refs.buyAmountUsd}} of ${{refs.tokenReq.response.data.symbol}}...",
56+
variant: "secondary",
57+
},
58+
else: {
59+
type: "text",
60+
label:
61+
"Calculating best swap route for ${{refs.tokenReq.response.data.symbol}}...",
62+
variant: "secondary",
63+
},
4864
},
4965
],
5066
},

0 commit comments

Comments
 (0)