From 97a69df99f432b4396a7102b10724f6977e092e3 Mon Sep 17 00:00:00 2001 From: wormat Date: Wed, 14 Sep 2022 19:13:50 +0100 Subject: [PATCH 01/11] feat(core): Add PoolState interface --- packages/core/src/pool.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) 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; +} From e6c4ed0b433603c59a48b9ec54baf51b0ae13586 Mon Sep 17 00:00:00 2001 From: wormat Date: Mon, 24 Oct 2022 13:17:00 +0200 Subject: [PATCH 02/11] feat(core): Add Client.getPoolState method --- packages/core/src/client.ts | 2 ++ 1 file changed, 2 insertions(+) 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, From b694721f890105e236ffb95e726d18c9bcfdef60 Mon Sep 17 00:00:00 2001 From: wormat Date: Thu, 15 Sep 2022 11:38:41 +0100 Subject: [PATCH 03/11] feat(evm): Add getPoolState method to EvmClient --- packages/evm/src/client.ts | 45 ++++++++++++++++++++++++++++++++++++-- packages/evm/src/utils.ts | 19 +++++++++++++++- 2 files changed, 61 insertions(+), 3 deletions(-) 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); From b06fa63ac47b2537e823575ee75496a5ab8f1f30 Mon Sep 17 00:00:00 2001 From: wormat Date: Thu, 15 Sep 2022 12:54:50 +0100 Subject: [PATCH 04/11] feat(solana): Add getPoolState method to SolanaClient --- packages/solana/src/client.ts | 74 +++++++++++++++- .../src/serialization/poolState.test.ts | 69 +++++++-------- .../solana/src/serialization/poolState.ts | 86 ++++++------------- packages/solana/src/utils/amount.ts | 12 +++ 4 files changed, 144 insertions(+), 97 deletions(-) create mode 100644 packages/solana/src/utils/amount.ts 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); From bcc3c151f5ef448f5c5098b95dae2e5273e1f1c2 Mon Sep 17 00:00:00 2001 From: wormat Date: Mon, 24 Oct 2022 13:37:24 +0200 Subject: [PATCH 05/11] chore(aptos): Add dummy AptosClient.getPoolState method --- packages/aptos/src/client.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 > { From 52e7cddd73c5ba3e634d9c10d590f697bb0aa485 Mon Sep 17 00:00:00 2001 From: wormat Date: Mon, 24 Oct 2022 18:08:38 +0200 Subject: [PATCH 06/11] chore(solana-usdc-usdt-swap): Pin @swim-io dependencies to v0.39.0 --- packages/solana-usdc-usdt-swap/package.json | 4 ++-- yarn.lock | 17 +++++++++++++---- 2 files changed, 15 insertions(+), 6 deletions(-) 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/yarn.lock b/yarn.lock index 9170f8b16..98a4b7a15 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7514,7 +7514,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 +7723,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 +7766,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: From 3ba70196d0bb575053b3f3dd7837ffdbba5faba7 Mon Sep 17 00:00:00 2001 From: wormat Date: Wed, 21 Sep 2022 19:15:34 +0100 Subject: [PATCH 07/11] WIP: revert --- apps/ui/package.json | 8 ++++---- yarn.lock | 46 ++++++-------------------------------------- 2 files changed, 10 insertions(+), 44 deletions(-) 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/yarn.lock b/yarn.lock index 98a4b7a15..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: @@ -7980,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:^" From 8660370979e27ad463bda040e9f27cb62836ca8d Mon Sep 17 00:00:00 2001 From: wormat Date: Tue, 25 Oct 2022 15:59:14 +0200 Subject: [PATCH 08/11] refactor(ui): Update for new PoolState --- .../useSolanaPoolOperationsMutation.ts | 3 - apps/ui/src/hooks/swim/usePoolMaths.ts | 4 +- apps/ui/src/hooks/swim/usePoolStateQueries.ts | 21 ++-- .../deserializeLegacySolanaPoolState.test.ts | 117 ++++++++++++++++++ .../deserializeLegacySolanaPoolState.ts | 74 +++++++++++ apps/ui/src/models/solana/index.ts | 1 + apps/ui/src/models/swim/SwimDefiInstructor.ts | 4 - apps/ui/src/models/swim/SwimInitializer.ts | 6 +- .../src/models/swim/deserializeSwimPoolV2.ts | 44 ------- .../swim/doSingleSolanaPoolOperation.ts | 7 +- .../swim/doSingleSolanaPoolOperationV2.ts | 13 +- apps/ui/src/models/swim/pool.test.ts | 82 +----------- apps/ui/src/models/swim/pool.ts | 97 ++++----------- 13 files changed, 238 insertions(+), 235 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/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 }, }; }; From 015649cf88a6cf93e1dc17094ceacf1af053f1b6 Mon Sep 17 00:00:00 2001 From: wormat Date: Tue, 25 Oct 2022 18:18:02 +0200 Subject: [PATCH 09/11] fix(ui): Make deserializeLegacySolanaPoolState work --- .../deserializeLegacySolanaPoolState.ts | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/apps/ui/src/models/solana/deserializeLegacySolanaPoolState.ts b/apps/ui/src/models/solana/deserializeLegacySolanaPoolState.ts index 74ce53f55..25be14533 100644 --- a/apps/ui/src/models/solana/deserializeLegacySolanaPoolState.ts +++ b/apps/ui/src/models/solana/deserializeLegacySolanaPoolState.ts @@ -6,11 +6,13 @@ import { struct, u128, u32, + u64, 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"; +import { SOLANA_ECOSYSTEM_ID } from "@swim-io/solana"; +import type BN from "bn.js"; type U8 = (property?: string) => Layout; @@ -30,6 +32,32 @@ export interface LegacySolanaPoolState readonly ecosystem: SolanaEcosystemId; } +interface DecimalBN { + readonly value: BN; + readonly decimals: number; +} + +const decimal = (property = "decimal"): Layout => + struct([u64("value"), u8("decimals")], property); + +interface AmpFactor { + readonly initialValue: DecimalBN; + readonly initialTs: BN; + readonly targetValue: DecimalBN; + readonly targetTs: BN; +} + +const ampFactor = (property = "ampFactor"): Layout => + struct( + [ + decimal("initialValue"), + i64("initialTs"), + decimal("targetValue"), + i64("targetTs"), + ], + property, + ); + export const solanaPool = ( numberOfTokens: number, property = "solanaPool", From 14b5fe381006d6e6030878f9ff2f393dae302e13 Mon Sep 17 00:00:00 2001 From: Nico Miicro Date: Tue, 25 Oct 2022 22:52:10 +0300 Subject: [PATCH 10/11] refactor(ui): use ampFactor from @swim-io/solana --- .../deserializeLegacySolanaPoolState.ts | 29 +------------------ 1 file changed, 1 insertion(+), 28 deletions(-) diff --git a/apps/ui/src/models/solana/deserializeLegacySolanaPoolState.ts b/apps/ui/src/models/solana/deserializeLegacySolanaPoolState.ts index 25be14533..82adf0eef 100644 --- a/apps/ui/src/models/solana/deserializeLegacySolanaPoolState.ts +++ b/apps/ui/src/models/solana/deserializeLegacySolanaPoolState.ts @@ -11,8 +11,7 @@ import { } from "@project-serum/borsh"; import type { Layout } from "@project-serum/borsh"; import type { SolanaEcosystemId, SolanaPoolState } from "@swim-io/solana"; -import { SOLANA_ECOSYSTEM_ID } from "@swim-io/solana"; -import type BN from "bn.js"; +import { SOLANA_ECOSYSTEM_ID, ampFactor } from "@swim-io/solana"; type U8 = (property?: string) => Layout; @@ -32,32 +31,6 @@ export interface LegacySolanaPoolState readonly ecosystem: SolanaEcosystemId; } -interface DecimalBN { - readonly value: BN; - readonly decimals: number; -} - -const decimal = (property = "decimal"): Layout => - struct([u64("value"), u8("decimals")], property); - -interface AmpFactor { - readonly initialValue: DecimalBN; - readonly initialTs: BN; - readonly targetValue: DecimalBN; - readonly targetTs: BN; -} - -const ampFactor = (property = "ampFactor"): Layout => - struct( - [ - decimal("initialValue"), - i64("initialTs"), - decimal("targetValue"), - i64("targetTs"), - ], - property, - ); - export const solanaPool = ( numberOfTokens: number, property = "solanaPool", From 29ac2a8b7a1bf81f0b5d32a58935278e0b29447d Mon Sep 17 00:00:00 2001 From: Nico Miicro Date: Wed, 26 Oct 2022 10:31:26 +0300 Subject: [PATCH 11/11] chore(ui): remove unused import --- apps/ui/src/models/solana/deserializeLegacySolanaPoolState.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/ui/src/models/solana/deserializeLegacySolanaPoolState.ts b/apps/ui/src/models/solana/deserializeLegacySolanaPoolState.ts index 82adf0eef..74ce53f55 100644 --- a/apps/ui/src/models/solana/deserializeLegacySolanaPoolState.ts +++ b/apps/ui/src/models/solana/deserializeLegacySolanaPoolState.ts @@ -6,7 +6,6 @@ import { struct, u128, u32, - u64, u8, } from "@project-serum/borsh"; import type { Layout } from "@project-serum/borsh";