UNPKG

soroswap-utils

Version:

Utilities for interacting with Soroswap, the decentralized exchange (DEX) on Soroban, which is the smart contracts platform of the Stellar network.

164 lines (136 loc) 5.6 kB
import { Address, BASE_FEE, Contract, nativeToScVal, Networks, scValToNative, SorobanRpc, TransactionBuilder, xdr, } from "@stellar/stellar-sdk"; import { getAssetData as readAssetData } from "./assets"; import { getConfig } from "./config"; import type { Asset, PoolData } from "./types"; import { validateString } from "./utils"; const transactionTimeout = 30; type SorobanFunctions = "all_pairs" | "get_reserves" | "k_last" | "token_0" | "token_1"; const callSorobanFunction = async ( contractAddress: string, sorobanFunctionName: SorobanFunctions, sorobanFunctionArguments?: Readonly<xdr.ScVal>, ): Promise<SorobanRpc.Api.RawSimulateTransactionResponse> => { const { rpc: { url, wallet }, } = getConfig(); const server = new SorobanRpc.Server(url); // We always need to fetch the source account, to make sure we have the // latest sequence number. const sourceAccount = await server.getAccount(wallet); const contract = new Contract(contractAddress); const call = sorobanFunctionArguments ? contract.call(sorobanFunctionName, sorobanFunctionArguments) : contract.call(sorobanFunctionName); const transaction = new TransactionBuilder(sourceAccount, { fee: BASE_FEE, networkPassphrase: Networks.TESTNET, }) .addOperation(call) .setTimeout(transactionTimeout) .build(); // Rule disabled because it refers to a dependency we cannot change. // eslint-disable-next-line no-underscore-dangle return await server._simulateTransaction(transaction); }; const callPoolContract = async <SomePoolData>( poolAddress: string, sorobanFunctionName: "get_reserves" | "k_last" | "token_0" | "token_1", ): Promise<SomePoolData> => { const result = await callSorobanFunction(poolAddress, sorobanFunctionName); const firstResult = result.results?.[0]; if (firstResult === undefined) { throw new Error("Calling the contract failed"); } return scValToNative(xdr.ScVal.fromXDR(firstResult.xdr, "base64")) as SomePoolData; }; const getAssetData = async ( poolAddress: string, functionName: "token_0" | "token_1", ): Promise<Asset> => { const assetAddress = await callPoolContract<string>(poolAddress, functionName); return await readAssetData(assetAddress); }; /** * Retrieves the total number of liquidity pools from the Soroswap Factory * contract's storage, returning the total number of pairs. * * @returns A promise that resolves to the total number of liquidity pools. * @throws If the contract data cannot be read. */ const getLiquidityPoolCount = async (): Promise<number> => { const config = getConfig(); const server = new SorobanRpc.Server(config.rpc.url); const { val: value } = await server.getContractData( config.contracts.factory, xdr.ScVal.scvLedgerKeyContractInstance(), ); const storage = value.contractData().val().instance().storage(); if (storage === null) { throw new Error("Could not read the contract data"); } // Rule disabled because this property is not camelCased in // the smart contract // eslint-disable-next-line @typescript-eslint/naming-convention return (scValToNative(xdr.ScVal.scvMap(storage)) as { TotalPairs: number }).TotalPairs; }; const getLiquidityPoolAddress = async (index: number) => { const config = getConfig(); const liquidityPoolAddress = await callSorobanFunction( config.contracts.factory, "all_pairs", nativeToScVal(index, { type: "u32" }), ); if (!liquidityPoolAddress.results) { return undefined; } const address = xdr.ScVal.fromXDR( validateString( liquidityPoolAddress.results[0]?.xdr, "Invalid response: missing pool address", ), "base64", ); return Address.fromScVal(address).toString(); }; /** * Retrieves all liquidity pool addresses from the Soroswap Factory contract. * Since there is no function to do that directly, this function fetches the * addresses by iterating through all pairs using their index. * * @returns A promise that resolves to an array of liquidity pool addresses. */ const getLiquidityPoolAddresses = async (): Promise<string[]> => await Promise.all( Array.from( { length: await getLiquidityPoolCount() }, // eslint-disable-next-line no-underscore-dangle, @typescript-eslint/naming-convention async (_unused, index) => await getLiquidityPoolAddress(index), ), ).then((results) => results.filter((address): address is string => address !== undefined)); /** * Retrieves data about a liquidity pool. * * @param poolAddress The address of the liquidity pool. * @returns A promise that resolves to the data about the liquidity pool. * @throws If any contract call fails or if asset data cannot be retrieved. */ const getLiquidityPoolData = async (poolAddress: string): Promise<PoolData> => ({ // We call Soroban functions on the contract to populate the pool data. // Then we try to get more detailed data about the tokens. constantProductOfReserves: await callPoolContract<number>(poolAddress, "k_last"), firstToken: await getAssetData(poolAddress, "token_0"), poolContract: poolAddress, reserves: await callPoolContract<[number, number]>(poolAddress, "get_reserves"), secondToken: await getAssetData(poolAddress, "token_1"), }); export { getLiquidityPoolAddresses, getLiquidityPoolCount, getLiquidityPoolData };