Skip to content

Commit 799a873

Browse files
author
openweb3
committed
update
1 parent 229d6a2 commit 799a873

File tree

7 files changed

+461
-412
lines changed

7 files changed

+461
-412
lines changed

demo/app/swap/atoms.ts

Lines changed: 283 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
1+
import { formatUnits, parseUnits } from "viem";
12
import { apiAtom } from "@/lib/atoms";
23
import { PvqProgram } from "@open-web3/pvq";
34
import { atom } from "jotai";
45
import { atomWithQuery } from "jotai-tanstack-query";
56
import { guestSwapInfoProgram } from "./assets/guest-swap-info";
67
import metadata from "./assets/guest-swap-info-metadata.json";
8+
import type { Bytes, Vec, Option, u128 } from "@polkadot/types";
9+
import type { Codec, ITuple } from "@polkadot/types/types";
10+
import { compactStripLength, u8aToString } from "@polkadot/util";
11+
import { atomFamily } from "jotai/utils";
712

813
export const lpProgramAtom = atom<PvqProgram | null>((get) => {
914
const api = get(apiAtom);
@@ -16,27 +21,292 @@ export const poolListAtom = atomWithQuery((get) => ({
1621
queryFn: async () => {
1722
const program = get(lpProgramAtom);
1823
if (!program) throw new Error("Program not initialized");
19-
const lpList = await program.executeQuery(
24+
const lpList = await program.executeQuery<Vec<ITuple<[Bytes, Bytes]>>>(
2025
"entrypoint_list_pools",
2126
undefined,
2227
[]
2328
);
24-
return lpList;
29+
return lpList.map(
30+
([assetId1, assetId2]) => [assetId1.toHex(), assetId2.toHex()] as const
31+
);
2532
},
2633
enabled: !!get(lpProgramAtom),
2734
}));
2835

29-
export const assetsInfoAtom = atomWithQuery((get) => ({
30-
queryKey: ["assetsInfo"],
31-
queryFn: async () => {
32-
const program = get(lpProgramAtom);
36+
export const assetsInfoAtom = atomWithQuery((get) => {
37+
const { data: poolList, isFetched } = get(poolListAtom);
38+
const program = get(lpProgramAtom);
39+
const assetIds = [
40+
...new Set(
41+
poolList?.flatMap(([assetId1, assetId2]) => [assetId1, assetId2])
42+
),
43+
];
44+
45+
return {
46+
queryKey: ["assetsInfo", assetIds],
47+
queryFn: async (): Promise<
48+
{
49+
assetId: string;
50+
decimals: number;
51+
name: string;
52+
symbol: string;
53+
}[]
54+
> => {
55+
if (!program) throw new Error("Program not initialized");
56+
const result = await Promise.all(
57+
assetIds!
58+
.filter((assetId) => assetId !== "0x040d000000")
59+
.map(async (assetId) => {
60+
// console.log("assetId1", assetId);
61+
62+
const res = await program.executeQuery<Option<Codec>>(
63+
"entrypoint_asset_info",
64+
undefined,
65+
[assetId]
66+
);
67+
68+
if (res.isEmpty) return null;
69+
70+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
71+
const asset: any = res.unwrap();
72+
const [, name] = compactStripLength(asset.name);
73+
const [, symbol] = compactStripLength(asset.symbol);
74+
return {
75+
assetId: asset.assetId.toHex() as string,
76+
decimals: asset.decimals.toNumber() as number,
77+
name: u8aToString(name),
78+
symbol: u8aToString(symbol),
79+
};
80+
})
81+
);
82+
83+
return result
84+
.concat({
85+
assetId: "0x040d000000",
86+
decimals: 10,
87+
name: "lcDOT",
88+
symbol: "lcDOT",
89+
})
90+
.filter(Boolean) as {
91+
assetId: string;
92+
decimals: number;
93+
name: string;
94+
symbol: string;
95+
}[];
96+
},
97+
enabled: isFetched && !!program,
98+
};
99+
});
100+
101+
export const assetsInfoFamily = atomFamily((id: string) =>
102+
atom((get) => {
103+
const { data: assetsInfo } = get(assetsInfoAtom);
104+
105+
return assetsInfo?.find((asset) => asset.assetId === id);
106+
})
107+
);
108+
109+
export const poolListDetailAtom = atomWithQuery((get) => {
110+
const { data: poolList, isFetched: isPoolListFetched } = get(poolListAtom);
111+
const { data: assetsInfo, isFetched: isAssetsInfoFetched } =
112+
get(assetsInfoAtom);
113+
114+
return {
115+
queryKey: ["poolListDetail"],
116+
queryFn: async () => {
117+
console.log("poolList", poolList, isPoolListFetched, isAssetsInfoFetched);
118+
return poolList?.map(([assetId1, assetId2]) => {
119+
const asset1 = assetsInfo?.find((asset) => asset.assetId === assetId1);
120+
const asset2 = assetsInfo?.find((asset) => asset.assetId === assetId2);
121+
return {
122+
asset1: asset1!,
123+
asset2: asset2!,
124+
key: `${assetId1}-${assetId2}`,
125+
};
126+
});
127+
},
128+
enabled: isPoolListFetched && isAssetsInfoFetched,
129+
};
130+
});
131+
132+
export const selectedPoolAtom = atom<string | null>(null);
133+
export const selectedPoolInfoAtom = atom((get) => {
134+
const selectedPool = get(selectedPoolAtom);
135+
const { data: poolListDetail } = get(poolListDetailAtom);
136+
return poolListDetail?.find((pool) => pool.key === selectedPool);
137+
});
138+
139+
export const poolSizeAtom = atomWithQuery((get) => {
140+
const selectedPool = get(selectedPoolInfoAtom);
141+
const program = get(lpProgramAtom);
142+
143+
return {
144+
queryKey: ["poolSize", selectedPool],
145+
queryFn: async () => {
146+
if (!program) throw new Error("Program not initialized");
147+
if (!selectedPool) throw new Error("selectedPool is not set");
148+
console.log("querying pool size", selectedPool);
149+
const poolSize = await program.executeQuery<Option<ITuple<[u128, u128]>>>(
150+
"entrypoint_get_liquidity_pool",
151+
undefined,
152+
[selectedPool.asset1.assetId, selectedPool.asset2.assetId]
153+
);
154+
if (poolSize.isEmpty) return;
155+
const [asset1Amount, asset2Amount] = poolSize.unwrap();
156+
const asset1AmountFormatted = formatUnits(
157+
asset1Amount.toBigInt(),
158+
selectedPool.asset1.decimals
159+
);
160+
const asset2AmountFormatted = formatUnits(
161+
asset2Amount.toBigInt(),
162+
selectedPool.asset2.decimals
163+
);
164+
return {
165+
size: [asset1Amount.toBigInt(), asset2Amount.toBigInt()],
166+
asset1: selectedPool.asset1,
167+
asset2: selectedPool.asset2,
168+
asset1Amount: asset1AmountFormatted,
169+
asset2Amount: asset2AmountFormatted,
170+
price: Number(
171+
(
172+
Number(asset2AmountFormatted) / Number(asset1AmountFormatted)
173+
).toFixed(4)
174+
),
175+
};
176+
},
177+
enabled: !!selectedPool && !!program,
178+
};
179+
});
180+
181+
export const getQuoteAmountAtom = atom((get) => {
182+
const program = get(lpProgramAtom);
183+
184+
return async (baseToken: string, quoteToken: string, baseAmount: string) => {
33185
if (!program) throw new Error("Program not initialized");
34-
const assetsInfo = await program.executeQuery(
35-
"entrypoint_asset_info",
186+
187+
const quoteTokenDecimals = get(assetsInfoFamily(quoteToken))?.decimals;
188+
const baseTokenDecimals = get(assetsInfoFamily(baseToken))?.decimals;
189+
if (!quoteTokenDecimals || !baseTokenDecimals)
190+
throw new Error("quoteTokenDecimals or baseTokenDecimals is not set");
191+
192+
const quoteAmount = await program.executeQuery<Option<u128>>(
193+
"entrypoint_quote_price_exact_tokens_for_tokens",
36194
undefined,
37-
[]
195+
[baseToken, quoteToken, parseUnits(baseAmount, baseTokenDecimals)]
38196
);
39-
return assetsInfo;
40-
},
41-
enabled: !!get(lpProgramAtom),
42-
}));
197+
198+
console.log(
199+
`Get ${quoteToken} amount:`,
200+
baseToken,
201+
quoteToken,
202+
baseAmount,
203+
quoteAmount.toHuman()
204+
);
205+
206+
if (quoteAmount.isEmpty) return "0";
207+
return formatUnits(
208+
quoteAmount.unwrap().toBigInt() as bigint,
209+
quoteTokenDecimals
210+
);
211+
};
212+
});
213+
214+
export const getBaseAmountAtom = atom((get) => {
215+
const program = get(lpProgramAtom);
216+
217+
return async (baseToken: string, quoteToken: string, quoteAmount: string) => {
218+
if (!program) throw new Error("Program not initialized");
219+
220+
const quoteTokenDecimals = get(assetsInfoFamily(quoteToken))?.decimals;
221+
const baseTokenDecimals = get(assetsInfoFamily(baseToken))?.decimals;
222+
if (!quoteTokenDecimals || !baseTokenDecimals)
223+
throw new Error("quoteTokenDecimals or baseTokenDecimals is not set");
224+
225+
const baseAmount = await program.executeQuery<Option<u128>>(
226+
"entrypoint_quote_price_tokens_for_exact_tokens",
227+
undefined,
228+
[baseToken, quoteToken, parseUnits(quoteAmount, quoteTokenDecimals)]
229+
);
230+
231+
console.log(
232+
`Get ${baseToken} amount:`,
233+
baseToken,
234+
quoteToken,
235+
quoteAmount,
236+
baseAmount.toHuman()
237+
);
238+
239+
if (baseAmount.isEmpty) return "0";
240+
241+
return formatUnits(
242+
baseAmount.unwrap().toBigInt() as bigint,
243+
baseTokenDecimals
244+
);
245+
};
246+
});
247+
248+
export const getReceiveAmountAtom = atom((get) => {
249+
return async (
250+
sellToken: string,
251+
buyToken: string,
252+
amount: string,
253+
isSell: boolean
254+
) => {
255+
const selectedPoolInfo = get(selectedPoolInfoAtom);
256+
const { refetch } = get(poolSizeAtom);
257+
const { data: poolSize } = await refetch();
258+
if (!poolSize?.size) throw new Error("poolSize is not set");
259+
if (!selectedPoolInfo) throw new Error("selectedPoolInfo is not set");
260+
const baseAssetId = selectedPoolInfo.asset1.assetId;
261+
const quoteAssetId = selectedPoolInfo.asset2.assetId;
262+
const isSwaped =
263+
sellToken === baseAssetId && buyToken === quoteAssetId
264+
? false
265+
: sellToken === quoteAssetId && buyToken === baseAssetId
266+
? true
267+
: undefined;
268+
269+
const parsedAmount = parseUnits(
270+
amount,
271+
!isSwaped
272+
? isSell
273+
? selectedPoolInfo.asset1.decimals
274+
: selectedPoolInfo.asset2.decimals
275+
: isSell
276+
? selectedPoolInfo.asset2.decimals
277+
: selectedPoolInfo.asset1.decimals
278+
);
279+
const receiveDecimals = !isSwaped
280+
? isSell
281+
? selectedPoolInfo.asset2.decimals
282+
: selectedPoolInfo.asset1.decimals
283+
: isSell
284+
? selectedPoolInfo.asset1.decimals
285+
: selectedPoolInfo.asset2.decimals;
286+
287+
if (parsedAmount <= BigInt(0)) return "0";
288+
289+
const isBaseToQuote =
290+
sellToken === baseAssetId && buyToken === quoteAssetId;
291+
const isQuoteToBase =
292+
sellToken === quoteAssetId && buyToken === baseAssetId;
293+
294+
if (!isBaseToQuote && !isQuoteToBase) {
295+
throw new Error("Token pair does not match the pool");
296+
}
297+
298+
const reserveIn = isBaseToQuote ? poolSize?.size[0] : poolSize?.size[1];
299+
const reserveOut = isBaseToQuote ? poolSize?.size[1] : poolSize?.size[0];
300+
301+
if (isSell) {
302+
const dx = parsedAmount;
303+
const dy = (dx * reserveOut) / (reserveIn + dx);
304+
return formatUnits(dy, receiveDecimals);
305+
} else {
306+
const dy = parsedAmount;
307+
if (dy >= reserveOut) return "Infinity"; // 超出池子
308+
const dx = (dy * reserveIn) / (reserveOut - dy);
309+
return formatUnits(dx, receiveDecimals);
310+
}
311+
};
312+
});

0 commit comments

Comments
 (0)