diff --git a/apps/ui/package.json b/apps/ui/package.json index a87d9fba5..82e4419b5 100644 --- a/apps/ui/package.json +++ b/apps/ui/package.json @@ -50,12 +50,12 @@ "@sentry/types": "^7.9.0", "@solana/spl-token": "^0.3.5", "@solana/web3.js": "^1.62.0", - "@swim-io/aptos": "^0.40.0", - "@swim-io/core": "^0.40.0", - "@swim-io/evm": "^0.40.0", + "@swim-io/aptos": "workspace:^", + "@swim-io/core": "workspace:^", + "@swim-io/evm": "workspace:^", "@swim-io/evm-contracts": "^0.40.0", "@swim-io/pool-math": "^0.40.0", - "@swim-io/solana": "^0.40.0", + "@swim-io/solana": "workspace:^", "@swim-io/solana-contracts": "^0.40.0", "@swim-io/token-projects": "^0.40.0", "@swim-io/utils": "^0.40.0", diff --git a/apps/ui/src/hooks/interaction/useSolanaPoolOperationsMutation.ts b/apps/ui/src/hooks/interaction/useSolanaPoolOperationsMutation.ts index c848f5d2f..915a51ab8 100644 --- a/apps/ui/src/hooks/interaction/useSolanaPoolOperationsMutation.ts +++ b/apps/ui/src/hooks/interaction/useSolanaPoolOperationsMutation.ts @@ -17,7 +17,6 @@ import { } from "../solana"; export const useSolanaPoolOperationsMutation = () => { - const { env } = useEnvironment(); const config = useEnvironment(selectConfig, shallow); const { pools } = config; const { data: splTokenAccounts = [] } = useUserSolanaTokenAccountsQuery(); @@ -65,7 +64,6 @@ export const useSolanaPoolOperationsMutation = () => { let inputTxId = inputState.txId; if (inputTxId === null) { inputTxId = await doSingleSolanaPoolOperation( - env, solanaClient, wallet, splTokenAccounts, @@ -97,7 +95,6 @@ export const useSolanaPoolOperationsMutation = () => { inputTx, ); const outputTxId = await doSingleSolanaPoolOperation( - env, solanaClient, wallet, splTokenAccounts, diff --git a/apps/ui/src/hooks/swim/usePoolMaths.ts b/apps/ui/src/hooks/swim/usePoolMaths.ts index 4bf1e6c08..91a13962c 100644 --- a/apps/ui/src/hooks/swim/usePoolMaths.ts +++ b/apps/ui/src/hooks/swim/usePoolMaths.ts @@ -71,13 +71,13 @@ const getPoolMath = ({ // lpFee const humanLpFee = atomicToHuman( - new Decimal(poolState.lpFee), + new Decimal(poolState.lpFee.value), poolSpec.feeDecimals, ); // governanceFee const humanGovernanceFee = atomicToHuman( - new Decimal(poolState.governanceFee), + new Decimal(poolState.governanceFee.value), poolSpec.feeDecimals, ); diff --git a/apps/ui/src/hooks/swim/usePoolStateQueries.ts b/apps/ui/src/hooks/swim/usePoolStateQueries.ts index 629a970b6..c102815d1 100644 --- a/apps/ui/src/hooks/swim/usePoolStateQueries.ts +++ b/apps/ui/src/hooks/swim/usePoolStateQueries.ts @@ -3,13 +3,11 @@ import { EVM_ECOSYSTEMS } from "@swim-io/evm"; import { SOLANA_ECOSYSTEM_ID } from "@swim-io/solana"; import type { UseQueryResult } from "react-query"; import { useQueries } from "react-query"; -import shallow from "zustand/shallow.js"; import type { PoolSpec } from "../../config"; -import { selectConfig } from "../../core/selectors"; import { useEnvironment } from "../../core/store"; import type { PoolState } from "../../models"; -import { getEvmPoolState, getSolanaPoolState } from "../../models"; +import { getLegacySolanaPoolState } from "../../models"; import { useGetEvmClient } from "../evm"; import { useSolanaClient } from "../solana"; @@ -17,8 +15,7 @@ export const usePoolStateQueries = ( poolSpecs: readonly PoolSpec[], ): readonly UseQueryResult[] => { const { env } = useEnvironment(); - const { tokens } = useEnvironment(selectConfig, shallow); - const getEvmConnection = useGetEvmClient(); + const getEvmClient = useGetEvmClient(); const solanaClient = useSolanaClient(); return useQueries( @@ -27,23 +24,21 @@ export const usePoolStateQueries = ( queryFn: async () => { const { ecosystem } = poolSpec; if (ecosystem === SOLANA_ECOSYSTEM_ID) { - return await getSolanaPoolState(solanaClient, poolSpec); + if (poolSpec.isLegacyPool) { + return await getLegacySolanaPoolState(solanaClient, poolSpec); + } + return await solanaClient.getPoolState(poolSpec.id); } if (ecosystem === APTOS_ECOSYSTEM_ID) { return null; // TODO aptos } - const evmConnection = getEvmConnection(ecosystem); + const evmClient = getEvmClient(ecosystem); const routingContractAddress = EVM_ECOSYSTEMS[ecosystem].chains[env]?.routingContractAddress ?? null; if (routingContractAddress === null) { return null; } - return await getEvmPoolState( - evmConnection, - poolSpec, - tokens, - routingContractAddress, - ); + return await evmClient.getPoolState(poolSpec.id); }, })), ) as readonly UseQueryResult[]; diff --git a/apps/ui/src/models/solana/deserializeLegacySolanaPoolState.test.ts b/apps/ui/src/models/solana/deserializeLegacySolanaPoolState.test.ts new file mode 100644 index 000000000..61306bcbe --- /dev/null +++ b/apps/ui/src/models/solana/deserializeLegacySolanaPoolState.test.ts @@ -0,0 +1,117 @@ +import { Buffer } from "buffer"; + +import { PublicKey } from "@solana/web3.js"; +import BN from "bn.js"; + +import type { LegacySolanaPoolState } from "./deserializeLegacySolanaPoolState"; +import { deserializeLegacySolanaPoolState } from "./deserializeLegacySolanaPoolState"; + +describe("deserializeLegacySolanaPoolState", () => { + it("deserializes a SolanaPoolState", () => { + const serialized = Buffer.from( + "00000100000000000000000000000000000000e8030000000000000000000000000000002c01000064000000990e9632b0b9f2e636feb3f0a4220f8aadf9677b451c982a4151af42e0362e8800c6fa7af3bedbad3a3d65f36aabc97431b1bbe4c2d2f6e0e47ca60203452f5d61ce010e60afedb22717bd63192f54145a3f965a33bb82d2c7029eb2ce1e20826487f81d7f931ba1c5db9f5a8b2ac5e149ef9c76d9cf196615bd21163316e8c410bdd7aa20228a7bc21e67ddfe78d5d89b986d4bf5c8d5dc9d4574d81ab11e5a02012262c2067049b5c6d6a6869e7a37bbee162637f78192c3b15e8427676a422574616a65b31ff1d8f707eb279bf8a729a6644151b16d72f9694af6ae499881ed02020202000048ccc8aa094ba7b3495776e123587f2454a935671548ccdc3f4311a9febbdd18fb56a83f5d24d5e7513f96b8c24bff58e7259e92f2fd6f01162f9b0b5188d23e32bf5157ba942716dbab775cde82f881ededa5a96b325714e2bef602679dc3cd1205cdb06ade7ab0c78b50a6e7cc2dd83edfa10423348951b7ce231b6c920334bfcf845603efc68ddea00872ad53de92ed69227e373bdca1a21f78782ec87fec7b90e07d2a4fd0d055d08430d9524a755cacd7a7a531ed91705e8b823e59d820cf609300f5b15b7009876930926f1b5c4a6ecdbc035219127e8ff47ef369abbc7ef4d44674e963fe6e94097d729f1c29c382a3ca684cfd454347f97ee142959e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008d8fcd9e56d407000000000000000000", + "hex", + ); + + const expected: LegacySolanaPoolState = { + ecosystem: "solana", + bump: 0, + isPaused: false, + ampFactor: { + initialValue: { + value: new BN(1), + decimals: 0, + }, + initialTs: new BN(0), + targetValue: { + value: new BN(1000), + decimals: 0, + }, + targetTs: new BN(0), + }, + lpFee: 300, + governanceFee: 100, + lpMintKey: new PublicKey("BJUH9GJLaMSLV1E7B3SQLCy9eCfyr6zsrwGcpS2MkqR1"), + lpDecimalEqualizer: 0, + tokenMintKeys: [ + new PublicKey("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"), + new PublicKey("Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB"), + new PublicKey("A9mUU4qviSctJVPJdBJWkb28deg915LYJKrzQ19ji3FM"), + new PublicKey("Dn4noZ5jgGfkntzcQSUZ8czkreiZ1ForXYoV2H8Dm7S1"), + new PublicKey("5RpUwQ8wtdPCZHhu6MERp2RGrpobsbZ6MH5dDHkUjs2"), + new PublicKey("8qJSyQprMC57TWKaYEmetUR3UUiTP2M3hXdcvFhkZdmv"), + ], + tokenDecimalEqualizers: [2, 2, 2, 2, 0, 0], + tokenKeys: [ + new PublicKey("5uBU2zUG8xTLA6XwwcTFWib1p7EjCBzWbiy44eVASTfV"), + new PublicKey("Hv7yPYnGs6fpN3o1NZvkima9mKDrRDJtNxf23oKLCjau"), + new PublicKey("4R6b4aibi46JzAnuA8ZWXrHAsR1oZBTZ8dqkuer3LsbS"), + new PublicKey("2DMUL42YEb4g1HAKXhUxL3Yjfgoj4VvRqKwheorfFcPV"), + new PublicKey("DukQAFyxR41nbbq2FBUDMyrtF2CRmWBREjZaTVj4u9As"), + new PublicKey("9KMH3p8cUocvQRbJfKRAStKG52xCCWNmEPsJm5gc8fzw"), + ], + governanceKey: new PublicKey( + "ExWoeFoyYwCFx2cp9PZzj4eYL5fsDEFQEpC8REsksNpb", + ), + governanceFeeKey: new PublicKey( + "9Yau6DnqYasBUKcyxQJQZqThvUnqZ32ZQuUCcC2AdT9P", + ), + preparedGovernanceKey: PublicKey.default, + governanceTransitionTs: new BN(0), + preparedLpFee: 0, + preparedGovernanceFee: 0, + feeTransitionTs: new BN(0), + previousDepth: new BN(2203793333522317), + }; + const decoded = deserializeLegacySolanaPoolState(6, serialized); + expect(decoded.bump).toBe(expected.bump); + expect(decoded.isPaused).toBe(expected.isPaused); + expect( + decoded.ampFactor.initialValue.value.eq( + expected.ampFactor.initialValue.value, + ), + ).toBe(true); + expect(decoded.ampFactor.initialValue.decimals).toBe( + expected.ampFactor.initialValue.decimals, + ); + expect(decoded.ampFactor.initialTs.eq(expected.ampFactor.initialTs)).toBe( + true, + ); + expect( + decoded.ampFactor.targetValue.value.eq( + expected.ampFactor.targetValue.value, + ), + ).toBe(true); + expect(decoded.ampFactor.targetValue.decimals).toBe( + expected.ampFactor.targetValue.decimals, + ); + expect(decoded.ampFactor.targetTs.eq(expected.ampFactor.targetTs)).toBe( + true, + ); + expect(decoded.lpFee).toBe(expected.lpFee); + expect(decoded.governanceFee).toBe(expected.governanceFee); + expect(decoded.lpMintKey).toStrictEqual(expected.lpMintKey); + expect(decoded.lpDecimalEqualizer).toBe(expected.lpDecimalEqualizer); + decoded.tokenMintKeys.forEach((tokenMintKey, i) => { + expect(tokenMintKey).toStrictEqual(expected.tokenMintKeys[i]); + }); + decoded.tokenDecimalEqualizers.forEach((tokenDecimalEqualizer, i) => { + expect(tokenDecimalEqualizer).toBe(expected.tokenDecimalEqualizers[i]); + }); + decoded.tokenKeys.forEach((tokenKey, i) => { + expect(tokenKey).toStrictEqual(expected.tokenKeys[i]); + }); + expect(decoded.governanceKey).toStrictEqual(expected.governanceKey); + expect(decoded.governanceFeeKey).toStrictEqual(expected.governanceFeeKey); + expect(decoded.preparedGovernanceKey).toStrictEqual( + expected.preparedGovernanceKey, + ); + expect( + decoded.governanceTransitionTs.eq(expected.governanceTransitionTs), + ).toBe(true); + expect(decoded.preparedLpFee).toBe(expected.preparedLpFee); + expect(decoded.preparedGovernanceFee).toBe(expected.preparedGovernanceFee); + expect(decoded.feeTransitionTs.eq(expected.feeTransitionTs)).toBe(true); + expect(decoded.previousDepth.eq(expected.previousDepth)).toBe(true); + }); +}); diff --git a/apps/ui/src/models/solana/deserializeLegacySolanaPoolState.ts b/apps/ui/src/models/solana/deserializeLegacySolanaPoolState.ts new file mode 100644 index 000000000..74ce53f55 --- /dev/null +++ b/apps/ui/src/models/solana/deserializeLegacySolanaPoolState.ts @@ -0,0 +1,74 @@ +import { + array, + bool, + i64, + publicKey, + struct, + u128, + u32, + u8, +} from "@project-serum/borsh"; +import type { Layout } from "@project-serum/borsh"; +import type { SolanaEcosystemId, SolanaPoolState } from "@swim-io/solana"; +import { SOLANA_ECOSYSTEM_ID, ampFactor } from "@swim-io/solana"; + +type U8 = (property?: string) => Layout; + +export interface LegacySolanaPoolState + extends Omit< + SolanaPoolState, + | "governanceFee" + | "lpFee" + | "pauseKey" + | "preparedGovernanceFee" + | "preparedLpFee" + > { + readonly governanceFee: number; + readonly lpFee: number; + readonly preparedGovernanceFee: number; + readonly preparedLpFee: number; + readonly ecosystem: SolanaEcosystemId; +} + +export const solanaPool = ( + numberOfTokens: number, + property = "solanaPool", +): Layout> => + struct( + [ + u8("bump"), + bool("isPaused"), + ampFactor(), + u32("lpFee"), + u32("governanceFee"), + publicKey("lpMintKey"), + u8("lpDecimalEqualizer"), + array(publicKey(), numberOfTokens, "tokenMintKeys"), + array((u8 as U8)(), numberOfTokens, "tokenDecimalEqualizers"), + array(publicKey(), numberOfTokens, "tokenKeys"), + publicKey("governanceKey"), + publicKey("governanceFeeKey"), + publicKey("preparedGovernanceKey"), + i64("governanceTransitionTs"), + u32("preparedLpFee"), + u32("preparedGovernanceFee"), + i64("feeTransitionTs"), + u128("previousDepth"), + ], + property, + ); + +export const deserializeLegacySolanaPoolState = ( + numberOfTokens: number, + accountData: Buffer, +): LegacySolanaPoolState => { + const layout = solanaPool(numberOfTokens); + if (accountData.length !== layout.span) { + throw new Error("Incorrect account data length"); + } + const decoded = layout.decode(accountData); + return { + ...decoded, + ecosystem: SOLANA_ECOSYSTEM_ID, + }; +}; diff --git a/apps/ui/src/models/solana/index.ts b/apps/ui/src/models/solana/index.ts index a7829508c..2f79d6c02 100644 --- a/apps/ui/src/models/solana/index.ts +++ b/apps/ui/src/models/solana/index.ts @@ -1,2 +1,3 @@ +export * from "./deserializeLegacySolanaPoolState"; export * from "./findOrCreateSplTokenAccount"; export * from "./getSwimUsdBalanceChange"; diff --git a/apps/ui/src/models/swim/SwimDefiInstructor.ts b/apps/ui/src/models/swim/SwimDefiInstructor.ts index e249446fc..5bd443acb 100644 --- a/apps/ui/src/models/swim/SwimDefiInstructor.ts +++ b/apps/ui/src/models/swim/SwimDefiInstructor.ts @@ -1,7 +1,6 @@ import { TOKEN_PROGRAM_ID, createApproveInstruction } from "@solana/spl-token"; import type { AccountMeta, Transaction } from "@solana/web3.js"; import { Keypair, PublicKey, TransactionInstruction } from "@solana/web3.js"; -import type { Env } from "@swim-io/core"; import { SOLANA_ECOSYSTEM_ID, createMemoIx, @@ -35,7 +34,6 @@ import type { } from "./operation"; export class SwimDefiInstructor { - private readonly env: Env; private readonly solanaClient: SolanaClient; private readonly signer: SolanaWalletAdapter; private readonly programId: PublicKey; @@ -49,7 +47,6 @@ export class SwimDefiInstructor { private userTokenAccounts: readonly PublicKey[]; public constructor( - env: Env, solanaClient: SolanaClient, signer: SolanaWalletAdapter, swimProgramAddress: string, @@ -72,7 +69,6 @@ export class SwimDefiInstructor { "Number of user token accounts does not match number of token mints", ); } - this.env = env; this.solanaClient = solanaClient; this.signer = signer; this.programId = new PublicKey(swimProgramAddress); diff --git a/apps/ui/src/models/swim/SwimInitializer.ts b/apps/ui/src/models/swim/SwimInitializer.ts index 710273cba..219df5729 100644 --- a/apps/ui/src/models/swim/SwimInitializer.ts +++ b/apps/ui/src/models/swim/SwimInitializer.ts @@ -15,7 +15,7 @@ import { SystemProgram, TransactionInstruction, } from "@solana/web3.js"; -import { createTx, swimPool } from "@swim-io/solana"; +import { createTx } from "@swim-io/solana"; import type { DecimalBN, SolanaClient, @@ -23,6 +23,8 @@ import type { } from "@swim-io/solana"; import { chunks } from "@swim-io/utils"; +import { solanaPool } from "../solana"; + import { SwimInstruction, initInstruction } from "./instructions"; export class SwimInitializer { @@ -148,7 +150,7 @@ export class SwimInitializer { if (!this.stateAccount) { throw new Error("No state account"); } - const layout = swimPool(this.numberOfTokens); + const layout = solanaPool(this.numberOfTokens); const lamports = await this.solanaClient.connection.getMinimumBalanceForRentExemption( layout.span, diff --git a/apps/ui/src/models/swim/deserializeSwimPoolV2.ts b/apps/ui/src/models/swim/deserializeSwimPoolV2.ts deleted file mode 100644 index 23c145889..000000000 --- a/apps/ui/src/models/swim/deserializeSwimPoolV2.ts +++ /dev/null @@ -1,44 +0,0 @@ -import type { Buffer } from "buffer"; - -import type { BN, Idl } from "@project-serum/anchor"; -import { BorshAccountsCoder } from "@project-serum/anchor"; -import type { PublicKey } from "@solana/web3.js"; -import type { AmpFactor, SwimPoolState } from "@swim-io/solana"; -import { idl } from "@swim-io/solana-contracts"; - -interface Value { - readonly value: number; -} - -interface TwoPoolState { - readonly nonce: number; - readonly lpMintKey: PublicKey; - readonly lpDecimalEqualizer: number; - readonly tokenMintKeys: readonly PublicKey[]; - readonly tokenDecimalEqualizers: readonly number[]; - readonly tokenKeys: readonly PublicKey[]; - readonly isPaused: boolean; - readonly ampFactor: AmpFactor; - readonly lpFee: Value; - readonly governanceFee: Value; - readonly governanceKey: PublicKey; - readonly governanceFeeKey: PublicKey; - readonly preparedGovernanceKey: PublicKey; - readonly governanceTransitionTs: BN; - readonly preparedLpFee: Value; - readonly preparedGovernanceFee: Value; - readonly feeTransitionTs: BN; - readonly previousDepth: BN; -} - -export const deserializeSwimPoolV2 = (poolData: Buffer): SwimPoolState => { - const PoolDecoder = new BorshAccountsCoder(idl.twoPool as Idl); - const poolState: TwoPoolState = PoolDecoder.decode("TwoPool", poolData); - return { - ...poolState, - lpFee: poolState.lpFee.value, - governanceFee: poolState.governanceFee.value, - preparedLpFee: poolState.preparedLpFee.value, - preparedGovernanceFee: poolState.preparedGovernanceFee.value, - }; -}; diff --git a/apps/ui/src/models/swim/doSingleSolanaPoolOperation.ts b/apps/ui/src/models/swim/doSingleSolanaPoolOperation.ts index d77634e37..a3f0a1eef 100644 --- a/apps/ui/src/models/swim/doSingleSolanaPoolOperation.ts +++ b/apps/ui/src/models/swim/doSingleSolanaPoolOperation.ts @@ -1,4 +1,3 @@ -import type { Env } from "@swim-io/core"; import { SOLANA_ECOSYSTEM_ID, findTokenAccountForMint, @@ -23,10 +22,9 @@ import { SwimDefiInstruction } from "./instructions"; import type { Interaction } from "./interaction"; import type { OperationSpec } from "./operation"; import type { TokensByPoolId } from "./pool"; -import { getSolanaPoolState } from "./pool"; +import { getLegacySolanaPoolState } from "./pool"; export const doSingleSolanaPoolOperation = async ( - env: Env, solanaClient: SolanaClient, wallet: SolanaWalletAdapter, splTokenAccounts: readonly TokenAccount[], @@ -38,7 +36,7 @@ export const doSingleSolanaPoolOperation = async ( if (walletAddress === null) { throw new Error("Missing Solana wallet"); } - const poolState = await getSolanaPoolState(solanaClient, poolSpec); + const poolState = await getLegacySolanaPoolState(solanaClient, poolSpec); if (poolState === null) { throw new Error("Missing pool state"); } @@ -56,7 +54,6 @@ export const doSingleSolanaPoolOperation = async ( findTokenAccountForMint(mint, walletAddress, splTokenAccounts), ); const instructor = new SwimDefiInstructor( - env, solanaClient, wallet, poolSpec.contract, diff --git a/apps/ui/src/models/swim/doSingleSolanaPoolOperationV2.ts b/apps/ui/src/models/swim/doSingleSolanaPoolOperationV2.ts index faa8e64cc..d9bfb6c2b 100644 --- a/apps/ui/src/models/swim/doSingleSolanaPoolOperationV2.ts +++ b/apps/ui/src/models/swim/doSingleSolanaPoolOperationV2.ts @@ -20,7 +20,6 @@ import { getSolanaTokenDetails } from "../../config"; import { SwimDefiInstruction } from "./instructions"; import type { OperationSpec } from "./operation"; import type { TokensByPoolId } from "./pool"; -import { getSolanaPoolState } from "./pool"; const getApproveAndRevokeIxs = async ( splToken: Program, @@ -85,10 +84,7 @@ export const doSingleSolanaPoolOperationV2 = async ({ throw new Error("Invalid wallet"); } const walletAddress = walletPublicKey.toBase58(); - const poolState = await getSolanaPoolState(solanaClient, poolSpec); - if (poolState === null) { - throw new Error("Missing pool state"); - } + const poolState = await solanaClient.getPoolState(poolSpec.id); const lpTokenMintAddress = getSolanaTokenDetails(poolTokens.lpToken).address; const userLpAccount = findTokenAccountForMint( lpTokenMintAddress, @@ -98,6 +94,7 @@ export const doSingleSolanaPoolOperationV2 = async ({ const tokenMintAddresses = poolTokens.tokens.map( (token) => getSolanaTokenDetails(token).address, ); + const poolTokenAccounts = [...poolSpec.tokenAccounts.values()]; const userTokenAccounts = tokenMintAddresses.map((mint) => findTokenAccountForMint(mint, walletAddress, splTokenAccounts), ); @@ -123,9 +120,9 @@ export const doSingleSolanaPoolOperationV2 = async ({ throw new Error("Invalid user token account"); } const commonAccounts = { - poolTokenAccount0: poolState.tokenKeys[0], - poolTokenAccount1: poolState.tokenKeys[1], - lpMint: poolState.lpMintKey, + poolTokenAccount0: new PublicKey(poolTokenAccounts[0]), + poolTokenAccount1: new PublicKey(poolTokenAccounts[1]), + lpMint: new PublicKey(lpTokenMintAddress), governanceFee: poolState.governanceFeeKey, userTransferAuthority: userTransferAuthority.publicKey, userTokenAccount0: userTokenAccount0, diff --git a/apps/ui/src/models/swim/pool.test.ts b/apps/ui/src/models/swim/pool.test.ts index 221fdef2e..536b90baa 100644 --- a/apps/ui/src/models/swim/pool.test.ts +++ b/apps/ui/src/models/swim/pool.test.ts @@ -1,27 +1,17 @@ import type solana from "@solana/web3.js"; import { Env } from "@swim-io/core"; -import type { EvmClient, EvmTx } from "@swim-io/evm"; +import type { EvmTx } from "@swim-io/evm"; import { EvmEcosystemId } from "@swim-io/evm"; -import { Routing__factory } from "@swim-io/evm-contracts"; import type { SolanaTx } from "@swim-io/solana"; import { SOLANA_ECOSYSTEM_ID } from "@swim-io/solana"; -import { findOrThrow } from "@swim-io/utils"; -import Decimal from "decimal.js"; import type { ethers } from "ethers"; -import { BigNumber } from "ethers"; import { mock, mockDeep } from "jest-mock-extended"; -import type { Config, EvmPoolSpec } from "../../config"; -import { - CONFIGS, - TESTNET_POOLS_FOR_RESTRUCTURE, - TESTNET_SWIMUSD, - TESTNET_TOKENS, - TESTNET_TOKENS_FOR_RESTRUCTURE, -} from "../../config"; +import type { Config } from "../../config"; +import { CONFIGS } from "../../config"; import { parsedWormholeRedeemEvmUnlockWrappedTx } from "../../fixtures/solana/txs"; -import { getEvmPoolState, getTokensByPool, isPoolTx } from "./pool"; +import { getTokensByPool, isPoolTx } from "./pool"; describe("Pool tests", () => { describe("getTokensByPool", () => { @@ -85,68 +75,4 @@ describe("Pool tests", () => { expect(isPoolTx(contractAddress, tx)).toBe(true); }); }); - - describe("getEvmPoolState", () => { - const MOCK_STATE = { - paused: false, - balances: [ - [ - "0x4873edbb0B4b5b48A6FBe50CacB85e58D0b62ab5", - BigNumber.from("2087941202"), - ], - [ - "0x45B167CF5b14007Ca0490dCfB7C4B870Ec0C0Aa6", - BigNumber.from("2052006916"), - ], - [ - "0x996f42BdB0CB71F831C2eFB05Ac6d0d226979e5B", - BigNumber.from("2117774978"), - ], - ], - totalLpSupply: [ - "0xf3eb1180A64827A602A7e02883b7299191527073", - BigNumber.from("1968249268"), - ], - ampFactor: [BigNumber.from("1000"), 3], - lpFee: [BigNumber.from("300"), 6], - governanceFee: [BigNumber.from("100"), 6], - }; - - beforeEach(() => { - Routing__factory.connect = jest.fn().mockReturnValue({ - getPoolStates: () => Promise.resolve([MOCK_STATE]), - }); - }); - - it("should return EVM pool state in human decimal", async () => { - const ethereumPool = findOrThrow( - TESTNET_POOLS_FOR_RESTRUCTURE, - (poolSpec) => poolSpec.id === "testnet-ethereum-usdc-usdt", - ); - const tokens = [ - ...TESTNET_TOKENS, - TESTNET_SWIMUSD, - ...TESTNET_TOKENS_FOR_RESTRUCTURE, - ]; - const state = await getEvmPoolState( - {} as EvmClient, - ethereumPool as EvmPoolSpec, - tokens, - "MOCK_ADDRESS", - ); - expect(state).toEqual({ - ampFactor: new Decimal("1"), - balances: [ - new Decimal("2087.941202"), - new Decimal("2052.006916"), - new Decimal("2117.774978"), - ], - ecosystem: "ethereum", - governanceFee: new Decimal("0.0001"), - isPaused: false, - lpFee: new Decimal("0.0003"), - totalLpSupply: new Decimal("1968.249268"), - }); - }); - }); }); diff --git a/apps/ui/src/models/swim/pool.ts b/apps/ui/src/models/swim/pool.ts index 636fd6ca6..9ffde79b5 100644 --- a/apps/ui/src/models/swim/pool.ts +++ b/apps/ui/src/models/swim/pool.ts @@ -1,47 +1,33 @@ import { PublicKey } from "@solana/web3.js"; -import type { EvmClient, EvmEcosystemId } from "@swim-io/evm"; +import type { PoolState as BasePoolState } from "@swim-io/core"; +import type { EvmEcosystemId } from "@swim-io/evm"; import { isEvmEcosystemId } from "@swim-io/evm"; -import { Routing__factory } from "@swim-io/evm-contracts"; -import { - SOLANA_ECOSYSTEM_ID, - deserializeSwimPool, - isSolanaTx, -} from "@swim-io/solana"; +import { SOLANA_ECOSYSTEM_ID, isSolanaTx } from "@swim-io/solana"; import type { + SolanaPoolState as BaseSolanaPoolState, SolanaClient, SolanaEcosystemId, SolanaTx, - SwimPoolState, } from "@swim-io/solana"; import type { ReadonlyRecord } from "@swim-io/utils"; -import { atomicToHuman, findOrThrow } from "@swim-io/utils"; -import Decimal from "decimal.js"; +import { findOrThrow } from "@swim-io/utils"; -import { bnOrBigNumberToDecimal } from "../../amounts"; import type { Config, - EvmPoolSpec, PoolSpec, SolanaPoolSpec, TokenConfig, } from "../../config"; -import { getTokenDetailsForEcosystem } from "../../config"; import type { Tx } from "../crossEcosystem"; +import { deserializeLegacySolanaPoolState } from "../solana"; -import { deserializeSwimPoolV2 } from "./deserializeSwimPoolV2"; - -export interface SolanaPoolState extends SwimPoolState { +export interface SolanaPoolState + extends Omit { readonly ecosystem: SolanaEcosystemId; } -export interface EvmPoolState { +export interface EvmPoolState extends BasePoolState { readonly ecosystem: EvmEcosystemId; - readonly isPaused: boolean; - readonly balances: readonly Decimal[]; - readonly totalLpSupply: Decimal; - readonly ampFactor: Decimal; - readonly lpFee: Decimal; - readonly governanceFee: Decimal; } export type PoolState = SolanaPoolState | EvmPoolState; @@ -84,10 +70,13 @@ export const isPoolTx = ( export const isSolanaPool = (pool: PoolSpec): pool is SolanaPoolSpec => pool.ecosystem === SOLANA_ECOSYSTEM_ID; -export const getSolanaPoolState = async ( +export const getLegacySolanaPoolState = async ( solanaClient: SolanaClient, poolSpec: SolanaPoolSpec, ): Promise => { + if (!poolSpec.isLegacyPool) { + throw new Error("Invalid pool version"); + } const numberOfTokens = poolSpec.tokens.length; const accountInfo = await solanaClient.connection.getAccountInfo( new PublicKey(poolSpec.address), @@ -95,60 +84,16 @@ export const getSolanaPoolState = async ( if (accountInfo === null) { return null; } - const swimPool = poolSpec.isLegacyPool - ? deserializeSwimPool(numberOfTokens, accountInfo.data) - : deserializeSwimPoolV2(accountInfo.data); - - return { - ...swimPool, - ecosystem: SOLANA_ECOSYSTEM_ID, - }; -}; - -export const getEvmPoolState = async ( - evmClient: EvmClient, - poolSpec: EvmPoolSpec, - tokens: readonly TokenConfig[], - routingContractAddress: string, -): Promise => { - const { ecosystem, address } = poolSpec; - const contract = Routing__factory.connect( - routingContractAddress, - evmClient.provider, - ); - const lpToken = findOrThrow(tokens, ({ id }) => id === poolSpec.lpToken); - const poolTokens = poolSpec.tokens.map((tokenId) => - findOrThrow(tokens, ({ id }) => id === tokenId), + const deserialized = deserializeLegacySolanaPoolState( + numberOfTokens, + accountInfo.data, ); - const lpTokenDetails = getTokenDetailsForEcosystem(lpToken, ecosystem); - if (lpTokenDetails === null) { - throw new Error("Token details not found"); - } - const [state] = await contract.getPoolStates([address]); return { - isPaused: state.paused, - ecosystem, - balances: poolTokens.map((token, i) => { - const tokenDetails = getTokenDetailsForEcosystem(token, ecosystem); - if (tokenDetails === null) { - throw new Error("Token details not found"); - } - return atomicToHuman( - new Decimal(state.balances[i][1].toString()), - tokenDetails.decimals, - ); - }), - totalLpSupply: atomicToHuman( - new Decimal(state.totalLpSupply[1].toString()), - lpTokenDetails.decimals, - ), - ampFactor: bnOrBigNumberToDecimal(state.ampFactor[0]).div( - 10 ** state.ampFactor[1], - ), - lpFee: bnOrBigNumberToDecimal(state.lpFee[0]).div(10 ** state.lpFee[1]), - governanceFee: bnOrBigNumberToDecimal(state.governanceFee[0]).div( - 10 ** state.governanceFee[1], - ), + ...deserialized, + governanceFee: { value: deserialized.governanceFee }, + lpFee: { value: deserialized.lpFee }, + preparedGovernanceFee: { value: deserialized.preparedGovernanceFee }, + preparedLpFee: { value: deserialized.preparedLpFee }, }; }; diff --git a/packages/aptos/src/client.ts b/packages/aptos/src/client.ts index 48e3f89c6..224d91e63 100644 --- a/packages/aptos/src/client.ts +++ b/packages/aptos/src/client.ts @@ -1,4 +1,4 @@ -import type { TokenDetails, TxGeneratorResult } from "@swim-io/core"; +import type { PoolState, TokenDetails, TxGeneratorResult } from "@swim-io/core"; import { Client } from "@swim-io/core"; import { atomicToHuman } from "@swim-io/utils"; import { @@ -109,6 +109,10 @@ export class AptosClient extends Client< throw new Error("Not implemented"); } + public getPoolState(): Promise { + throw new Error("Not implemented"); + } + public generateInitiatePortalTransferTxs(): AsyncGenerator< TxGeneratorResult > { diff --git a/packages/core/src/client.ts b/packages/core/src/client.ts index 312808b33..e8fe268e5 100644 --- a/packages/core/src/client.ts +++ b/packages/core/src/client.ts @@ -3,6 +3,7 @@ import type { TokenProjectId } from "@swim-io/token-projects"; import type Decimal from "decimal.js"; import type { ChainConfig } from "./chain"; +import type { PoolState } from "./pool"; import type { TokenDetails } from "./token"; import type { Tx } from "./tx"; @@ -82,6 +83,7 @@ export abstract class Client< owner: string, tokenDetails: readonly TokenDetails[], ): Promise; + public abstract getPoolState(id: string): Promise; public abstract generateInitiatePortalTransferTxs( params: InitiatePortalTransferParams, diff --git a/packages/core/src/pool.ts b/packages/core/src/pool.ts index 8cce3f985..f1a19c880 100644 --- a/packages/core/src/pool.ts +++ b/packages/core/src/pool.ts @@ -1,3 +1,5 @@ +import type Decimal from "decimal.js"; + /** Ecosystem-neutral configuration object for a Swim liquidity pool */ export interface PoolConfig { readonly id: string; @@ -12,3 +14,12 @@ export interface PoolConfig { readonly isLegacyPool?: boolean; readonly isDisabled?: boolean; } + +export interface PoolState { + readonly isPaused: boolean; + readonly ampFactor: Decimal; + readonly lpFee: Decimal; + readonly governanceFee: Decimal; + readonly balances: readonly Decimal[]; + readonly totalLpSupply: Decimal; +} diff --git a/packages/evm/src/client.ts b/packages/evm/src/client.ts index 5e80f9b32..7fd086f0f 100644 --- a/packages/evm/src/client.ts +++ b/packages/evm/src/client.ts @@ -9,18 +9,23 @@ import type { CompletePortalTransferParams, InitiatePortalTransferParams, InitiatePropellerParams, + PoolState, TokenDetails, TxGeneratorResult, } from "@swim-io/core"; import { ERC20__factory, Routing__factory } from "@swim-io/evm-contracts"; -import { isNotNull } from "@swim-io/utils"; +import { findOrThrow, isNotNull } from "@swim-io/utils"; import Decimal from "decimal.js"; import type { ethers, providers } from "ethers"; import { utils as ethersUtils } from "ethers"; import type { EvmChainConfig, EvmEcosystemId, EvmTx } from "./protocol"; import { EvmTxType } from "./protocol"; -import { appendHexDataToEvmTx } from "./utils"; +import { + appendHexDataToEvmTx, + bigNumberToHumanDecimal, + decimalStructOutputToDecimal, +} from "./utils"; import type { EvmWalletAdapter } from "./walletAdapters"; type BaseProvider = providers.BaseProvider; @@ -152,6 +157,42 @@ export class EvmClient extends Client< ); } + public async getPoolState(poolId: string): Promise { + const routingContract = Routing__factory.connect( + this.chainConfig.routingContractAddress, + this.provider, + ); + const { address, lpTokenId, tokenIds } = findOrThrow( + this.chainConfig.pools, + (pool) => pool.id === poolId, + ); + const lpToken = findOrThrow( + this.chainConfig.tokens, + ({ id }) => id === lpTokenId, + ); + const poolTokens = tokenIds.map((tokenId) => + findOrThrow(this.chainConfig.tokens, ({ id }) => id === tokenId), + ); + + const [state] = await routingContract.getPoolStates([address]); + return { + isPaused: state.paused, + balances: poolTokens.map((token, i) => + bigNumberToHumanDecimal( + state.balances[i].balance, + token.nativeDetails.decimals, + ), + ), + totalLpSupply: bigNumberToHumanDecimal( + state.totalLpSupply.balance, + lpToken.nativeDetails.decimals, + ), + ampFactor: decimalStructOutputToDecimal(state.ampFactor), + lpFee: decimalStructOutputToDecimal(state.lpFee), + governanceFee: decimalStructOutputToDecimal(state.governanceFee), + }; + } + public async *generateInitiatePortalTransferTxs({ atomicAmount, interactionId, diff --git a/packages/evm/src/utils.ts b/packages/evm/src/utils.ts index d3b275c5c..431163a64 100644 --- a/packages/evm/src/utils.ts +++ b/packages/evm/src/utils.ts @@ -1,4 +1,6 @@ -import type { ethers } from "ethers"; +import Decimal from "decimal.js"; +import type { BigNumber, ethers } from "ethers"; +import { utils as ethersUtils } from "ethers"; /** * The Wormhole EVM token bridge contract does not offer memo logging, meaning we would need a separate smart contract to implement that. However, because the token bridge contract relies on msg.sender we cannot simply log and forward the call data, meaning we would essentially have to rewrite the whole contract ourselves. Thus we store the ID at the end of the call data where it has no effect on the smart contract functionality and can be retrieved later. @@ -10,3 +12,18 @@ export const appendHexDataToEvmTx = ( ...populatedTx, data: populatedTx.data ? `${populatedTx.data}${hexData}` : `0x${hexData}`, }); + +export interface DecimalStructOutput { + readonly value: BigNumber; + readonly decimals: number; +} + +export const bigNumberToHumanDecimal = ( + value: BigNumber, + decimals: number, +): Decimal => new Decimal(ethersUtils.formatUnits(value, decimals)); + +export const decimalStructOutputToDecimal = ({ + value, + decimals, +}: DecimalStructOutput): Decimal => bigNumberToHumanDecimal(value, decimals); diff --git a/packages/solana-usdc-usdt-swap/package.json b/packages/solana-usdc-usdt-swap/package.json index ba8163dc5..c722ee286 100644 --- a/packages/solana-usdc-usdt-swap/package.json +++ b/packages/solana-usdc-usdt-swap/package.json @@ -23,8 +23,8 @@ "prepare": "yarn verify && yarn build" }, "dependencies": { - "@swim-io/pool-math": "workspace:^", - "@swim-io/solana": "workspace:^" + "@swim-io/pool-math": "~0.39.0", + "@swim-io/solana": "~0.39.0" }, "devDependencies": { "@project-serum/borsh": "^0.2.3", diff --git a/packages/solana/src/client.ts b/packages/solana/src/client.ts index 2f9dfdbaa..99a9a55aa 100644 --- a/packages/solana/src/client.ts +++ b/packages/solana/src/client.ts @@ -30,6 +30,7 @@ import type { CompletePortalTransferParams, InitiatePortalTransferParams, InitiatePropellerParams, + PoolState, TokenDetails, TxGeneratorResult, } from "@swim-io/core"; @@ -38,7 +39,13 @@ import type { Propeller } from "@swim-io/solana-contracts"; import { idl } from "@swim-io/solana-contracts"; import { TokenProjectId } from "@swim-io/token-projects"; import type { ReadonlyRecord } from "@swim-io/utils"; -import { atomicToHuman, chunks, humanToAtomic, sleep } from "@swim-io/utils"; +import { + atomicToHuman, + chunks, + findOrThrow, + humanToAtomic, + sleep, +} from "@swim-io/utils"; import BN from "bn.js"; import Decimal from "decimal.js"; @@ -49,12 +56,16 @@ import type { } from "./protocol"; import { SOLANA_ECOSYSTEM_ID, SolanaTxType } from "./protocol"; import type { TokenAccount } from "./serialization"; -import { deserializeTokenAccount } from "./serialization"; +import { + deserializeSolanaPoolState, + deserializeTokenAccount, +} from "./serialization"; import { createApproveAndRevokeIxs, createTx, parsedTxToSolanaTx, } from "./utils"; +import { atomicNumberToHuman, decimalBnToHuman } from "./utils/amount"; import { extractOutputAmountFromAddTx } from "./utils/propeller"; import type { SolanaWalletAdapter } from "./walletAdapters"; import { @@ -67,6 +78,10 @@ export const DEFAULT_MAX_RETRIES = 10; export const DEFAULT_COMMITMENT_LEVEL: Finality = "confirmed"; const DEFAULT_SLEEP_MS = 1000; +interface SolanaPoolState extends PoolState { + readonly governanceFeeKey: PublicKey; +} + type WithOptionalAuxiliarySigner = T & { readonly auxiliarySigner?: Keypair; }; @@ -220,6 +235,61 @@ export class SolanaClient extends Client< ); } + public async getPoolState(poolId: string): Promise { + const { + address: poolAddress, + feeDecimals, + lpTokenId, + tokenIds, + } = findOrThrow(this.chainConfig.pools, (pool) => pool.id === poolId); + const accountInfo = await this.connection.getAccountInfo( + new PublicKey(poolAddress), + ); + if (accountInfo === null) { + throw new Error("Failed to load pool state"); + } + const solanaPoolState = deserializeSolanaPoolState(accountInfo.data); + + const poolTokenDetails = tokenIds.map((tokenId) => { + const tokenConfig = findOrThrow( + this.chainConfig.tokens, + (token) => token.id === tokenId, + ); + return getTokenDetails(this.chainConfig, tokenConfig.projectId); + }); + const balances = await this.getTokenBalances(poolAddress, poolTokenDetails); + + const lpTokenConfig = findOrThrow( + this.chainConfig.tokens, + (token) => token.id === lpTokenId, + ); + const lpTokenDetails = getTokenDetails( + this.chainConfig, + lpTokenConfig.projectId, + ); + const { value: lpTokenSupply } = await this.connection.getTokenSupply( + new PublicKey(lpTokenDetails.address), + ); + const totalLpSupply = atomicToHuman( + new Decimal(lpTokenSupply.amount), + lpTokenSupply.decimals, + ); + + return { + isPaused: solanaPoolState.isPaused, + // TODO: do proper interpolation + ampFactor: decimalBnToHuman(solanaPoolState.ampFactor.initialValue), + lpFee: atomicNumberToHuman(solanaPoolState.lpFee.value, feeDecimals), + governanceFee: atomicNumberToHuman( + solanaPoolState.governanceFee.value, + feeDecimals, + ), + balances, + totalLpSupply, + governanceFeeKey: solanaPoolState.governanceFeeKey, + }; + } + public async *generateInitiatePortalTransferTxs({ atomicAmount, targetChainId, diff --git a/packages/solana/src/serialization/poolState.test.ts b/packages/solana/src/serialization/poolState.test.ts index 3952856b2..d99deb5cc 100644 --- a/packages/solana/src/serialization/poolState.test.ts +++ b/packages/solana/src/serialization/poolState.test.ts @@ -3,19 +3,17 @@ import { Buffer } from "buffer"; import { PublicKey } from "@solana/web3.js"; import BN from "bn.js"; -import type { SwimPoolState } from "./poolState"; -import { deserializeSwimPool } from "./poolState"; +import type { SolanaPoolState } from "./poolState"; +import { deserializeSolanaPoolState } from "./poolState"; -describe("deserializeSwimPool", () => { - it("deserializes a SwimPoolState", () => { - const numTokens = 6; +describe("deserializeSolanaPoolState", () => { + it("deserializes a SolanaPoolState", () => { const serialized = Buffer.from( - "00000100000000000000000000000000000000e8030000000000000000000000000000002c01000064000000990e9632b0b9f2e636feb3f0a4220f8aadf9677b451c982a4151af42e0362e8800c6fa7af3bedbad3a3d65f36aabc97431b1bbe4c2d2f6e0e47ca60203452f5d61ce010e60afedb22717bd63192f54145a3f965a33bb82d2c7029eb2ce1e20826487f81d7f931ba1c5db9f5a8b2ac5e149ef9c76d9cf196615bd21163316e8c410bdd7aa20228a7bc21e67ddfe78d5d89b986d4bf5c8d5dc9d4574d81ab11e5a02012262c2067049b5c6d6a6869e7a37bbee162637f78192c3b15e8427676a422574616a65b31ff1d8f707eb279bf8a729a6644151b16d72f9694af6ae499881ed02020202000048ccc8aa094ba7b3495776e123587f2454a935671548ccdc3f4311a9febbdd18fb56a83f5d24d5e7513f96b8c24bff58e7259e92f2fd6f01162f9b0b5188d23e32bf5157ba942716dbab775cde82f881ededa5a96b325714e2bef602679dc3cd1205cdb06ade7ab0c78b50a6e7cc2dd83edfa10423348951b7ce231b6c920334bfcf845603efc68ddea00872ad53de92ed69227e373bdca1a21f78782ec87fec7b90e07d2a4fd0d055d08430d9524a755cacd7a7a531ed91705e8b823e59d820cf609300f5b15b7009876930926f1b5c4a6ecdbc035219127e8ff47ef369abbc7ef4d44674e963fe6e94097d729f1c29c382a3ca684cfd454347f97ee142959e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008d8fcd9e56d407000000000000000000", + "20c6d088ba17fa93ff0001000000000000000000000000000000002c010000000000000000000000000000002c01000064000000296b21c9a4722da898b5cba4f10cbf7693a6ea4af06938cab91c2d88afe267190054e7ff24b975efb9d141828d813d45a0e8d996f7ece50fb6c0ea6209814540606f55543e2dfdef31aa4341ab41500e34fa1fdd29d3180b479396998345e896d400002ecb88c3adfc243a8d4d4213091f8e70920abc33db7b2cefa1ba7c431dbefa9168cffcd754af9b3ebf6a194e99d9ec5a443949cf81a0b9dcf87fc5322daee2633651145b5235dd54b936805e22d8d4ad567c670ac7d7d0d56717ac9627e195ce87bf6424e0aab4369c10322c5e6cb388ecbff669bbd7df4a88617b1cd1f33d63d56eda0d27700d468b7ed03d29fe763f5105deaba768ca3247e60737f64d281f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a7716c6bb9b700000000000000000000", "hex", ); - - const expected: SwimPoolState = { - nonce: 0, + const expected: SolanaPoolState = { + bump: 255, isPaused: false, ampFactor: { initialValue: { @@ -24,47 +22,41 @@ describe("deserializeSwimPool", () => { }, initialTs: new BN(0), targetValue: { - value: new BN(1000), + value: new BN(300), decimals: 0, }, targetTs: new BN(0), }, - lpFee: 300, - governanceFee: 100, - lpMintKey: new PublicKey("BJUH9GJLaMSLV1E7B3SQLCy9eCfyr6zsrwGcpS2MkqR1"), + lpFee: { value: 300 }, + governanceFee: { value: 100 }, + lpMintKey: new PublicKey("3ngTtoyP9GFybFifX1dr7gCFXFiM2Wr6NfXn6EuU7k6C"), lpDecimalEqualizer: 0, tokenMintKeys: [ - new PublicKey("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"), - new PublicKey("Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB"), - new PublicKey("A9mUU4qviSctJVPJdBJWkb28deg915LYJKrzQ19ji3FM"), - new PublicKey("Dn4noZ5jgGfkntzcQSUZ8czkreiZ1ForXYoV2H8Dm7S1"), - new PublicKey("5RpUwQ8wtdPCZHhu6MERp2RGrpobsbZ6MH5dDHkUjs2"), - new PublicKey("8qJSyQprMC57TWKaYEmetUR3UUiTP2M3hXdcvFhkZdmv"), + new PublicKey("6iSRgpK4oiqJZuhpLsTecW3n9xBKUq9N3VPQN7RinYwq"), + new PublicKey("8VbikoRxEoyYzTDzDcPTSsGk2E5mM7fK1WrVpKrVd75M"), ], - tokenDecimalEqualizers: [2, 2, 2, 2, 0, 0], + tokenDecimalEqualizers: [0, 0], tokenKeys: [ - new PublicKey("5uBU2zUG8xTLA6XwwcTFWib1p7EjCBzWbiy44eVASTfV"), - new PublicKey("Hv7yPYnGs6fpN3o1NZvkima9mKDrRDJtNxf23oKLCjau"), - new PublicKey("4R6b4aibi46JzAnuA8ZWXrHAsR1oZBTZ8dqkuer3LsbS"), - new PublicKey("2DMUL42YEb4g1HAKXhUxL3Yjfgoj4VvRqKwheorfFcPV"), - new PublicKey("DukQAFyxR41nbbq2FBUDMyrtF2CRmWBREjZaTVj4u9As"), - new PublicKey("9KMH3p8cUocvQRbJfKRAStKG52xCCWNmEPsJm5gc8fzw"), + new PublicKey("49fm8MaATyD4BwaqxXmjASGuR3WLg8PL1SvMiYpyTdrx"), + new PublicKey("849M4dvrdoUqsn7t6eVWWNos8Q8RfLJxRTzQC46KGoYE"), ], governanceKey: new PublicKey( - "ExWoeFoyYwCFx2cp9PZzj4eYL5fsDEFQEpC8REsksNpb", + "A8uJnBYSmjEFtjfuCFCaXCFNtqfzoBBYXpPDVvR3kvkS", ), governanceFeeKey: new PublicKey( - "9Yau6DnqYasBUKcyxQJQZqThvUnqZ32ZQuUCcC2AdT9P", + "FN9strke8tiDYmRNH3LFtg9zjJpTsxgTPHUegsQsUiai", ), + pauseKey: new PublicKey("4f2ivZ8B13d5CTGQpRWcLYjgnqD7ut7mezj3sj1BwZMw"), preparedGovernanceKey: PublicKey.default, governanceTransitionTs: new BN(0), - preparedLpFee: 0, - preparedGovernanceFee: 0, + preparedLpFee: { value: 0 }, + preparedGovernanceFee: { value: 0 }, feeTransitionTs: new BN(0), - previousDepth: new BN(2203793333522317), + previousDepth: new BN(202006999101863), }; - const decoded = deserializeSwimPool(numTokens, serialized); - expect(decoded.nonce).toBe(expected.nonce); + const decoded = deserializeSolanaPoolState(serialized); + + expect(decoded.bump).toBe(expected.bump); expect(decoded.isPaused).toBe(expected.isPaused); expect( decoded.ampFactor.initialValue.value.eq( @@ -88,8 +80,8 @@ describe("deserializeSwimPool", () => { expect(decoded.ampFactor.targetTs.eq(expected.ampFactor.targetTs)).toBe( true, ); - expect(decoded.lpFee).toBe(expected.lpFee); - expect(decoded.governanceFee).toBe(expected.governanceFee); + expect(decoded.lpFee.value).toBe(expected.lpFee.value); + expect(decoded.governanceFee.value).toBe(expected.governanceFee.value); expect(decoded.lpMintKey).toStrictEqual(expected.lpMintKey); expect(decoded.lpDecimalEqualizer).toBe(expected.lpDecimalEqualizer); decoded.tokenMintKeys.forEach((tokenMintKey, i) => { @@ -103,14 +95,17 @@ describe("deserializeSwimPool", () => { }); expect(decoded.governanceKey).toStrictEqual(expected.governanceKey); expect(decoded.governanceFeeKey).toStrictEqual(expected.governanceFeeKey); + expect(decoded.pauseKey).toStrictEqual(expected.pauseKey); expect(decoded.preparedGovernanceKey).toStrictEqual( expected.preparedGovernanceKey, ); expect( decoded.governanceTransitionTs.eq(expected.governanceTransitionTs), ).toBe(true); - expect(decoded.preparedLpFee).toBe(expected.preparedLpFee); - expect(decoded.preparedGovernanceFee).toBe(expected.preparedGovernanceFee); + expect(decoded.preparedLpFee.value).toBe(expected.preparedLpFee.value); + expect(decoded.preparedGovernanceFee.value).toBe( + expected.preparedGovernanceFee.value, + ); expect(decoded.feeTransitionTs.eq(expected.feeTransitionTs)).toBe(true); expect(decoded.previousDepth.eq(expected.previousDepth)).toBe(true); }); diff --git a/packages/solana/src/serialization/poolState.ts b/packages/solana/src/serialization/poolState.ts index e7f8deff6..a7ad7252c 100644 --- a/packages/solana/src/serialization/poolState.ts +++ b/packages/solana/src/serialization/poolState.ts @@ -1,27 +1,20 @@ import type { Buffer } from "buffer"; -import type { Layout } from "@project-serum/borsh"; -import { - array, - bool, - i64, - publicKey, - struct, - u128, - u32, - u8, -} from "@project-serum/borsh"; +import { BorshAccountsCoder } from "@project-serum/anchor"; import type { PublicKey } from "@solana/web3.js"; +import { idl } from "@swim-io/solana-contracts"; import type BN from "bn.js"; import type { AmpFactor } from "./ampFactor"; -import { ampFactor } from "./ampFactor"; export type Timestamp = BN; -type U8 = (property?: string) => Layout; -export interface SwimPoolConstantState { - readonly nonce: number; +export interface NumberValue { + readonly value: number; +} + +export interface SolanaPoolConstantState { + readonly bump: number; readonly lpMintKey: PublicKey; readonly lpDecimalEqualizer: number; readonly tokenMintKeys: readonly PublicKey[]; @@ -29,60 +22,37 @@ export interface SwimPoolConstantState { readonly tokenKeys: readonly PublicKey[]; } -export interface SwimPoolMutableState { +export interface SolanaPoolMutableState { readonly isPaused: boolean; readonly ampFactor: AmpFactor; - readonly lpFee: number; - readonly governanceFee: number; + readonly lpFee: NumberValue; + readonly governanceFee: NumberValue; readonly governanceKey: PublicKey; readonly governanceFeeKey: PublicKey; + readonly pauseKey: PublicKey; readonly preparedGovernanceKey: PublicKey; readonly governanceTransitionTs: Timestamp; - readonly preparedLpFee: number; - readonly preparedGovernanceFee: number; + readonly preparedLpFee: NumberValue; + readonly preparedGovernanceFee: NumberValue; readonly feeTransitionTs: Timestamp; readonly previousDepth: BN; } -export interface SwimPoolState - extends SwimPoolConstantState, - SwimPoolMutableState {} +export interface SolanaPoolState + extends SolanaPoolConstantState, + SolanaPoolMutableState {} -export const swimPool = ( - numberOfTokens: number, - property = "swimPool", -): Layout => - struct( - [ - u8("nonce"), - bool("isPaused"), - ampFactor(), - u32("lpFee"), - u32("governanceFee"), - publicKey("lpMintKey"), - u8("lpDecimalEqualizer"), - array(publicKey(), numberOfTokens, "tokenMintKeys"), - array((u8 as U8)(), numberOfTokens, "tokenDecimalEqualizers"), - array(publicKey(), numberOfTokens, "tokenKeys"), - publicKey("governanceKey"), - publicKey("governanceFeeKey"), - publicKey("preparedGovernanceKey"), - i64("governanceTransitionTs"), - u32("preparedLpFee"), - u32("preparedGovernanceFee"), - i64("feeTransitionTs"), - u128("previousDepth"), - ], - property, - ); +const TWO_POOL_ACCOUNT_NAME = "TwoPool"; -export const deserializeSwimPool = ( - numberOfTokens: number, - poolData: Buffer, -): SwimPoolState => { - const layout = swimPool(numberOfTokens); - if (poolData.length !== layout.span) { - throw new Error("Incorrect pool data length"); +export const deserializeSolanaPoolState = ( + accountData: Buffer, +): SolanaPoolState => { + const twoPoolDiscriminator = BorshAccountsCoder.accountDiscriminator( + TWO_POOL_ACCOUNT_NAME, + ); + if (twoPoolDiscriminator.compare(accountData.subarray(0, 8)) !== 0) { + throw new Error("Invalid account data"); } - return layout.decode(poolData); + const decoder = new BorshAccountsCoder(idl.twoPool); + return decoder.decode(TWO_POOL_ACCOUNT_NAME, accountData); }; diff --git a/packages/solana/src/utils/amount.ts b/packages/solana/src/utils/amount.ts new file mode 100644 index 000000000..adac826c2 --- /dev/null +++ b/packages/solana/src/utils/amount.ts @@ -0,0 +1,12 @@ +import { atomicToHuman } from "@swim-io/utils"; +import Decimal from "decimal.js"; + +import type { DecimalBN } from "../serialization"; + +export const decimalBnToHuman = (decimalBn: DecimalBN): Decimal => + atomicToHuman(new Decimal(decimalBn.value.toString()), decimalBn.decimals); + +export const atomicNumberToHuman = ( + atomicNumber: number, + decimals: number, +): Decimal => atomicToHuman(new Decimal(atomicNumber.toString()), decimals); diff --git a/yarn.lock b/yarn.lock index 9170f8b16..745cdfc8d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7156,21 +7156,7 @@ __metadata: languageName: node linkType: hard -"@swim-io/aptos@npm:^0.40.0": - version: 0.40.0 - resolution: "@swim-io/aptos@npm:0.40.0" - dependencies: - "@swim-io/core": ^0.40.0 - "@swim-io/utils": ^0.40.0 - aptos: ^1.3.13 - peerDependencies: - decimal.js: ^10.3.1 - eventemitter3: ^4.0.7 - checksum: c20ed5033c04f12909ba46605ea40a5866db25cfe5ae08a8fe2e99c1a99fab2c8488e11d5737c74b5cc6ae282f2819097fd71e8a9af2a7dc507f08785d3a8496 - languageName: node - linkType: hard - -"@swim-io/aptos@workspace:packages/aptos": +"@swim-io/aptos@workspace:^, @swim-io/aptos@workspace:packages/aptos": version: 0.0.0-use.local resolution: "@swim-io/aptos@workspace:packages/aptos" dependencies: @@ -7426,27 +7412,7 @@ __metadata: languageName: node linkType: hard -"@swim-io/evm@npm:^0.40.0": - version: 0.40.0 - resolution: "@swim-io/evm@npm:0.40.0" - dependencies: - "@swim-io/core": ^0.40.0 - "@swim-io/evm-contracts": ^0.40.0 - "@swim-io/token-projects": ^0.40.0 - "@swim-io/utils": ^0.40.0 - graphql: ^16.6.0 - graphql-request: ^4.3.0 - moralis: ^1.8.0 - peerDependencies: - "@certusone/wormhole-sdk": ^0.6.2 - decimal.js: ^10.3.1 - ethers: ^5.6.9 - eventemitter3: ^4.0.7 - checksum: ba65ee3dcf51c9a4c1e329a7597b41c4226028f2fc76afd57c9e8185b20e04ef5563d47cdf017ec9aa122e148162408b16d544a167c6953b251a77fc83cebdfb - languageName: node - linkType: hard - -"@swim-io/evm@workspace:packages/evm": +"@swim-io/evm@workspace:^, @swim-io/evm@workspace:packages/evm": version: 0.0.0-use.local resolution: "@swim-io/evm@workspace:packages/evm" dependencies: @@ -7514,7 +7480,16 @@ __metadata: languageName: node linkType: hard -"@swim-io/pool-math@workspace:^, @swim-io/pool-math@workspace:packages/pool-math": +"@swim-io/pool-math@npm:~0.39.0": + version: 0.39.0 + resolution: "@swim-io/pool-math@npm:0.39.0" + peerDependencies: + decimal.js: ^10.3.1 + checksum: 68f86c9a614630192dd54a04a7f96765823e2fb2444a8807de172714570695f0ee0f8c82a676b682973dc64f54eca422b95828b7df9eacfa9b71d5cc5a819a63 + languageName: node + linkType: hard + +"@swim-io/pool-math@workspace:packages/pool-math": version: 0.0.0-use.local resolution: "@swim-io/pool-math@workspace:packages/pool-math" dependencies: @@ -7714,8 +7689,8 @@ __metadata: "@solana/spl-token": ^0.2.0 "@solana/web3.js": ^1.31.0 "@swim-io/eslint-config": "workspace:^" - "@swim-io/pool-math": "workspace:^" - "@swim-io/solana": "workspace:^" + "@swim-io/pool-math": ~0.39.0 + "@swim-io/solana": ~0.39.0 "@swim-io/tsconfig": "workspace:^" "@types/bn.js": ^5.1.0 "@types/jest": ^28.1.3 @@ -7757,7 +7732,7 @@ __metadata: languageName: node linkType: hard -"@swim-io/solana@npm:^0.39.0": +"@swim-io/solana@npm:^0.39.0, @swim-io/solana@npm:~0.39.0": version: 0.39.0 resolution: "@swim-io/solana@npm:0.39.0" dependencies: @@ -7971,13 +7946,13 @@ __metadata: "@storybook/manager-webpack4": ^6.5.10 "@storybook/node-logger": ^6.5.10 "@storybook/react": ^6.5.10 - "@swim-io/aptos": ^0.40.0 - "@swim-io/core": ^0.40.0 + "@swim-io/aptos": "workspace:^" + "@swim-io/core": "workspace:^" "@swim-io/eslint-config": "workspace:^" - "@swim-io/evm": ^0.40.0 + "@swim-io/evm": "workspace:^" "@swim-io/evm-contracts": ^0.40.0 "@swim-io/pool-math": ^0.40.0 - "@swim-io/solana": ^0.40.0 + "@swim-io/solana": "workspace:^" "@swim-io/solana-contracts": ^0.40.0 "@swim-io/token-projects": ^0.40.0 "@swim-io/tsconfig": "workspace:^"