From 5a4df11464f80f902f445768c296a5b0f309ddfb Mon Sep 17 00:00:00 2001 From: Donnie Date: Sat, 23 Sep 2023 06:39:38 +0800 Subject: [PATCH] refactor!: improve all scallop class --- src/builders/coreBuilder.ts | 417 +++++++++++++++++ src/builders/index.ts | 30 ++ src/{txBuilders => builders}/oracle.ts | 249 ++++------ src/builders/spoolBuilder.ts | 285 +++++++++++ src/constants/common.ts | 1 + src/constants/index.ts | 1 + src/constants/spool.ts | 6 + src/models/index.ts | 4 +- src/models/scallop.ts | 104 +++-- src/models/scallopAddress.ts | 494 +++++++++++--------- src/models/scallopBuilder.ts | 155 ++++++ src/models/scallopClient.ts | 394 +++++++++++++--- src/models/scallopQuery.ts | 145 ++++++ src/models/scallopUtils.ts | 226 ++++++--- src/queries/{market.ts => coreQuery.ts} | 94 +++- src/queries/index.ts | 5 +- src/queries/obligation.ts | 39 -- src/queries/spoolQuery.ts | 166 +++++++ src/txBuilders/coin.ts | 33 -- src/txBuilders/getPythObjectIds.ts | 14 - src/txBuilders/index.ts | 1 - src/txBuilders/normalMethods.ts | 216 --------- src/txBuilders/quickMethods.ts | 227 --------- src/types/address.ts | 104 +++++ src/types/{txBuilder.ts => builder/core.ts} | 74 ++- src/types/builder/index.ts | 7 + src/types/builder/spool.ts | 70 +++ src/types/{data.ts => data/core.ts} | 86 +--- src/types/data/index.ts | 2 + src/types/data/spool.ts | 42 ++ src/types/index.ts | 7 +- src/types/model.ts | 36 +- test/address.spec.ts | 316 +++++++++---- test/builder.spec.ts | 222 +++++++++ test/index.spec.ts | 241 +++++++--- test/query.spec.ts | 83 ++++ test/txBuilder.spec.ts | 142 ------ test/utils.spec.ts | 84 ++++ tsconfig.json | 2 +- 39 files changed, 3272 insertions(+), 1552 deletions(-) create mode 100644 src/builders/coreBuilder.ts create mode 100644 src/builders/index.ts rename src/{txBuilders => builders}/oracle.ts (58%) create mode 100644 src/builders/spoolBuilder.ts create mode 100644 src/constants/spool.ts create mode 100644 src/models/scallopBuilder.ts create mode 100644 src/models/scallopQuery.ts rename src/queries/{market.ts => coreQuery.ts} (73%) delete mode 100644 src/queries/obligation.ts create mode 100644 src/queries/spoolQuery.ts delete mode 100644 src/txBuilders/coin.ts delete mode 100644 src/txBuilders/getPythObjectIds.ts delete mode 100644 src/txBuilders/index.ts delete mode 100644 src/txBuilders/normalMethods.ts delete mode 100644 src/txBuilders/quickMethods.ts create mode 100644 src/types/address.ts rename src/types/{txBuilder.ts => builder/core.ts} (68%) create mode 100644 src/types/builder/index.ts create mode 100644 src/types/builder/spool.ts rename src/types/{data.ts => data/core.ts} (71%) create mode 100644 src/types/data/index.ts create mode 100644 src/types/data/spool.ts create mode 100644 test/builder.spec.ts create mode 100644 test/query.spec.ts delete mode 100644 test/txBuilder.spec.ts create mode 100644 test/utils.spec.ts diff --git a/src/builders/coreBuilder.ts b/src/builders/coreBuilder.ts new file mode 100644 index 0000000..13f9e5d --- /dev/null +++ b/src/builders/coreBuilder.ts @@ -0,0 +1,417 @@ +import { TransactionBlock, SUI_CLOCK_OBJECT_ID } from '@mysten/sui.js'; +import { SuiTxBlock as SuiKitTxBlock } from '@scallop-io/sui-kit'; +import { getObligations } from '../queries'; +import { updateOracles } from './oracle'; +import type { SuiTxArg } from '@scallop-io/sui-kit'; +import type { ScallopBuilder } from '../models'; +import type { + CoreIds, + GenerateCoreNormalMethod, + GenerateCoreQuickMethod, + SuiTxBlockWithCoreNormalMethods, + CoreTxBlock, + ScallopTxBlock, +} from '../types'; + +/** + * Check and get the sender from the transaction block. + * + * @param txBlock - txBlock created by SuiKit. + * @return Sender of transaction. + */ +const requireSender = (txBlock: SuiKitTxBlock) => { + const sender = txBlock.blockData.sender; + if (!sender) { + throw new Error('Sender is required'); + } + return sender; +}; + +/** + * Check and get Obligation information from transaction block. + * + * @description + * If the obligation id is provided, direactly return it. + * If both obligation id and key is provided, direactly return them. + * Otherwise, automatically get obligation id and key from the sender. + * + * @param builder - Scallop builder instance. + * @param txBlock - txBlock created by SuiKit. + * @param obligationId - Obligation id. + * @param obligationKey - Obligation key. + * @return Obligation id and key. + */ +const requireObligationInfo = async ( + ...params: [ + builder: ScallopBuilder, + txBlock: SuiKitTxBlock, + obligationId?: SuiTxArg | undefined, + obligationKey?: SuiTxArg | undefined, + ] +) => { + const [builder, txBlock, obligationId, obligationKey] = params; + if (params.length === 3 && obligationId) return { obligationId }; + if (params.length === 4 && obligationId && obligationKey) + return { obligationId, obligationKey }; + const sender = requireSender(txBlock); + const obligations = await getObligations(builder.query, sender); + if (obligations.length === 0) { + throw new Error(`No obligation found for sender ${sender}`); + } + return { + obligationId: obligations[0].id, + obligationKey: obligations[0].keyId, + }; +}; + +/** + * Generate core normal methods. + * + * @param builder Scallop builder instance. + * @param txBlock TxBlock created by SuiKit. + * @return Core normal methods. + */ +const generateCoreNormalMethod: GenerateCoreNormalMethod = ({ + builder, + txBlock, +}) => { + const coreIds: CoreIds = { + protocolPkg: builder.address.get('core.packages.protocol.id'), + market: builder.address.get('core.market'), + version: builder.address.get('core.version'), + coinDecimalsRegistry: builder.address.get('core.coinDecimalsRegistry'), + xOracle: builder.address.get('core.oracles.xOracle'), + }; + return { + openObligation: () => + txBlock.moveCall( + `${coreIds.protocolPkg}::open_obligation::open_obligation`, + [coreIds.version] + ), + returnObligation: (obligation, obligationHotPotato) => + txBlock.moveCall( + `${coreIds.protocolPkg}::open_obligation::return_obligation`, + [coreIds.version, obligation, obligationHotPotato] + ), + openObligationEntry: () => + txBlock.moveCall( + `${coreIds.protocolPkg}::open_obligation::open_obligation_entry`, + [coreIds.version] + ), + addCollateral: (obligation, coin, coinName) => { + const coinPackageId = builder.address.get(`core.coins.${coinName}.id`); + const coinType = builder.utils.parseCoinType(coinPackageId, coinName); + return txBlock.moveCall( + `${coreIds.protocolPkg}::deposit_collateral::deposit_collateral`, + [coreIds.version, obligation, coreIds.market, coin], + [coinType] + ); + }, + takeCollateral: (obligation, obligationKey, amount, coinName) => { + const coinPackageId = builder.address.get(`core.coins.${coinName}.id`); + const coinType = builder.utils.parseCoinType(coinPackageId, coinName); + return txBlock.moveCall( + `${coreIds.protocolPkg}::withdraw_collateral::withdraw_collateral`, + [ + coreIds.version, + obligation, + obligationKey, + coreIds.market, + coreIds.coinDecimalsRegistry, + amount, + coreIds.xOracle, + SUI_CLOCK_OBJECT_ID, + ], + [coinType] + ); + }, + deposit: (coin, coinName) => { + const coinPackageId = builder.address.get(`core.coins.${coinName}.id`); + const coinType = builder.utils.parseCoinType(coinPackageId, coinName); + return txBlock.moveCall( + `${coreIds.protocolPkg}::mint::mint`, + [coreIds.version, coreIds.market, coin, SUI_CLOCK_OBJECT_ID], + [coinType] + ); + }, + depositEntry: (coin, coinName) => { + const coinPackageId = builder.address.get(`core.coins.${coinName}.id`); + const coinType = builder.utils.parseCoinType(coinPackageId, coinName); + return txBlock.moveCall( + `${coreIds.protocolPkg}::mint::mint_entry`, + [coreIds.version, coreIds.market, coin, SUI_CLOCK_OBJECT_ID], + [coinType] + ); + }, + withdraw: (marketCoin, coinName) => { + const coinPackageId = builder.address.get(`core.coins.${coinName}.id`); + const coinType = builder.utils.parseCoinType(coinPackageId, coinName); + return txBlock.moveCall( + `${coreIds.protocolPkg}::redeem::redeem`, + [coreIds.version, coreIds.market, marketCoin, SUI_CLOCK_OBJECT_ID], + [coinType] + ); + }, + withdrawEntry: (marketCoin, coinName) => { + const coinPackageId = builder.address.get(`core.coins.${coinName}.id`); + const coinType = builder.utils.parseCoinType(coinPackageId, coinName); + return txBlock.moveCall( + `${coreIds.protocolPkg}::redeem::redeem_entry`, + [coreIds.version, coreIds.market, marketCoin, SUI_CLOCK_OBJECT_ID], + [coinType] + ); + }, + borrow: (obligation, obligationKey, amount, coinName) => { + const coinPackageId = builder.address.get(`core.coins.${coinName}.id`); + const coinType = builder.utils.parseCoinType(coinPackageId, coinName); + return txBlock.moveCall( + `${coreIds.protocolPkg}::borrow::borrow`, + [ + coreIds.version, + obligation, + obligationKey, + coreIds.market, + coreIds.coinDecimalsRegistry, + amount, + coreIds.xOracle, + SUI_CLOCK_OBJECT_ID, + ], + [coinType] + ); + }, + borrowEntry: (obligation, obligationKey, amount, coinName) => { + const coinPackageId = builder.address.get(`core.coins.${coinName}.id`); + const coinType = builder.utils.parseCoinType(coinPackageId, coinName); + return txBlock.moveCall( + `${coreIds.protocolPkg}::borrow::borrow_entry`, + [ + coreIds.version, + obligation, + obligationKey, + coreIds.market, + coreIds.coinDecimalsRegistry, + amount, + coreIds.xOracle, + SUI_CLOCK_OBJECT_ID, + ], + [coinType] + ); + }, + repay: (obligation, coin, coinName) => { + const coinPackageId = builder.address.get(`core.coins.${coinName}.id`); + const coinType = builder.utils.parseCoinType(coinPackageId, coinName); + return txBlock.moveCall( + `${coreIds.protocolPkg}::repay::repay`, + [ + coreIds.version, + obligation, + coreIds.market, + coin, + SUI_CLOCK_OBJECT_ID, + ], + [coinType] + ); + }, + borrowFlashLoan: (amount, coinName) => { + const coinPackageId = builder.address.get(`core.coins.${coinName}.id`); + const coinType = builder.utils.parseCoinType(coinPackageId, coinName); + return txBlock.moveCall( + `${coreIds.protocolPkg}::flash_loan::borrow_flash_loan`, + [coreIds.version, coreIds.market, amount], + [coinType] + ); + }, + repayFlashLoan: (coin, loan, coinName) => { + const coinPackageId = builder.address.get(`core.coins.${coinName}.id`); + const coinType = builder.utils.parseCoinType(coinPackageId, coinName); + return txBlock.moveCall( + `${coreIds.protocolPkg}::flash_loan::repay_flash_loan`, + [coreIds.version, coreIds.market, coin, loan], + [coinType] + ); + }, + }; +}; + +/** + * Generate core quick methods. + * + * @description + * The quick methods are the same as the normal methods, but they will automatically + * help users organize transaction blocks, include query obligation info, and transfer + * coins to the sender. So, they are all asynchronous methods. + * + * @param builder Scallop builder instance. + * @param txBlock TxBlock created by SuiKit. + * @return Core quick methods. + */ +const generateCoreQuickMethod: GenerateCoreQuickMethod = ({ + builder, + txBlock, +}) => { + return { + addCollateralQuick: async (amount, coinName, obligationId) => { + const sender = requireSender(txBlock); + const { obligationId: obligationArg } = await requireObligationInfo( + builder, + txBlock, + obligationId + ); + + if (coinName === 'sui') { + const [suiCoin] = txBlock.splitSUIFromGas([amount]); + txBlock.addCollateral(obligationArg, suiCoin, coinName); + } else { + const { leftCoin, takeCoin } = await builder.selectCoin( + txBlock, + coinName, + amount, + sender + ); + txBlock.addCollateral(obligationArg, takeCoin, coinName); + txBlock.transferObjects([leftCoin], sender); + } + }, + takeCollateralQuick: async ( + amount, + coinName, + obligationId, + obligationKey + ) => { + const obligationInfo = await requireObligationInfo( + builder, + txBlock, + obligationId, + obligationKey + ); + const updateCoinNames = await builder.utils.getObligationCoinNames( + obligationInfo.obligationId as string + ); + await updateOracles(builder, txBlock, updateCoinNames); + return txBlock.takeCollateral( + obligationInfo.obligationId, + obligationInfo.obligationKey as SuiTxArg, + amount, + coinName + ); + }, + depositQuick: async (amount, coinName) => { + const sender = requireSender(txBlock); + if (coinName === 'sui') { + const [suiCoin] = txBlock.splitSUIFromGas([amount]); + return txBlock.deposit(suiCoin, coinName); + } else { + const { leftCoin, takeCoin } = await builder.selectCoin( + txBlock, + coinName, + amount, + sender + ); + txBlock.transferObjects([leftCoin], sender); + return txBlock.deposit(takeCoin, coinName); + } + }, + withdrawQuick: async (amount, coinName) => { + const sender = requireSender(txBlock); + const { leftCoin, takeCoin } = await builder.selectMarketCoin( + txBlock, + coinName, + amount, + sender + ); + txBlock.transferObjects([leftCoin], sender); + return txBlock.withdraw(takeCoin, coinName); + }, + borrowQuick: async (amount, coinName, obligationId, obligationKey) => { + const obligationInfo = await requireObligationInfo( + builder, + txBlock, + obligationId, + obligationKey + ); + const obligationCoinNames = await builder.utils.getObligationCoinNames( + obligationInfo.obligationId as string + ); + const updateCoinNames = [...obligationCoinNames, coinName]; + await updateOracles(builder, txBlock, updateCoinNames); + return txBlock.borrow( + obligationInfo.obligationId, + obligationInfo.obligationKey as SuiTxArg, + amount, + coinName + ); + }, + repayQuick: async (amount, coinName, obligationId) => { + const sender = requireSender(txBlock); + const obligationInfo = await requireObligationInfo( + builder, + txBlock, + obligationId + ); + + if (coinName === 'sui') { + const [suiCoin] = txBlock.splitSUIFromGas([amount]); + return txBlock.repay(obligationInfo.obligationId, suiCoin, coinName); + } else { + const { leftCoin, takeCoin } = await builder.selectCoin( + txBlock, + coinName, + amount, + sender + ); + txBlock.transferObjects([leftCoin], sender); + return txBlock.repay(obligationInfo.obligationId, takeCoin, coinName); + } + }, + updateAssetPricesQuick: async (coinNames) => { + return updateOracles(builder, txBlock, coinNames); + }, + }; +}; + +/** + * Create an enhanced transaction block instance for interaction with core modules of the Scallop contract. + * + * @param builder Scallop builder instance. + * @param initTxBlock Scallop txBlock, txBlock created by SuiKit, or original transaction block. + * @return Scallop core txBlock. + */ +export const newCoreTxBlock = ( + builder: ScallopBuilder, + initTxBlock?: ScallopTxBlock | SuiKitTxBlock | TransactionBlock +) => { + const txBlock = + initTxBlock instanceof TransactionBlock + ? new SuiKitTxBlock(initTxBlock) + : initTxBlock + ? initTxBlock + : new SuiKitTxBlock(); + + const normalMethod = generateCoreNormalMethod({ + builder, + txBlock, + }); + + const normalTxBlock = new Proxy(txBlock, { + get: (target, prop) => { + if (prop in normalMethod) { + return Reflect.get(normalMethod, prop); + } + return Reflect.get(target, prop); + }, + }) as SuiTxBlockWithCoreNormalMethods; + + const quickMethod = generateCoreQuickMethod({ + builder, + txBlock: normalTxBlock, + }); + + return new Proxy(normalTxBlock, { + get: (target, prop) => { + if (prop in quickMethod) { + return Reflect.get(quickMethod, prop); + } + return Reflect.get(target, prop); + }, + }) as CoreTxBlock; +}; diff --git a/src/builders/index.ts b/src/builders/index.ts new file mode 100644 index 0000000..2f1b936 --- /dev/null +++ b/src/builders/index.ts @@ -0,0 +1,30 @@ +import { TransactionBlock } from '@mysten/sui.js'; +import { SuiTxBlock as SuiKitTxBlock } from '@scallop-io/sui-kit'; +import { newCoreTxBlock } from './coreBuilder'; +import { newSpoolTxBlock } from './spoolBuilder'; +import type { ScallopBuilder } from '../models'; +import type { ScallopTxBlock } from '../types'; + +/** + * Create a new ScallopTxBlock instance. + * + * @param builder - Scallop builder instance. + * @param txBlock - Scallop txBlock, txBlock created by SuiKit, or original transaction block. + * @return ScallopTxBlock + */ +export const newScallopTxBlock = ( + builder: ScallopBuilder, + initTxBlock?: ScallopTxBlock | SuiKitTxBlock | TransactionBlock +): ScallopTxBlock => { + const spoolTxBlock = newSpoolTxBlock(builder, initTxBlock); + const coreTxBlock = newCoreTxBlock(builder, spoolTxBlock); + + return new Proxy(coreTxBlock, { + get: (target, prop) => { + if (prop in spoolTxBlock) { + return Reflect.get(spoolTxBlock, prop); + } + return Reflect.get(target, prop); + }, + }) as ScallopTxBlock; +}; diff --git a/src/txBuilders/oracle.ts b/src/builders/oracle.ts similarity index 58% rename from src/txBuilders/oracle.ts rename to src/builders/oracle.ts index 2ac4996..63f9498 100644 --- a/src/txBuilders/oracle.ts +++ b/src/builders/oracle.ts @@ -1,131 +1,39 @@ -import { SUI_CLOCK_OBJECT_ID, TransactionArgument } from '@mysten/sui.js'; -import { SuiTxBlock, SuiKit } from '@scallop-io/sui-kit'; +import { SUI_CLOCK_OBJECT_ID } from '@mysten/sui.js'; +import type { TransactionArgument } from '@mysten/sui.js'; +import type { SuiTxBlock as SuiKitTxBlock } from '@scallop-io/sui-kit'; import { SuiPythClient, SuiPriceServiceConnection, } from '@pythnetwork/pyth-sui-js'; -import { ScallopAddress, ScallopUtils } from '../models'; -import { SupportCoins, SupportAssetCoins, SupportOracleType } from '../types'; -import { queryObligation } from '../queries'; - -export const updateOraclesForWithdrawCollateral = async ( - txBlock: SuiTxBlock, - address: ScallopAddress, - scallopUtils: ScallopUtils, - suiKit: SuiKit, - obligationId: string, - isTestnet: boolean -) => { - const obligationCoinNames = await getObligationCoinNames( - suiKit, - obligationId, - address, - scallopUtils - ); - return updateOracles( - txBlock, - suiKit, - address, - scallopUtils, - obligationCoinNames, - isTestnet - ); -}; - -export const updateOraclesForLiquidation = async ( - txBlock: SuiTxBlock, - address: ScallopAddress, - scallopUtils: ScallopUtils, - suiKit: SuiKit, - obligationId: string, - isTestnet: boolean -) => { - const obligationCoinNames = await getObligationCoinNames( - suiKit, - obligationId, - address, - scallopUtils - ); - return updateOracles( - txBlock, - suiKit, - address, - scallopUtils, - obligationCoinNames, - isTestnet - ); -}; - -export const updateOraclesForBorrow = async ( - txBlock: SuiTxBlock, - address: ScallopAddress, - scallopUtils: ScallopUtils, - suiKit: SuiKit, - obligationId: string, - borrowCoinName: SupportAssetCoins, - isTestnet: boolean -) => { - const obligationCoinNames = await getObligationCoinNames( - suiKit, - obligationId, - address, - scallopUtils - ); - const updateCoinNames = [ - ...new Set([...obligationCoinNames, borrowCoinName]), - ]; - return updateOracles( - txBlock, - suiKit, - address, - scallopUtils, - updateCoinNames, - isTestnet - ); -}; - -const getObligationCoinNames = async ( - suiKit: SuiKit, - obligationId: string, - address: ScallopAddress, - scallopUtils: ScallopUtils -) => { - const obligation = await queryObligation(obligationId, address, suiKit); - const collateralCoinTypes = obligation.collaterals.map((collateral) => { - return `0x${collateral.type.name}`; - }); - const debtCoinTypes = obligation.debts.map((debt) => { - return `0x${debt.type.name}`; - }); - const obligationCoinTypes = [ - ...new Set([...collateralCoinTypes, ...debtCoinTypes]), - ]; - const obligationCoinNames = obligationCoinTypes.map((coinType) => { - return scallopUtils.getCoinNameFromCoinType(coinType); - }); - return obligationCoinNames; -}; +import type { ScallopBuilder } from '../models'; +import type { SupportCoins, SupportOracleType } from '../types'; +/** + * Update the price of the oracle for multiple coin. + * + * @param builder - The scallop builder. + * @param txBlock - TxBlock created by SuiKit. + * @param coinNames - The coin names. + */ export const updateOracles = async ( - txBlock: SuiTxBlock, - suiKit: SuiKit, - address: ScallopAddress, - scallopUtils: ScallopUtils, - coinNames: SupportCoins[], - isTestnet: boolean + builder: ScallopBuilder, + txBlock: SuiKitTxBlock, + coinNames: SupportCoins[] ) => { - const rules: SupportOracleType[] = isTestnet ? ['pyth'] : ['pyth']; + const rules: SupportOracleType[] = builder.isTestnet ? ['pyth'] : ['pyth']; if (rules.includes('pyth')) { const pythClient = new SuiPythClient( - suiKit.provider(), - address.get('core.oracles.pyth.state'), - address.get('core.oracles.pyth.wormholeState') + builder.suiKit.provider(), + builder.address.get('core.oracles.pyth.state'), + builder.address.get('core.oracles.pyth.wormholeState') ); const priceIds = coinNames.map((coinName) => - address.get(`core.coins.${coinName}.oracle.pyth.feed`) + builder.address.get(`core.coins.${coinName}.oracle.pyth.feed`) ); const pythConnection = new SuiPriceServiceConnection( - isTestnet ? 'hermes-beta.pyth.network' : 'https://hermes.pyth.network' + builder.isTestnet + ? 'https://hermes-beta.pyth.network' + : 'https://hermes.pyth.network' ); const priceUpdateData = await pythConnection.getPriceFeedsUpdateData( priceIds @@ -137,37 +45,44 @@ export const updateOracles = async ( ); } - const updateCoinNames = [...new Set(coinNames)]; - for (const coinName of updateCoinNames) { - await updateOracle(txBlock, rules, address, scallopUtils, coinName); + // Remove duplicate coin names. + const updateCoinTypes = [...new Set(coinNames)]; + for (const coinName of updateCoinTypes) { + await updateOracle(builder, txBlock, coinName, rules); } }; +/** + * Update the price of the oracle for specific coin. + * + * @param builder - The scallop builder. + * @param txBlock - TxBlock created by SuiKit. + * @param coinName - The coin name. + */ const updateOracle = async ( - txBlock: SuiTxBlock, - rules: SupportOracleType[], - address: ScallopAddress, - scallopUtils: ScallopUtils, - coinName: SupportCoins + builder: ScallopBuilder, + txBlock: SuiKitTxBlock, + coinName: SupportCoins, + rules: SupportOracleType[] ) => { - const coinPackageId = address.get(`core.coins.${coinName}.id`); - const coinType = scallopUtils.parseCoinType(coinPackageId, coinName); + const coinPackageId = builder.address.get(`core.coins.${coinName}.id`); + const coinType = builder.utils.parseCoinType(coinPackageId, coinName); updatePrice( txBlock, rules, - address.get('core.packages.xOracle.id'), - address.get('core.oracles.xOracle'), - address.get('core.packages.pyth.id'), - address.get('core.oracles.pyth.registry'), - address.get('core.oracles.pyth.state'), - address.get(`core.coins.${coinName}.oracle.pyth.feedObject`), - address.get('core.packages.switchboard.id'), - address.get('core.oracles.switchboard.registry'), - address.get(`core.coins.${coinName}.oracle.switchboard`), - address.get('core.packages.supra.id'), - address.get('core.oracles.supra.registry'), - address.get(`core.oracles.supra.holder`), + builder.address.get('core.packages.xOracle.id'), + builder.address.get('core.oracles.xOracle'), + builder.address.get('core.packages.pyth.id'), + builder.address.get('core.oracles.pyth.registry'), + builder.address.get('core.oracles.pyth.state'), + builder.address.get(`core.coins.${coinName}.oracle.pyth.feedObject`), + builder.address.get('core.packages.switchboard.id'), + builder.address.get('core.oracles.switchboard.registry'), + builder.address.get(`core.coins.${coinName}.oracle.switchboard`), + builder.address.get('core.packages.supra.id'), + builder.address.get('core.oracles.supra.registry'), + builder.address.get(`core.oracles.supra.holder`), coinType ); }; @@ -190,10 +105,10 @@ const updateOracle = async ( * @param supraRegistryId - The registry id from supra package. * @param supraHolderId - The holder id from supra package. * @param coinType - The type of coin. - * @returns Sui-Kit type transaction block. + * @returns TxBlock created by SuiKit. */ -function updatePrice( - txBlock: SuiTxBlock, +const updatePrice = ( + txBlock: SuiKitTxBlock, rules: SupportOracleType[], xOraclePackageId: string, xOracleId: TransactionArgument | string, @@ -208,7 +123,7 @@ function updatePrice( supraRegistryId: TransactionArgument | string, supraHolderId: TransactionArgument | string, coinType: string -) { +) => { const request = priceUpdateRequest( txBlock, xOraclePackageId, @@ -254,7 +169,7 @@ function updatePrice( coinType ); return txBlock; -} +}; /** * Construct a transaction block for request price update. @@ -263,18 +178,18 @@ function updatePrice( * @param packageId - The xOracle package id. * @param xOracleId - The xOracle Id from xOracle package. * @param coinType - The type of coin. - * @returns Sui-Kit type transaction block. + * @returns TxBlock created by SuiKit. */ -function priceUpdateRequest( - txBlock: SuiTxBlock, +const priceUpdateRequest = ( + txBlock: SuiKitTxBlock, packageId: string, xOracleId: TransactionArgument | string, coinType: string -) { +) => { const target = `${packageId}::x_oracle::price_update_request`; const typeArgs = [coinType]; return txBlock.moveCall(target, [xOracleId], typeArgs); -} +}; /** * Construct a transaction block for confirm price update request. @@ -284,20 +199,20 @@ function priceUpdateRequest( * @param xOracleId - The xOracle Id from xOracle package. * @param request - The result of the request. * @param coinType - The type of coin. - * @returns Sui-Kit type transaction block. + * @returns TxBlock created by SuiKit. */ -function confirmPriceUpdateRequest( - txBlock: SuiTxBlock, +const confirmPriceUpdateRequest = ( + txBlock: SuiKitTxBlock, packageId: string, xOracleId: TransactionArgument | string, request: TransactionArgument, coinType: string -) { +) => { const target = `${packageId}::x_oracle::confirm_price_update_request`; const typeArgs = [coinType]; txBlock.moveCall(target, [xOracleId, request, SUI_CLOCK_OBJECT_ID], typeArgs); return txBlock; -} +}; /** * Construct a transaction block for update supra price. @@ -308,22 +223,22 @@ function confirmPriceUpdateRequest( * @param holderId - The holder id from supra package. * @param registryId - The registry id from supra package. * @param coinType - The type of coin. - * @returns Sui-Kit type transaction block. + * @returns TxBlock created by SuiKit. */ -function updateSupraPrice( - txBlock: SuiTxBlock, +const updateSupraPrice = ( + txBlock: SuiKitTxBlock, packageId: string, request: TransactionArgument, holderId: TransactionArgument | string, registryId: TransactionArgument | string, coinType: string -) { +) => { txBlock.moveCall( `${packageId}::rule::set_price`, [request, holderId, registryId, SUI_CLOCK_OBJECT_ID], [coinType] ); -} +}; /** * Construct a transaction block for update switchboard price. @@ -334,22 +249,22 @@ function updateSupraPrice( * @param aggregatorId - The aggregator id from switchboard package. * @param registryId - The registry id from switchboard package. * @param coinType - The type of coin. - * @returns Sui-Kit type transaction block. + * @returns TxBlock created by SuiKit. */ -function updateSwitchboardPrice( - txBlock: SuiTxBlock, +const updateSwitchboardPrice = ( + txBlock: SuiKitTxBlock, packageId: string, request: TransactionArgument, aggregatorId: TransactionArgument | string, registryId: TransactionArgument | string, coinType: string -) { +) => { txBlock.moveCall( `${packageId}::rule::set_price`, [request, aggregatorId, registryId, SUI_CLOCK_OBJECT_ID], [coinType] ); -} +}; /** * Construct a transaction block for update pyth price. @@ -363,20 +278,20 @@ function updateSwitchboardPrice( * @param vaaFromFeeId - The vaa from pyth api with feed id. * @param registryId - The registry id from pyth package. * @param coinType - The type of coin. - * @returns Sui-Kit type transaction block. + * @returns TxBlock created by SuiKit. */ -function updatePythPrice( - txBlock: SuiTxBlock, +const updatePythPrice = ( + txBlock: SuiKitTxBlock, packageId: string, request: TransactionArgument, stateId: TransactionArgument | string, feedObjectId: TransactionArgument | string, registryId: TransactionArgument | string, coinType: string -) { +) => { txBlock.moveCall( `${packageId}::rule::set_price`, [request, stateId, feedObjectId, registryId, SUI_CLOCK_OBJECT_ID], [coinType] ); -} +}; diff --git a/src/builders/spoolBuilder.ts b/src/builders/spoolBuilder.ts new file mode 100644 index 0000000..4c16a2e --- /dev/null +++ b/src/builders/spoolBuilder.ts @@ -0,0 +1,285 @@ +import { TransactionBlock, SUI_CLOCK_OBJECT_ID } from '@mysten/sui.js'; +import { SuiTxBlock as SuiKitTxBlock } from '@scallop-io/sui-kit'; +import { scallopRewardType } from '../constants/spool'; +import { getStakeAccounts } from '../queries/spoolQuery'; +import type { SuiTxArg } from '@scallop-io/sui-kit'; +import type { ScallopBuilder } from '../models'; +import type { + SpoolIds, + GenerateSpoolNormalMethod, + GenerateSpoolQuickMethod, + SuiTxBlockWithSpoolNormalMethods, + SpoolTxBlock, + SupportCoins, + SupportStakeMarketCoins, + ScallopTxBlock, +} from '../types'; + +/** + * Check and get the sender from the transaction block. + * + * @param txBlock - txBlock created by SuiKit. + * @return Sender of transaction. + */ +const requireSender = (txBlock: SuiKitTxBlock) => { + const sender = txBlock.blockData.sender; + if (!sender) { + throw new Error('Sender is required'); + } + return sender; +}; + +/** + * Check and get stake account information from transaction block. + * + * @description + * If the stake account id is provided, direactly return it. + * Otherwise, automatically get stake account id from the sender. + * + * @param builder - Scallop builder instance. + * @param txBlock - txBlock created by SuiKit. + * @param marketCoinName - The name of the market coin supported for staking. + * @param stakeAccountId - Stake account id. + * @return Stake account id. + */ +const requireStakeAccountInfo = async ( + ...params: [ + builder: ScallopBuilder, + txBlock: SuiKitTxBlock, + marketCoinName: SupportStakeMarketCoins, + stakeAccountId?: SuiTxArg, + ] +) => { + const [builder, txBlock, marketCoinName, stakeAccountId] = params; + if (params.length === 4 && stakeAccountId) return { stakeAccountId }; + const sender = requireSender(txBlock); + const accounts = await getStakeAccounts(builder.query, sender); + if (accounts[marketCoinName].length === 0) { + throw new Error(`No stake account found for sender ${sender}`); + } + return { + // Use the first stake account id as default. + stakeAccountId: accounts[marketCoinName][0].id, + }; +}; + +/** + * Generate spool normal methods. + * + * @param builder Scallop builder instance. + * @param txBlock TxBlock created by SuiKit . + * @return Spool normal methods. + */ +const generateSpoolNormalMethod: GenerateSpoolNormalMethod = ({ + builder, + txBlock, +}) => { + const spoolIds: SpoolIds = { + spoolPkg: builder.address.get('spool.id'), + }; + return { + createStakeAccount: (marketCoinName) => { + const coinName = marketCoinName.slice(1) as SupportCoins; + const coinPackageId = builder.address.get(`core.coins.${coinName}.id`); + const marketCoinType = builder.utils.parseMarketCoinType( + coinPackageId, + coinName + ); + const stakePoolId = builder.address.get( + `spool.pools.${marketCoinName}.id` + ); + return txBlock.moveCall( + `${spoolIds.spoolPkg}::user::new_spool_account`, + [stakePoolId, SUI_CLOCK_OBJECT_ID], + [marketCoinType] + ); + }, + stake: (stakeAccount, coin, marketCoinName) => { + const coinName = marketCoinName.slice(1) as SupportCoins; + const coinPackageId = builder.address.get(`core.coins.${coinName}.id`); + const marketCoinType = builder.utils.parseMarketCoinType( + coinPackageId, + coinName + ); + const stakePoolId = builder.address.get( + `spool.pools.${marketCoinName}.id` + ); + txBlock.moveCall( + `${spoolIds.spoolPkg}::user::stake`, + [stakePoolId, stakeAccount, coin, SUI_CLOCK_OBJECT_ID], + [marketCoinType] + ); + }, + unstake: (stakeAccount, amount, marketCoinName) => { + const coinName = marketCoinName.slice(1) as SupportCoins; + const coinPackageId = builder.address.get(`core.coins.${coinName}.id`); + const marketCoinType = builder.utils.parseMarketCoinType( + coinPackageId, + coinName + ); + const stakePoolId = builder.address.get( + `spool.pools.${marketCoinName}.id` + ); + return txBlock.moveCall( + `${spoolIds.spoolPkg}::user::unstake`, + [stakePoolId, stakeAccount, amount, SUI_CLOCK_OBJECT_ID], + [marketCoinType] + ); + }, + claim: (stakeAccount, marketCoinName) => { + const stakePoolId = builder.address.get( + `spool.pools.${marketCoinName}.id` + ); + const rewardPoolId = builder.address.get( + `spool.pools.${marketCoinName}.rewardPoolId` + ); + const coinName = marketCoinName.slice(1) as SupportCoins; + const coinPackageId = builder.address.get(`core.coins.${coinName}.id`); + const marketCoinType = builder.utils.parseMarketCoinType( + coinPackageId, + coinName + ); + const rewardCoinName = scallopRewardType[marketCoinName]; + const rewardCoinPackageId = builder.address.get( + `core.coins.${rewardCoinName}.id` + ); + const rewardType = builder.utils.parseCoinType( + rewardCoinPackageId, + rewardCoinName + ); + return txBlock.moveCall( + `${spoolIds.spoolPkg}::user::redeem_rewards`, + [stakePoolId, rewardPoolId, stakeAccount, SUI_CLOCK_OBJECT_ID], + [marketCoinType, rewardType] + ); + }, + }; +}; + +/** + * Generate spool quick methods. + * + * @description + * The quick methods are the same as the normal methods, but they will automatically + * help users organize transaction blocks, include get stake account info, and transfer + * coins to the sender. So, they are all asynchronous methods. + * + * @param builder Scallop builder instance. + * @param txBlock TxBlock created by SuiKit . + * @return Spool quick methods. + */ +const generateSpoolQuickMethod: GenerateSpoolQuickMethod = ({ + builder, + txBlock, +}) => { + return { + stakeQuick: async (amountOrMarketCoin, marketCoinName, stakeAccountId) => { + const sender = requireSender(txBlock); + const stakeAccountInfo = await requireStakeAccountInfo( + builder, + txBlock, + marketCoinName, + stakeAccountId + ); + + const coinName = marketCoinName.slice(1) as SupportCoins; + const coinPackageId = builder.address.get(`core.coins.${coinName}.id`); + const marketCoinType = builder.utils.parseMarketCoinType( + coinPackageId, + coinName + ); + if (typeof amountOrMarketCoin === 'number') { + const coins = await builder.utils.selectCoins( + sender, + amountOrMarketCoin, + marketCoinType + ); + const [takeCoin, leftCoin] = txBlock.takeAmountFromCoins( + coins, + amountOrMarketCoin + ); + txBlock.stake( + stakeAccountInfo.stakeAccountId, + takeCoin, + marketCoinName + ); + txBlock.transferObjects([leftCoin], sender); + } else { + txBlock.stake( + stakeAccountInfo.stakeAccountId, + amountOrMarketCoin, + marketCoinName + ); + } + }, + unstakeQuick: async (amount, marketCoinName, stakeAccountId) => { + const stakeAccountInfo = await requireStakeAccountInfo( + builder, + txBlock, + marketCoinName, + stakeAccountId + ); + return txBlock.unstake( + stakeAccountInfo.stakeAccountId, + amount, + marketCoinName + ); + }, + claimQuick: async (marketCoinName, stakeAccountId) => { + const stakeAccountInfo = await requireStakeAccountInfo( + builder, + txBlock, + marketCoinName, + stakeAccountId + ); + return txBlock.claim(stakeAccountInfo.stakeAccountId, marketCoinName); + }, + }; +}; + +/** + * Create an enhanced transaction block instance for interaction with spool modules of the Scallop contract. + * + * @param builder Scallop builder instance. + * @param initTxBlock Scallop txBlock, txBlock created by SuiKit, or original transaction block. + * @return Scallop spool txBlock. + */ +export const newSpoolTxBlock = ( + builder: ScallopBuilder, + initTxBlock?: ScallopTxBlock | SuiKitTxBlock | TransactionBlock +) => { + const txBlock = + initTxBlock instanceof TransactionBlock + ? new SuiKitTxBlock(initTxBlock) + : initTxBlock + ? initTxBlock + : new SuiKitTxBlock(); + + const normalMethod = generateSpoolNormalMethod({ + builder, + txBlock, + }); + + const normalTxBlock = new Proxy(txBlock, { + get: (target, prop) => { + if (prop in normalMethod) { + return Reflect.get(normalMethod, prop); + } + return Reflect.get(target, prop); + }, + }) as SuiTxBlockWithSpoolNormalMethods; + + const quickMethod = generateSpoolQuickMethod({ + builder, + txBlock: normalTxBlock, + }); + + return new Proxy(normalTxBlock, { + get: (target, prop) => { + if (prop in quickMethod) { + return Reflect.get(quickMethod, prop); + } + return Reflect.get(target, prop); + }, + }) as SpoolTxBlock; +}; diff --git a/src/constants/common.ts b/src/constants/common.ts index 7675183..df80487 100644 --- a/src/constants/common.ts +++ b/src/constants/common.ts @@ -25,6 +25,7 @@ export const SUPPORT_COLLATERAL_COINS = [ 'sol', 'cetus', ] as const; +export const SUPPORT_STACK_MARKET_COINS = ['ssui', 'susdc'] as const; export const SUPPORT_ORACLES = ['supra', 'switchboard', 'pyth'] as const; diff --git a/src/constants/index.ts b/src/constants/index.ts index d0b9323..197861d 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -1 +1,2 @@ export * from './common'; +export * from './spool'; diff --git a/src/constants/spool.ts b/src/constants/spool.ts new file mode 100644 index 0000000..557e056 --- /dev/null +++ b/src/constants/spool.ts @@ -0,0 +1,6 @@ +import type { RewardType } from '../types'; + +export const scallopRewardType: RewardType = { + ssui: 'sui', + susdc: 'sui', +}; diff --git a/src/models/index.ts b/src/models/index.ts index f9fd7ca..c19a31c 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -1,4 +1,6 @@ export * from './scallop'; +export * from './scallopAddress'; export * from './scallopClient'; +export * from './scallopBuilder'; +export * from './scallopQuery'; export * from './scallopUtils'; -export * from './scallopAddress'; diff --git a/src/models/scallop.ts b/src/models/scallop.ts index 5a9ca96..9d4ae27 100644 --- a/src/models/scallop.ts +++ b/src/models/scallop.ts @@ -1,76 +1,110 @@ import { SuiKit } from '@scallop-io/sui-kit'; import { ScallopAddress } from './scallopAddress'; import { ScallopClient } from './scallopClient'; +import { ScallopBuilder } from './scallopBuilder'; +import { ScallopQuery } from './scallopQuery'; import { ScallopUtils } from './scallopUtils'; -import { newScallopTxBlock } from '../txBuilders'; import { ADDRESSES_ID } from '../constants'; -import type { NetworkType } from '@scallop-io/sui-kit'; -import type { ScallopParams, ScallopTxBlock } from '../types'; +import type { ScallopParams } from '../types/'; /** - * ### Scallop - * + * @description * The main instance that controls interaction with the Scallop contract. * - * #### Usage - * + * @example * ```typescript * const sdk = new Scallop(); + * const scallopUtils= await sdk.getScallopUtils(); + * const scallopAddress = await sdk.getScallopAddress(); + * const scallopBuilder = await sdk.createScallopBuilder(); + * const scallopClient = await sdk.createScallopClient(); * ``` */ export class Scallop { public params: ScallopParams; public suiKit: SuiKit; - public address: ScallopAddress; + + private _address: ScallopAddress; public constructor(params: ScallopParams) { this.params = params; this.suiKit = new SuiKit(params); - this.address = new ScallopAddress({ + this._address = new ScallopAddress({ id: params?.addressesId || ADDRESSES_ID, network: params?.networkType, }); } /** - * Create an instance to operate the transaction block, making it more convenient to organize transaction combinations. - * @return Scallop Transaction Builder + * Get a scallop address instance that already has read addresses. + * + * @param id - The API id of the addresses. + * @return Scallop Address. */ - public async createTxBuilder() { - await this.address.read(); - const scallopUtils = new ScallopUtils(this.params); - const suiKit = new SuiKit(this.params); - const isTestnet = this.params.networkType === 'testnet'; - return { - createTxBlock: () => { - return newScallopTxBlock(suiKit, this.address, scallopUtils, isTestnet); - }, - signAndSendTxBlock: (txBlock: ScallopTxBlock) => { - return suiKit.signAndSendTxn(txBlock); - }, - }; + public async getScallopAddress(id?: string) { + await this._address.read(id); + + return this._address; } /** - * Create an instance to collect the addresses, making it easier to get object addresses from lending contract. + * Create a scallop builder instance that already has initial data. * - * @param id - The API id of the addresses. - * @param auth - The authentication API key. - * @param network - Specifies which network's addresses you want to set. - * @return Scallop Address + * @return Scallop Builder. */ - public createAddress(id: string, auth: string, network: NetworkType) { - return new ScallopAddress({ id, auth, network }); + public async createScallopBuilder() { + if (!this._address.getAddresses()) await this._address.read(); + const scallopBuilder = new ScallopBuilder(this.params, { + suiKit: this.suiKit, + address: this._address, + }); + + return scallopBuilder; } /** - * Create an instance that provides contract interaction operations for general users. + * Create a scallop client instance that already has initial data. * * @param walletAddress - When user cannot provide a secret key or mnemonic, the scallop client cannot directly derive the address of the transaction the user wants to sign. This argument specifies the wallet address for signing the transaction. - * @return Scallop Client + * @return Scallop Client. */ public async createScallopClient(walletAddress?: string) { - await this.address.read(); - return new ScallopClient(this.params, this.address, walletAddress); + if (!this._address.getAddresses()) await this._address.read(); + const scallopClient = new ScallopClient( + { ...this.params, walletAddress }, + { suiKit: this.suiKit, address: this._address } + ); + + return scallopClient; + } + + /** + * Create a scallop query instance. + * + * @return Scallop Query. + */ + public async createScallopQuery() { + if (!this._address.getAddresses()) await this._address.read(); + const scallopQuery = new ScallopQuery(this.params, { + suiKit: this.suiKit, + address: this._address, + }); + + return scallopQuery; + } + + /** + * Create a scallop utils instance. + * + * @return Scallop Utils. + */ + public async createScallopUtils() { + if (!this._address.getAddresses()) await this._address.read(); + const scallopUtils = new ScallopUtils(this.params, { + suiKit: this.suiKit, + address: this._address, + }); + + return scallopUtils; } } diff --git a/src/models/scallopAddress.ts b/src/models/scallopAddress.ts index 76c07ee..eb25b86 100644 --- a/src/models/scallopAddress.ts +++ b/src/models/scallopAddress.ts @@ -7,15 +7,178 @@ import type { AddressStringPath, } from '../types'; +const EMPTY_ADDRESSES: AddressesInterface = { + core: { + version: '', + versionCap: '', + market: '', + adminCap: '', + coinDecimalsRegistry: '', + coins: { + btc: { + id: '', + metaData: '', + treasury: '', + oracle: { + supra: '', + switchboard: '', + pyth: { + feed: '', + feedObject: '', + }, + }, + }, + eth: { + id: '', + metaData: '', + treasury: '', + oracle: { + supra: '', + switchboard: '', + pyth: { + feed: '', + feedObject: '', + }, + }, + }, + usdc: { + id: '', + metaData: '', + treasury: '', + oracle: { + supra: '', + switchboard: '', + pyth: { + feed: '', + feedObject: '', + }, + }, + }, + usdt: { + id: '', + metaData: '', + treasury: '', + oracle: { + supra: '', + switchboard: '', + pyth: { + feed: '', + feedObject: '', + }, + }, + }, + sui: { + id: '', + metaData: '', + treasury: '', + oracle: { + supra: '', + switchboard: '', + pyth: { + feed: '', + feedObject: '', + }, + }, + }, + }, + oracles: { + xOracle: '', + xOracleCap: '', + supra: { + registry: '', + registryCap: '', + holder: '', + }, + switchboard: { + registry: '', + registryCap: '', + }, + pyth: { + registry: '', + registryCap: '', + state: '', + wormhole: '', + wormholeState: '', + }, + }, + packages: { + coinDecimalsRegistry: { + id: '', + upgradeCap: '', + }, + math: { + id: '', + upgradeCap: '', + }, + whitelist: { + id: '', + upgradeCap: '', + }, + x: { + id: '', + upgradeCap: '', + }, + protocol: { + id: '', + upgradeCap: '', + }, + query: { + id: '', + upgradeCap: '', + }, + pyth: { + id: '', + upgradeCap: '', + }, + switchboard: { + id: '', + upgradeCap: '', + }, + xOracle: { + id: '', + upgradeCap: '', + }, + // Deploy for faucet on testnet + testCoin: { + id: '', + upgradeCap: '', + }, + }, + }, + spool: { + id: '', + adminCap: '', + pools: { + ssui: { + id: '', + rewardPoolId: '', + }, + susdc: { + id: '', + rewardPoolId: '', + }, + }, + }, +}; + /** + * @description * it provides methods for managing addresses. + * + * @example + * ```typescript + * const scallopAddress = new ScallopAddress(); + * scallopAddress.
(); + * await scallopAddress.
(); + * ``` */ export class ScallopAddress { - private _apiClient: AxiosInstance; - private _id?: string; private readonly _auth?: string; - private readonly _network: NetworkType; - private _addresses?: AddressesInterface; + + private _requestClient: AxiosInstance; + private _id?: string; + private _network: NetworkType; + private _currentAddresses?: AddressesInterface; private _addressesMap: Map; public constructor(params: ScallopAddressParams) { @@ -25,7 +188,7 @@ export class ScallopAddress { this._id = id; this._network = network || 'mainnet'; this._addressesMap = new Map(); - this._apiClient = axios.create({ + this._requestClient = axios.create({ baseURL: API_BASE_URL, headers: { 'Content-Type': 'application/json', @@ -41,7 +204,7 @@ export class ScallopAddress { * @returns The addresses API id. */ public getId() { - return this._id; + return this._id || undefined; } /** @@ -51,7 +214,7 @@ export class ScallopAddress { * @returns The address at the provided path. */ public get(path: AddressStringPath) { - if (this._addresses) { + if (this._currentAddresses) { const value = path .split('.') .reduce( @@ -59,7 +222,7 @@ export class ScallopAddress { typeof nestedAddressObj === 'object' ? nestedAddressObj[key] : nestedAddressObj, - this._addresses + this._currentAddresses ); return value || undefined; } else { @@ -68,14 +231,14 @@ export class ScallopAddress { } /** - * Set the address at the provided path. + * Sets the address for the specified path, it does not interact with the API. * * @param path - The path of the address to set. * @param address - The address be setted to the tartget path. * @returns The addresses. */ public set(path: AddressStringPath, address: string) { - if (this._addresses) { + if (this._currentAddresses) { const keys = path.split('.'); keys.reduce((nestedAddressObj: any, key: string, index) => { if (index === keys.length - 1) { @@ -83,181 +246,52 @@ export class ScallopAddress { } else { return nestedAddressObj[key]; } - }, this._addresses); + }, this._currentAddresses); } - return this._addresses; + return this._currentAddresses; } /** - * Get the addresses. + * Synchronize the specified network addresses from the addresses map to the + * current addresses and change the default network to specified network. + * + * @param network - Specifies which network's addresses you want to get. + * @return Current addresses + */ + public switchCurrentAddresses(network: NetworkType) { + if (this._addressesMap.has(network)) { + this._currentAddresses = this._addressesMap.get(network); + this._network = network; + } + return this._currentAddresses; + } + + /** + * Get the addresses, If `network` is not provided, returns the current + * addresses or the default network addresses in the addresses map. * * @param network - Specifies which network's addresses you want to get. - * @returns The addresses. */ public getAddresses(network?: NetworkType) { if (network) { return this._addressesMap.get(network); } else { - return this._addresses; + return this._currentAddresses ?? this._addressesMap.get(this._network); } } /** - * Set the addresses into addresses map. + * Set the addresses into addresses map. If the specified network is the same + * as the current network, the current addresses will be updated at the same time. * - * @param network - Specifies which network's addresses you want to set. * @param addresses - The addresses be setted to the tartget network. + * @param network - Specifies which network's addresses you want to set. * @returns The addresses. */ - public setAddresses(network?: NetworkType, addresses?: AddressesInterface) { + public setAddresses(addresses: AddressesInterface, network?: NetworkType) { const targetNetwork = network || this._network; - const targetAddresses = addresses || this._addresses || undefined; - if (targetAddresses) { - this._addressesMap.set(targetNetwork, targetAddresses); - } else { - // TODO: change to new format version - this._addressesMap.set(targetNetwork, { - core: { - version: '', - versionCap: '', - market: '', - adminCap: '', - coinDecimalsRegistry: '', - coins: { - btc: { - id: '', - metaData: '', - treasury: '', - oracle: { - supra: '', - switchboard: '', - pyth: { - feed: '', - feedObject: '', - }, - }, - }, - eth: { - id: '', - metaData: '', - treasury: '', - oracle: { - supra: '', - switchboard: '', - pyth: { - feed: '', - feedObject: '', - }, - }, - }, - usdc: { - id: '', - metaData: '', - treasury: '', - oracle: { - supra: '', - switchboard: '', - pyth: { - feed: '', - feedObject: '', - }, - }, - }, - usdt: { - id: '', - metaData: '', - treasury: '', - oracle: { - supra: '', - switchboard: '', - pyth: { - feed: '', - feedObject: '', - }, - }, - }, - sui: { - id: '', - metaData: '', - treasury: '', - oracle: { - supra: '', - switchboard: '', - pyth: { - feed: '', - feedObject: '', - }, - }, - }, - }, - oracles: { - xOracle: '', - xOracleCap: '', - supra: { - registry: '', - registryCap: '', - holder: '', - }, - switchboard: { - registry: '', - registryCap: '', - }, - pyth: { - registry: '', - registryCap: '', - state: '', - wormhole: '', - wormholeState: '', - }, - }, - packages: { - coinDecimalsRegistry: { - id: '', - upgradeCap: '', - }, - math: { - id: '', - upgradeCap: '', - }, - whitelist: { - id: '', - upgradeCap: '', - }, - x: { - id: '', - upgradeCap: '', - }, - protocol: { - id: '', - upgradeCap: '', - }, - query: { - id: '', - upgradeCap: '', - }, - // Deploy by pyth on testnet - pyth: { - id: '', - upgradeCap: '', - }, - // Deploy by ourself on testnet - switchboard: { - id: '', - upgradeCap: '', - }, - xOracle: { - id: '', - upgradeCap: '', - }, - // Deploy for faucet on testnet - testCoin: { - id: '', - upgradeCap: '', - }, - }, - }, - }); - } + if (targetNetwork === this._network) this._currentAddresses = addresses; + this._addressesMap.set(targetNetwork, addresses); } /** @@ -270,34 +304,45 @@ export class ScallopAddress { } /** - * Create a new address through the API and synchronize it back to the - * instance. If the `network` is not specified, the mainnet is used by default. - * If no `addresses` is provided, an addresses with all empty strings is created - * by default. + * Create a new addresses through the API and synchronize it back to the + * instance. + * + * @description + * If the `network` is not specified, the mainnet is used by default. + * If no `addresses` from instance or parameter is provided, an addresses with + * all empty strings is created by default. * * This function only allows for one addresses to be input into a specific network * at a time, and does not provide an addresses map for setting addresses * across all networks at once. * - * @param network - Specifies which network's addresses you want to set. - * @param addresses - The addresses be setted to the tartget network. - * @param auth - The authentication API key. - * @returns The addresses. + * @param params.addresses - The addresses be setted to the tartget network. + * @param params.network - Specifies which network's addresses you want to set. + * @param params.auth - The authentication API key. + * @param params.memo - Add memo to the addresses created in the API. + * @returns All addresses. */ - public async create( - network?: NetworkType, - addresses?: AddressesInterface, - auth?: string - ) { + public async create(params?: { + addresses?: AddressesInterface | undefined; + network?: NetworkType | undefined; + auth?: string | undefined; + memo?: string | undefined; + }) { + const { addresses, network, auth, memo } = params ?? {}; const apiKey = auth || this._auth || undefined; const targetNetwork = network || this._network; - const targetAddresses = addresses || this._addresses || undefined; + const targetAddresses = + addresses || + this._currentAddresses || + this._addressesMap.get(targetNetwork) || + EMPTY_ADDRESSES; + if (apiKey !== undefined) { this._addressesMap.clear(); - this.setAddresses(targetNetwork, targetAddresses); - const response = await this._apiClient.post( + this.setAddresses(targetAddresses, targetNetwork); + const response = await this._requestClient.post( `${API_BASE_URL}/addresses`, - JSON.stringify(Object.fromEntries(this._addressesMap)), + JSON.stringify({ ...Object.fromEntries(this._addressesMap), memo }), { headers: { 'Content-Type': 'application/json', @@ -311,12 +356,12 @@ export class ScallopAddress { response.data )) { if (['localnet', 'devnet', 'testnet', 'mainnet'].includes(network)) { - if (network === this._network) this._addresses = addresses; + if (network === this._network) this._currentAddresses = addresses; this._addressesMap.set(network as NetworkType, addresses); } } this._id = response.data.id; - return this._addresses; + return this.getAllAddresses(); } else { throw Error('Failed to create addresses.'); } @@ -326,17 +371,16 @@ export class ScallopAddress { } /** - * It doesn't read the data stored in the address instance, but reads and - * synchronizes the data from the API into instance. + * Read and synchronizes all addresses from the API into instance. * * @param id - The id of the addresses to get. - * @returns The addresses. + * @returns All addresses. */ public async read(id?: string) { const addressesId = id || this._id || undefined; if (addressesId !== undefined) { - const response = await this._apiClient.get( + const response = await this._requestClient.get( `${API_BASE_URL}/addresses/${addressesId}`, { headers: { @@ -350,53 +394,67 @@ export class ScallopAddress { response.data )) { if (['localnet', 'devnet', 'testnet', 'mainnet'].includes(network)) { - if (network === this._network) this._addresses = addresses; + if (network === this._network) this._currentAddresses = addresses; this._addressesMap.set(network as NetworkType, addresses); } } this._id = response.data.id; - return this._addresses; + return this.getAllAddresses(); } else { throw Error('Failed to create addresses.'); } + } else { + throw Error('Please provide API addresses id.'); } } /** - * Update the address through the API and synchronize it back to the - * instance. If the `network` is not specified, the mainnet is used by default. - * If no `addresses` is provided, an addresses with all empty strings is created - * by default. + * Update the addresses through the API and synchronize it back to the + * instance. + * + * @description + * If the `network` is not specified, the mainnet is used by default. + * If no `addresses` from instance or parameter is provided, an addresses with + * all empty strings is created by default. * * This function only allows for one addresses to be input into a specific network * at a time, and does not provide an addresses map for setting addresses * across all networks at once. * - * @param id - The id of the addresses to update. - * @param network - Specifies which network's addresses you want to set. - * @param addresses - The addresses be setted to the tartget network. - * @param auth - The authentication api key. - * @returns The addresses. + * @param params.id - The id of the addresses to update. + * @param params.addresses - The addresses be setted to the tartget network. + * @param params.network - Specifies which network's addresses you want to set. + * @param params.auth - The authentication api key. + * @param params.memo - Add memo to the addresses created in the API. + * @returns All addresses. */ - public async update( - id?: string, - network?: NetworkType, - addresses?: AddressesInterface, - auth?: string - ) { + public async update(params?: { + id?: string; + addresses?: AddressesInterface | undefined; + network?: NetworkType | undefined; + auth?: string | undefined; + memo?: string | undefined; + }) { + const { id, addresses, network, auth, memo } = params ?? {}; const apiKey = auth || this._auth || undefined; const targetId = id || this._id || undefined; const targetNetwork = network || this._network; - const targetAddresses = addresses || this._addresses || undefined; - if (targetId === undefined) throw Error('Require addresses id.'); + const targetAddresses = + addresses || + this._currentAddresses || + this._addressesMap.get(targetNetwork) || + EMPTY_ADDRESSES; + + if (targetId === undefined) + throw Error('Require specific addresses id to be updated.'); if (apiKey !== undefined) { if (id !== this._id) { this._addressesMap.clear(); } - this.setAddresses(targetNetwork, targetAddresses); - const response = await this._apiClient.put( + this.setAddresses(targetAddresses, targetNetwork); + const response = await this._requestClient.put( `${API_BASE_URL}/addresses/${targetId}`, - JSON.stringify(Object.fromEntries(this._addressesMap)), + JSON.stringify({ ...Object.fromEntries(this._addressesMap), memo }), { headers: { 'Content-Type': 'application/json', @@ -410,12 +468,12 @@ export class ScallopAddress { response.data )) { if (['localnet', 'devnet', 'testnet', 'mainnet'].includes(network)) { - if (network === this._network) this._addresses = addresses; + if (network === this._network) this._currentAddresses = addresses; this._addressesMap.set(network as NetworkType, addresses); } } this._id = response.data.id; - return this._addresses; + return this.getAllAddresses(); } else { throw Error('Failed to update addresses.'); } @@ -425,8 +483,8 @@ export class ScallopAddress { } /** - * Deletes all addresses of a specified id through the API and synchronizes - * them back to the instance. + * Deletes all addresses of a specified id through the API and clear all + * addresses in the instance. * * @param id - The id of the addresses to delete. * @param auth - The authentication API key. @@ -434,9 +492,11 @@ export class ScallopAddress { public async delete(id?: string, auth?: string) { const apiKey = auth || this._auth || undefined; const targetId = id || this._id || undefined; - if (targetId === undefined) throw Error('Require addresses id.'); + + if (targetId === undefined) + throw Error('Require specific addresses id to be deleted.'); if (apiKey !== undefined) { - const response = await this._apiClient.delete( + const response = await this._requestClient.delete( `${API_BASE_URL}/addresses/${targetId}`, { headers: { @@ -448,7 +508,7 @@ export class ScallopAddress { if (response.status === 200) { this._id = undefined; - this._addresses = undefined; + this._currentAddresses = undefined; this._addressesMap.clear(); } else { throw Error('Failed to delete addresses.'); diff --git a/src/models/scallopBuilder.ts b/src/models/scallopBuilder.ts new file mode 100644 index 0000000..4ff54b2 --- /dev/null +++ b/src/models/scallopBuilder.ts @@ -0,0 +1,155 @@ +import { normalizeSuiAddress } from '@mysten/sui.js'; +import { SuiKit } from '@scallop-io/sui-kit'; +import { ScallopAddress } from './scallopAddress'; +import { ScallopQuery } from './scallopQuery'; +import { ScallopUtils } from './scallopUtils'; +import { ADDRESSES_ID } from '../constants'; +import { newScallopTxBlock } from '../builders'; +import type { + TransactionBlock, + SuiTransactionBlockResponse, +} from '@mysten/sui.js'; +import type { SuiTxBlock as SuiKitTxBlock } from '@scallop-io/sui-kit'; +import type { + ScallopInstanceParams, + ScallopBuilderParams, + ScallopTxBlock, + SupportCoins, +} from '../types'; + +/** + * @description + * it provides methods for operating the transaction block, making it more convenient to organize transaction combinations. + * + * @example + * ```typescript + * const scallopBuilder = new ScallopBuilder(); + * await scallopBuilder.init(); + * const txBlock = scallopBuilder.(); + * ``` + */ +export class ScallopBuilder { + public readonly params: ScallopBuilderParams; + public readonly isTestnet: boolean; + + public suiKit: SuiKit; + public address: ScallopAddress; + public query: ScallopQuery; + public utils: ScallopUtils; + public walletAddress: string; + + public constructor( + params: ScallopBuilderParams, + instance?: ScallopInstanceParams + ) { + this.params = params; + this.suiKit = instance?.suiKit ?? new SuiKit(params); + this.address = + instance?.address ?? + new ScallopAddress({ + id: params?.addressesId || ADDRESSES_ID, + network: params?.networkType, + }); + this.query = + instance?.query ?? + new ScallopQuery(params, { + suiKit: this.suiKit, + address: this.address, + }); + this.utils = + instance?.utils ?? + new ScallopUtils(this.params, { + suiKit: this.suiKit, + address: this.address, + query: this.query, + }); + this.walletAddress = normalizeSuiAddress( + params?.walletAddress || this.suiKit.currentAddress() + ); + this.isTestnet = params.networkType + ? params.networkType === 'testnet' + : false; + } + + /** + * Request the scallop API to initialize data. + * + * @param forece Whether to force initialization. + */ + public async init(forece: boolean = false) { + if (forece || !this.address.getAddresses()) { + await this.address.read(); + } + await this.query.init(forece); + await this.utils.init(forece); + } + + /** + * Create a scallop txBlock instance that enhances transaction block. + * + * @param txBlock Scallop txBlock, txBlock created by SuiKit, or original transaction block. + * @return Scallop txBlock. + */ + public createTxBlock( + txBlock?: ScallopTxBlock | SuiKitTxBlock | TransactionBlock + ) { + return newScallopTxBlock(this, txBlock); + } + + /** + * Specifying the sender's amount of coins to get coins args from transaction result. + * + * @param txBlock Scallop txBlock or txBlock created by SuiKit . + * @param coinName Specific support coin name. + * @param amount Amount of coins to be selected. + * @param sender Sender address. + * @return Take coin and left coin. + */ + public async selectCoin( + txBlock: ScallopTxBlock | SuiKitTxBlock, + coinName: SupportCoins, + amount: number, + sender: string + ) { + const coinPackageId = this.address.get(`core.coins.${coinName}.id`); + const coinType = this.utils.parseCoinType(coinPackageId, coinName); + const coins = await this.utils.selectCoins(sender, amount, coinType); + const [takeCoin, leftCoin] = txBlock.takeAmountFromCoins(coins, amount); + return { takeCoin, leftCoin }; + } + + /** + * Specifying the sender's amount of market coins to get coins args from transaction result. + * + * @param txBlock Scallop txBlock or txBlock created by SuiKit . + * @param coinName Specific support coin name. + * @param amount Amount of coins to be selected. + * @param sender Sender address. + * @return Take coin and left coin. + */ + public async selectMarketCoin( + txBlock: ScallopTxBlock | SuiKitTxBlock, + coinName: SupportCoins, + amount: number, + sender: string + ) { + const coinPackageId = this.address.get(`core.coins.${coinName}.id`); + const coinType = this.utils.parseMarketCoinType(coinPackageId, coinName); + const coins = await this.utils.selectCoins(sender, amount, coinType); + const [takeCoin, leftCoin] = txBlock.takeAmountFromCoins(coins, amount); + return { takeCoin, leftCoin }; + } + + /** + * Execute Scallop txBlock using the `signAndSendTxn` methods in suikit. + * + * @param txBlock Scallop txBlock, txBlock created by SuiKit, or original transaction block. + */ + public async signAndSendTxBlock( + txBlock: ScallopTxBlock | SuiKitTxBlock | TransactionBlock + ) { + return (await this.suiKit.signAndSendTxn( + txBlock + )) as SuiTransactionBlockResponse; + } +} diff --git a/src/models/scallopClient.ts b/src/models/scallopClient.ts index 7449beb..e2fe193 100644 --- a/src/models/scallopClient.ts +++ b/src/models/scallopClient.ts @@ -2,8 +2,9 @@ import { normalizeSuiAddress } from '@mysten/sui.js'; import { SuiKit } from '@scallop-io/sui-kit'; import { ScallopAddress } from './scallopAddress'; import { ScallopUtils } from './scallopUtils'; -import { newScallopTxBlock } from '../txBuilders'; -import { queryObligation, queryMarket, getObligations } from '../queries'; +import { ScallopBuilder } from './scallopBuilder'; +import { ScallopQuery } from './scallopQuery'; +import { ADDRESSES_ID } from '../constants'; import type { TransactionArgument, SuiTransactionBlockResponse, @@ -11,96 +12,355 @@ import type { import type { SuiTxArg } from '@scallop-io/sui-kit'; import type { ScallopClientFnReturnType, - ScallopParams, + ScallopInstanceParams, + ScallopClientParams, SupportAssetCoins, SupportCollateralCoins, SupportCoins, + SupportStakeMarketCoins, ScallopTxBlock, } from '../types'; /** - * ### Scallop Client - * - * it provides contract interaction operations for general users. - * - * #### Usage + * @description + * It provides contract interaction operations for general users. * + * @example * ```typescript - * const client = new Scallop(); - * client.(); + * const scallopClient = new ScallopClient(); + * await scallopClient.init(); + * scallopClient.(); + * await scallopClient.(); * ``` */ export class ScallopClient { + public readonly params: ScallopClientParams; + public suiKit: SuiKit; public address: ScallopAddress; + public builder: ScallopBuilder; + public query: ScallopQuery; + public utils: ScallopUtils; public walletAddress: string; - private readonly _utils: ScallopUtils; - private readonly _isTestnet: boolean; - public constructor( - params: ScallopParams, - address: ScallopAddress, - walletAddress?: string, - isTestnet?: boolean + params: ScallopClientParams, + instance?: ScallopInstanceParams ) { - this.suiKit = new SuiKit(params); - this.address = address; + this.params = params; + this.suiKit = instance?.suiKit ?? new SuiKit(params); + this.address = + instance?.address ?? + new ScallopAddress({ + id: params?.addressesId || ADDRESSES_ID, + network: params?.networkType, + }); + this.query = + instance?.query ?? + new ScallopQuery(params, { + suiKit: this.suiKit, + address: this.address, + }); + this.utils = + instance?.utils ?? + new ScallopUtils(params, { + suiKit: this.suiKit, + address: this.address, + query: this.query, + }); + this.builder = + instance?.builder ?? + new ScallopBuilder(params, { + suiKit: this.suiKit, + address: this.address, + query: this.query, + utils: this.utils, + }); this.walletAddress = normalizeSuiAddress( - walletAddress || this.suiKit.currentAddress() + params?.walletAddress || this.suiKit.currentAddress() ); - this._utils = new ScallopUtils(params); - this._isTestnet = - isTestnet || - (params.networkType ? params.networkType === 'testnet' : false); } - createTxBlock() { - return newScallopTxBlock( - this.suiKit, - this.address, - this._utils, - this._isTestnet - ); + /** + * Request the scallop API to initialize data. + * + * @param forece Whether to force initialization. + */ + public async init(forece: boolean = false) { + if (forece || !this.address.getAddresses()) { + await this.address.read(); + } + await this.query.init(forece); + await this.utils.init(forece); + await this.builder.init(forece); } + /* === Query Method === */ + /** * Query market data. * + * @description + * This method might be @deprecated in the future, please use the {@link ScallopQuery} query instance instead. + * * @param rateType - How interest rates are calculated. * @return Market data */ - public async queryMarket(rateType: 'apy' | 'apr' = 'apr') { - return queryMarket(this.address, this.suiKit, this._utils, rateType); + public async queryMarket(rateType?: 'apy' | 'apr') { + return await this.query.getMarket(rateType); } /** - * Query obligations data. + * Get obligations data. + * + * @description + * This method might be @deprecated in the future, please use the {@link ScallopQuery} query instance instead. * * @param ownerAddress - The owner address. - * @return Obligations data + * @return Obligations data. */ - async getObligations(ownerAddress?: string) { + public async getObligations(ownerAddress?: string) { const owner = ownerAddress || this.walletAddress; - return getObligations(owner, this.suiKit); + return await this.query.getObligations(owner); } /** * Query obligation data. * - * @param obligationId - The obligation id from protocol package. + * @description + * This method might be @deprecated in the future, please use the {@link ScallopQuery} query instance instead. + * + * @param obligationId - The obligation id. * @return Obligation data */ public async queryObligation(obligationId: string) { - return queryObligation(obligationId, this.address, this.suiKit); + return await this.query.getObligation(obligationId); } /** - * Open obligation. + * Query all stake accounts data. + * + * @description + * This method might be @deprecated in the future, please use the {@link ScallopQuery} query instance instead. + * + * @param ownerAddress - The owner address. + * @return All stake accounts data. + */ + async getAllStakeAccounts(ownerAddress?: string) { + const owner = ownerAddress || this.walletAddress; + return await this.query.getAllStakeAccounts(owner); + } + + /** + * Query stake account data. + * + * @description + * This method might be @deprecated in the future, please use the {@link ScallopQuery} query instance instead. + * + * @param marketCoinName - Support stake market coin. + * @param ownerAddress - The owner address. + * @return Stake accounts data + */ + async getStakeAccounts( + marketCoinName: SupportStakeMarketCoins, + ownerAddress?: string + ) { + const owner = ownerAddress || this.walletAddress; + return await this.query.getStakeAccounts(marketCoinName, owner); + } + + /** + * Query stake pool data. + * + * @description + * This method might be @deprecated in the future, please use the {@link ScallopQuery} query instance instead. + * + * @param marketCoinName - Support stake market coin. + * @return Stake pool data. + */ + async getStakePool(marketCoinName: SupportStakeMarketCoins) { + return await this.query.getStakePool(marketCoinName); + } + + /** + * Query reward pool data. + * + * @description + * This method might be @deprecated in the future, please use the {@link ScallopQuery} query instance instead. + * + * @param marketCoinName - Support stake market coin. + * @return Reward pool data. + */ + async getRewardPool(marketCoinName: SupportStakeMarketCoins) { + return await this.query.getRewardPool(marketCoinName); + } + + /* === Spool Method === */ + + /** + * Create stake account. * * @param sign - Decide to directly sign the transaction or return the transaction block. + * @param walletAddress - The wallet address of the owner. * @return Transaction block response or transaction block */ + public async createStakeAccount( + marketCoinName: SupportStakeMarketCoins + ): Promise; + public async createStakeAccount( + marketCoinName: SupportStakeMarketCoins, + sign?: S, + walletAddress?: string + ): Promise>; + public async createStakeAccount( + marketCoinName: SupportStakeMarketCoins, + sign: S = true as S, + walletAddress?: string + ): Promise> { + const txBlock = this.builder.createTxBlock(); + const sender = walletAddress || this.walletAddress; + txBlock.setSender(sender); + + const account = txBlock.createStakeAccount(marketCoinName); + txBlock.transferObjects([account], sender); + + if (sign) { + return (await this.suiKit.signAndSendTxn( + txBlock + )) as ScallopClientFnReturnType; + } else { + return txBlock.txBlock as ScallopClientFnReturnType; + } + } + + /** + * Stake market coin into the specific spool. + * + * @param marketCoinName - Types of market coin. + * @param amount - The amount of coins would deposit. + * @param sign - Decide to directly sign the transaction or return the transaction block. + * @param stakeAccountId - The stake account object. + * @param walletAddress - The wallet address of the owner. + * @return Transaction block response or transaction block + */ + public async stake( + marketCoinName: SupportStakeMarketCoins, + amount: number + ): Promise; + public async stake( + marketCoinName: SupportStakeMarketCoins, + amount: number, + sign?: S, + stakeAccountId?: SuiTxArg, + walletAddress?: string + ): Promise>; + public async stake( + marketCoinName: SupportStakeMarketCoins, + amount: number, + sign: S = true as S, + stakeAccountId?: SuiTxArg, + walletAddress?: string + ): Promise> { + const txBlock = this.builder.createTxBlock(); + const sender = walletAddress || this.walletAddress; + txBlock.setSender(sender); + + const stakeAccountInfo = await this.query.getStakeAccounts(marketCoinName); + const targetStakeAccount = stakeAccountId || stakeAccountInfo[0].id; + if (targetStakeAccount) { + await txBlock.stakeQuick(amount, marketCoinName, targetStakeAccount); + } else { + const account = txBlock.createStakeAccount(marketCoinName); + await txBlock.stakeQuick(amount, marketCoinName, account); + txBlock.transferObjects([account], sender); + } + + if (sign) { + return (await this.suiKit.signAndSendTxn( + txBlock + )) as ScallopClientFnReturnType; + } else { + return txBlock.txBlock as ScallopClientFnReturnType; + } + } + + /** + * Unstake market coin from the specific spool. + * + * @param marketCoinName - Types of mak coin. + * @param amount - The amount of coins would deposit. + * @param sign - Decide to directly sign the transaction or return the transaction block. + * @param accountId - The stake account object. + * @param walletAddress - The wallet address of the owner. + * @return Transaction block response or transaction block + */ + public async unstake( + marketCoinName: SupportStakeMarketCoins, + amount: number, + sign: S = true as S, + accountId?: string, + walletAddress?: string + ): Promise> { + const txBlock = this.builder.createTxBlock(); + const sender = walletAddress || this.walletAddress; + txBlock.setSender(sender); + + const marketCoin = await txBlock.unstakeQuick( + amount, + marketCoinName, + accountId + ); + txBlock.transferObjects([marketCoin], sender); + + if (sign) { + return (await this.suiKit.signAndSendTxn( + txBlock + )) as ScallopClientFnReturnType; + } else { + return txBlock.txBlock as ScallopClientFnReturnType; + } + } + + /** + * Claim reward coin from the specific spool. + * + * @param marketCoinName - Types of mak coin. + * @param amount - The amount of coins would deposit. + * @param sign - Decide to directly sign the transaction or return the transaction block. + * @param accountId - The stake account object. + * @param walletAddress - The wallet address of the owner. + * @return Transaction block response or transaction block + */ + public async claim( + marketCoinName: SupportStakeMarketCoins, + sign: S = true as S, + accountId?: string, + walletAddress?: string + ): Promise> { + const txBlock = this.builder.createTxBlock(); + const sender = walletAddress || this.walletAddress; + txBlock.setSender(sender); + + const rewardCoin = await txBlock.claimQuick(marketCoinName, accountId); + txBlock.transferObjects([rewardCoin], sender); + + if (sign) { + return (await this.suiKit.signAndSendTxn( + txBlock + )) as ScallopClientFnReturnType; + } else { + return txBlock.txBlock as ScallopClientFnReturnType; + } + } + + /* === Core Method === */ + + /** + * Open obligation. + * + * @param sign - Decide to directly sign the transaction or return the transaction block. + * @return Transaction block response or transaction block. + */ public async openObligation(): Promise; public async openObligation( sign?: S @@ -108,7 +368,7 @@ export class ScallopClient { public async openObligation( sign: S = true as S ): Promise> { - const txBlock = this.createTxBlock(); + const txBlock = this.builder.createTxBlock(); txBlock.openObligationEntry(); if (sign) { return (await this.suiKit.signAndSendTxn( @@ -127,7 +387,7 @@ export class ScallopClient { * @param sign - Decide to directly sign the transaction or return the transaction block. * @param obligationId - The obligation object. * @param walletAddress - The wallet address of the owner. - * @return Transaction block response or transaction block + * @return Transaction block response or transaction block. */ public async depositCollateral( coinName: SupportCollateralCoins, @@ -147,12 +407,14 @@ export class ScallopClient { obligationId?: SuiTxArg, walletAddress?: string ): Promise> { - const txBlock = this.createTxBlock(); + const txBlock = this.builder.createTxBlock(); const sender = walletAddress || this.walletAddress; txBlock.setSender(sender); - if (obligationId) { - await txBlock.addCollateralQuick(amount, coinName, obligationId); + const obligations = await this.query.getObligations(sender); + const tarketObligationId = obligationId || obligations[0].id; + if (tarketObligationId) { + await txBlock.addCollateralQuick(amount, coinName, tarketObligationId); } else { const [obligation, obligationKey, hotPotato] = txBlock.openObligation(); await txBlock.addCollateralQuick(amount, coinName, obligation); @@ -178,7 +440,7 @@ export class ScallopClient { * @param obligationId - The obligation object. * @param obligationKey - The obligation key object to verifying obligation authority. * @param walletAddress - The wallet address of the owner. - * @return Transaction block response or transaction block + * @return Transaction block response or transaction block. */ public async withdrawCollateral( coinName: SupportCollateralCoins, @@ -188,7 +450,7 @@ export class ScallopClient { obligationKey: string, walletAddress?: string ): Promise> { - const txBlock = this.createTxBlock(); + const txBlock = this.builder.createTxBlock(); const sender = walletAddress || this.walletAddress; txBlock.setSender(sender); @@ -216,7 +478,7 @@ export class ScallopClient { * @param amount - The amount of coins would deposit. * @param sign - Decide to directly sign the transaction or return the transaction block. * @param walletAddress - The wallet address of the owner. - * @return Transaction block response or transaction block + * @return Transaction block response or transaction block. */ public async deposit( coinName: SupportAssetCoins, @@ -234,7 +496,7 @@ export class ScallopClient { sign: S = true as S, walletAddress?: string ): Promise> { - const txBlock = this.createTxBlock(); + const txBlock = this.builder.createTxBlock(); const sender = walletAddress || this.walletAddress; txBlock.setSender(sender); @@ -257,7 +519,7 @@ export class ScallopClient { * @param amount - The amount of coins would withdraw. * @param sign - Decide to directly sign the transaction or return the transaction block. * @param walletAddress - The wallet address of the owner. - * @return Transaction block response or transaction block + * @return Transaction block response or transaction block. */ public async withdraw( coinName: SupportAssetCoins, @@ -275,7 +537,7 @@ export class ScallopClient { sign: S = true as S, walletAddress?: string ): Promise> { - const txBlock = this.createTxBlock(); + const txBlock = this.builder.createTxBlock(); const sender = walletAddress || this.walletAddress; txBlock.setSender(sender); @@ -292,7 +554,7 @@ export class ScallopClient { } /** - * borrow asset from the specific pool. + * Borrow asset from the specific pool. * * @param coinName - Types of asset coin. * @param amount - The amount of coins would borrow. @@ -300,7 +562,7 @@ export class ScallopClient { * @param obligationId - The obligation object. * @param obligationKey - The obligation key object to verifying obligation authority. * @param walletAddress - The wallet address of the owner. - * @return Transaction block response or transaction block + * @return Transaction block response or transaction block. */ public async borrow( coinName: SupportAssetCoins, @@ -310,7 +572,7 @@ export class ScallopClient { obligationKey: string, walletAddress?: string ): Promise> { - const txBlock = this.createTxBlock(); + const txBlock = this.builder.createTxBlock(); const sender = walletAddress || this.walletAddress; txBlock.setSender(sender); @@ -339,7 +601,7 @@ export class ScallopClient { * @param sign - Decide to directly sign the transaction or return the transaction block. * @param obligationId - The obligation object. * @param walletAddress - The wallet address of the owner. - * @return Transaction block response or transaction block + * @return Transaction block response or transaction block. */ public async repay( coinName: SupportAssetCoins, @@ -348,7 +610,7 @@ export class ScallopClient { obligationId: string, walletAddress?: string ): Promise> { - const txBlock = this.createTxBlock(); + const txBlock = this.builder.createTxBlock(); const sender = walletAddress || this.walletAddress; txBlock.setSender(sender); @@ -370,7 +632,7 @@ export class ScallopClient { * @param amount - The amount of coins would repay. * @param callback - The callback function to build transaction block and return coin argument. * @param sign - Decide to directly sign the transaction or return the transaction block. - * @return Transaction block response or transaction block + * @return Transaction block response or transaction block. */ public async flashLoan( coinName: SupportAssetCoins, @@ -398,9 +660,9 @@ export class ScallopClient { ) => TransactionArgument, sign: S = true as S ): Promise> { - const txBlock = this.createTxBlock(); + const txBlock = this.builder.createTxBlock(); const [coin, loan] = txBlock.borrowFlashLoan(amount, coinName); - txBlock.repayFlashLoan(callback(txBlock, coin), loan, coinName); + txBlock.repayFlashLoan(await callback(txBlock, coin), loan, coinName); if (sign) { return (await this.suiKit.signAndSendTxn( @@ -411,17 +673,19 @@ export class ScallopClient { } } + /* === Other Method === */ + /** * Mint and get test coin. * * @remarks - * Only be used on the test network. + * Only be used on the test network. * * @param coinName - Types of coins supported on the test network. * @param amount - The amount of coins minted and received. * @param receiveAddress - The wallet address that receives the coins. * @param sign - Decide to directly sign the transaction or return the transaction block. - * @return Transaction block response or transaction block + * @return Transaction block response or transaction block. */ public async mintTestCoin( coinName: Exclude, @@ -439,11 +703,15 @@ export class ScallopClient { sign: S = true as S, receiveAddress?: string ): Promise> { - if (!this._isTestnet) { + const isTestnet = this.params.networkType + ? this.params.networkType === 'testnet' + : false; + + if (!isTestnet) { throw new Error('Only be used on the test network.'); } - const txBlock = this.createTxBlock(); + const txBlock = this.builder.createTxBlock(); const recipient = receiveAddress || this.walletAddress; const packageId = this.address.get('core.packages.testCoin.id'); const treasuryId = this.address.get(`core.coins.${coinName}.treasury`); diff --git a/src/models/scallopQuery.ts b/src/models/scallopQuery.ts new file mode 100644 index 0000000..8a3f365 --- /dev/null +++ b/src/models/scallopQuery.ts @@ -0,0 +1,145 @@ +import { SuiKit } from '@scallop-io/sui-kit'; +import { ScallopAddress } from './scallopAddress'; +import { ScallopUtils } from './scallopUtils'; +import { ADDRESSES_ID } from '../constants'; +import { + queryMarket, + getObligations, + queryObligation, + getStakeAccounts, + getStakePool, + getRewardPool, +} from '../queries'; +import { + ScallopQueryParams, + ScallopInstanceParams, + SupportStakeMarketCoins, +} from '../types'; + +/** + * @description + * it provides methods for getting on-chain data from the Scallop contract. + * + * @example + * ```typescript + * const scallopQuery = new ScallopQuery(); + * await scallopQuery.init(); + * scallopQuery.(); + * await scallopQuery.(); + * ``` + */ +export class ScallopQuery { + public readonly params: ScallopQueryParams; + + public suiKit: SuiKit; + public address: ScallopAddress; + public utils: ScallopUtils; + + public constructor( + params: ScallopQueryParams, + instance?: ScallopInstanceParams + ) { + this.params = params; + this.suiKit = instance?.suiKit ?? new SuiKit(params); + this.address = + instance?.address ?? + new ScallopAddress({ + id: params?.addressesId || ADDRESSES_ID, + network: params?.networkType, + }); + this.utils = + instance?.utils ?? + new ScallopUtils(this.params, { + suiKit: this.suiKit, + address: this.address, + query: this, + }); + } + + /** + * Request the scallop API to initialize data. + * + * @param forece Whether to force initialization. + */ + public async init(forece: boolean = false) { + if (forece || !this.address.getAddresses()) { + await this.address.read(); + } + await this.utils.init(forece); + } + + /** + * Get market data. + * + * @param rateType - How interest rates are calculated. + * @return Market data. + */ + public async getMarket(rateType: 'apy' | 'apr' = 'apr') { + return await queryMarket(this, rateType); + } + + /** + * Get obligations data. + * + * @param ownerAddress - The owner address. + * @return Obligations data. + */ + public async getObligations(ownerAddress?: string) { + return await getObligations(this, ownerAddress); + } + + /** + * Get obligation data. + * + * @param obligationId - The obligation id. + * @return Obligation data. + */ + public async getObligation(obligationId: string) { + return queryObligation(this, obligationId); + } + + /** + * Get stake accounts data for all stake pools. + * + * @param ownerAddress - The owner address. + * @return All Stake accounts data. + */ + public async getAllStakeAccounts(ownerAddress?: string) { + return await getStakeAccounts(this, ownerAddress); + } + + /** + * Get stake accounts data for specific stake pool. + * + * @param marketCoinName - Support stake market coin. + * @param ownerAddress - The owner address. + * @return Stake accounts data. + */ + public async getStakeAccounts( + marketCoinName: SupportStakeMarketCoins, + ownerAddress?: string + ) { + const allStakeAccount = await this.getAllStakeAccounts(ownerAddress); + return allStakeAccount[marketCoinName]; + } + + /** + * Get stake pool data. + * + * @param marketCoinName - The market coin name. + * @return Stake pool data. + */ + public async getStakePool(marketCoinName: SupportStakeMarketCoins) { + return await getStakePool(this, marketCoinName); + } + + /** + * Get reward pool data. + * + * @param marketCoinName - The market coin name. + * @return Reward pool data. + */ + public async getRewardPool(marketCoinName: SupportStakeMarketCoins) { + return await getRewardPool(this, marketCoinName); + } +} diff --git a/src/models/scallopUtils.ts b/src/models/scallopUtils.ts index 2af4adc..af2578c 100644 --- a/src/models/scallopUtils.ts +++ b/src/models/scallopUtils.ts @@ -4,69 +4,93 @@ import { normalizeStructTag, } from '@mysten/sui.js'; import { SuiKit } from '@scallop-io/sui-kit'; -import { PROTOCOL_OBJECT_ID } from '../constants/common'; -import type { ScallopParams, SupportCoins } from '../types'; +import { PriceServiceConnection } from '@pythnetwork/price-service-client'; +import { ScallopAddress } from './scallopAddress'; +import { ScallopQuery } from './scallopQuery'; +import { + ADDRESSES_ID, + PROTOCOL_OBJECT_ID, + scallopRewardType, +} from '../constants'; +import { queryObligation } from '../queries'; +import type { + ScallopUtilsParams, + ScallopInstanceParams, + SupportCoins, + SupportStakeMarketCoins, +} from '../types'; /** - * ### Scallop Utils - * + * @description * Integrates some helper functions frequently used in interactions with the Scallop contract. * - * #### Usage - * + * @example * ```typescript - * const utils = new ScallopUtils(); - * utils.(); + * const scallopUtils = new ScallopUtils(); + * await scallopUtils.init(); + * scallopUtils.(); + * await scallopUtils.(); * ``` */ export class ScallopUtils { + public readonly params: ScallopUtilsParams; private _suiKit: SuiKit; + private _address: ScallopAddress; + private _query: ScallopQuery; - public constructor(params: ScallopParams) { - this._suiKit = new SuiKit(params); + public constructor( + params: ScallopUtilsParams, + instance?: ScallopInstanceParams + ) { + this.params = params; + this._suiKit = instance?.suiKit ?? new SuiKit(params); + this._address = + instance?.address ?? + new ScallopAddress({ + id: params?.addressesId || ADDRESSES_ID, + network: params?.networkType, + }); + this._query = + instance?.query ?? + new ScallopQuery(params, { + suiKit: this._suiKit, + address: this._address, + }); } /** - * @description Select coin id that add up to the given amount as transaction arguments. - * @param owner The address of the owner. - * @param amount The amount that is needed for the coin. - * @param coinType The coin type, default is 0x2::SUI::SUI. - * @return The selected transaction coin arguments. + * Request the scallop API to initialize data. + * + * @param forece Whether to force initialization. */ - public async selectCoins( - owner: string, - amount: number, - coinType: string = SUI_TYPE_ARG - ) { - const coins = await this._suiKit.suiInteractor.selectCoins( - owner, - amount, - coinType - ); - return coins.map((c) => c.objectId); + public async init(forece: boolean = false) { + if (forece || !this._address.getAddresses()) { + await this._address.read(); + } + await this._query.init(forece); } /** - * @description Handle non-standard coins. + * Convert coin name to coin type. + * + * @description + * The Coin type of wormhole is fixed `coin:Coin`. Here using package id + * to determine and return the type. + * * @param coinPackageId Package id of coin. - * @param coinName specific support coin name. - * @return coinType. + * @param coinName Specific support coin name. + * @return Coin type. */ public parseCoinType(coinPackageId: string, coinName: string) { - if (coinName === 'sui') return normalizeStructTag(SUI_TYPE_ARG); + if (coinName === 'sui') + return normalizeStructTag(`${coinPackageId}::sui::SUI`); const wormHoleCoins = [ - // USDC - '0x5d4b302506645c37ff133b98c4b50a5ae14841659738d6d733d59d0d217a93bf', - // USDT - '0xc060006111016b8a020ad5b33834984a437aaa7d3c74c18e09a95d48aceab08c', - // ETH - '0xaf8cd5edc19c4512f4259f0bee101a40d41ebed738ade5874359610ef8eeced5', - // BTC - '0x027792d9fed7f9844eb4839566001bb6f6cb4804f66aa2da6fe1ee242d896881', - // SOL - '0xb7844e289a8410e50fb3ca48d69eb9cf29e27d223ef90353fe1bd8e27ff8f3f8', - // APT - '0x3a5143bb1196e3bcdfab6203d1683ae29edd26294fc8bfeafe4aaa9d2704df37', + this._address.get(`core.coins.usdc.id`), + this._address.get(`core.coins.usdt.id`), + this._address.get(`core.coins.eth.id`), + this._address.get(`core.coins.btc.id`), + this._address.get(`core.coins.sol.id`), + this._address.get(`core.coins.apt.id`), ]; if (wormHoleCoins.includes(coinPackageId)) { return `${coinPackageId}::coin::COIN`; @@ -76,25 +100,23 @@ export class ScallopUtils { } /** - * @description Handle non-standard coin names. - * @param coinPackageId Package id of coin. - * @param coinName specific support coin name. - * @return coinType. + * Convert coin type to coin name.. + * + * @description + * The coin name cannot be obtained directly from the wormhole type. Here + * the package id is used to determine and return a specific name. + * + * @param coinType Specific support coin type. + * @return Coin Name. */ - public getCoinNameFromCoinType(coinType: string) { + public parseCoinName(coinType: string) { const wormHoleCoinTypes = [ - // USDC - '0x5d4b302506645c37ff133b98c4b50a5ae14841659738d6d733d59d0d217a93bf::coin::COIN', - // USDT - '0xc060006111016b8a020ad5b33834984a437aaa7d3c74c18e09a95d48aceab08c::coin::COIN', - // ETH - '0xaf8cd5edc19c4512f4259f0bee101a40d41ebed738ade5874359610ef8eeced5::coin::COIN', - // BTC - '0x027792d9fed7f9844eb4839566001bb6f6cb4804f66aa2da6fe1ee242d896881::coin::COIN', - // SOL - '0xb7844e289a8410e50fb3ca48d69eb9cf29e27d223ef90353fe1bd8e27ff8f3f8::coin::COIN', - // APT - '0x3a5143bb1196e3bcdfab6203d1683ae29edd26294fc8bfeafe4aaa9d2704df37::coin::COIN', + `${this._address.get(`core.coins.usdc.id`)}::coin::COIN`, + `${this._address.get(`core.coins.usdt.id`)}::coin::COIN`, + `${this._address.get(`core.coins.eth.id`)}::coin::COIN`, + `${this._address.get(`core.coins.btc.id`)}::coin::COIN`, + `${this._address.get(`core.coins.sol.id`)}::coin::COIN`, + `${this._address.get(`core.coins.apt.id`)}::coin::COIN`, ]; if (coinType === wormHoleCoinTypes[0]) { @@ -115,12 +137,11 @@ export class ScallopUtils { } /** - * @description Handle market coin types. + * Convert market coin name to market coin type. * * @param coinPackageId Package id of coin. - * @param coinName specific support coin name. - * - * @return marketCoinType. + * @param coinName Specific support coin name. + * @return Market coin type. */ public parseMarketCoinType(coinPackageId: string, coinName: string) { const coinType = this.parseCoinType( @@ -129,4 +150,83 @@ export class ScallopUtils { ); return `${PROTOCOL_OBJECT_ID}::reserve::MarketCoin<${coinType}>`; } + + /** + * Select coin id that add up to the given amount as transaction arguments. + * + * @param owner The address of the owner. + * @param amount The amount that is needed for the coin. + * @param coinType The coin type, default is 0x2::SUI::SUI. + * @return The selected transaction coin arguments. + */ + public async selectCoins( + owner: string, + amount: number, + coinType: string = SUI_TYPE_ARG + ) { + const coins = await this._suiKit.suiInteractor.selectCoins( + owner, + amount, + coinType + ); + return coins.map((c) => c.objectId); + } + + /** + * Get reward type of stake pool. + * + * @param marketCoinName - Support stake market coin. + * @return Reward coin name. + */ + public getRewardCoinName = (marketCoinName: SupportStakeMarketCoins) => { + return scallopRewardType[marketCoinName]; + }; + + /** + * Get all coin names in the obligation record by obligation id. + * + * @description + * This can often be used to determine which assets in an obligation require + * price updates before interacting with specific instructions of the Scallop contract. + * + * @param obligationId The obligation id. + * @return Coin Names. + */ + public async getObligationCoinNames(obligationId: string) { + const obligation = await queryObligation(this._query, obligationId); + const collateralCoinTypes = obligation.collaterals.map((collateral) => { + return `0x${collateral.type.name}`; + }); + const debtCoinTypes = obligation.debts.map((debt) => { + return `0x${debt.type.name}`; + }); + const obligationCoinTypes = [ + ...new Set([...collateralCoinTypes, ...debtCoinTypes]), + ]; + const obligationCoinNames = obligationCoinTypes.map((coinType) => { + return this.parseCoinName(coinType); + }); + return obligationCoinNames; + } + + /** + * Fetch price feed VAAs of interest from the Pyth. + * + * @param priceIds Array of hex-encoded price ids. + * @param isTestnet Specify whether it is a test network. + * @return Array of base64 encoded VAAs. + */ + public async getVaas(priceIds: string[], isTestnet?: boolean) { + const connection = new PriceServiceConnection( + isTestnet + ? 'https://xc-testnet.pyth.network' + : 'https://xc-mainnet.pyth.network', + { + priceFeedRequestConfig: { + binary: true, + }, + } + ); + return await connection.getLatestVaas(priceIds); + } } diff --git a/src/queries/market.ts b/src/queries/coreQuery.ts similarity index 73% rename from src/queries/market.ts rename to src/queries/coreQuery.ts index 4c255e0..74b8cf8 100644 --- a/src/queries/market.ts +++ b/src/queries/coreQuery.ts @@ -1,6 +1,7 @@ -import { SuiKit, SuiTxBlock } from '@scallop-io/sui-kit'; +import { SuiTxBlock as SuiKitTxBlock } from '@scallop-io/sui-kit'; import BigNumber from 'bignumber.js'; -import { ScallopAddress, ScallopUtils } from '../models'; +import { PROTOCOL_OBJECT_ID } from '../constants'; +import type { ScallopQuery } from '../models'; import { MarketInterface, MarketDataInterface, @@ -8,20 +9,29 @@ import { CollateralPoolInterface, SupportCollateralCoins, SupportAssetCoins, + ObligationInterface, } from '../types'; +/** + * Query market data. + * + * @description + * Use inspectTxn call to obtain the data provided in the scallop contract query module + * + * @param query - The Scallop query instance. + * @param rateType - How interest rates are calculated. + * @return Market data. + */ export const queryMarket = async ( - scallopAddress: ScallopAddress, - suiKit: SuiKit, - scallopUtils: ScallopUtils, + query: ScallopQuery, rateType: 'apy' | 'apr' ) => { - const packageId = scallopAddress.get('core.packages.query.id'); - const marketId = scallopAddress.get('core.market'); - const txBlock = new SuiTxBlock(); + const packageId = query.address.get('core.packages.query.id'); + const marketId = query.address.get('core.market'); + const txBlock = new SuiKitTxBlock(); const queryTarget = `${packageId}::market_query::market_data`; txBlock.moveCall(queryTarget, [marketId]); - const queryResult = await suiKit.inspectTxn(txBlock); + const queryResult = await query.suiKit.inspectTxn(txBlock); const marketData = queryResult.events[0].parsedJson as MarketDataInterface; const assets: AssetPoolInterface[] = []; @@ -101,12 +111,10 @@ export const queryMarket = async ( supplyRate = supplyRate.isFinite() ? supplyRate : BigNumber(0); // base data - const coin = scallopUtils.getCoinNameFromCoinType( - coinType - ) as SupportAssetCoins; + const coin = query.utils.parseCoinName(coinType) as SupportAssetCoins; const symbol = coin.toUpperCase() as Uppercase; - const marketCoinType = scallopUtils.parseMarketCoinType( - scallopAddress.get(`core.coins.${coin}.id`), + const marketCoinType = query.utils.parseMarketCoinType( + query.address.get(`core.coins.${coin}.id`), coin ); const wrappedType = @@ -180,9 +188,7 @@ export const queryMarket = async ( const totalCollateralAmount = Number(collateral.totalCollateralAmount); // base data - const coin = scallopUtils.getCoinNameFromCoinType( - coinType - ) as SupportCollateralCoins; + const coin = query.utils.parseCoinName(coinType) as SupportCollateralCoins; const symbol = coin.toUpperCase() as Uppercase; const wrappedType = coin === 'usdc' || @@ -220,3 +226,57 @@ export const queryMarket = async ( data: marketData, } as MarketInterface; }; + +/** + * Query all owned obligations. + * + * @param query - The Scallop query instance. + * @param ownerAddress - The owner address. + * @return Owned obligations. + */ +export const getObligations = async ( + query: ScallopQuery, + ownerAddress?: string +) => { + const owner = ownerAddress || query.suiKit.currentAddress(); + const keyObjectRefs = await query.suiKit.provider().getOwnedObjects({ + owner, + filter: { + StructType: `${PROTOCOL_OBJECT_ID}::obligation::ObligationKey`, + }, + }); + const keyIds = keyObjectRefs.data + .map((ref: any) => ref?.data?.objectId) + .filter((id: any) => id !== undefined) as string[]; + const keyObjects = await query.suiKit.getObjects(keyIds); + const obligations: { id: string; keyId: string }[] = []; + for (const keyObject of keyObjects) { + const keyId = keyObject.objectId; + const fields = keyObject.objectFields as any; + const obligationId = fields['ownership']['fields']['of']; + obligations.push({ id: obligationId, keyId }); + } + return obligations; +}; + +/** + * Query obligation data. + * + * @description + * Use inspectTxn call to obtain the data provided in the scallop contract query module + * + * @param query - The Scallop query instance. + * @param obligationId - The obligation id. + * @return Obligation data. + */ +export const queryObligation = async ( + query: ScallopQuery, + obligationId: string +) => { + const packageId = query.address.get('core.packages.query.id'); + const queryTarget = `${packageId}::obligation_query::obligation_data`; + const txBlock = new SuiKitTxBlock(); + txBlock.moveCall(queryTarget, [obligationId]); + const queryResult = await query.suiKit.inspectTxn(txBlock); + return queryResult.events[0].parsedJson as ObligationInterface; +}; diff --git a/src/queries/index.ts b/src/queries/index.ts index 62384ba..117a32d 100644 --- a/src/queries/index.ts +++ b/src/queries/index.ts @@ -1,2 +1,3 @@ -export { queryMarket } from './market'; -export { queryObligation, getObligations } from './obligation'; +export * from './coreQuery'; +export * from './spoolQuery'; +// export { getBorrowings, getCollaterals, getLendings } from './portfolio'; diff --git a/src/queries/obligation.ts b/src/queries/obligation.ts deleted file mode 100644 index 414a9d5..0000000 --- a/src/queries/obligation.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { SuiKit, SuiTxBlock } from '@scallop-io/sui-kit'; -import { PROTOCOL_OBJECT_ID } from '../constants/common'; -import type { ScallopAddress } from '../models'; -import type { ObligationInterface } from '../types'; - -export const queryObligation = async ( - obligationId: string, - scallopAddress: ScallopAddress, - suiKit: SuiKit -) => { - const packageId = scallopAddress.get('core.packages.query.id'); - const queryTarget = `${packageId}::obligation_query::obligation_data`; - const txBlock = new SuiTxBlock(); - txBlock.moveCall(queryTarget, [obligationId]); - const queryResult = await suiKit.inspectTxn(txBlock); - return queryResult.events[0].parsedJson as ObligationInterface; -}; - -export const getObligations = async (ownerAddress: string, suiKit: SuiKit) => { - const owner = ownerAddress || suiKit.currentAddress(); - const keyObjectRefs = await suiKit.provider().getOwnedObjects({ - owner, - filter: { - StructType: `${PROTOCOL_OBJECT_ID}::obligation::ObligationKey`, - }, - }); - const keyIds = keyObjectRefs.data - .map((ref: any) => ref?.data?.objectId) - .filter((id: any) => id !== undefined) as string[]; - const keyObjects = await suiKit.getObjects(keyIds); - const obligations: { id: string; keyId: string }[] = []; - for (const keyObject of keyObjects) { - const keyId = keyObject.objectId; - const fields = keyObject.objectFields as any; - const obligationId = fields['ownership']['fields']['of']; - obligations.push({ id: obligationId, keyId }); - } - return obligations; -}; diff --git a/src/queries/spoolQuery.ts b/src/queries/spoolQuery.ts new file mode 100644 index 0000000..f647dd8 --- /dev/null +++ b/src/queries/spoolQuery.ts @@ -0,0 +1,166 @@ +import { + getObjectFields, + getObjectType, + getObjectId, + normalizeStructTag, +} from '@mysten/sui.js'; +import type { ScallopQuery } from '../models'; +import type { + StakePool, + RewardPool, + StakeAccount, + SupportStakeMarketCoins, + SupportCoins, +} from '../types'; + +/** + * Get all stake accounts of the owner. + * + * @param query - The Scallop query instance. + * @param ownerAddress - Owner address + * @returns Stake accounts + */ +export const getStakeAccounts = async ( + query: ScallopQuery, + ownerAddress?: string +) => { + const owner = ownerAddress || query.suiKit.currentAddress(); + const spoolPkgId = query.address.get('spool.id'); + const stakeAccountType = `${spoolPkgId}::spool_account::SpoolAccount`; + const stakeObjects = await query.suiKit.provider().getOwnedObjects({ + owner, + filter: { StructType: stakeAccountType }, + options: { + showContent: true, + showType: true, + }, + }); + + const stakeAccounts: Record = { + ssui: [], + susdc: [], + }; + + const stakeCointTypes: Record = Object.keys( + stakeAccounts + ).reduce( + (types, marketCoinName) => { + const coinName = marketCoinName.slice(1) as SupportCoins; + const coinPackageId = query.address.get(`core.coins.${coinName}.id`); + const marketCoinType = query.utils.parseMarketCoinType( + coinPackageId, + coinName + ); + + types[ + marketCoinName as SupportStakeMarketCoins + ] = `${spoolPkgId}::spool_account::SpoolAccount<${marketCoinType}>`; + return types; + }, + {} as Record + ); + + for (const object of stakeObjects.data) { + const id = getObjectId(object) as string; + const type = getObjectType(object) as string; + + const fields = getObjectFields(object) as any; + const spoolId = String(fields.spool_id); + const staked = Number(fields.stakes); + const index = Number(fields.index); + const points = Number(fields.points); + + if (normalizeStructTag(type) === stakeCointTypes.ssui) { + stakeAccounts.ssui.push({ id, type, spoolId, staked, index, points }); + } else if (normalizeStructTag(type) === stakeCointTypes.susdc) { + stakeAccounts.susdc.push({ id, type, spoolId, staked, index, points }); + } + } + return stakeAccounts; +}; + +/** + * Get stake account of the owner. + * + * @param query - The Scallop query instance. + * @param marketCoinName - Support stake market coins + * @return Stake account + */ +export const getStakePool = async ( + query: ScallopQuery, + marketCoinName: SupportStakeMarketCoins +) => { + const poolId = query.address.get(`spool.pools.${marketCoinName}.id`); + const poolObject = await query.suiKit.provider().getObject({ + id: poolId, + options: { showContent: true }, + }); + const id = getObjectId(poolObject) as string; + const type = getObjectType(poolObject) as string; + const fields = getObjectFields(poolObject) as any; + const lastUpdate = Number(fields.last_update); + const index = Number(fields.index); + const totalStaked = Number(fields.stakes); + const maxStake = Number(fields.max_stakes); + const distributedPoint = Number(fields.distributed_point); + const maxPoint = Number(fields.max_distributed_point); + const pointPerPeriod = Number(fields.distributed_point_per_period); + const period = Number(fields.point_distribution_time); + const createdAt = Number(fields.created_at); + + const stakePool: StakePool = { + id, + type, + lastUpdate, + index, + totalStaked, + maxStake, + distributedPoint, + maxPoint, + pointPerPeriod, + period, + createdAt, + }; + + return stakePool; +}; + +/** + * Get reward pool of the owner. + * @param query - The Scallop query instance. + * @param marketCoinName - Support stake market coins + * @return Reward pool + */ +export const getRewardPool = async ( + query: ScallopQuery, + marketCoinName: SupportStakeMarketCoins +) => { + const poolId = query.address.get( + `spool.pools.${marketCoinName}.rewardPoolId` + ); + + const poolObject = await query.suiKit.provider().getObject({ + id: poolId, + options: { showContent: true }, + }); + const id = getObjectId(poolObject) as string; + const type = getObjectType(poolObject) as string; + const fields = getObjectFields(poolObject) as any; + const stakePoolId = String(fields.spool_id); + const ratioNumerator = Number(fields.exchange_rate_numerator); + const ratioDenominator = Number(fields.exchange_rate_denominator); + const rewards = Number(fields.rewards); + const claimedRewards = Number(fields.claimed_rewards); + + const rewardPool: RewardPool = { + id, + type, + stakePoolId, + ratioNumerator, + ratioDenominator, + rewards, + claimedRewards, + }; + + return rewardPool; +}; diff --git a/src/txBuilders/coin.ts b/src/txBuilders/coin.ts deleted file mode 100644 index ab90473..0000000 --- a/src/txBuilders/coin.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { SuiTxBlock } from '@scallop-io/sui-kit'; -import { ScallopAddress, ScallopUtils } from '../models'; -import { SupportCoins } from '../types'; - -export const selectCoin = async ( - txBlock: SuiTxBlock, - scallopAddress: ScallopAddress, - scallopUtils: ScallopUtils, - coinName: SupportCoins, - amount: number, - sender: string -) => { - const coinPackageId = scallopAddress.get(`core.coins.${coinName}.id`); - const coinType = scallopUtils.parseCoinType(coinPackageId, coinName); - const coins = await scallopUtils.selectCoins(sender, amount, coinType); - const [takeCoin, leftCoin] = txBlock.takeAmountFromCoins(coins, amount); - return { takeCoin, leftCoin }; -}; - -export const selectMarketCoin = async ( - txBlock: SuiTxBlock, - scallopAddress: ScallopAddress, - scallopUtils: ScallopUtils, - coinName: SupportCoins, - amount: number, - sender: string -) => { - const coinPackageId = scallopAddress.get(`core.coins.${coinName}.id`); - const coinType = scallopUtils.parseMarketCoinType(coinPackageId, coinName); - const coins = await scallopUtils.selectCoins(sender, amount, coinType); - const [takeCoin, leftCoin] = txBlock.takeAmountFromCoins(coins, amount); - return { takeCoin, leftCoin }; -}; diff --git a/src/txBuilders/getPythObjectIds.ts b/src/txBuilders/getPythObjectIds.ts deleted file mode 100644 index 8060856..0000000 --- a/src/txBuilders/getPythObjectIds.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { SuiPythClient } from '@pythnetwork/pyth-sui-js'; -import { JsonRpcProvider, mainnetConnection } from '@mysten/sui.js'; - -export async function getPythObjectId(priceId: string) { - const pythStateId = - '0x1f9310238ee9298fb703c3419030b35b22bb1cc37113e3bb5007c99aec79e5b8'; - const wormholeStateId = - '0xaeab97f96cf9877fee2883315d459552b2b921edc16d7ceac6eab944dd88919c'; - const provider = new JsonRpcProvider(mainnetConnection); - const pythClient = new SuiPythClient(provider, pythStateId, wormholeStateId); - - const priceInfoObjectId = await pythClient.getPriceFeedObjectId(priceId); - return priceInfoObjectId; -} diff --git a/src/txBuilders/index.ts b/src/txBuilders/index.ts deleted file mode 100644 index 7f33d33..0000000 --- a/src/txBuilders/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { newScallopTxBlock } from './quickMethods'; diff --git a/src/txBuilders/normalMethods.ts b/src/txBuilders/normalMethods.ts deleted file mode 100644 index a946f92..0000000 --- a/src/txBuilders/normalMethods.ts +++ /dev/null @@ -1,216 +0,0 @@ -import { SUI_CLOCK_OBJECT_ID } from '@mysten/sui.js'; -import { SuiTxBlock } from '@scallop-io/sui-kit'; -import { ScallopAddress, ScallopUtils } from '../models'; -import type { - ScallopNormalMethodsHandler, - SuiTxBlockWithNormalScallopMethods, - CoreIds, -} from '../types'; - -const scallopNormalMethodsHandler: ScallopNormalMethodsHandler = { - openObligation: - ({ txBlock, coreIds }) => - () => - txBlock.moveCall( - `${coreIds.protocolPkg}::open_obligation::open_obligation`, - [coreIds.version] - ), - returnObligation: - ({ txBlock, coreIds }) => - (obligation, obligationHotPotato) => - txBlock.moveCall( - `${coreIds.protocolPkg}::open_obligation::return_obligation`, - [coreIds.version, obligation, obligationHotPotato] - ), - openObligationEntry: - ({ txBlock, coreIds }) => - () => - txBlock.moveCall( - `${coreIds.protocolPkg}::open_obligation::open_obligation_entry`, - [coreIds.version] - ), - addCollateral: - ({ txBlock, coreIds, scallopUtils, scallopAddress }) => - (obligation, coin, coinName) => { - const coinPackageId = scallopAddress.get(`core.coins.${coinName}.id`); - const coinType = scallopUtils.parseCoinType(coinPackageId, coinName); - return txBlock.moveCall( - `${coreIds.protocolPkg}::deposit_collateral::deposit_collateral`, - [coreIds.version, obligation, coreIds.market, coin], - [coinType] - ); - }, - takeCollateral: - ({ txBlock, coreIds, scallopAddress, scallopUtils }) => - (obligation, obligationKey, amount, coinName) => { - const coinPackageId = scallopAddress.get(`core.coins.${coinName}.id`); - const coinType = scallopUtils.parseCoinType(coinPackageId, coinName); - return txBlock.moveCall( - `${coreIds.protocolPkg}::withdraw_collateral::withdraw_collateral`, - [ - coreIds.version, - obligation, - obligationKey, - coreIds.market, - coreIds.dmlR, - amount, - coreIds.oracle, - SUI_CLOCK_OBJECT_ID, - ], - [coinType] - ); - }, - deposit: - ({ txBlock, coreIds, scallopAddress, scallopUtils }) => - (coin, coinName) => { - const coinPackageId = scallopAddress.get(`core.coins.${coinName}.id`); - const coinType = scallopUtils.parseCoinType(coinPackageId, coinName); - return txBlock.moveCall( - `${coreIds.protocolPkg}::mint::mint`, - [coreIds.version, coreIds.market, coin, SUI_CLOCK_OBJECT_ID], - [coinType] - ); - }, - depositEntry: - ({ txBlock, coreIds, scallopAddress, scallopUtils }) => - (coin, coinName) => { - const coinPackageId = scallopAddress.get(`core.coins.${coinName}.id`); - const coinType = scallopUtils.parseCoinType(coinPackageId, coinName); - return txBlock.moveCall( - `${coreIds.protocolPkg}::mint::mint_entry`, - [coreIds.version, coreIds.market, coin, SUI_CLOCK_OBJECT_ID], - [coinType] - ); - }, - withdraw: - ({ txBlock, coreIds, scallopAddress, scallopUtils }) => - (marketCoin, coinName) => { - const coinPackageId = scallopAddress.get(`core.coins.${coinName}.id`); - const coinType = scallopUtils.parseCoinType(coinPackageId, coinName); - return txBlock.moveCall( - `${coreIds.protocolPkg}::redeem::redeem`, - [coreIds.version, coreIds.market, marketCoin, SUI_CLOCK_OBJECT_ID], - [coinType] - ); - }, - withdrawEntry: - ({ txBlock, coreIds, scallopAddress, scallopUtils }) => - (marketCoin, coinName) => { - const coinPackageId = scallopAddress.get(`core.coins.${coinName}.id`); - const coinType = scallopUtils.parseCoinType(coinPackageId, coinName); - return txBlock.moveCall( - `${coreIds.protocolPkg}::redeem::redeem_entry`, - [coreIds.version, coreIds.market, marketCoin, SUI_CLOCK_OBJECT_ID], - [coinType] - ); - }, - borrow: - ({ txBlock, coreIds, scallopAddress, scallopUtils }) => - (obligation, obligationKey, amount, coinName) => { - const coinPackageId = scallopAddress.get(`core.coins.${coinName}.id`); - const coinType = scallopUtils.parseCoinType(coinPackageId, coinName); - return txBlock.moveCall( - `${coreIds.protocolPkg}::borrow::borrow`, - [ - coreIds.version, - obligation, - obligationKey, - coreIds.market, - coreIds.dmlR, - amount, - coreIds.oracle, - SUI_CLOCK_OBJECT_ID, - ], - [coinType] - ); - }, - borrowEntry: - ({ txBlock, coreIds, scallopAddress, scallopUtils }) => - (obligation, obligationKey, amount, coinName) => { - const coinPackageId = scallopAddress.get(`core.coins.${coinName}.id`); - const coinType = scallopUtils.parseCoinType(coinPackageId, coinName); - return txBlock.moveCall( - `${coreIds.protocolPkg}::borrow::borrow_entry`, - [ - coreIds.version, - obligation, - obligationKey, - coreIds.market, - coreIds.dmlR, - amount, - coreIds.oracle, - SUI_CLOCK_OBJECT_ID, - ], - [coinType] - ); - }, - repay: - ({ txBlock, coreIds, scallopAddress, scallopUtils }) => - (obligation, coin, coinName) => { - const coinPackageId = scallopAddress.get(`core.coins.${coinName}.id`); - const coinType = scallopUtils.parseCoinType(coinPackageId, coinName); - return txBlock.moveCall( - `${coreIds.protocolPkg}::repay::repay`, - [ - coreIds.version, - obligation, - coreIds.market, - coin, - SUI_CLOCK_OBJECT_ID, - ], - [coinType] - ); - }, - borrowFlashLoan: - ({ txBlock, coreIds, scallopAddress, scallopUtils }) => - (amount, coinName) => { - const coinPackageId = scallopAddress.get(`core.coins.${coinName}.id`); - const coinType = scallopUtils.parseCoinType(coinPackageId, coinName); - return txBlock.moveCall( - `${coreIds.protocolPkg}::flash_loan::borrow_flash_loan`, - [coreIds.version, coreIds.market, amount], - [coinType] - ); - }, - repayFlashLoan: - ({ txBlock, coreIds, scallopAddress, scallopUtils }) => - (coin, loan, coinName) => { - const coinPackageId = scallopAddress.get(`core.coins.${coinName}.id`); - const coinType = scallopUtils.parseCoinType(coinPackageId, coinName); - return txBlock.moveCall( - `${coreIds.protocolPkg}::flash_loan::repay_flash_loan`, - [coreIds.version, coreIds.market, coin, loan], - [coinType] - ); - }, -}; - -export const newTxBlock = ( - scallopAddress: ScallopAddress, - scallopUtils: ScallopUtils -) => { - const coreIds: CoreIds = { - protocolPkg: scallopAddress.get('core.packages.protocol.id'), - market: scallopAddress.get('core.market'), - version: scallopAddress.get('core.version'), - dmlR: scallopAddress.get('core.coinDecimalsRegistry'), - oracle: scallopAddress.get('core.oracles.xOracle'), - }; - const txBlock = new SuiTxBlock(); - const txBlockProxy = new Proxy(txBlock, { - get: (target, prop) => { - if (prop in scallopNormalMethodsHandler) { - return scallopNormalMethodsHandler[ - prop as keyof ScallopNormalMethodsHandler - ]({ - txBlock: target, - coreIds, - scallopAddress, - scallopUtils, - }); - } - return target[prop as keyof SuiTxBlock]; - }, - }); - return txBlockProxy as SuiTxBlockWithNormalScallopMethods; -}; diff --git a/src/txBuilders/quickMethods.ts b/src/txBuilders/quickMethods.ts deleted file mode 100644 index 73e06d9..0000000 --- a/src/txBuilders/quickMethods.ts +++ /dev/null @@ -1,227 +0,0 @@ -/** - * This file contains the complex transaction builder, which contains multiple calls in a single transaction. - */ -import { SuiTxBlock, SuiKit } from '@scallop-io/sui-kit'; -import { ScallopAddress, ScallopUtils } from '../models'; -import { getObligations } from '../queries'; -import { newTxBlock } from './normalMethods'; -import { - updateOraclesForBorrow, - updateOraclesForWithdrawCollateral, - updateOracles, -} from './oracle'; -import { selectCoin, selectMarketCoin } from './coin'; -import type { SuiTxArg } from '@scallop-io/sui-kit'; -import type { ScallopQuickMethodsHandler, ScallopTxBlock } from '../types'; - -const requireSender = (txBlock: SuiTxBlock) => { - const sender = txBlock.blockData.sender; - if (!sender) { - throw new Error('Sender is required'); - } - return sender; -}; - -const requireObligationInfo = async ( - ...args: [ - txBlock: SuiTxBlock, - suiKit: SuiKit, - obligationId?: SuiTxArg | undefined, - obligationKey?: SuiTxArg | undefined, - ] -) => { - const [txBlock, suiKit, obligationId, obligationKey] = args; - if (args.length === 3 && obligationId) return { obligationId }; - if (args.length === 4 && obligationId && obligationKey) - return { obligationId, obligationKey }; - const sender = requireSender(txBlock); - const obligations = await getObligations(sender, suiKit); - if (obligations.length === 0) { - throw new Error(`No obligation found for sender ${sender}`); - } - return { - obligationId: obligations[0].id, - obligationKey: obligations[0].keyId, - }; -}; - -const scallopQuickMethodsHandler: ScallopQuickMethodsHandler = { - addCollateralQuick: - ({ txBlock, scallopAddress, scallopUtils, suiKit }) => - async (amount, coinName, obligationId) => { - const sender = requireSender(txBlock); - const { obligationId: obligationArg } = await requireObligationInfo( - txBlock, - suiKit, - obligationId - ); - - if (coinName === 'sui') { - const [suiCoin] = txBlock.splitSUIFromGas([amount]); - txBlock.addCollateral(obligationArg, suiCoin, coinName); - } else { - const { leftCoin, takeCoin } = await selectCoin( - txBlock, - scallopAddress, - scallopUtils, - coinName, - amount, - sender - ); - txBlock.addCollateral(obligationArg, takeCoin, coinName); - txBlock.transferObjects([leftCoin], sender); - } - }, - takeCollateralQuick: - ({ txBlock, suiKit, scallopUtils, scallopAddress, isTestnet }) => - async (amount, coinName, obligationId, obligationKey) => { - const { obligationId: obligationArg, obligationKey: obligationKeyArg } = - await requireObligationInfo( - txBlock, - suiKit, - obligationId, - obligationKey - ); - - await updateOraclesForWithdrawCollateral( - txBlock, - scallopAddress, - scallopUtils, - suiKit, - obligationArg as string, - isTestnet - ); - return txBlock.takeCollateral( - obligationArg, - obligationKeyArg as SuiTxArg, - amount, - coinName - ); - }, - depositQuick: - ({ txBlock, scallopUtils, scallopAddress }) => - async (amount, coinName) => { - const sender = requireSender(txBlock); - if (coinName === 'sui') { - const [suiCoin] = txBlock.splitSUIFromGas([amount]); - return txBlock.deposit(suiCoin, coinName); - } else { - const { leftCoin, takeCoin } = await selectCoin( - txBlock, - scallopAddress, - scallopUtils, - coinName, - amount, - sender - ); - txBlock.transferObjects([leftCoin], sender); - return txBlock.deposit(takeCoin, coinName); - } - }, - withdrawQuick: - ({ txBlock, scallopUtils, scallopAddress }) => - async (amount, coinName) => { - const sender = requireSender(txBlock); - const { leftCoin, takeCoin } = await selectMarketCoin( - txBlock, - scallopAddress, - scallopUtils, - coinName, - amount, - sender - ); - txBlock.transferObjects([leftCoin], sender); - return txBlock.withdraw(takeCoin, coinName); - }, - borrowQuick: - ({ txBlock, suiKit, scallopUtils, scallopAddress, isTestnet }) => - async (amount, coinName, obligationId, obligationKey) => { - const { obligationId: obligationArg, obligationKey: obligationKeyArg } = - await requireObligationInfo( - txBlock, - suiKit, - obligationId, - obligationKey - ); - - await updateOraclesForBorrow( - txBlock, - scallopAddress, - scallopUtils, - suiKit, - obligationArg as string, - coinName, - isTestnet - ); - return txBlock.borrow( - obligationArg, - obligationKeyArg as SuiTxArg, - amount, - coinName - ); - }, - repayQuick: - ({ txBlock, suiKit, scallopUtils, scallopAddress }) => - async (amount, coinName, obligationId) => { - const sender = requireSender(txBlock); - const { obligationId: obligationArg } = await requireObligationInfo( - txBlock, - suiKit, - obligationId - ); - - if (coinName === 'sui') { - const [suiCoin] = txBlock.splitSUIFromGas([amount]); - return txBlock.repay(obligationArg, suiCoin, coinName); - } else { - const { leftCoin, takeCoin } = await selectCoin( - txBlock, - scallopAddress, - scallopUtils, - coinName, - amount, - sender - ); - txBlock.transferObjects([leftCoin], sender); - return txBlock.repay(obligationArg, takeCoin, coinName); - } - }, - updateAssetPricesQuick: - ({ txBlock, suiKit, scallopUtils, scallopAddress, isTestnet }) => - async (coinNames) => { - return updateOracles( - txBlock, - suiKit, - scallopAddress, - scallopUtils, - coinNames, - isTestnet - ); - }, -}; - -export const newScallopTxBlock = ( - suiKit: SuiKit, - scallopAddress: ScallopAddress, - scallopUtils: ScallopUtils, - isTestnet: boolean -) => { - const txBlock = newTxBlock(scallopAddress, scallopUtils); - const txBlockProxy = new Proxy(txBlock, { - get: (target, prop) => { - if (prop in scallopQuickMethodsHandler) { - return scallopQuickMethodsHandler[ - prop as keyof ScallopQuickMethodsHandler - ]({ - txBlock: target, - suiKit, - scallopAddress, - scallopUtils, - isTestnet, - }); - } - return target[prop as keyof SuiTxBlock]; - }, - }); - return txBlockProxy as ScallopTxBlock; -}; diff --git a/src/types/address.ts b/src/types/address.ts new file mode 100644 index 0000000..610f079 --- /dev/null +++ b/src/types/address.ts @@ -0,0 +1,104 @@ +import { SUPPORT_ORACLES } from '../constants'; +import type { + SupportCoins, + SupportOracleType, + SupportPackageType, + SupportStakeMarketCoins, +} from './data'; + +export interface AddressesInterface { + core: { + version: string; + versionCap: string; + market: string; + adminCap: string; + coinDecimalsRegistry: string; + coins: Partial< + Record< + SupportCoins, + { + id: string; + treasury: string; + metaData: string; + oracle: { + [K in SupportOracleType]: K extends (typeof SUPPORT_ORACLES)[0] + ? string + : K extends (typeof SUPPORT_ORACLES)[1] + ? string + : K extends (typeof SUPPORT_ORACLES)[2] + ? { + feed: string; + feedObject: string; + } + : never; + }; + } + > + >; + oracles: { + [K in SupportOracleType]: K extends (typeof SUPPORT_ORACLES)[0] + ? { + registry: string; + registryCap: string; + holder: string; + } + : K extends (typeof SUPPORT_ORACLES)[1] + ? { + registry: string; + registryCap: string; + } + : K extends (typeof SUPPORT_ORACLES)[2] + ? { + registry: string; + registryCap: string; + state: string; + wormhole: string; + wormholeState: string; + } + : never; + } & { xOracle: string; xOracleCap: string }; + packages: Partial< + Record< + SupportPackageType, + { + id: string; + upgradeCap: string; + } + > + >; + }; + spool: { + id: string; + adminCap: string; + pools: Partial< + Record< + SupportStakeMarketCoins, + { + id: string; + rewardPoolId: string; + } + > + >; + }; +} + +type AddressPathsProps = T extends string + ? [] + : { + [K in Extract]: [K, ...AddressPathsProps]; + }[Extract]; + +type Join = T extends [] + ? never + : T extends [infer F] + ? F + : T extends [infer F, ...infer R] + ? F extends string + ? `${F}${D}${Join, D>}` + : never + : string; + +export type AddressStringPath = Join< + AddressPathsProps, + '.' +>; diff --git a/src/types/txBuilder.ts b/src/types/builder/core.ts similarity index 68% rename from src/types/txBuilder.ts rename to src/types/builder/core.ts index 3797152..eebe33d 100644 --- a/src/types/txBuilder.ts +++ b/src/types/builder/core.ts @@ -1,14 +1,22 @@ import type { TransactionArgument } from '@mysten/sui.js'; -import type { SuiTxBlock, SuiTxArg, SuiKit } from '@scallop-io/sui-kit'; -import type { ScallopAddress, ScallopUtils } from '../models'; -import type { SupportCollateralCoins, SupportAssetCoins } from './data'; +import type { + SuiTxBlock as SuiKitTxBlock, + SuiTxArg, +} from '@scallop-io/sui-kit'; +import type { ScallopBuilder } from '../../models'; +import type { SupportCollateralCoins, SupportAssetCoins } from '../data'; type TransactionResult = TransactionArgument & TransactionArgument[]; -/** - * ========== Scallop Normal Methods ========== - */ -export type ScallopNormalMethods = { +export type CoreIds = { + protocolPkg: string; + market: string; + version: string; + coinDecimalsRegistry: string; + xOracle: string; +}; + +export type CoreNormalMethods = { openObligation: () => TransactionResult; returnObligation: ( obligation: SuiTxArg, @@ -61,31 +69,7 @@ export type ScallopNormalMethods = { ) => void; }; -export type CoreIds = { - protocolPkg: string; - market: string; - version: string; - dmlR: string; // coinDecimalsRegistry - oracle: string; -}; - -export type ScallopNormalMethodsHandler = { - [key in keyof ScallopNormalMethods]: (params: { - txBlock: SuiTxBlock; - coreIds: CoreIds; - scallopAddress: ScallopAddress; - scallopUtils: ScallopUtils; - }) => ScallopNormalMethods[key]; -}; - -export type SuiTxBlockWithNormalScallopMethods = SuiTxBlock & - ScallopNormalMethods; - -/** - * ========== Scallop Quick Methods ========== - */ - -export type ScallopQuickMethods = { +export type CoreQuickMethods = { addCollateralQuick: ( amount: number, coinName: SupportCollateralCoins, @@ -119,18 +103,16 @@ export type ScallopQuickMethods = { updateAssetPricesQuick: (coinNames: SupportAssetCoins[]) => Promise; }; -export type ScallopQuickMethodsHandler = { - [key in keyof ScallopQuickMethods]: (params: { - txBlock: SuiTxBlockWithNormalScallopMethods; - suiKit: SuiKit; - scallopAddress: ScallopAddress; - scallopUtils: ScallopUtils; - isTestnet: boolean; - }) => ScallopQuickMethods[key]; -}; +export type SuiTxBlockWithCoreNormalMethods = SuiKitTxBlock & CoreNormalMethods; + +export type CoreTxBlock = SuiTxBlockWithCoreNormalMethods & CoreQuickMethods; + +export type GenerateCoreNormalMethod = (params: { + builder: ScallopBuilder; + txBlock: SuiKitTxBlock; +}) => CoreNormalMethods; -/** - * ========== Scallop Tx Block ========== - */ -export type ScallopTxBlock = SuiTxBlockWithNormalScallopMethods & - ScallopQuickMethods; +export type GenerateCoreQuickMethod = (params: { + builder: ScallopBuilder; + txBlock: SuiTxBlockWithCoreNormalMethods; +}) => CoreQuickMethods; diff --git a/src/types/builder/index.ts b/src/types/builder/index.ts new file mode 100644 index 0000000..3797079 --- /dev/null +++ b/src/types/builder/index.ts @@ -0,0 +1,7 @@ +import type { CoreTxBlock } from './core'; +import type { SpoolTxBlock } from './spool'; + +export type * from './core'; +export type * from './spool'; + +export type ScallopTxBlock = CoreTxBlock & SpoolTxBlock; diff --git a/src/types/builder/spool.ts b/src/types/builder/spool.ts new file mode 100644 index 0000000..817566d --- /dev/null +++ b/src/types/builder/spool.ts @@ -0,0 +1,70 @@ +import type { TransactionArgument } from '@mysten/sui.js'; +import type { + SuiTxBlock as SuiKitTxBlock, + SuiTxArg, +} from '@scallop-io/sui-kit'; +import type { ScallopBuilder } from '../../models'; +import type { SupportStakeMarketCoins } from '../data'; + +type TransactionResult = TransactionArgument & TransactionArgument[]; + +export type SpoolIds = { + spoolPkg: string; +}; + +export type SpoolNormalMethods = { + createStakeAccount: ( + marketCoinName: SupportStakeMarketCoins + ) => TransactionResult; + stake: ( + stakeAccount: SuiTxArg, + coin: SuiTxArg, + marketCoinName: SupportStakeMarketCoins + ) => void; + unstake: ( + stakeAccount: SuiTxArg, + amount: number, + marketCoinName: SupportStakeMarketCoins + ) => TransactionResult; + claim: ( + stakeAccount: SuiTxArg, + marketCoinName: SupportStakeMarketCoins + ) => TransactionResult; +}; + +export type SpoolQuickMethods = { + stakeQuick( + amountOrMarketCoin: number, + marketCoinName: SupportStakeMarketCoins, + stakeAccountId?: SuiTxArg + ): Promise; + stakeQuick( + amountOrMarketCoin: TransactionResult, + marketCoinName: SupportStakeMarketCoins, + stakeAccountId?: SuiTxArg + ): Promise; + unstakeQuick( + amount: number, + marketCoinName: SupportStakeMarketCoins, + stakeAccountId?: SuiTxArg + ): Promise; + claimQuick( + marketCoinName: SupportStakeMarketCoins, + stakeAccountId?: SuiTxArg + ): Promise; +}; + +export type SuiTxBlockWithSpoolNormalMethods = SuiKitTxBlock & + SpoolNormalMethods; + +export type SpoolTxBlock = SuiTxBlockWithSpoolNormalMethods & SpoolQuickMethods; + +export type GenerateSpoolNormalMethod = (params: { + builder: ScallopBuilder; + txBlock: SuiKitTxBlock; +}) => SpoolNormalMethods; + +export type GenerateSpoolQuickMethod = (params: { + builder: ScallopBuilder; + txBlock: SuiTxBlockWithSpoolNormalMethods; +}) => SpoolQuickMethods; diff --git a/src/types/data.ts b/src/types/data/core.ts similarity index 71% rename from src/types/data.ts rename to src/types/data/core.ts index 7ad18e9..c78a3ed 100644 --- a/src/types/data.ts +++ b/src/types/data/core.ts @@ -3,7 +3,7 @@ import { SUPPORT_COLLATERAL_COINS, SUPPORT_ORACLES, SUPPORT_PACKAGES, -} from '../constants/common'; +} from '../../constants'; export type SupportAssetCoins = (typeof SUPPORT_ASSET_COINS)[number]; export type SupportCollateralCoins = (typeof SUPPORT_COLLATERAL_COINS)[number]; @@ -188,87 +188,3 @@ export interface ObligationInterface { borrowIndex: string; }[]; } - -export interface AddressesInterface { - core: { - version: string; - versionCap: string; - market: string; - adminCap: string; - coinDecimalsRegistry: string; - coins: Partial< - Record< - SupportCoins, - { - id: string; - treasury: string; - metaData: string; - oracle: { - [K in SupportOracleType]: K extends (typeof SUPPORT_ORACLES)[0] - ? string - : K extends (typeof SUPPORT_ORACLES)[1] - ? string - : K extends (typeof SUPPORT_ORACLES)[2] - ? { - feed: string; - feedObject: string; - } - : never; - }; - } - > - >; - oracles: { - [K in SupportOracleType]: K extends (typeof SUPPORT_ORACLES)[0] - ? { - registry: string; - registryCap: string; - holder: string; - } - : K extends (typeof SUPPORT_ORACLES)[1] - ? { - registry: string; - registryCap: string; - } - : K extends (typeof SUPPORT_ORACLES)[2] - ? { - registry: string; - registryCap: string; - state: string; - wormhole: string; - wormholeState: string; - } - : never; - } & { xOracle: string; xOracleCap: string }; - packages: Partial< - Record< - SupportPackageType, - { - id: string; - upgradeCap: string; - } - > - >; - }; -} - -type AddressPathsProps = T extends string - ? [] - : { - [K in Extract]: [K, ...AddressPathsProps]; - }[Extract]; - -type Join = T extends [] - ? never - : T extends [infer F] - ? F - : T extends [infer F, ...infer R] - ? F extends string - ? `${F}${D}${Join, D>}` - : never - : string; - -export type AddressStringPath = Join< - AddressPathsProps, - '.' ->; diff --git a/src/types/data/index.ts b/src/types/data/index.ts new file mode 100644 index 0000000..4e830f2 --- /dev/null +++ b/src/types/data/index.ts @@ -0,0 +1,2 @@ +export type * from './core'; +export type * from './spool'; diff --git a/src/types/data/spool.ts b/src/types/data/spool.ts new file mode 100644 index 0000000..dc9c9f3 --- /dev/null +++ b/src/types/data/spool.ts @@ -0,0 +1,42 @@ +import { SUPPORT_STACK_MARKET_COINS } from '../../constants'; +import type { SupportCoins } from './core'; + +export type SupportStakeMarketCoins = + (typeof SUPPORT_STACK_MARKET_COINS)[number]; + +export type RewardType = { + [key in SupportStakeMarketCoins]: SupportCoins; +}; + +export interface StakeAccount { + id: string; + type: string; + spoolId: string; + staked: number; + index: number; + points: number; +} + +export interface StakePool { + id: string; + type: string; + lastUpdate: number; + index: number; + totalStaked: number; + maxStake: number; + distributedPoint: number; + maxPoint: number; + pointPerPeriod: number; + period: number; + createdAt: number; +} + +export interface RewardPool { + id: string; + type: string; + stakePoolId: string; + ratioNumerator: number; + ratioDenominator: number; + rewards: number; + claimedRewards: number; +} diff --git a/src/types/index.ts b/src/types/index.ts index d0d4d0a..8938068 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,3 +1,4 @@ -export * from './data'; -export * from './model'; -export * from './txBuilder'; +export type * from './builder'; +export type * from './data'; +export type * from './address'; +export type * from './model'; diff --git a/src/types/model.ts b/src/types/model.ts index 950fa22..6fb73d0 100644 --- a/src/types/model.ts +++ b/src/types/model.ts @@ -2,16 +2,44 @@ import type { SuiTransactionBlockResponse, TransactionBlock, } from '@mysten/sui.js'; -import type { SuiKitParams, NetworkType } from '@scallop-io/sui-kit'; +import type { SuiKit, SuiKitParams, NetworkType } from '@scallop-io/sui-kit'; +import type { + ScallopAddress, + ScallopQuery, + ScallopUtils, + ScallopBuilder, +} from '../models'; export type ScallopClientFnReturnType = T extends true ? SuiTransactionBlockResponse : TransactionBlock; -export type ScallopParams = { - addressesId?: string; -} & SuiKitParams; + +export type ScallopInstanceParams = { + suiKit?: SuiKit; + address?: ScallopAddress; + query?: ScallopQuery; + utils?: ScallopUtils; + builder?: ScallopBuilder; +}; + export type ScallopAddressParams = { id: string; auth?: string; network?: NetworkType; }; + +export type ScallopParams = { + addressesId?: string; +} & SuiKitParams; + +export type ScallopClientParams = ScallopParams & { + walletAddress?: string; +}; + +export type ScallopBuilderParams = ScallopParams & { + walletAddress?: string; +}; + +export type ScallopQueryParams = ScallopParams; + +export type ScallopUtilsParams = ScallopParams; diff --git a/test/address.spec.ts b/test/address.spec.ts index ea1fc79..645ff49 100644 --- a/test/address.spec.ts +++ b/test/address.spec.ts @@ -4,15 +4,15 @@ import { NetworkType } from '@scallop-io/sui-kit'; import { ScallopAddress } from '../src'; import type { AddressesInterface } from '../src'; -const ENABLE_LOG = false; - dotenv.config(); -const TEST_ADDRESSES_ID = '6507ed0bb0f6b1a2a84abed3'; +const ENABLE_LOG = true; + +const TEST_ADDRESSES_ID = ''; const NETWORK: NetworkType = 'mainnet'; describe('Test Scallop Address', () => { - const addressBuilder = new ScallopAddress({ + const scallopAddress = new ScallopAddress({ id: TEST_ADDRESSES_ID, auth: process.env.API_KEY, network: NETWORK, @@ -20,198 +20,308 @@ describe('Test Scallop Address', () => { console.info('\x1b[32mAddresses Id: \x1b[33m', TEST_ADDRESSES_ID); it('Should get new addresses after create', async () => { - const oldId = addressBuilder.getId(); - if (oldId === undefined) { - const addresses = await addressBuilder.create(); - if (ENABLE_LOG) console.info('addresses:', addresses); - expect(!!addresses).toBe(true); + const oldAddressesId = scallopAddress.getId(); + if (oldAddressesId === undefined) { + const allAddresses = await scallopAddress.create(); + if (ENABLE_LOG) console.info('All addresses:', allAddresses); + expect(!!allAddresses).toBe(true); } - const addressesId = addressBuilder.getId(); - expect(oldId).not.toEqual(addressesId); - await addressBuilder.delete(); + const newAddressesId = scallopAddress.getId(); + expect(oldAddressesId).not.toEqual(newAddressesId); + await scallopAddress.delete(); }); - it.skip('Should get new addresses after update', async () => { - const addressesId = addressBuilder.getId(); + it('Should get new addresses after update', async () => { + let addressesId = scallopAddress.getId(); let oldAddresses: AddressesInterface | undefined = undefined; if (addressesId === undefined) { - oldAddresses = await addressBuilder.create(); + await scallopAddress.create({ memo: 'Scallop sdk addresses unit test' }); + addressesId = scallopAddress.getId(); + oldAddresses = scallopAddress.getAddresses(); } else { - oldAddresses = addressBuilder.getAddresses(); + oldAddresses = scallopAddress.getAddresses(); } const testAddresse: AddressesInterface = JSON.parse(` { "core": { - "market": "0xb8982283c6164535183408afc0baa5162a8120d7d593b5fac4e31977b4d9d95b", - "adminCap": "0x4c8e82f449a399aeab829ab8cd96315f5da5c55c3f46370a27278329248a72f7", - "coinDecimalsRegistry": "0xd8b258de0e170be7f78ce57607e995241ab83549415dd5083f395c8971db3d52", + "version": "0x07871c4b3c847a0f674510d4978d5cf6f960452795e8ff6f189fd2088a3f6ac7", + "versionCap": "0x590a4011cb649b3878f3ea14b3a78674642a9548d79b7e091ef679574b158a07", + "market": "0xa757975255146dc9686aa823b7838b507f315d704f428cbadad2f4ea061939d9", + "adminCap": "0x09689d018e71c337d9db6d67cbca06b74ed92196103624028ccc3ecea411777c", + "coinDecimalsRegistry": "0x200abe9bf19751cc566ae35aa58e2b7e4ff688fc1130f8d8909ea09bc137d668", "coins": { + "cetus": { + "id": "0x06864a6f921804860930db6ddbe2e16acdf8504495ea7481637a1c8b9a8fe54b", + "metaData": "0x4c0dce55eff2db5419bbd2d239d1aa22b4a400c01bbb648b058a9883989025da", + "treasury": "", + "oracle": { + "supra": "", + "switchboard": "", + "pyth": { + "feed": "e5b274b2611143df055d6e7cd8d93fe1961716bcd4dca1cad87a83bc1e78c1ef", + "feedObject": "0x2caadc4fb259ec57d6ee5ea8b6256376851955dffdb679d5e5526c5b6f8d865f" + } + } + }, + "apt": { + "id": "0x3a5143bb1196e3bcdfab6203d1683ae29edd26294fc8bfeafe4aaa9d2704df37", + "metaData": "0xc969c5251f372c0f34c32759f1d315cf1ea0ee5e4454b52aea08778eacfdd0a8", + "treasury": "", + "oracle": { + "supra": "", + "switchboard": "", + "pyth": { + "feed": "03ae4db29ed4ae33d323568895aa00337e658e348b37509f5372ae51f0af00d5", + "feedObject": "0x02ed0bfc818a060e1378eccda6a6c1dc6b4360b499bdaa23e3c69bb9ba2bfc96" + } + } + }, + "sol": { + "id": "0xb7844e289a8410e50fb3ca48d69eb9cf29e27d223ef90353fe1bd8e27ff8f3f8", + "metaData": "0x4d2c39082b4477e3e79dc4562d939147ab90c42fc5f3e4acf03b94383cd69b6e", + "treasury": "", + "oracle": { + "supra": "", + "switchboard": "", + "pyth": { + "feed": "ef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d", + "feedObject": "0x0ea409db743138a6a35d067de468b1f43944f970267a9026de9794a86e3a0ac3" + } + } + }, "btc": { - "id": "0x96f98f84c2d351fe152eebb3b937897d33bae6ee07ae8f60028dca16952862cd", - "metaData": "0x8cdf03c532bcfd9cf18c78f9451f42e824821dcd3821944e829e815083c88b07", - "treasury": "0xc7f8a285b22440707a3f15032e5ed2c820809481e03236dc7d103de4c8a5b5ba", + "id": "0x027792d9fed7f9844eb4839566001bb6f6cb4804f66aa2da6fe1ee242d896881", + "metaData": "0x5d3c6e60eeff8a05b693b481539e7847dfe33013e7070cdcb387f5c0cac05dfd", + "treasury": "", "oracle": { "supra": "", - "switchboard": "0x0c6d92e9c2184957b17bc1c29cf400ee64826a0ec0636a365341d7b8357e8a78", + "switchboard": "", "pyth": { - "feed": "f9c0172ba10dfa4d19088d94f5bf61d3b54d5bd7483a322a982e1373ee8ea31b", - "feedObject": "0x878b118488aeb5763b5f191675c3739a844ce132cb98150a465d9407d7971e7c" + "feed": "e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43", + "feedObject": "0x144ec4135c65af207b97b3d2dfea9972efc7d80cc13a960ae1d808a3307d90ca" } } }, "eth": { - "id": "0x96f98f84c2d351fe152eebb3b937897d33bae6ee07ae8f60028dca16952862cd", - "metaData": "0x913329557602d09364d2aea832377ffc94f7f4d40885c66db630c9c7875b97ce", - "treasury": "0x860cad87d30808b5aedfa541fdccdb02229dcb455923b4cf2f31227c0ca4b2a4", + "id": "0xaf8cd5edc19c4512f4259f0bee101a40d41ebed738ade5874359610ef8eeced5", + "metaData": "0x8900e4ceede3363bef086d6b50ca89d816d0e90bf6bc46efefe1f8455e08f50f", + "treasury": "", "oracle": { "supra": "", - "switchboard": "0xbc42f735df32d48bc7cba194230d9fa9686a8a3d86f69d96788d6a102dcc2ffe", + "switchboard": "", "pyth": { - "feed": "ca80ba6dc32e08d06f1aa886011eed1d77c77be9eb761cc10d72b7d0a2fd57a6", - "feedObject": "0x8deeebad0a8fb86d97e6ad396cc84639da5a52ae4bbc91c78eb7abbf3e641ed6" + "feed": "ff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace", + "feedObject": "0xaa6adc565636860729907ef3e7fb7808d80c8a425a5fd417ae47bb68e2dcc2e3" } } }, "usdc": { - "id": "0x96f98f84c2d351fe152eebb3b937897d33bae6ee07ae8f60028dca16952862cd", - "metaData": "0xbeadb2703f2bf464a58ef3cdb363c2ba71ab53ccdce433d052894b78989c40ed", - "treasury": "0x06cebd4202dd077813e1fb11dd4f08b7c045b3ef61f89b2ed29085fdf0743b5f", + "id": "0x5d4b302506645c37ff133b98c4b50a5ae14841659738d6d733d59d0d217a93bf", + "metaData": "0x4fbf84f3029bd0c0b77164b587963be957f853eccf834a67bb9ecba6ec80f189", + "treasury": "", "oracle": { "supra": "", - "switchboard": "0xe34bcf55598f7c92b7fe41131c5bded0071f0863cb826d1f8880f1a442401326", + "switchboard": "", "pyth": { - "feed": "41f3625971ca2ed2263e78573fe5ce23e13d2558ed3f2e47ab0f84fb9e7ae722", - "feedObject": "0xa3d3e81bd7e890ac189b3f581b511f89333b94f445c914c983057e1ac09ff296" + "feed": "eaa020c61cc479712813461ce153894a96a6c00b21ed0cfc2798d1f9a9e9c94a", + "feedObject": "0x1db46472aa29f5a41dd4dc41867fdcbc1594f761e607293c40bdb66d7cd5278f" } } }, "usdt": { - "id": "0x96f98f84c2d351fe152eebb3b937897d33bae6ee07ae8f60028dca16952862cd", - "metaData": "0xd8636c1ba598422cac38894c26f9cfe1521ac4be04ae27d72e8ba3a6e4d764f5", - "treasury": "0xfcfbec0818911487f86f950fa324e325c789dd2fa3cea38481fb2d884fa2032c", + "id": "0xc060006111016b8a020ad5b33834984a437aaa7d3c74c18e09a95d48aceab08c", + "metaData": "0xfb0e3eb97dd158a5ae979dddfa24348063843c5b20eb8381dd5fa7c93699e45c", + "treasury": "", "oracle": { "supra": "", - "switchboard": "0xef8b7044faa3cc5350623995e73a1e6e6a11dd38327410d5b988c823fdb22e60", + "switchboard": "", "pyth": { - "feed": "1fc18861232290221461220bd4e2acd1dcdfbc89c84092c93c18bdc7756c1588", - "feedObject": "0x83f74b8a33b540cbf1edd24e219eac1215d1668a711ca2be3aa5d703763f91db" + "feed": "2b89b9dc8fdf9f34709a5b106b472f0f39bb6ca9ce04b0fd7f2e971688e2e53b", + "feedObject": "0x64f8db86bef3603472cf446c7ab40278af7f4bcda97c7599ad4cb33d228e31eb" } } }, "sui": { - "id": "", + "id": "0x0000000000000000000000000000000000000000000000000000000000000002", "metaData": "", "treasury": "", "oracle": { "supra": "", - "switchboard": "0xf62fa4e71dc90ae4d92c4a1ce146c6bd4229349810b468328ab6ffabf6f64e13", + "switchboard": "0xbca474133638352ba83ccf7b5c931d50f764b09550e16612c9f70f1e21f3f594", "pyth": { - "feed": "50c67b3fd225db8912a424dd4baed60ffdde625ed2feaaf283724f9608fea266", - "feedObject": "0xe38dbe2ff3322f1500fff45d0046101f371eebce47c067c5e9233248c4878c28" + "feed": "23d7315113f5b1d3ba7a83604c44b94d79f4fd69af77f804fc7f920a6dc65744", + "feedObject": "0x168aa44fa92b27358beb17643834078b1320be6adf1b3bb0c7f018ac3591db1a" } } } }, "oracles": { - "xOracle": "0xa16829dc76adbd526c9066e909e324edc3f5d3151cba7c74c86ea77532ff921b", - "xOracleCap": "0x45941160c121644d5482d38404e4964c341e1afbe9c0379e76433def7a73fa43", + "xOracle": "0x93d5bf0936b71eb27255941e532fac33b5a5c7759e377b4923af0a1359ad494f", + "xOracleCap": "0x1edeae568fde99e090dbdec4bcdbd33a15f53a1ce1f87aeef1a560dedf4b4a90", + "supra": { + "registry": "", + "registryCap": "", + "holder": "" + }, "switchboard": { - "registry": "0x92ab1100382144473cd1efae456eab1f4c9e3e04f83dcfb327c58f11bc61998f", - "registryCap": "0xfa72ea81a9a0a986e77caee899af4326f7c0dc96bbb5b2b9bf9d1a2a5345a07a" + "registry": "", + "registryCap": "" }, "pyth": { - "state": "0xd8afde3a48b4ff7212bd6829a150f43f59043221200d63504d981f62bff2e27a", - "wormhole": "0xcc029e2810f17f9f43f52262f40026a71fbdca40ed3803ad2884994361910b7e", - "wormholeState": "0xebba4cc4d614f7a7cdbe883acc76d1cc767922bc96778e7b68be0d15fce27c02" + "registry": "0x8c30c13df429e16d4e7be8726f25acfd8f5a6992d84a81eb36967e95bf18c889", + "registryCap": "0x85ceae1f42746452ca0b6506414ce038b04447723b7d41990545647b3d48a713", + "state": "0xf9ff3ef935ef6cdfb659a203bf2754cebeb63346e29114a535ea6f41315e5a3f", + "wormhole": "0x5306f64e312b581766351c07af79c72fcb1cd25147157fdc2f8ad76de9a3fb6a", + "wormholeState": "0xaeab97f96cf9877fee2883315d459552b2b921edc16d7ceac6eab944dd88919c" } }, "packages": { "coinDecimalsRegistry": { - "id": "0xeafc3d2e82e24a2ef270e6265ba72d7dd6036a743c03fa0e6c2f901fd5e145c0", - "upgradeCap": "0x89b94ca221ab4734708e74e496bc4737fe92b4aa1b48949ff3fc683a291aa33d" + "id": "0xca5a5a62f01c79a104bf4d31669e29daa387f325c241de4edbe30986a9bc8b0d", + "upgradeCap": "0x34e76a945d29f195bc53ca704fa70877d1cf3a5d7bbfdda1b13e633fff13c0f6" }, "math": { - "id": "0xa0b56ddd51e0d816c0fc985b86f7377b00761910895805448dc6b3aeec677adf", - "upgradeCap": "0xebbf16ad136f720de214bb0421c7a5c2e7d09b79018914ba4609d73bef7a86c0" + "id": "0xad013d5fde39e15eabda32b3dbdafd67dac32b798ce63237c27a8f73339b9b6f", + "upgradeCap": "0x3a329598231de02e6135c62284b66005b41cad1d9ab7ca2dc79c08293aba2ec6" }, "whitelist": { - "id": "0x9bad3512db164661beb0ba21f7786f0f17cdcd777c2a0d58e16032e05981d25c", - "upgradeCap": "0x3bae741561fdc8826e31145f61c41009f48c2d82c58f42237d89014ea6b1c525" + "id": "0x1318fdc90319ec9c24df1456d960a447521b0a658316155895014a6e39b5482f", + "upgradeCap": "0xf5a22aea23db664f7b69855b6a546747f17c1ec4230319cfc17225e462b05761" }, "x": { - "id": "0x6a216b7f73cb3a7bd448d278a704de8a2b1dac474da91719cb015fbb7824b029", - "upgradeCap": "0x2eb858a0cf795e014e433f63569264ed49396a405c0ddd141ff1db21a4fa16f1" + "id": "0x779b5c547976899f5474f3a5bc0db36ddf4697ad7e5a901db0415c2281d28162", + "upgradeCap": "0x3f203f6fff6a69d151e4f1cd931f22b68c489ef2759765662fc7baf673943c9e" }, "protocol": { - "id": "0x8941cb2209639cf158059855e5b395d5b05da4385ac21668c8422e9268b9e39b", - "upgradeCap": "0x9ddcf80d6a40d83e96a3a3ad9a902268101b547872abeb8eab4fc9021cc0f2bd" + "id": "0xc05a9cdf09d2f2451dea08ca72641e013834baef3d2ea5fcfee60a9d1dc3c7d9", + "upgradeCap": "0x38527d154618d1fd5a644b90717fe07cf0e9f26b46b63e9568e611a3f86d5c1a" }, "query": { - "id": "0x9e70ff97501eb23fb6fd7f2dd6fb983b22c031e4ddd3bc84585d9d3cebb7c6ce", - "upgradeCap": "0xa89d155ab24ab2b3db0a9c4e7a8f928a8a74aa6a2c78a003f637d73d1dff77af" + "id": "0xbd4f1adbef14cf6ddf31cf637adaa7227050424286d733dc44e6fd3318fc6ba3", + "upgradeCap": "0x3d0ef1c744c6f957d5bd5298908ad1bdef031767aa9f137313a8ccea6db9cca3" }, - "pyth": { - "id": "0x975e063f398f720af4f33ec06a927f14ea76ca24f7f8dd544aa62ab9d5d15f44", + "supra": { + "id": "", "upgradeCap": "" }, + "pyth": { + "id": "0xaac1fdb607b884cc256c59dc307bb78b6ba95b97e22d4415fe87ad99689ea462", + "upgradeCap": "0x3b96c287dcdc1660bdae26e34a52ac07e826a438b12c2ced9addce91daf90ac5" + }, "switchboard": { - "id": "0xc5e3d08d0c65ba6fe12e822a3186b30ea22dd0efd19f95b21eeba00f60cfcff6", - "upgradeCap": "0x91b755d94170f48e61ee814ab106ea4ab759c35af0f738facdd1b5e153b319d9" + "id": "", + "upgradeCap": "" }, "xOracle": { - "id": "0x9a5a259f0690182cc3d99fce6df69c1256f529b79ac6e7f19a29d4de8b4d85a4", - "upgradeCap": "0x4b27a0edc6b080eccada3f8d790f89feb32f5d1b5e9bc33ffa2794a03aac47d5" + "id": "0x1478a432123e4b3d61878b629f2c692969fdb375644f1251cd278a4b1e7d7cd6", + "upgradeCap": "0x0f928a6b2e26b73330fecaf9b44acfc9800a4a9794d6415c2a3153bc70e3c1f0" }, "testCoin": { - "id": "0x96f98f84c2d351fe152eebb3b937897d33bae6ee07ae8f60028dca16952862cd", - "upgradeCap": "0xe2ee88272b72a1cb57673b5879d947b0a8ca41b4c6f862e950ff294c43a27699" + "id": "", + "upgradeCap": "" } } + }, + "spool": { + "id": "0xe87f1b2d498106a2c61421cec75b7b5c5e348512b0dc263949a0e7a3c256571a", + "adminCap": "0xdd8a047cbbf802bfcde5288b8ef1910965d789cc614da11d39af05fca0bd020a", + "pools": { + "ssui": { + "id": "0x4f0ba970d3c11db05c8f40c64a15b6a33322db3702d634ced6536960ab6f3ee4", + "rewardPoolId": "0x162250ef72393a4ad3d46294c4e1bdfcb03f04c869d390e7efbfc995353a7ee9" + }, + "susdc": { + "id": "0x4ace6648ddc64e646ba47a957c562c32c9599b3bba8f5ac1aadb2ae23a2f8ca0", + "rewardPoolId": "0xf4268cc9b9413b9bfe09e8966b8de650494c9e5784bf0930759cfef4904daff8" + } + } } } `); - const addresses = await addressBuilder.update( - undefined, - undefined, - testAddresse - ); - if (ENABLE_LOG) console.info('addresses:', addresses); - expect(!!addresses).toBe(true); - expect(addresses).not.toEqual(oldAddresses); + await scallopAddress.update({ + addresses: testAddresse, + }); + const updatedAddresses = scallopAddress.getAddresses(); + if (ENABLE_LOG) { + console.log('Id', addressesId); + console.info('Addresses:', updatedAddresses); + } + expect(!!updatedAddresses).toBe(true); + expect(updatedAddresses).not.toEqual(oldAddresses); + await scallopAddress.delete(); }); it('Should read and get addresses success', async () => { - const addressesId = addressBuilder.getId(); - if (addressesId === undefined) await addressBuilder.create(); + let addressesId = scallopAddress.getId(); + if (addressesId === undefined) { + const oldAddresses = scallopAddress.getAddresses(); + expect(oldAddresses).toEqual(undefined); + await scallopAddress.create({ memo: 'Scallop sdk addresses unit test' }); + addressesId = scallopAddress.getId(); + } - await addressBuilder.read(); - const addresses = addressBuilder.getAddresses(); - const allAddresses = addressBuilder.getAllAddresses(); + await scallopAddress.read(); + const addresses = scallopAddress.getAddresses(); + const allAddresses = scallopAddress.getAllAddresses(); if (ENABLE_LOG) { - console.info('addresses:', addresses); - console.info('allAddresses:', allAddresses); + console.info('Id:', addressesId); + console.info('Addresses:', addresses); + console.info('All addresses:', allAddresses); } expect(addresses).toEqual(allAddresses[NETWORK]); + await scallopAddress.delete(); }); it('Should get success', async () => { - const addressesId = addressBuilder.getId(); - if (addressesId === undefined) await addressBuilder.create(); + let addressesId = scallopAddress.getId(); + if (addressesId === undefined) { + await scallopAddress.create({ memo: 'Scallop sdk addresses unit test' }); + addressesId = scallopAddress.getId(); + } - const addresses = addressBuilder.getAddresses(); - const usdcCoinId = addressBuilder.get('core.coins.usdc.id'); - if (ENABLE_LOG) console.info('usdcCoinId', usdcCoinId); - expect(usdcCoinId).toEqual(addresses?.core.coins.usdc?.id); + const addresses = scallopAddress.getAddresses(); + const usdcCoinId = scallopAddress.get('core.coins.usdc.id'); + if (ENABLE_LOG) { + console.info('Id:', addressesId); + console.info('UsdcCoinId', usdcCoinId); + } + expect(usdcCoinId).toEqual(addresses?.core.coins.usdc?.id || undefined); + await scallopAddress.delete(); }); it('Should set success', async () => { - const addressesId = addressBuilder.getId(); - if (addressesId === undefined) await addressBuilder.create(); - const oldUsdcCoinId = addressBuilder.get('core.coins.usdc.id'); - const newAddresses = addressBuilder.set('core.coins.usdc.id', '0x00'); - if (ENABLE_LOG) - console.info('usdcCoinId', newAddresses?.core.coins.usdc?.id); - expect(oldUsdcCoinId).not.toEqual(newAddresses?.core.coins.usdc?.id); - await addressBuilder.delete(); + let addressesId = scallopAddress.getId(); + if (addressesId === undefined) { + await scallopAddress.create({ memo: 'Scallop sdk addresses unit test' }); + addressesId = scallopAddress.getId(); + } + const oldUsdcCoinId = scallopAddress.get('core.coins.usdc.id'); + scallopAddress.set('core.coins.usdc.id', '0x00'); + const newAddresses = scallopAddress.get('core.coins.usdc.id'); + if (ENABLE_LOG) { + console.info('Id:', addressesId); + console.info('Old usdcCoinId', oldUsdcCoinId); + console.info('New usdcCoinId', newAddresses); + } + expect(newAddresses).not.toEqual(oldUsdcCoinId); + await scallopAddress.delete(); + }); + + it('Should switch current addresses success', async () => { + let addressesId = scallopAddress.getId(); + if (addressesId === undefined) { + await scallopAddress.create({ memo: 'Scallop sdk addresses unit test' }); + addressesId = scallopAddress.getId(); + } + const testnetAddresses = scallopAddress.getAddresses('testnet'); + const currentAddresses = scallopAddress.switchCurrentAddresses('testnet'); + if (ENABLE_LOG) { + console.info('Id:', addressesId); + console.info('Testnet Addresses', testnetAddresses); + console.info('Current addresses', currentAddresses); + } + expect(testnetAddresses).toEqual(undefined); + expect(!!currentAddresses).toBe(true); + await scallopAddress.delete(); }); }); diff --git a/test/builder.spec.ts b/test/builder.spec.ts new file mode 100644 index 0000000..4064ee3 --- /dev/null +++ b/test/builder.spec.ts @@ -0,0 +1,222 @@ +import * as dotenv from 'dotenv'; +import { describe, it, expect } from 'vitest'; +import { TransactionBlock } from '@mysten/sui.js'; +import { Scallop } from '../src'; +import type { NetworkType } from '@scallop-io/sui-kit'; + +dotenv.config(); + +const ENABLE_LOG = true; + +const NETWORK: NetworkType = 'mainnet'; + +describe('Test Scallop Core Builder', async () => { + const scallopSDK = new Scallop({ + secretKey: process.env.SECRET_KEY, + networkType: NETWORK, + }); + const sender = scallopSDK.suiKit.currentAddress(); + const scallopBuilder = await scallopSDK.createScallopBuilder(); + + console.info('Sender:', sender); + + it('"openObligationEntry" should succeed', async () => { + const tx = scallopBuilder.createTxBlock(); + tx.openObligationEntry(); + const openObligationResult = await scallopBuilder.signAndSendTxBlock(tx); + if (ENABLE_LOG) { + console.info('OpenObligationResult:', openObligationResult); + } + expect(openObligationResult.effects.status.status).toEqual('success'); + }); + + it('"addCollateralQuick" should succeed', async () => { + const tx = scallopBuilder.createTxBlock(); + // Sender is required to invoke "addCollateralQuick" + tx.setSender(sender); + await tx.addCollateralQuick(10 ** 7, 'sui'); + const addCollateralQuickResult = await scallopBuilder.signAndSendTxBlock( + tx + ); + if (ENABLE_LOG) { + console.info('AddCollateralQuickResult:', addCollateralQuickResult); + } + expect(addCollateralQuickResult.effects.status.status).toEqual('success'); + }); + + it('"takeCollateralQuick" should succeed', async () => { + const tx = scallopBuilder.createTxBlock(); + // Sender is required to invoke "takeCollateralQuick" + tx.setSender(sender); + const coin = await tx.takeCollateralQuick(10 ** 7, 'sui'); + tx.transferObjects([coin], sender); + const takeCollateralQuickResult = await scallopBuilder.signAndSendTxBlock( + tx + ); + if (ENABLE_LOG) { + console.info('TakeCollateralQuickResult:', takeCollateralQuickResult); + } + expect(takeCollateralQuickResult.effects.status.status).toEqual('success'); + }); + + it('"depositQuick" should succeed', async () => { + const tx = scallopBuilder.createTxBlock(); + // Sender is required to invoke "depositQuick" + tx.setSender(sender); + const marketCoin = await tx.depositQuick(10 ** 8, 'sui'); + tx.transferObjects([marketCoin], sender); + const depositQuickResult = await scallopBuilder.signAndSendTxBlock(tx); + if (ENABLE_LOG) { + console.info('DepositQuickResult:', depositQuickResult); + } + expect(depositQuickResult.effects.status.status).toEqual('success'); + }); + + it('"withdrawQuick" should succeed', async () => { + const tx = scallopBuilder.createTxBlock(); + // Sender is required to invoke "withdrawQuick" + tx.setSender(sender); + const coin = await tx.withdrawQuick(10 ** 8, 'sui'); + tx.transferObjects([coin], sender); + const withdrawQuickResult = await scallopBuilder.signAndSendTxBlock(tx); + if (ENABLE_LOG) { + console.info('WithdrawQuickResult:', withdrawQuickResult); + } + expect(withdrawQuickResult.effects.status.status).toEqual('success'); + }); + + it('"borrowQuick" should succeed', async () => { + const tx = scallopBuilder.createTxBlock(); + // Sender is required to invoke "borrowQuick" + tx.setSender(sender); + const borrowedCoin = await tx.borrowQuick(10 ** 8, 'sui'); + // Transfer borrowed coin to sender + tx.transferObjects([borrowedCoin], sender); + const borrowQuickResult = await scallopBuilder.signAndSendTxBlock(tx); + if (ENABLE_LOG) { + console.info('BorrowQuickResult:', borrowQuickResult); + } + expect(borrowQuickResult.effects.status.status).toEqual('success'); + }); + + it('"repayQuick" should succeed', async () => { + const tx = scallopBuilder.createTxBlock(); + // Sender is required to invoke "repayQuick" + tx.setSender(sender); + await tx.repayQuick(10 ** 8, 'sui'); + const repayQuickResult = await scallopBuilder.signAndSendTxBlock(tx); + if (ENABLE_LOG) { + console.info('RepayQuickResult:', repayQuickResult); + } + expect(repayQuickResult.effects.status.status).toEqual('success'); + }); + + it('"borrowFlashLoan" & "repayFlashLoan" should succeed', async () => { + const tx = scallopBuilder.createTxBlock(); + const [coin, loan] = tx.borrowFlashLoan(10 ** 8, 'sui'); + /** + * Do something with the borrowed coin here + * such as pass it to a dex to make a profit. + */ + tx.repayFlashLoan(coin, loan, 'sui'); + const borrowFlashLoanResult = await scallopBuilder.signAndSendTxBlock(tx); + if (ENABLE_LOG) { + console.info('BorrowFlashLoanResult:', borrowFlashLoanResult); + } + expect(borrowFlashLoanResult.effects.status.status).toEqual('success'); + }); + + it('"updateAssetPricesQuick" should succeed', async () => { + const tx = scallopBuilder.createTxBlock(); + await tx.updateAssetPricesQuick(['sui', 'usdc']); + const updateAssetPricesResult = await scallopBuilder.signAndSendTxBlock(tx); + if (ENABLE_LOG) { + console.info('UpdateAssetPricesResult:', updateAssetPricesResult); + } + expect(updateAssetPricesResult.effects.status.status).toEqual('success'); + }); + + it('"ScallopTxBlock" should be an instance of "TransactionBlock"', async () => { + const tx = scallopBuilder.createTxBlock(); + expect(tx.txBlock).toBeInstanceOf(TransactionBlock); + /** + * For example, you can do the following: + * 1. split SUI from gas + * 2. depoit SUI to Scallop + * 3. transfer SUI Market Coin to sender + */ + const suiTxBlock = tx.txBlock; + const [coin] = suiTxBlock.splitCoins(suiTxBlock.gas, [ + suiTxBlock.pure(10 ** 6), + ]); + const marketCoin = tx.deposit(coin, 'sui'); + suiTxBlock.transferObjects([marketCoin], suiTxBlock.pure(sender)); + const txBlockResult = await scallopBuilder.signAndSendTxBlock(tx); + if (ENABLE_LOG) { + console.info('TxBlockResult:', txBlockResult); + } + expect(txBlockResult.effects.status.status).toEqual('success'); + }); +}); + +describe('Test Scallop Spool Builder', async () => { + const scallopSDK = new Scallop({ + secretKey: process.env.SECRET_KEY, + networkType: NETWORK, + }); + const sender = scallopSDK.suiKit.currentAddress(); + const scallopBuilder = await scallopSDK.createScallopBuilder(); + + console.info('Sender:', sender); + + it('"createStakeAccount" should succeed', async () => { + const tx = scallopBuilder.createTxBlock(); + const stakeAccount = tx.createStakeAccount('ssui'); + tx.transferObjects([stakeAccount], sender); + const createStakeAccountResult = await scallopBuilder.signAndSendTxBlock( + tx + ); + if (ENABLE_LOG) { + console.info('CreateStakeAccountResult:', createStakeAccountResult); + } + expect(createStakeAccountResult.effects.status.status).toEqual('success'); + }); + + it('"stakeQuick" should succeed', async () => { + const tx = scallopBuilder.createTxBlock(); + // Sender is required to invoke "stakeQuick" + tx.setSender(sender); + await tx.stakeQuick(10 ** 8, 'ssui'); + const stakeQuickResult = await scallopBuilder.signAndSendTxBlock(tx); + if (ENABLE_LOG) { + console.info('StakeQuickResult:', stakeQuickResult); + } + expect(stakeQuickResult.effects.status.status).toEqual('success'); + }); + + it('"unstakeQuick" should succeed', async () => { + const tx = scallopBuilder.createTxBlock(); + // Sender is required to invoke "unstakeQuick" + tx.setSender(sender); + const marketCoin = await tx.unstakeQuick(10 ** 8, 'ssui'); + tx.transferObjects([marketCoin], sender); + const unstakeQuickResult = await scallopBuilder.signAndSendTxBlock(tx); + if (ENABLE_LOG) { + console.info('UnstakeQuickResult:', unstakeQuickResult); + } + expect(unstakeQuickResult.effects.status.status).toEqual('success'); + }); + + it('"claimQuick" should succeed', async () => { + const tx = scallopBuilder.createTxBlock(); + // Sender is required to invoke "claimQuick" + tx.setSender(sender); + const rewardCoin = await tx.claimQuick('ssui'); + tx.transferObjects([rewardCoin], sender); + const claimQuickResult = await scallopBuilder.signAndSendTxBlock(tx); + if (ENABLE_LOG) { + console.info('ClaimQuickResult:', claimQuickResult); + } + expect(claimQuickResult.effects.status.status).toEqual('success'); + }); +}); diff --git a/test/index.spec.ts b/test/index.spec.ts index 32bf63e..d8f91c1 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -1,114 +1,185 @@ import * as dotenv from 'dotenv'; import { describe, it, expect } from 'vitest'; import { NetworkType } from '@scallop-io/sui-kit'; -import { ScallopClient, ScallopAddress, ADDRESSES_ID } from '../src'; - -const ENABLE_LOG = false; +import { Scallop } from '../src'; dotenv.config(); +const ENABLE_LOG = true; + // At present, the contract of the testnet is stale and cannot be used normally, please use the mainnet for testing. const NETWORK: NetworkType = 'mainnet'; -/** - * Remove `.skip` to proceed with testing according to requirements. - */ -describe('Test Scallop interact with contract', async () => { - const scallopAddress = new ScallopAddress({ - // id: '649bdadbb946e3b1853f3d4d', // Test the latest contract usage within scallop team - id: ADDRESSES_ID, - network: NETWORK, - }); - await scallopAddress.read(); - const client = new ScallopClient( - { - secretKey: process.env.SECRET_KEY, - networkType: NETWORK, - }, - scallopAddress - ); - console.info('\x1b[32mYour wallet: \x1b[33m', client.walletAddress); - - it('Should get market query data', async () => { +describe('Test Scallop Client - Query Method', async () => { + const scallopSDK = new Scallop({ + secretKey: process.env.SECRET_KEY, + networkType: NETWORK, + }); + const client = await scallopSDK.createScallopClient(); + console.info('Your wallet:', client.walletAddress); + + it('Should query market data', async () => { const marketData = await client.queryMarket(); if (ENABLE_LOG) { - console.info('marketData:'); - console.dir(marketData, { depth: null, colors: true }); + console.info('MarketData:', marketData); } expect(!!marketData).toBe(true); }); - it('Should open a obligation account', async () => { - const openObligationResult = await client.openObligation(); - if (ENABLE_LOG) console.info('openObligationResult:', openObligationResult); - expect(openObligationResult.effects?.status.status).toEqual('success'); + it('Should get obligations data', async () => { + const obligationsData = await client.getObligations(); + if (ENABLE_LOG) { + console.info('Obligations data:', obligationsData); + } + expect(!!obligationsData).toBe(true); }); - it('Should get obligations and its query data', async () => { - const obligations = await client.getObligations(); - console.info('obligations', obligations); - - for (const { id } of obligations) { - const obligationData = await client.queryObligation(id); - if (ENABLE_LOG) { - console.info('id:', id); - console.info('obligationData:'); - console.dir(obligationData, { depth: null, colors: true }); - } - expect(!!obligationData).toBe(true); + it('Should get obligation data', async () => { + const obligationsData = await client.getObligations(); + expect(obligationsData.length).toBeGreaterThan(0); + const obligationData = await client.queryObligation(obligationsData[0].id); + if (ENABLE_LOG) { + console.info('Obligation data:', obligationData); } + expect(!!obligationData).toBe(true); }); - // only for testnet - it.skip('Should get test coin', async () => { - const mintTestCoinResult = await client.mintTestCoin('usdc', 10 ** 11); - if (ENABLE_LOG) console.info('mintTestCoinResult:', mintTestCoinResult); - expect(mintTestCoinResult.effects?.status.status).toEqual('success'); + it('Should get all stake accounts data', async () => { + const allStakeAccountsData = await client.getAllStakeAccounts(); + if (ENABLE_LOG) { + console.info('All stakeAccounts data:', allStakeAccountsData); + } + expect(!!allStakeAccountsData).toBe(true); }); - it('Should depoist collateral successfully', async () => { - const obligations = await client.getObligations(); + it('Should get stake accounts data', async () => { + const stakeAccountsData = await client.getStakeAccounts('ssui'); + if (ENABLE_LOG) { + console.info('StakeAccounts data:', stakeAccountsData); + } + expect(!!stakeAccountsData).toBe(true); + }); + + it('Should get stake pool data', async () => { + const stakePoolData = await client.getStakePool('ssui'); + if (ENABLE_LOG) { + console.info('Stake pool data:', stakePoolData); + } + expect(!!stakePoolData).toBe(true); + }); + + it('Should get reward pool data', async () => { + const rewardPoolData = await client.getRewardPool('ssui'); + if (ENABLE_LOG) { + console.info('Reward pool data:', rewardPoolData); + } + expect(!!rewardPoolData).toBe(true); + }); +}); + +describe('Test Scallop Client - Spool Method', async () => { + const scallopSDK = new Scallop({ + secretKey: process.env.SECRET_KEY, + networkType: NETWORK, + }); + const client = await scallopSDK.createScallopClient(); + console.info('Your wallet:', client.walletAddress); + + it('Should create stake account success', async () => { + const createStakeAccountResult = await client.createStakeAccount('ssui'); + if (ENABLE_LOG) { + console.info('CreateStakeAccountResult:', createStakeAccountResult); + } + expect(createStakeAccountResult.effects.status.status).toEqual('success'); + }); + + it('Should stake success', async () => { + const stakeResult = await client.stake('ssui', 10 ** 8); + if (ENABLE_LOG) { + console.info('StakeResult:', stakeResult); + } + expect(stakeResult.effects.status.status).toEqual('success'); + }); + + it('Should unstake success', async () => { + const unstakeResult = await client.unstake('ssui', 10 ** 8); + if (ENABLE_LOG) { + console.info('UnstakeResult:', unstakeResult); + } + expect(unstakeResult.effects.status.status).toEqual('success'); + }); + + it('Should claim success', async () => { + const claimResult = await client.claim('ssui'); + if (ENABLE_LOG) { + console.info('ClaimResult:', claimResult); + } + expect(claimResult.effects.status.status).toEqual('success'); + }); +}); + +describe('Test Scallop Client - Core Method', async () => { + const scallopSDK = new Scallop({ + secretKey: process.env.SECRET_KEY, + networkType: NETWORK, + }); + const client = await scallopSDK.createScallopClient(); + console.info('Your wallet:', client.walletAddress); + + it('Should open obligation success', async () => { + const openObligationResult = await client.openObligation(); + if (ENABLE_LOG) { + console.info('OpenObligationResult:', openObligationResult); + } + expect(openObligationResult.effects?.status.status).toEqual('success'); + }); + + it('Should depoist collateral success', async () => { const depositCollateralResult = await client.depositCollateral( 'sui', - 2 * 10 ** 9, - true, - obligations[0]?.id + 10 ** 8 ); - if (ENABLE_LOG) - console.info('depositCollateralResult:', depositCollateralResult); + if (ENABLE_LOG) { + console.info('DepositCollateralResult:', depositCollateralResult); + } expect(depositCollateralResult.effects?.status.status).toEqual('success'); }); - it('Should withdraw collateral successfully', async () => { + it('Should withdraw collateral success', async () => { const obligations = await client.getObligations(); - if (obligations.length === 0) throw Error('Obligation is required.'); + expect(obligations.length).toBeGreaterThan(0); const withdrawCollateralResult = await client.withdrawCollateral( 'sui', - 1 * 10 ** 9, + 10 ** 8, true, obligations[0].id, obligations[0].keyId ); - if (ENABLE_LOG) - console.info('withdrawCollateralResult:', withdrawCollateralResult); + if (ENABLE_LOG) { + console.info('WithdrawCollateralResult:', withdrawCollateralResult); + } expect(withdrawCollateralResult.effects?.status.status).toEqual('success'); }); - it('Should depoist asset successfully', async () => { - const depositResult = await client.deposit('sui', 2 * 10 ** 9); - console.info('depositResult:', depositResult); + it('Should depoist asset success', async () => { + const depositResult = await client.deposit('sui', 2 * 10 ** 8); + if (ENABLE_LOG) { + console.info('DepositResult:', depositResult); + } expect(depositResult.effects?.status.status).toEqual('success'); }); - it('Should withdraw asset successfully', async () => { - const withdrawResult = await client.withdraw('sui', 1 * 10 ** 9); - if (ENABLE_LOG) console.info('withdrawResult:', withdrawResult); + it('Should withdraw asset success', async () => { + const withdrawResult = await client.withdraw('sui', 2 * 10 ** 8); + if (ENABLE_LOG) { + console.info('WithdrawResult:', withdrawResult); + } expect(withdrawResult.effects?.status.status).toEqual('success'); }); - it('Should borrow asset successfully', async () => { + it('Should borrow asset success', async () => { const obligations = await client.getObligations(); - if (obligations.length === 0) throw Error('Obligation is required.'); + expect(obligations.length).toBeGreaterThan(0); const borrowResult = await client.borrow( 'sui', 3 * 10 ** 8, @@ -116,32 +187,56 @@ describe('Test Scallop interact with contract', async () => { obligations[0].id, obligations[0].keyId ); - if (ENABLE_LOG) console.info('borrowResult:', borrowResult); + if (ENABLE_LOG) { + console.info('BorrowResult:', borrowResult); + } expect(borrowResult.effects?.status.status).toEqual('success'); }); - it('Should repay asset successfully', async () => { + it('Should repay asset success', async () => { const obligations = await client.getObligations(); - if (obligations.length === 0) throw Error('Obligation is required.'); + expect(obligations.length).toBeGreaterThan(0); const repayResult = await client.repay( 'sui', 3 * 10 ** 8, true, obligations[0].id ); - if (ENABLE_LOG) console.info('repayResult:', repayResult); + if (ENABLE_LOG) { + console.info('RepayResult:', repayResult); + } expect(repayResult.effects?.status.status).toEqual('success'); }); it('Should flash loan successfully', async () => { const flashLoanResult = await client.flashLoan( 'sui', - 10 ** 9, - (txBlock, coin) => { + 10 ** 8, + async (_txBlock, coin) => { return coin; } ); - if (ENABLE_LOG) console.info('flashLoanResult:', flashLoanResult); + if (ENABLE_LOG) { + console.info('FlashLoanResult:', flashLoanResult); + } expect(flashLoanResult.effects?.status.status).toEqual('success'); }); }); + +describe('Test Scallop Client - Other Method', async () => { + const scallopSDK = new Scallop({ + secretKey: process.env.SECRET_KEY, + networkType: NETWORK, + }); + const client = await scallopSDK.createScallopClient(); + console.info('Your wallet:', client.walletAddress); + + // only for testnet + it.skip('Should get test coin', async () => { + const mintTestCoinResult = await client.mintTestCoin('usdc', 10 ** 11); + if (ENABLE_LOG) { + console.info('MintTestCoinResult:', mintTestCoinResult); + } + expect(mintTestCoinResult.effects?.status.status).toEqual('success'); + }); +}); diff --git a/test/query.spec.ts b/test/query.spec.ts new file mode 100644 index 0000000..b2c0a4b --- /dev/null +++ b/test/query.spec.ts @@ -0,0 +1,83 @@ +import * as dotenv from 'dotenv'; +import { describe, it, expect } from 'vitest'; +import { Scallop, SUPPORT_STACK_MARKET_COINS } from '../src'; +import type { NetworkType } from '@scallop-io/sui-kit'; + +dotenv.config(); + +const ENABLE_LOG = true; + +const NETWORK: NetworkType = 'mainnet'; + +describe('Test Query Scallop Contract On Chain Data', async () => { + const scallopSDK = new Scallop({ + secretKey: process.env.SECRET_KEY, + networkType: NETWORK, + }); + const scallopQuery = await scallopSDK.createScallopQuery(); + + it('Should get market data', async () => { + const marketData = await scallopQuery.getMarket(); + if (ENABLE_LOG) { + console.info('MarketData:'); + console.dir(marketData, { depth: null, colors: true }); + } + expect(!!marketData).toBe(true); + }); + + it('Should get obligations and its all obligation data', async () => { + const obligations = await scallopQuery.getObligations(); + + if (ENABLE_LOG) { + console.info('Obligations', obligations); + } + expect(!!obligations).toBe(true); + + for (const { id } of obligations) { + const obligationData = await scallopQuery.getObligation(id); + + if (ENABLE_LOG) { + console.info('Id:', id); + console.info('ObligationData:'); + console.dir(obligationData, { depth: null, colors: true }); + } + expect(!!obligationData).toBe(true); + } + }); + + it('Should get all stake accounts data', async () => { + const allStakeAccounts = await scallopQuery.getAllStakeAccounts(); + + if (ENABLE_LOG) { + console.info('All stake accounts:'); + console.dir(allStakeAccounts, { depth: null, colors: true }); + } + expect(!!allStakeAccounts).toBe(true); + }); + + it('Should get all stake pool data', async () => { + for (const marketCoinName of SUPPORT_STACK_MARKET_COINS) { + const stakePool = await scallopQuery.getStakePool(marketCoinName); + + if (ENABLE_LOG) { + console.info('MarketCoinName:', marketCoinName); + console.info('StakePool:'); + console.dir(stakePool, { depth: null, colors: true }); + } + expect(!!stakePool).toBe(true); + } + }); + + it('Should get all reward pool data', async () => { + for (const marketCoinName of SUPPORT_STACK_MARKET_COINS) { + const rewardPool = await scallopQuery.getRewardPool(marketCoinName); + + if (ENABLE_LOG) { + console.info('MarketCoinName:', marketCoinName); + console.info('RewardPool:'); + console.dir(rewardPool, { depth: null, colors: true }); + } + expect(!!rewardPool).toBe(true); + } + }); +}); diff --git a/test/txBuilder.spec.ts b/test/txBuilder.spec.ts deleted file mode 100644 index 317fb70..0000000 --- a/test/txBuilder.spec.ts +++ /dev/null @@ -1,142 +0,0 @@ -import * as dotenv from 'dotenv'; -import { describe, test, expect } from 'vitest'; -import { TransactionBlock } from '@mysten/sui.js'; -import { NetworkType } from '@scallop-io/sui-kit'; -import { Scallop } from '../src'; - -const ENABLE_LOG = false; - -dotenv.config(); - -const NETWORK: NetworkType = 'mainnet'; - -describe('Test Scallop transaction builder', async () => { - const scallopSDK = new Scallop({ - secretKey: process.env.SECRET_KEY, - networkType: NETWORK, - }); - const sender = scallopSDK.suiKit.currentAddress(); - const txBuilder = await scallopSDK.createTxBuilder(); - console.info('\x1b[32mSender: \x1b[33m', sender); - - test('"openObligationEntry" should create a shared obligation, and send obligationKey to sender', async () => { - const tx = txBuilder.createTxBlock(); - tx.openObligationEntry(); - const openObligationResult = await txBuilder.signAndSendTxBlock(tx); - if (ENABLE_LOG) console.info('openObligationResult:', openObligationResult); - expect(openObligationResult.effects.status.status).toEqual('success'); - }); - - test('"borrowQuick" should borrow SUI, and return borrowed SUI', async () => { - const tx = txBuilder.createTxBlock(); - // Sender is required to invoke "borrowQuick" - tx.setSender(sender); - const borrowedCoin = await tx.borrowQuick(11 * 10 ** 6, 'sui'); - // Transfer borrowed coin to sender - tx.transferObjects([borrowedCoin], sender); - const borrowQuickResult = await txBuilder.signAndSendTxBlock(tx); - if (ENABLE_LOG) console.info('borrowQuickResult:', borrowQuickResult); - expect(borrowQuickResult.effects.status.status).toEqual('success'); - }); - - test('"repayQuick" should repay SUI', async () => { - const tx = txBuilder.createTxBlock(); - // Sender is required to invoke "repayQuick" - tx.setSender(sender); - await tx.repayQuick(2.1 * 10 ** 7, 'sui'); - const repayQuickResult = await txBuilder.signAndSendTxBlock(tx); - if (ENABLE_LOG) console.info('repayQuickResult:', repayQuickResult); - expect(repayQuickResult.effects.status.status).toEqual('success'); - }); - - test('"depositQuick" should deposit SUI, and return the "SUI sCoin"', async () => { - const tx = txBuilder.createTxBlock(); - // Sender is required to invoke "depositQuick" - tx.setSender(sender); - const sCoin = await tx.depositQuick(9 * 10 ** 7, 'sui'); - tx.transferObjects([sCoin], sender); - const depositQuickResult = await txBuilder.signAndSendTxBlock(tx); - if (ENABLE_LOG) console.info('depositQuickResult:', depositQuickResult); - expect(depositQuickResult.effects.status.status).toEqual('success'); - }); - - test('"withdrawQuick" should burn "SUI sCoin", and return SUI and the interest', async () => { - const tx = txBuilder.createTxBlock(); - // Sender is required to invoke "withdrawQuick" - tx.setSender(sender); - const coin = await tx.withdrawQuick(9 * 10 ** 7, 'sui'); - tx.transferObjects([coin], sender); - const withdrawQuickResult = await txBuilder.signAndSendTxBlock(tx); - if (ENABLE_LOG) console.info('withdrawQuickResult:', withdrawQuickResult); - expect(withdrawQuickResult.effects.status.status).toEqual('success'); - }); - - test('"addCollateralQuick" should add SUI as collateral', async () => { - const tx = txBuilder.createTxBlock(); - // Sender is required to invoke "addCollateralQuick" - tx.setSender(sender); - await tx.addCollateralQuick(10 ** 7, 'sui'); - const addCollateralQuickResult = await txBuilder.signAndSendTxBlock(tx); - if (ENABLE_LOG) - console.info('addCollateralQuickResult:', addCollateralQuickResult); - expect(addCollateralQuickResult.effects.status.status).toEqual('success'); - }); - - test('"takeCollateralQuick" should take SUI from collateral', async () => { - const tx = txBuilder.createTxBlock(); - // Sender is required to invoke "removeCollateralQuick" - tx.setSender(sender); - const coin = await tx.takeCollateralQuick(10 ** 7, 'sui'); - tx.transferObjects([coin], sender); - const removeCollateralQuickResult = await txBuilder.signAndSendTxBlock(tx); - if (ENABLE_LOG) - console.info('takeCollateralQuickResult:', removeCollateralQuickResult); - expect(removeCollateralQuickResult.effects.status.status).toEqual( - 'success' - ); - }); - - test('"borrowFlashLoan" & "repayFlashLoan" should be able to borrow and repay 1 USDC flashLoan from Scallop', async () => { - const tx = txBuilder.createTxBlock(); - const [coin, loan] = tx.borrowFlashLoan(10 ** 7, 'usdc'); - /** - * Do something with the borrowed coin - * such as pass it to a dex to make a profit - */ - // In the end, repay the loan - tx.repayFlashLoan(coin, loan, 'usdc'); - const borrowFlashLoanResult = await txBuilder.signAndSendTxBlock(tx); - if (ENABLE_LOG) - console.info('borrowFlashLoanResult:', borrowFlashLoanResult); - expect(borrowFlashLoanResult.effects.status.status).toEqual('success'); - }); - - test('"updateAssetPricesQuick" should update the prices of "SUI" and "USDC" for Scallop protocol', async () => { - const tx = txBuilder.createTxBlock(); - await tx.updateAssetPricesQuick(['sui', 'usdc']); - const updateAssetPricesResult = await txBuilder.signAndSendTxBlock(tx); - if (ENABLE_LOG) - console.info('updateAssetPricesResult:', updateAssetPricesResult); - expect(updateAssetPricesResult.effects.status.status).toEqual('success'); - }); - - test('"txBlock" is an instance of "TransactionBlock" from @mysten/sui.js', async () => { - const tx = txBuilder.createTxBlock(); - expect(tx.txBlock).toBeInstanceOf(TransactionBlock); - /** - * For example, you can do the following: - * 1. split SUI from gas - * 2. depoit SUI to Scallop - * 3. transfer SUI sCoin to sender - */ - const suiTxBlock = tx.txBlock; - const [coin] = suiTxBlock.splitCoins(suiTxBlock.gas, [ - suiTxBlock.pure(10 ** 6), - ]); - const sCoin = tx.deposit(coin, 'sui'); - suiTxBlock.transferObjects([sCoin], suiTxBlock.pure(sender)); - const txBlockResult = await txBuilder.signAndSendTxBlock(tx); - if (ENABLE_LOG) console.info('txBlockResult:', txBlockResult); - expect(txBlockResult.effects.status.status).toEqual('success'); - }); -}); diff --git a/test/utils.spec.ts b/test/utils.spec.ts new file mode 100644 index 0000000..c41a43f --- /dev/null +++ b/test/utils.spec.ts @@ -0,0 +1,84 @@ +import * as dotenv from 'dotenv'; +import { describe, it, expect } from 'vitest'; +import { PROTOCOL_OBJECT_ID } from '../src/constants'; +import { Scallop } from '../src'; +import type { NetworkType } from '@scallop-io/sui-kit'; + +dotenv.config(); + +const ENABLE_LOG = true; + +const NETWORK: NetworkType = 'mainnet'; + +describe('Test Scallop Utils', async () => { + const scallopSDK = new Scallop({ + secretKey: process.env.SECRET_KEY, + networkType: NETWORK, + }); + const scallopUtils = await scallopSDK.createScallopUtils(); + const address = await scallopSDK.getScallopAddress(); + + it('Should get coin type from coin name', async () => { + const usdcCoinType = scallopUtils.parseCoinType( + address.get('core.coins.usdc.id'), + 'usdc' + ); + const suiCoinType = scallopUtils.parseCoinType('0x2', 'sui'); + + const usdcAssertCoinType = `${address.get( + 'core.coins.usdc.id' + )}::coin::COIN`; + const suiAssertCoinType = `${address.get('core.coins.sui.id')}::sui::SUI`; + if (ENABLE_LOG) { + console.info('Usdc coin type:', usdcCoinType); + console.info('Sui coin type:', suiCoinType); + } + expect(usdcCoinType).toEqual(usdcAssertCoinType); + expect(suiCoinType).toEqual(suiAssertCoinType); + }); + + it('Should get coin name from coin type', async () => { + const usdcCoinName = scallopUtils.parseCoinName( + `${address.get('core.coins.usdc.id')}::coin::COIN` + ); + const suiCoinName = scallopUtils.parseCoinName('0x2::sui::SUI'); + + const usdcAssertCoinName = 'usdc'; + const suiAssertCoinName = 'sui'; + if (ENABLE_LOG) { + console.info('Usdc coin name:', usdcCoinName); + console.info('Sui coin name:', suiCoinName); + } + expect(usdcCoinName).toEqual(usdcAssertCoinName); + expect(suiCoinName).toEqual(suiAssertCoinName); + }); + + it('Should get market coin type from market coin type', async () => { + const usdcMarketCoinType = scallopUtils.parseMarketCoinType( + address.get('core.coins.usdc.id'), + 'usdc' + ); + const suiMarketCoinType = scallopUtils.parseMarketCoinType('0x2', 'sui'); + + const usdcAssertMarketCoinType = `${PROTOCOL_OBJECT_ID}::reserve::MarketCoin<${address.get( + 'core.coins.usdc.id' + )}::coin::COIN>`; + const suiAssertMarketCoinType = `${PROTOCOL_OBJECT_ID}::reserve::MarketCoin<${address.get( + 'core.coins.sui.id' + )}::sui::SUI>`; + if (ENABLE_LOG) { + console.info('Usdc market coin type:', usdcMarketCoinType); + console.info('Sui market coin type:', suiMarketCoinType); + } + expect(usdcMarketCoinType).toEqual(usdcAssertMarketCoinType); + expect(suiMarketCoinType).toEqual(suiAssertMarketCoinType); + }); + + it('Should get spool reward coin name', async () => { + const rewardCoinName = scallopUtils.getRewardCoinName('susdc'); + if (ENABLE_LOG) { + console.info('Reward coin name:', rewardCoinName); + } + expect(!!rewardCoinName).toBe(true); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index dddc3e4..57e3b79 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -24,7 +24,7 @@ "noEmit": false, "emitDeclarationOnly": true, "incremental": true, - "typeRoots": ["./src/types", "./node_modules/@types"] + "typeRoots": ["./node_modules/@types"] }, "include": ["./src/**/*"], "exclude": ["node_modules", "dist"]