From 611eb340d3808cdb10c3ab7236be9e383a52614e Mon Sep 17 00:00:00 2001 From: wormat Date: Mon, 24 Oct 2022 18:53:54 +0200 Subject: [PATCH] WIP: reset --- .../deserializeLegacySolanaPoolState.test.ts | 118 +++++++++++++++++ .../deserializeLegacySolanaPoolState.ts | 59 +++++++++ apps/ui/src/models/solana/index.ts | 1 + apps/ui/src/models/swim/SwimInitializer.ts | 4 +- .../src/models/swim/deserializeSwimPoolV2.ts | 44 ------- apps/ui/src/models/swim/pool.ts | 124 ++++++------------ 6 files changed, 220 insertions(+), 130 deletions(-) create mode 100644 apps/ui/src/models/solana/deserializeLegacySolanaPoolState.test.ts create mode 100644 apps/ui/src/models/solana/deserializeLegacySolanaPoolState.ts delete mode 100644 apps/ui/src/models/swim/deserializeSwimPoolV2.ts 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..3e76dc164 --- /dev/null +++ b/apps/ui/src/models/solana/deserializeLegacySolanaPoolState.test.ts @@ -0,0 +1,118 @@ +import { Buffer } from "buffer"; + +import { PublicKey } from "@solana/web3.js"; +import BN from "bn.js"; + +import { + deserializeLegacySolanaPoolState, + LegacySolanaPoolState, +} from "./deserializeLegacySolanaPoolState"; + +describe("deserializeLegacySolanaPoolState", () => { + it("deserializes a SolanaPoolState", () => { + const serialized = Buffer.from( + "00000100000000000000000000000000000000e8030000000000000000000000000000002c01000064000000990e9632b0b9f2e636feb3f0a4220f8aadf9677b451c982a4151af42e0362e8800c6fa7af3bedbad3a3d65f36aabc97431b1bbe4c2d2f6e0e47ca60203452f5d61ce010e60afedb22717bd63192f54145a3f965a33bb82d2c7029eb2ce1e20826487f81d7f931ba1c5db9f5a8b2ac5e149ef9c76d9cf196615bd21163316e8c410bdd7aa20228a7bc21e67ddfe78d5d89b986d4bf5c8d5dc9d4574d81ab11e5a02012262c2067049b5c6d6a6869e7a37bbee162637f78192c3b15e8427676a422574616a65b31ff1d8f707eb279bf8a729a6644151b16d72f9694af6ae499881ed02020202000048ccc8aa094ba7b3495776e123587f2454a935671548ccdc3f4311a9febbdd18fb56a83f5d24d5e7513f96b8c24bff58e7259e92f2fd6f01162f9b0b5188d23e32bf5157ba942716dbab775cde82f881ededa5a96b325714e2bef602679dc3cd1205cdb06ade7ab0c78b50a6e7cc2dd83edfa10423348951b7ce231b6c920334bfcf845603efc68ddea00872ad53de92ed69227e373bdca1a21f78782ec87fec7b90e07d2a4fd0d055d08430d9524a755cacd7a7a531ed91705e8b823e59d820cf609300f5b15b7009876930926f1b5c4a6ecdbc035219127e8ff47ef369abbc7ef4d44674e963fe6e94097d729f1c29c382a3ca684cfd454347f97ee142959e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008d8fcd9e56d407000000000000000000", + "hex", + ); + + const expected: LegacySolanaPoolState = { + nonce: 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: { value: 300 }, + governanceFee: { value: 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: { value: 0 }, + preparedGovernanceFee: { value: 0 }, + feeTransitionTs: new BN(0), + previousDepth: new BN(2203793333522317), + }; + const decoded = deserializeLegacySolanaPoolState(6, serialized); + expect(decoded.nonce).toBe(expected.nonce); + 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..b960a9887 --- /dev/null +++ b/apps/ui/src/models/solana/deserializeLegacySolanaPoolState.ts @@ -0,0 +1,59 @@ +import { + array, + bool, + i64, + publicKey, + struct, + u128, + u32, + u8, +} from "@project-serum/borsh"; +import type { Layout } from "@project-serum/borsh"; + +import { ampFactor, SolanaPoolState } from "@swim-io/solana"; + +type U8 = (property?: string) => Layout; + +export interface LegacySolanaPoolState + extends Omit { + readonly nonce: number; +} + +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"); + } + return layout.decode(accountData); +}; 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/SwimInitializer.ts b/apps/ui/src/models/swim/SwimInitializer.ts index 710273cba..c9bbde3cd 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, solanaPool } from "@swim-io/solana"; import type { DecimalBN, SolanaClient, @@ -148,7 +148,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/pool.ts b/apps/ui/src/models/swim/pool.ts index 636fd6ca6..ef0422a60 100644 --- a/apps/ui/src/models/swim/pool.ts +++ b/apps/ui/src/models/swim/pool.ts @@ -1,41 +1,26 @@ import { PublicKey } from "@solana/web3.js"; -import type { EvmClient, EvmEcosystemId } from "@swim-io/evm"; +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 type { - SolanaClient, - SolanaEcosystemId, - SolanaTx, - SwimPoolState, -} from "@swim-io/solana"; +import { SOLANA_ECOSYSTEM_ID, isSolanaTx } from "@swim-io/solana"; +import type { SolanaClient, SolanaPoolState, SolanaTx } from "@swim-io/solana"; import type { ReadonlyRecord } from "@swim-io/utils"; -import { atomicToHuman, findOrThrow } from "@swim-io/utils"; +import { findOrThrow } from "@swim-io/utils"; import Decimal from "decimal.js"; -import { bnOrBigNumberToDecimal } from "../../amounts"; import type { Config, - EvmPoolSpec, + EcosystemId, PoolSpec, SolanaPoolSpec, TokenConfig, } from "../../config"; -import { getTokenDetailsForEcosystem } from "../../config"; import type { Tx } from "../crossEcosystem"; -import { deserializeSwimPoolV2 } from "./deserializeSwimPoolV2"; - -export interface SolanaPoolState extends SwimPoolState { - readonly ecosystem: SolanaEcosystemId; -} +import { deserializeLegacySolanaPoolState } from "../solana"; +import { ChainConfig, PoolState as CorePoolState } from "@swim-io/core"; export interface EvmPoolState { - readonly ecosystem: EvmEcosystemId; + readonly ecosystemId: EvmEcosystemId; readonly isPaused: boolean; readonly balances: readonly Decimal[]; readonly totalLpSupply: Decimal; @@ -44,7 +29,9 @@ export interface EvmPoolState { readonly governanceFee: Decimal; } -export type PoolState = SolanaPoolState | EvmPoolState; +export type PoolState = CorePoolState & { + readonly ecosystemId: EcosystemId; +}; export type TokensByPoolId = ReadonlyRecord< string, // Pool ID @@ -84,79 +71,48 @@ 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 => { - const numberOfTokens = poolSpec.tokens.length; + chainConfig: ChainConfig, + poolId: string, +): Promise => { + const poolConfig = findOrThrow( + chainConfig.pools, + (pool) => pool.id === poolId, + ); + if (!poolConfig.isLegacyPool) { + const poolState = await solanaClient.getPoolState(poolId); + return { + ...poolState, + ecosystemId: SOLANA_ECOSYSTEM_ID, + }; + } + const numberOfTokens = poolConfig.tokenIds.length; const accountInfo = await solanaClient.connection.getAccountInfo( - new PublicKey(poolSpec.address), + new PublicKey(poolConfig.address), ); if (accountInfo === null) { - return null; + throw new Error("Missing account info"); } - const swimPool = poolSpec.isLegacyPool - ? deserializeSwimPool(numberOfTokens, accountInfo.data) - : deserializeSwimPoolV2(accountInfo.data); + const swimPool = deserializeLegacySolanaPoolState( + numberOfTokens, + 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 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], - ), + ecosystemId: SOLANA_ECOSYSTEM_ID, + balances: null, + totalLpSupply: null, }; }; export const isEvmPoolState = ( poolState: PoolState | null, ): poolState is EvmPoolState => - poolState !== null && isEvmEcosystemId(poolState.ecosystem); + poolState !== null && isEvmEcosystemId(poolState.ecosystemId); export const isSolanaPoolState = ( - poolState: PoolState, -): poolState is SolanaPoolState => poolState.ecosystem === SOLANA_ECOSYSTEM_ID; + poolState: PoolState | null, +): poolState is SolanaPoolState & PoolState => + poolState !== null && poolState.ecosystemId === SOLANA_ECOSYSTEM_ID;