diff --git a/package.json b/package.json index d601eb3..5ab7c05 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@compolabs/spark-orderbook-ts-sdk", - "version": "1.10.0", + "version": "1.10.1", "type": "module", "main": "./dist/index.сjs", "module": "./dist/index.js", diff --git a/src/IndexerApi.ts b/src/IndexerApi.ts index 62185b1..1bfdb6e 100644 --- a/src/IndexerApi.ts +++ b/src/IndexerApi.ts @@ -146,7 +146,7 @@ export class IndexerApi extends GraphClient { }, }); - if (!response) { + if (!response.data.TradeOrderEvent.length) { return { volume24h: BN.ZERO.toString(), high24h: BN.ZERO.toString(), diff --git a/src/ReadActions.ts b/src/ReadActions.ts index 07b68a8..91bb2fd 100644 --- a/src/ReadActions.ts +++ b/src/ReadActions.ts @@ -1,18 +1,12 @@ import { Address, Bech32Address, ZeroBytes32 } from "fuels"; import { Undefinable } from "tsdef"; -import { SparkMarket } from "./types/market"; -import { - AccountOutput, - AddressInput, - IdentityInput, -} from "./types/market/SparkMarket"; -import { MultiassetContract } from "./types/multiasset"; -import { SparkRegistry } from "./types/registry"; +import { AccountOutput, IdentityInput } from "./types/market/SparkMarket"; import { Vec } from "./types/registry/common"; import { AssetIdInput } from "./types/registry/SparkRegistry"; import BN from "./utils/BN"; +import { createContract } from "./utils/createContract"; import { MarketInfo, Markets, @@ -25,46 +19,53 @@ import { } from "./interface"; export class ReadActions { - getOrderbookVersion = async ( - options: Options, - ): Promise<{ address: string; version: number }> => { - const registryFactory = new SparkRegistry( - options.contractAddresses.registry, - options.wallet, - ); + private options: Options; + + constructor(options: Options) { + this.options = options; + } + + private get marketFactory() { + return createContract("SparkMarket", this.options); + } + + private get registryFactory() { + return createContract("SparkRegistry", this.options); + } + + private get assetsFactory() { + return createContract("MultiassetContract", this.options); + } + + private getMarketFactory(address?: string) { + return createContract("SparkMarket", this.options, address); + } + + private createIdentityInput(trader: Bech32Address): IdentityInput { + return { + Address: { + bits: new Address(trader).toB256(), + }, + }; + } - const data = await registryFactory.functions.config().get(); + async getOrderbookVersion(): Promise<{ address: string; version: number }> { + const data = await this.registryFactory.functions.config().get(); return { address: data.value[0]?.Address?.bits ?? data.value[0]?.ContractId?.bits ?? "", version: data.value[1], }; - }; - - getAsset = async ( - symbol: string, - options: Options, - ): Promise> => { - const assetsFactory = new MultiassetContract( - options.contractAddresses.multiAsset, - options.wallet, - ); + } - const data = await assetsFactory.functions.asset_get(symbol).get(); + async getAsset(symbol: string): Promise> { + const data = await this.assetsFactory.functions.asset_get(symbol).get(); return data.value?.bits; - }; - - fetchMarkets = async ( - assetIdPairs: [string, string][], - options: Options, - ): Promise => { - const registryFactory = new SparkRegistry( - options.contractAddresses.registry, - options.wallet, - ); + } + async fetchMarkets(assetIdPairs: [string, string][]): Promise { const assetIdInput: Vec<[AssetIdInput, AssetIdInput]> = assetIdPairs.map( ([baseTokenId, quoteTokenId]) => [ { bits: baseTokenId }, @@ -72,261 +73,169 @@ export class ReadActions { ], ); - const data = await registryFactory.functions.markets(assetIdInput).get(); + const data = await this.registryFactory.functions + .markets(assetIdInput) + .get(); - const markets = data.value.reduce( + return data.value.reduce( (prev, [baseAssetId, quoteAssetId, contractId]) => { if (!contractId) return prev; return { ...prev, [`${baseAssetId.bits}-${quoteAssetId.bits}`]: - contractId?.bits ?? ZeroBytes32, + contractId.bits ?? ZeroBytes32, }; }, {} as Markets, ); + } - return markets; - }; - - fetchMarketConfig = async ( - marketAddress: string, - options: Options, - ): Promise => { - const marketFactory = new SparkMarket(marketAddress, options.wallet); - + async fetchMarketConfig(marketAddress: string): Promise { + const marketFactory = this.getMarketFactory(marketAddress); const data = await marketFactory.functions.config().get(); - const market: MarketInfo = { - baseAssetId: data.value[0].bits, - baseAssetDecimals: data.value[1], - quoteAssetId: data.value[2].bits, - quoteAssetDecimals: data.value[3], - owner: - data.value[4]?.Address?.bits ?? data.value[4]?.ContractId?.bits ?? "", - priceDecimals: data.value[5], - version: data.value[6], - }; - - return market; - }; - - fetchMarketPrice = async (baseToken: string): Promise => { - console.warn("[fetchMarketPrice] NOT IMPLEMENTED FOR FUEL"); - return BN.ZERO; - }; + const [ + baseAssetId, + baseAssetDecimals, + quoteAssetId, + quoteAssetDecimals, + ownerIdentity, + priceDecimals, + version, + ] = data.value; - fetchUserMarketBalance = async ( - trader: Bech32Address, - options: Options, - ): Promise => { - const marketFactory = new SparkMarket( - options.contractAddresses.market, - options.wallet, - ); + const owner = + ownerIdentity?.Address?.bits ?? ownerIdentity?.ContractId?.bits ?? ""; - const traderAddress = new Address(trader).toB256(); - - const address: AddressInput = { - bits: traderAddress, - }; - - const user: IdentityInput = { - Address: address, + return { + baseAssetId: baseAssetId.bits, + baseAssetDecimals, + quoteAssetId: quoteAssetId.bits, + quoteAssetDecimals, + owner, + priceDecimals, + version, }; + } - const result = await marketFactory.functions.account(user).get(); + async fetchUserMarketBalance( + trader: Bech32Address, + ): Promise { + const user = this.createIdentityInput(trader); + const result = await this.marketFactory.functions.account(user).get(); - const locked = { - base: result.value?.locked.base.toString() ?? BN.ZERO.toString(), - quote: result.value?.locked.quote.toString() ?? BN.ZERO.toString(), - }; - - const liquid = { - base: result.value?.liquid.base.toString() ?? BN.ZERO.toString(), - quote: result.value?.liquid.quote.toString() ?? BN.ZERO.toString(), - }; + const { liquid, locked } = result.value ?? {}; return { - liquid, - locked, + liquid: { + base: liquid?.base.toString() ?? "0", + quote: liquid?.quote.toString() ?? "0", + }, + locked: { + base: locked?.base.toString() ?? "0", + quote: locked?.quote.toString() ?? "0", + }, }; - }; + } - fetchUserMarketBalanceByContracts = async ( + async fetchUserMarketBalanceByContracts( trader: Bech32Address, contractsAddresses: string[], - options: Options, - ): Promise => { - const traderAddress = new Address(trader).toB256(); - - const address: AddressInput = { - bits: traderAddress, - }; - - const user: IdentityInput = { - Address: address, - }; - - const baseMarketContract = new SparkMarket( - contractsAddresses[0], - options.wallet, - ); - - const promises = contractsAddresses.map((contractAddress) => { - return new SparkMarket(contractAddress, options.wallet).functions.account( - user, - ); + ): Promise { + const user = this.createIdentityInput(trader); + const calls = contractsAddresses.map((address) => { + const market = this.getMarketFactory(address); + return market.functions.account(user); }); - const result = await baseMarketContract.multiCall(promises).get(); + const baseMarketContract = this.getMarketFactory(contractsAddresses[0]); + const result = await baseMarketContract.multiCall(calls).get(); return result.value.map((data: AccountOutput) => ({ liquid: { - base: data.liquid.base.toString() ?? BN.ZERO.toString(), - quote: data.liquid.quote.toString() ?? BN.ZERO.toString(), + base: data.liquid.base.toString() ?? "0", + quote: data.liquid.quote.toString() ?? "0", }, locked: { - base: data.locked.base.toString() ?? BN.ZERO.toString(), - quote: data.locked.quote.toString() ?? BN.ZERO.toString(), + base: data.locked.base.toString() ?? "0", + quote: data.locked.quote.toString() ?? "0", }, })); - }; + } - fetchOrderById = async ( + async fetchOrderById( orderId: string, - options: Options, - ): Promise => { - const marketFactory = new SparkMarket( - options.contractAddresses.market, - options.wallet, - ); - - const result = await marketFactory.functions.order(orderId).get(); + ): Promise { + const result = await this.marketFactory.functions.order(orderId).get(); if (!result.value) return undefined; - const baseSize = new BN(result.value.amount.toString()); - const basePrice = new BN(result.value.price.toString()); + const { amount, price, order_type, owner } = result.value; return { id: orderId, - orderType: result.value.order_type as unknown as OrderType, - trader: result.value.owner.Address?.bits ?? "", - baseSize, - orderPrice: basePrice, - }; - }; - - fetchOrderIdsByAddress = async ( - trader: Bech32Address, - options: Options, - ): Promise => { - const marketFactory = new SparkMarket( - options.contractAddresses.market, - options.wallet, - ); - - const traderAddress = new Address(trader).toB256(); - - const address: AddressInput = { - bits: traderAddress, + orderType: order_type as unknown as OrderType, + trader: owner.Address?.bits ?? "", + baseSize: new BN(amount.toString()), + orderPrice: new BN(price.toString()), }; + } - const user: IdentityInput = { - Address: address, - }; - - const result = await marketFactory.functions.user_orders(user).get(); + async fetchOrderIdsByAddress(trader: Bech32Address): Promise { + const user = this.createIdentityInput(trader); + const result = await this.marketFactory.functions.user_orders(user).get(); return result.value; - }; + } - fetchWalletBalance = async ( - assetId: string, - options: Options, - ): Promise => { - const balance = await options.wallet.getBalance(assetId); + async fetchWalletBalance(assetId: string): Promise { + const balance = await this.options.wallet.getBalance(assetId); return balance.toString(); - }; - - fetchMatcherFee = async (options: Options): Promise => { - const marketFactory = new SparkMarket( - options.contractAddresses.market, - options.wallet, - ); + } - const result = await marketFactory.functions.matcher_fee().get(); + async fetchMatcherFee(): Promise { + const result = await this.marketFactory.functions.matcher_fee().get(); return result.value.toString(); - }; + } - fetchProtocolFee = async (options: Options): Promise => { - const marketFactory = new SparkMarket( - options.contractAddresses.market, - options.wallet, - ); - - const result = await marketFactory.functions.protocol_fee().get(); + async fetchProtocolFee(): Promise { + const result = await this.marketFactory.functions.protocol_fee().get(); - const data = result.value.map((fee) => ({ + return result.value.map((fee) => ({ makerFee: fee.maker_fee.toString(), takerFee: fee.taker_fee.toString(), volumeThreshold: fee.volume_threshold.toString(), })); + } - return data; - }; - - fetchProtocolFeeForUser = async ( + async fetchProtocolFeeForUser( trader: Bech32Address, - options: Options, - ): Promise => { - const marketFactory = new SparkMarket( - options.contractAddresses.market, - options.wallet, - ); - - const identity: IdentityInput = { - Address: { - bits: new Address(trader).toB256(), - }, - }; - - const result = await marketFactory.functions - .protocol_fee_user(identity) + ): Promise { + const user = this.createIdentityInput(trader); + const result = await this.marketFactory.functions + .protocol_fee_user(user) .get(); return { makerFee: result.value[0].toString(), takerFee: result.value[1].toString(), }; - }; + } - fetchProtocolFeeAmountForUser = async ( + async fetchProtocolFeeAmountForUser( amount: string, trader: Bech32Address, - options: Options, - ): Promise => { - const marketFactory = new SparkMarket( - options.contractAddresses.market, - options.wallet, - ); - - const identity: IdentityInput = { - Address: { - bits: new Address(trader).toB256(), - }, - }; - - const result = await marketFactory.functions - .protocol_fee_user_amount(amount, identity) + ): Promise { + const user = this.createIdentityInput(trader); + const result = await this.marketFactory.functions + .protocol_fee_user_amount(amount, user) .get(); return { makerFee: result.value[0].toString(), takerFee: result.value[1].toString(), }; - }; + } } diff --git a/src/SparkOrderbook.ts b/src/SparkOrderbook.ts index fb4905c..6593a5b 100644 --- a/src/SparkOrderbook.ts +++ b/src/SparkOrderbook.ts @@ -8,7 +8,6 @@ import { } from "fuels"; import { Undefinable } from "tsdef"; -import BN from "./utils/BN"; import { NETWORK_ERROR, NetworkError } from "./utils/NetworkError"; import { DEFAULT_GAS_LIMIT_MULTIPLIER, DEFAULT_GAS_PRICE } from "./constants"; import { IndexerApi } from "./IndexerApi"; @@ -30,10 +29,12 @@ import { OptionsSpark, Order, OrderType, + ProtocolFee, SparkParams, SpotOrderWithoutTimestamp, TradeOrderEvent, UserMarketBalance, + UserProtocolFee, Volume, WithdrawAllType, WriteTransactionResponse, @@ -42,14 +43,10 @@ import { ReadActions } from "./ReadActions"; import { WriteActions } from "./WriteActions"; export class SparkOrderbook { - private read = new ReadActions(); - private write = new WriteActions(); - private providerPromise: Promise; - private provider: Undefinable; + private provider?: Provider; private options: OptionsSpark; - - private indexerApi: Undefinable; + private indexerApi?: IndexerApi; constructor(params: SparkParams) { this.options = { @@ -66,290 +63,256 @@ export class SparkOrderbook { this.providerPromise = Provider.create(params.networkUrl); } - get activeIndexerApi() { + private get activeIndexerApi(): IndexerApi { if (!this.indexerApi) { - throw new Error("Please set correct active indexer"); + throw new Error("Please set the correct active indexer."); } - return this.indexerApi; } - setActiveWallet = (wallet?: WalletLocked | WalletUnlocked) => { - const newOptions = { ...this.options }; - newOptions.wallet = wallet; - this.options = newOptions; - }; + async getProvider(): Promise { + if (!this.provider) { + this.provider = await this.providerPromise; + } + return this.provider; + } - setActiveMarket = (contractAddress: string, indexer: GraphClientConfig) => { - this.options = { - ...this.options, - contractAddresses: { - ...this.options.contractAddresses, - market: contractAddress, - }, - }; + async getProviderWallet(): Promise { + const provider = await this.getProvider(); + return Wallet.generate({ provider }); + } + + private async getReadOptions(): Promise { + const providerWallet = await this.getProviderWallet(); + return { ...this.options, wallet: providerWallet }; + } + + private getWriteOptions(): Options { + if (!this.options.wallet) { + throw new NetworkError(NETWORK_ERROR.UNKNOWN_WALLET); + } + return this.options as Options; + } + + private async getRead(shouldWrite = false): Promise { + const options = shouldWrite + ? this.getWriteOptions() + : await this.getReadOptions(); + return new ReadActions(options); + } + + private getWrite(): WriteActions { + const options = this.getWriteOptions(); + return new WriteActions(options); + } + + setActiveWallet(wallet?: WalletLocked | WalletUnlocked): void { + this.options.wallet = wallet; + } + + setActiveMarket(contractAddress: string, indexer: GraphClientConfig): void { + this.options.contractAddresses.market = contractAddress; if (this.indexerApi) { this.indexerApi.close(); } this.indexerApi = new IndexerApi(indexer); - }; + } - createOrder = async ( + async createOrder( order: CreateOrderParams, - ): Promise => { - return this.write.createOrder(order, this.getApiOptions()); - }; + ): Promise { + return this.getWrite().createOrder(order); + } - createOrderWithDeposit = async ( + async createOrderWithDeposit( order: CreateOrderWithDepositParams, allMarketContracts: string[], - ): Promise => { - return this.write.createOrderWithDeposit( - order, - allMarketContracts, - this.getApiOptions(), - ); - }; + ): Promise { + return this.getWrite().createOrderWithDeposit(order, allMarketContracts); + } - fulfillOrderManyWithDeposit = async ( + async fulfillOrderManyWithDeposit( order: FulfillOrderManyWithDepositParams, allMarketContracts: string[], - ): Promise => { - return this.write.fulfillOrderManyWithDeposit( + ): Promise { + return this.getWrite().fulfillOrderManyWithDeposit( order, allMarketContracts, - this.getApiOptions(), ); - }; + } - cancelOrder = async (orderId: string): Promise => { - return this.write.cancelOrder(orderId, this.getApiOptions()); - }; + async cancelOrder(orderId: string): Promise { + return this.getWrite().cancelOrder(orderId); + } - matchOrders = async ( + async matchOrders( sellOrderId: string, buyOrderId: string, - ): Promise => { - return this.write.matchOrders( - sellOrderId, - buyOrderId, - this.getApiOptions(), - ); - }; + ): Promise { + return this.getWrite().matchOrders(sellOrderId, buyOrderId); + } - fulfillOrderMany = async ( + async fulfillOrderMany( order: FulfillOrderManyParams, - ): Promise => { - return this.write.fulfillOrderMany(order, this.getApiOptions()); - }; + ): Promise { + return this.getWrite().fulfillOrderMany(order); + } - mintToken = async ( + async mintToken( token: Asset, amount: string, - ): Promise => { - return this.write.mintToken(token, amount, this.getApiOptions()); - }; + ): Promise { + return this.getWrite().mintToken(token, amount); + } - deposit = async ( + async deposit( token: Asset, amount: string, - ): Promise => { - return this.write.deposit(token, amount, this.getApiOptions()); - }; + ): Promise { + return this.getWrite().deposit(token, amount); + } - withdraw = async ( + async withdraw( amount: string, assetType: AssetType, - ): Promise => { - return this.write.withdraw(amount, assetType, this.getApiOptions()); - }; + ): Promise { + return this.getWrite().withdraw(amount, assetType); + } - withdrawAll = async ( + async withdrawAll( assets: WithdrawAllType[], - ): Promise => { - return this.write.withdrawAll(assets, this.getApiOptions()); - }; + ): Promise { + return this.getWrite().withdrawAll(assets); + } - withdrawAssets = async ( + async withdrawAssets( assetType: AssetType, allMarketContracts: string[], amount?: string, - ): Promise => { - return this.write.withdrawAssets( + ): Promise { + return this.getWrite().withdrawAssets( assetType, allMarketContracts, - this.getApiOptions(), amount, ); - }; + } - withdrawAllAssets = async ( + async withdrawAllAssets( allMarketContracts: string[], - ): Promise => { - return this.write.withdrawAllAssets( - allMarketContracts, - this.getApiOptions(), - ); - }; - - fetchMarkets = async (assetIdPairs: [string, string][]): Promise => { - const options = await this.getFetchOptions(); - - return this.read.fetchMarkets(assetIdPairs, options); - }; - - fetchMarketConfig = async (marketAddress: string): Promise => { - const options = await this.getFetchOptions(); - - return this.read.fetchMarketConfig(marketAddress, options); - }; - - fetchMarketPrice = async (baseToken: Asset): Promise => { - return this.read.fetchMarketPrice(baseToken.assetId); - }; + ): Promise { + return this.getWrite().withdrawAllAssets(allMarketContracts); + } - fetchOrders = async ( + async fetchOrders( params: GetOrdersParams, - ): Promise> => { + ): Promise> { return this.activeIndexerApi.getOrders(params); - }; + } - fetchActiveOrders = async ( + async fetchActiveOrders( params: GetActiveOrdersParams, - ): Promise>> => { + ): Promise>> { return this.activeIndexerApi.getActiveOrders(params); - }; + } - subscribeOrders = ( + subscribeOrders( params: GetOrdersParams, - ): Observable> => { + ): Observable> { return this.activeIndexerApi.subscribeOrders(params); - }; + } - subscribeActiveOrders = ( + subscribeActiveOrders( params: GetActiveOrdersParams, - ): Observable>> => { + ): Observable>> { return this.activeIndexerApi.subscribeActiveOrders(params); - }; + } - subscribeTradeOrderEvents = ( + subscribeTradeOrderEvents( params: GetTradeOrderEventsParams, - ): Observable> => { + ): Observable> { return this.activeIndexerApi.subscribeTradeOrderEvents(params); - }; + } - fetchVolume = async (params: GetTradeOrderEventsParams): Promise => { + async fetchVolume(params: GetTradeOrderEventsParams): Promise { return this.activeIndexerApi.getVolume(params); - }; + } - fetchOrderById = async ( - orderId: string, - ): Promise => { - const options = await this.getFetchOptions(); + async fetchMarkets(assetIdPairs: [string, string][]): Promise { + const read = await this.getRead(); + return read.fetchMarkets(assetIdPairs); + } - return this.read.fetchOrderById(orderId, options); - }; + async fetchMarketConfig(marketAddress: string): Promise { + const read = await this.getRead(); + return read.fetchMarketConfig(marketAddress); + } - fetchWalletBalance = async (asset: Asset): Promise => { - // We use getApiOptions because we need the user's wallet - return this.read.fetchWalletBalance(asset.assetId, this.getApiOptions()); - }; + async fetchOrderById( + orderId: string, + ): Promise { + const read = await this.getRead(); + return read.fetchOrderById(orderId); + } - fetchOrderIdsByAddress = async (trader: Bech32Address): Promise => { - const options = await this.getFetchOptions(); + async fetchWalletBalance(asset: Asset): Promise { + const read = await this.getRead(true); + return read.fetchWalletBalance(asset.assetId); + } - return this.read.fetchOrderIdsByAddress(trader, options); - }; + async fetchOrderIdsByAddress(trader: Bech32Address): Promise { + const read = await this.getRead(); + return read.fetchOrderIdsByAddress(trader); + } - fetchUserMarketBalance = async ( + async fetchUserMarketBalance( trader: Bech32Address, - ): Promise => { - const options = await this.getFetchOptions(); - - return this.read.fetchUserMarketBalance(trader, options); - }; + ): Promise { + const read = await this.getRead(); + return read.fetchUserMarketBalance(trader); + } - fetchUserMarketBalanceByContracts = async ( + async fetchUserMarketBalanceByContracts( trader: Bech32Address, contractsAddresses: string[], - ): Promise => { - const options = await this.getFetchOptions(); - - return this.read.fetchUserMarketBalanceByContracts( - trader, - contractsAddresses, - options, - ); - }; - - fetchMatcherFee = async () => { - const options = await this.getFetchOptions(); - - return this.read.fetchMatcherFee(options); - }; - - fetchProtocolFee = async () => { - const options = await this.getFetchOptions(); + ): Promise { + const read = await this.getRead(); + return read.fetchUserMarketBalanceByContracts(trader, contractsAddresses); + } - return this.read.fetchProtocolFee(options); - }; + async fetchMatcherFee(): Promise { + const read = await this.getRead(); + return read.fetchMatcherFee(); + } - fetchProtocolFeeForUser = async (trader: Bech32Address) => { - const options = await this.getFetchOptions(); + async fetchProtocolFee(): Promise { + const read = await this.getRead(); + return read.fetchProtocolFee(); + } - return this.read.fetchProtocolFeeForUser(trader, options); - }; + async fetchProtocolFeeForUser( + trader: Bech32Address, + ): Promise { + const read = await this.getRead(); + return read.fetchProtocolFeeForUser(trader); + } - fetchProtocolFeeAmountForUser = async ( + async fetchProtocolFeeAmountForUser( amount: string, trader: Bech32Address, - ) => { - const options = await this.getFetchOptions(); - - return this.read.fetchProtocolFeeAmountForUser(amount, trader, options); - }; - - getVersion = async () => { - const options = await this.getFetchOptions(); - - return this.read.getOrderbookVersion(options); - }; - - getAsset = async (symbol: string) => { - const options = await this.getFetchOptions(); - - return this.read.getAsset(symbol, options); - }; - - getProviderWallet = async () => { - const provider = await this.getProvider(); - return Wallet.generate({ provider }); - }; - - getProvider = async () => { - if (this.provider) { - return this.provider; - } - - this.provider = await this.providerPromise; - - return this.provider; - }; - - private getFetchOptions = async (): Promise => { - const providerWallet = await this.getProviderWallet(); - const options: Options = { ...this.options, wallet: providerWallet }; - - return options; - }; + ): Promise { + const read = await this.getRead(); + return read.fetchProtocolFeeAmountForUser(amount, trader); + } - private getApiOptions = (): Options => { - if (!this.options.wallet) { - throw new NetworkError(NETWORK_ERROR.UNKNOWN_WALLET); - } + async getVersion(): Promise<{ address: string; version: number }> { + const read = await this.getRead(); + return read.getOrderbookVersion(); + } - return this.options as Options; - }; + async getAsset(symbol: string): Promise> { + const read = await this.getRead(); + return read.getAsset(symbol); + } } diff --git a/src/WriteActions.ts b/src/WriteActions.ts index 67ebff3..bdaa500 100644 --- a/src/WriteActions.ts +++ b/src/WriteActions.ts @@ -4,23 +4,20 @@ import { MultiCallInvocationScope, } from "fuels"; -import { SparkMarket } from "./types/market"; import { AssetTypeInput, LimitTypeInput, OrderTypeInput, } from "./types/market/SparkMarket"; -import { MultiassetContract } from "./types/multiasset"; import { AssetIdInput, IdentityInput, } from "./types/multiasset/MultiassetContract"; import BN from "./utils/BN"; -import { - prepareDepositAndWithdrawals, - prepareFullWithdrawals, -} from "./utils/prepareDepositAndWithdrawals"; +import { createContract } from "./utils/createContract"; +import { prepareDepositAndWithdrawals } from "./utils/prepareDepositAndWithdrawals"; +import { prepareFullWithdrawals } from "./utils/prepareFullWithdrawals"; import { Asset, AssetType, @@ -34,138 +31,122 @@ import { } from "./interface"; export class WriteActions { - deposit = async ( - token: Asset, - amount: string, - options: Options, - ): Promise => { - const marketFactory = new SparkMarket( - options.contractAddresses.market, - options.wallet, + private options: Options; + + constructor(options: Options) { + this.options = options; + } + + private get marketFactory() { + return createContract( + "SparkMarket", + this.options, + this.options.contractAddresses.market, + ); + } + + private get multiAssetContract() { + return createContract( + "MultiassetContract", + this.options, + this.options.contractAddresses.multiAsset, ); + } + async deposit( + token: Asset, + amount: string, + ): Promise { const forward: CoinQuantityLike = { amount, assetId: token.assetId, }; - const tx = marketFactory.functions.deposit().callParams({ forward }); - - return this.sendTransaction(tx, options); - }; + const tx = this.marketFactory.functions.deposit().callParams({ forward }); + return this.sendTransaction(tx); + } - withdraw = async ( + async withdraw( amount: string, assetType: AssetType, - options: Options, - ): Promise => { - const marketFactory = new SparkMarket( - options.contractAddresses.market, - options.wallet, - ); - - const tx = marketFactory.functions.withdraw( + ): Promise { + const tx = this.marketFactory.functions.withdraw( amount, assetType as unknown as AssetTypeInput, ); + return this.sendTransaction(tx); + } - return this.sendTransaction(tx, options); - }; - - withdrawAll = async ( + async withdrawAll( assets: WithdrawAllType[], - options: Options, - ): Promise => { - const marketFactory = new SparkMarket( - options.contractAddresses.market, - options.wallet, - ); - - const assetCall = assets.map((el) => - marketFactory.functions.withdraw( - el.amount, - el.assetType as unknown as AssetTypeInput, + ): Promise { + const txs = assets.map((asset) => + this.marketFactory.functions.withdraw( + asset.amount, + asset.assetType as unknown as AssetTypeInput, ), ); - const tx = marketFactory.multiCall(assetCall); - - return this.sendMultiTransaction(tx, options); - }; + const multiTx = this.marketFactory.multiCall(txs); + return this.sendMultiTransaction(multiTx); + } - withdrawAssets = async ( + async withdrawAssets( assetType: AssetType, allMarketContracts: string[], - options: Options, amount?: string, - ): Promise => { - const marketFactory = new SparkMarket( - allMarketContracts[0], - options.wallet, - ); - + ): Promise { const withdrawTxs = await prepareFullWithdrawals({ - wallet: options.wallet, + wallet: this.options.wallet, allMarketContracts, assetType, amount, }); - const tx = marketFactory.multiCall(withdrawTxs); + const multiTx = this.marketFactory.multiCall(withdrawTxs); + return this.sendMultiTransaction(multiTx); + } - return this.sendMultiTransaction(tx, options); - }; - - withdrawAllAssets = async ( + async withdrawAllAssets( allMarketContracts: string[], - options: Options, - ): Promise => { - const marketFactory = new SparkMarket( - allMarketContracts[0], - options.wallet, - ); - - const withdrawTxsBase = await prepareFullWithdrawals({ - wallet: options.wallet, - allMarketContracts, - assetType: AssetType.Base, - amount: undefined, - }); - const withdrawTxsQuote = await prepareFullWithdrawals({ - wallet: options.wallet, - allMarketContracts, - assetType: AssetType.Quote, - amount: undefined, - }); + ): Promise { + const [withdrawTxsBase, withdrawTxsQuote] = await Promise.all([ + prepareFullWithdrawals({ + wallet: this.options.wallet, + allMarketContracts, + assetType: AssetType.Base, + }), + prepareFullWithdrawals({ + wallet: this.options.wallet, + allMarketContracts, + assetType: AssetType.Quote, + }), + ]); - const tx = marketFactory.multiCall([ + const multiTx = this.marketFactory.multiCall([ ...withdrawTxsBase, ...withdrawTxsQuote, ]); - - return this.sendMultiTransaction(tx, options); - }; - - createOrder = async ( - { amount, price, type }: CreateOrderParams, - options: Options, - ): Promise => { - const marketFactory = new SparkMarket( - options.contractAddresses.market, - options.wallet, - ); - - const tx = marketFactory.functions.open_order( + return this.sendMultiTransaction(multiTx); + } + + async createOrder( + params: CreateOrderParams, + ): Promise { + const { amount, price, type } = params; + const tx = this.marketFactory.functions.open_order( amount, type as unknown as OrderTypeInput, price, ); + return this.sendTransaction(tx); + } - return this.sendTransaction(tx, options); - }; - - createOrderWithDeposit = async ( - { + async createOrderWithDeposit( + params: CreateOrderWithDepositParams, + allMarketContracts: string[], + ): Promise { + const { amount, amountToSpend, amountFee, @@ -174,18 +155,11 @@ export class WriteActions { depositAssetId, feeAssetId, assetType, - }: CreateOrderWithDepositParams, - allMarketContracts: string[], - options: Options, - ): Promise => { - const baseMarketFactory = new SparkMarket( - options.contractAddresses.market, - options.wallet, - ); + } = params; const depositAndWithdrawalTxs = await prepareDepositAndWithdrawals({ - baseMarketFactory, - wallet: options.wallet, + baseMarketFactory: this.marketFactory, + wallet: this.options.wallet, amountToSpend, depositAssetId, feeAssetId, @@ -194,69 +168,40 @@ export class WriteActions { allMarketContracts, }); - console.log("depositAndWithdrawalTxs", depositAndWithdrawalTxs); - - const txs = baseMarketFactory.multiCall([ + const txs = [ ...depositAndWithdrawalTxs, - baseMarketFactory.functions.open_order( + this.marketFactory.functions.open_order( amount, type as unknown as OrderTypeInput, price, ), - ]); - - return this.sendMultiTransaction(txs, options); - }; - - cancelOrder = async ( - orderId: string, - options: Options, - ): Promise => { - const marketFactory = new SparkMarket( - options.contractAddresses.market, - options.wallet, - ); + ]; - const tx = marketFactory.functions.cancel_order(orderId); + const multiTx = this.marketFactory.multiCall(txs); + return this.sendMultiTransaction(multiTx); + } - return this.sendTransaction(tx, options); - }; + async cancelOrder(orderId: string): Promise { + const tx = this.marketFactory.functions.cancel_order(orderId); + return this.sendTransaction(tx); + } - matchOrders = async ( + async matchOrders( sellOrderId: string, buyOrderId: string, - options: Options, - ): Promise => { - const marketFactory = new SparkMarket( - options.contractAddresses.market, - options.wallet, - ); - - const tx = marketFactory.functions.match_order_pair( + ): Promise { + const tx = this.marketFactory.functions.match_order_pair( sellOrderId, buyOrderId, ); - - return this.sendTransaction(tx, options); - }; - - fulfillOrderMany = async ( - { - amount, - orderType, - limitType, - price, - slippage, - orders, - }: FulfillOrderManyParams, - options: Options, - ) => { - const marketFactory = new SparkMarket( - options.contractAddresses.market, - options.wallet, - ); - - const tx = marketFactory.functions.fulfill_order_many( + return this.sendTransaction(tx); + } + + async fulfillOrderMany( + params: FulfillOrderManyParams, + ): Promise { + const { amount, orderType, limitType, price, slippage, orders } = params; + const tx = this.marketFactory.functions.fulfill_order_many( amount, orderType as unknown as OrderTypeInput, limitType as unknown as LimitTypeInput, @@ -264,12 +209,14 @@ export class WriteActions { slippage, orders, ); + return this.sendTransaction(tx); + } - return this.sendTransaction(tx, options); - }; - - fulfillOrderManyWithDeposit = async ( - { + async fulfillOrderManyWithDeposit( + params: FulfillOrderManyWithDepositParams, + allMarketContracts: string[], + ): Promise { + const { amount, orderType, limitType, @@ -281,18 +228,11 @@ export class WriteActions { assetType, depositAssetId, feeAssetId, - }: FulfillOrderManyWithDepositParams, - allMarketContracts: string[], - options: Options, - ) => { - const baseMarketFactory = new SparkMarket( - options.contractAddresses.market, - options.wallet, - ); + } = params; const depositAndWithdrawalTxs = await prepareDepositAndWithdrawals({ - baseMarketFactory, - wallet: options.wallet, + baseMarketFactory: this.marketFactory, + wallet: this.options.wallet, amountToSpend, depositAssetId, assetType, @@ -301,11 +241,9 @@ export class WriteActions { feeAssetId, }); - console.log("depositAndWithdrawalTxs", depositAndWithdrawalTxs); - - const txs = baseMarketFactory.multiCall([ + const txs = [ ...depositAndWithdrawalTxs, - baseMarketFactory.functions.fulfill_order_many( + this.marketFactory.functions.fulfill_order_many( amount, orderType as unknown as OrderTypeInput, limitType as unknown as LimitTypeInput, @@ -313,49 +251,37 @@ export class WriteActions { slippage, orders, ), - ]); + ]; - return this.sendMultiTransaction(txs, options); - }; + const multiTx = this.marketFactory.multiCall(txs); + return this.sendMultiTransaction(multiTx); + } - mintToken = async ( + async mintToken( token: Asset, amount: string, - options: Options, - ): Promise => { - const tokenFactory = options.contractAddresses.multiAsset; - const tokenFactoryContract = new MultiassetContract( - tokenFactory, - options.wallet, - ); - + ): Promise { const mintAmount = BN.parseUnits(amount, token.decimals); - const asset: AssetIdInput = { - bits: token.assetId, - }; - + const asset: AssetIdInput = { bits: token.assetId }; const identity: IdentityInput = { - Address: { - bits: options.wallet.address.toB256(), - }, + Address: { bits: this.options.wallet.address.toB256() }, }; - const tx = await tokenFactoryContract.functions.mint( + const tx = this.multiAssetContract.functions.mint( identity, asset, mintAmount.toString(), ); - return this.sendTransaction(tx, options); - }; + return this.sendTransaction(tx); + } - private sendTransaction = async ( + private async sendTransaction( tx: FunctionInvocationScope, - options: Options, - ): Promise => { + ): Promise { const { gasUsed } = await tx.getTransactionCost(); - const gasLimit = gasUsed.mul(options.gasLimitMultiplier).toString(); + const gasLimit = gasUsed.mul(this.options.gasLimitMultiplier).toString(); const res = await tx.txParams({ gasLimit }).call(); const data = await res.waitForResult(); @@ -363,14 +289,13 @@ export class WriteActions { transactionId: res.transactionId, value: data.value, }; - }; + } - private sendMultiTransaction = async ( + private async sendMultiTransaction( txs: MultiCallInvocationScope, - options: Options, - ): Promise => { + ): Promise { const { gasUsed } = await txs.getTransactionCost(); - const gasLimit = gasUsed.mul(options.gasLimitMultiplier).toString(); + const gasLimit = gasUsed.mul(this.options.gasLimitMultiplier).toString(); const res = await txs.txParams({ gasLimit }).call(); const data = await res.waitForResult(); @@ -378,5 +303,5 @@ export class WriteActions { transactionId: res.transactionId, value: data.value, }; - }; + } } diff --git a/src/constants/index.ts b/src/constants/index.ts index d439ec8..7afd080 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -1,4 +1,2 @@ -export const DEFAULT_DECIMALS = 9; - export const DEFAULT_GAS_PRICE = "1"; export const DEFAULT_GAS_LIMIT_MULTIPLIER = "2"; diff --git a/src/interface.ts b/src/interface.ts index dd24012..add80b3 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -2,8 +2,6 @@ import { WalletLocked, WalletUnlocked } from "fuels"; import BN from "./utils/BN"; -export type MarketStatusOutput = "Opened" | "Paused" | "Closed"; - export interface OrderbookContracts { market: string; registry: string; @@ -62,12 +60,6 @@ export interface UserMarketBalance { }; } -export type MarketCreateEvent = { - id: string; - assetId: string; - decimal: number; -}; - export type WriteTransactionResponse = { transactionId: string; value: unknown; @@ -101,15 +93,6 @@ export interface WithdrawAllType { assetType: AssetType; } -export interface SpotMarketCreateEvent { - id: number; - asset_id: string; - asset_decimals: string; - timestamp: string; - createdAt: string; - updatedAt: string; -} - export interface GetOrdersParams { limit: number; market: string; diff --git a/src/utils/convertI64ToBn.ts b/src/utils/convertI64ToBn.ts deleted file mode 100644 index 03c8f09..0000000 --- a/src/utils/convertI64ToBn.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { BN as FuelBN } from "fuels"; - -import BN from "./BN"; - -export const convertI64ToBn = (input: { - value: FuelBN; - negative: boolean; -}): BN => { - return new BN(input.value.toString()).multipliedBy(input.negative ? -1 : 1); -}; diff --git a/src/utils/createContract.ts b/src/utils/createContract.ts new file mode 100644 index 0000000..7cc2bc4 --- /dev/null +++ b/src/utils/createContract.ts @@ -0,0 +1,36 @@ +import { Options } from "../interface"; +import { SparkMarket } from "../types/market"; +import { MultiassetContract } from "../types/multiasset"; +import { SparkRegistry } from "../types/registry"; + +interface ContractClasses { + SparkMarket: typeof SparkMarket; + SparkRegistry: typeof SparkRegistry; + MultiassetContract: typeof MultiassetContract; +} + +export const createContract = ( + contractName: T, + options: Options, + requiredAddress?: string, +): InstanceType => { + switch (contractName) { + case "SparkMarket": + return new SparkMarket( + requiredAddress ?? options.contractAddresses.market, + options.wallet, + ) as InstanceType; + case "SparkRegistry": + return new SparkRegistry( + requiredAddress ?? options.contractAddresses.registry, + options.wallet, + ) as InstanceType; + case "MultiassetContract": + return new MultiassetContract( + requiredAddress ?? options.contractAddresses.multiAsset, + options.wallet, + ) as InstanceType; + default: + throw new Error(`Unknown contract type: ${contractName}`); + } +}; diff --git a/src/utils/getTotalBalance.ts b/src/utils/getTotalBalance.ts new file mode 100644 index 0000000..11e4f16 --- /dev/null +++ b/src/utils/getTotalBalance.ts @@ -0,0 +1,65 @@ +import { WalletLocked, WalletUnlocked } from "fuels"; +import { AssetType } from "src/interface"; +import { SparkMarket } from "src/types/market"; +import { AccountOutput, IdentityInput } from "src/types/market/SparkMarket"; + +import BN from "./BN"; + +const getMarketContract = ( + contractAddress: string, + wallet: WalletLocked | WalletUnlocked, +) => new SparkMarket(contractAddress, wallet); + +// Helper function to get the total balance (contract + wallet) +export const getTotalBalance = async ({ + wallet, + assetType, + depositAssetId, + feeAssetId, + contracts, +}: { + wallet: WalletLocked | WalletUnlocked; + assetType: AssetType; + depositAssetId: string; + feeAssetId: string; + contracts: string[]; +}) => { + const isBase = assetType === AssetType.Base; + + const identity: IdentityInput = { + Address: { + bits: wallet.address.toB256(), + }, + }; + + const baseMarketFactory = getMarketContract(contracts[0], wallet); + + const getBalancePromises = contracts.map((contractAddress) => + getMarketContract(contractAddress, wallet).functions.account(identity), + ); + + const balanceMultiCallResult = await baseMarketFactory + .multiCall(getBalancePromises) + .get(); + + const [targetMarketBalance, ...otherContractBalances]: BN[] = + balanceMultiCallResult.value.map((balance: AccountOutput) => { + const asset = isBase ? balance.liquid.base : balance.liquid.quote; + return new BN(asset.toString()); + }); + + const [walletBalance, walletFeeBalance] = await Promise.all([ + wallet.getBalance(depositAssetId), + wallet.getBalance(feeAssetId), + ]); + const totalBalance = new BN(BN.sum(...otherContractBalances)).plus( + walletBalance.toString(), + ); + + return { + totalBalance, + otherContractBalances, + walletFeeBalance, + targetMarketBalance, + }; +}; diff --git a/src/utils/getUnixTime.ts b/src/utils/getUnixTime.ts deleted file mode 100644 index 2538104..0000000 --- a/src/utils/getUnixTime.ts +++ /dev/null @@ -1,4 +0,0 @@ -export default function getUnixTime(time: string | number) { - const date = new Date(time); - return Math.floor(date.getTime() / 1000); -} diff --git a/src/utils/prepareDepositAndWithdrawals.ts b/src/utils/prepareDepositAndWithdrawals.ts index dc40cad..2a2a37b 100644 --- a/src/utils/prepareDepositAndWithdrawals.ts +++ b/src/utils/prepareDepositAndWithdrawals.ts @@ -6,84 +6,16 @@ import { } from "fuels"; import { AssetType } from "src/interface"; import { SparkMarket } from "src/types/market"; -import { - AccountOutput, - AssetTypeInput, - ContractIdInput, - IdentityInput, -} from "src/types/market/SparkMarket"; +import { AssetTypeInput, ContractIdInput } from "src/types/market/SparkMarket"; import BN from "./BN"; +import { getTotalBalance } from "./getTotalBalance"; const getMarketContract = ( contractAddress: string, wallet: WalletLocked | WalletUnlocked, ) => new SparkMarket(contractAddress, wallet); -// Helper function to get the total balance (contract + wallet) -const getTotalBalance = async ({ - targetMarketFactory, - wallet, - assetType, - depositAssetId, - feeAssetId, - contracts, -}: { - targetMarketFactory: SparkMarket; - wallet: WalletLocked | WalletUnlocked; - assetType: AssetType; - depositAssetId: string; - feeAssetId: string; - contracts: string[]; -}) => { - const isBase = assetType === AssetType.Base; - - const identity: IdentityInput = { - Address: { - bits: wallet.address.toB256(), - }, - }; - - const baseMarketFactory = getMarketContract(contracts[0], wallet); - - const getBalancePromises = contracts.map((contractAddress) => - getMarketContract(contractAddress, wallet).functions.account(identity), - ); - - const balanceMultiCallResult = await baseMarketFactory - .multiCall(getBalancePromises) - .get(); - - const contractBalances: BN[] = balanceMultiCallResult.value.map( - (balance: AccountOutput) => { - const asset = isBase ? balance.liquid.base : balance.liquid.quote; - return new BN(asset.toString()); - }, - ); - - const contractsBalance = new BN(BN.sum(...contractBalances)); - - const [walletBalance, walletFeeBalance] = await Promise.all([ - wallet.getBalance(depositAssetId), - wallet.getBalance(feeAssetId), - ]); - const totalBalance = contractsBalance.plus(walletBalance.toString()); - - const targetMarketBalanceResult = await targetMarketFactory.functions - .account(identity) - .get(); - const targetMarketBalance = isBase - ? new BN(targetMarketBalanceResult.value.liquid.base.toString()) - : new BN(targetMarketBalanceResult.value.liquid.quote.toString()); - - return { - totalBalance, - contractBalances, - walletFeeBalance, - targetMarketBalance, - }; -}; - // Function to get deposit data and withdraw funds if necessary export const prepareDepositAndWithdrawals = async ({ baseMarketFactory, @@ -104,29 +36,34 @@ export const prepareDepositAndWithdrawals = async ({ amountToSpend: string; amountFee: string; }) => { + const sortedContracts = allMarketContracts.sort((a) => { + if (a.toLowerCase() === baseMarketFactory.id.toB256().toLowerCase()) + return -1; + return 0; + }); + const { totalBalance, - contractBalances, + otherContractBalances, walletFeeBalance, targetMarketBalance, } = await getTotalBalance({ - targetMarketFactory: baseMarketFactory, wallet, assetType, depositAssetId, feeAssetId, - contracts: allMarketContracts, + contracts: sortedContracts, }); - if (totalBalance.lt(amountToSpend)) { + if (walletFeeBalance.lt(amountFee)) { throw new Error( - `Insufficient balance: Need ${amountToSpend}, but only have ${totalBalance}`, + `Insufficient fee balance:\nFee: ${amountFee}\nWallet balance: ${walletFeeBalance}`, ); } - if (walletFeeBalance.lt(amountFee)) { + if (totalBalance.minus(amountFee).lt(amountToSpend)) { throw new Error( - `Insufficient fee balance: Need ${amountFee}, but only have ${walletFeeBalance}`, + `Insufficient balance:\nAmount to spend: ${amountToSpend}\nFee: ${amountFee}\nBalance: ${totalBalance}`, ); } @@ -134,14 +71,14 @@ export const prepareDepositAndWithdrawals = async ({ let remainingAmountNeeded = amountToSpendBN.minus(targetMarketBalance); // Create withdraw promises for each contract, withdrawing only what's necessary - const withdrawPromises = allMarketContracts + const withdrawPromises = sortedContracts .filter( (contractAddress) => contractAddress.toLowerCase() !== baseMarketFactory.id.toB256().toLowerCase(), ) .map((contractAddress, i) => { - let amount = contractBalances[i]; + let amount = otherContractBalances[i]; // Skip if there's no need to withdraw funds or if the contract balance is zero if ( @@ -186,7 +123,7 @@ export const prepareDepositAndWithdrawals = async ({ baseMarketFactory.functions.deposit().callParams({ forward: forwardFee }), ]; - if (remainingAmountNeeded.isPositive()) { + if (remainingAmountNeeded.isPositive() && !remainingAmountNeeded.isZero()) { const forward: CoinQuantityLike = { amount: remainingAmountNeeded.toString(), assetId: depositAssetId, @@ -199,67 +136,3 @@ export const prepareDepositAndWithdrawals = async ({ return contractCalls; }; - -// Function to withdraw the full balance from each contract -export const prepareFullWithdrawals = async ({ - wallet, - assetType, - allMarketContracts, - amount, -}: { - wallet: WalletLocked | WalletUnlocked; - assetType: AssetType; - allMarketContracts: string[]; - amount?: string; -}) => { - const identity: IdentityInput = { - Address: { - bits: wallet.address.toB256(), - }, - }; - - // Fetch total balances for each contract - const getBalancePromises = allMarketContracts.map((contractAddress) => - getMarketContract(contractAddress, wallet).functions.account(identity), - ); - - const baseMarketFactory = getMarketContract(allMarketContracts[0], wallet); - - const balanceMultiCallResult = await baseMarketFactory - .multiCall(getBalancePromises) - .get(); - - const isBase = assetType === AssetType.Base; - - const specifiedAmount = amount ? new BN(amount) : null; - - // Create withdraw promises for each contract, withdrawing only what's necessary - const withdrawPromises = allMarketContracts - .map((contractAddress, i) => { - const balance = balanceMultiCallResult.value[i]; - const maxAmount = isBase - ? new BN(balance.liquid.base.toString()) - : new BN(balance.liquid.quote.toString()); - - let withdrawAmount: BN; - if (specifiedAmount) { - withdrawAmount = specifiedAmount.gt(maxAmount) - ? maxAmount - : specifiedAmount; - } else { - withdrawAmount = maxAmount; - } - - if (withdrawAmount.isZero()) { - return null; // Skip if the balance is zero - } - - return getMarketContract(contractAddress, wallet).functions.withdraw( - withdrawAmount.toString(), - assetType as unknown as AssetTypeInput, - ); - }) - .filter(Boolean) as FunctionInvocationScope[]; - - return withdrawPromises; -}; diff --git a/src/utils/prepareFullWithdrawals.ts b/src/utils/prepareFullWithdrawals.ts new file mode 100644 index 0000000..949c41b --- /dev/null +++ b/src/utils/prepareFullWithdrawals.ts @@ -0,0 +1,81 @@ +import { + BN, + FunctionInvocationScope, + WalletLocked, + WalletUnlocked, +} from "fuels"; +import { AssetType } from "src/interface"; +import { + AssetTypeInput, + IdentityInput, + SparkMarket, +} from "src/types/market/SparkMarket"; + +const getMarketContract = ( + contractAddress: string, + wallet: WalletLocked | WalletUnlocked, +) => new SparkMarket(contractAddress, wallet); + +// Function to withdraw the full balance from each contract +export const prepareFullWithdrawals = async ({ + wallet, + assetType, + allMarketContracts, + amount, +}: { + wallet: WalletLocked | WalletUnlocked; + assetType: AssetType; + allMarketContracts: string[]; + amount?: string; +}) => { + const identity: IdentityInput = { + Address: { + bits: wallet.address.toB256(), + }, + }; + + // Fetch total balances for each contract + const getBalancePromises = allMarketContracts.map((contractAddress) => + getMarketContract(contractAddress, wallet).functions.account(identity), + ); + + const baseMarketFactory = getMarketContract(allMarketContracts[0], wallet); + + const balanceMultiCallResult = await baseMarketFactory + .multiCall(getBalancePromises) + .get(); + + const isBase = assetType === AssetType.Base; + + const specifiedAmount = amount ? new BN(amount) : null; + + // Create withdraw promises for each contract, withdrawing only what's necessary + const withdrawPromises = allMarketContracts + .map((contractAddress, i) => { + const balance = balanceMultiCallResult.value[i]; + const maxAmount = isBase + ? new BN(balance.liquid.base.toString()) + : new BN(balance.liquid.quote.toString()); + + let withdrawAmount: BN; + if (specifiedAmount) { + withdrawAmount = specifiedAmount.gt(maxAmount) + ? maxAmount + : specifiedAmount; + } else { + withdrawAmount = maxAmount; + } + + if (withdrawAmount.isZero()) { + return null; // Skip if the balance is zero + } + + return getMarketContract(contractAddress, wallet).functions.withdraw( + withdrawAmount.toString(), + assetType as unknown as AssetTypeInput, + ); + }) + .filter(Boolean) as FunctionInvocationScope[]; + + return withdrawPromises; +}; diff --git a/tests/basic.test.ts b/tests/basic.test.ts deleted file mode 100644 index 3938111..0000000 --- a/tests/basic.test.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { beforeEach, describe, expect, it } from "@jest/globals"; -import { Provider, Wallet, WalletUnlocked } from "fuels"; - -import Spark, { - BETA_CONTRACT_ADDRESSES, - BETA_TOKENS, - TESTNET_INDEXER_URL, - TESTNET_NETWORK, -} from "../src"; - -import { PRIVATE_KEY_ALICE, TEST_TIMEOUT } from "./constants"; - -const TOKENS_LIST = Object.values(BETA_TOKENS).map( - ({ decimals, assetId, symbol, priceFeed }) => ({ - address: assetId, - symbol, - decimals, - priceFeed, - }), -); - -const TOKENS_BY_SYMBOL = TOKENS_LIST.reduce( - (acc, t) => ({ ...acc, [t.symbol]: t }), - {}, -); - -describe("Basic Tests", () => { - let wallet: WalletUnlocked; - let spark: Spark; - - beforeEach(async () => { - const provider = await Provider.create(TESTNET_NETWORK.url); - wallet = Wallet.fromPrivateKey(PRIVATE_KEY_ALICE, provider); - - spark = new Spark({ - networkUrl: TESTNET_NETWORK.url, - contractAddresses: BETA_CONTRACT_ADDRESSES, - indexerApiUrl: TESTNET_INDEXER_URL, - wallet, - }); - }); - - it( - "Should get all orders", - async () => { - const allOrders = await spark.fetchOrders({ - baseToken: TOKENS_BY_SYMBOL["BTC"].address, - limit: 10, - isActive: true, - }); - - console.log(allOrders.map((o) => o.id)); - - expect(allOrders).toHaveLength(10); - - console.log(allOrders); - }, - TEST_TIMEOUT, - ); -}); diff --git a/tests/constants.ts b/tests/constants.ts deleted file mode 100644 index b4a2461..0000000 --- a/tests/constants.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { BETA_TOKENS } from "../src"; - -// eslint-disable-next-line @typescript-eslint/no-var-requires -require("dotenv").config(); - -export const TEST_TIMEOUT = 60_000; // 1min; - -export const PRIVATE_KEY_ALICE = process.env.ALICE ?? ""; - -export interface TokenAsset { - address: string; - symbol: string; - decimals: number; - priceFeed: string; -} - -export const TOKENS_LIST: TokenAsset[] = Object.values(BETA_TOKENS).map( - ({ decimals, assetId, symbol, priceFeed }) => ({ - address: assetId, - symbol, - decimals, - priceFeed, - }), -); - -export const TOKENS_BY_SYMBOL: Record = TOKENS_LIST.reduce( - (acc, t) => ({ ...acc, [t.symbol]: t }), - {}, -); - -export const TOKENS_BY_ASSET_ID: Record = - TOKENS_LIST.reduce( - (acc, t) => ({ ...acc, [t.address.toLowerCase()]: t }), - {}, - ); - -export const FAUCET_AMOUNTS = { - ETH: "0.001", - USDC: "3000", - BTC: "0.01", - UNI: "50", -}; diff --git a/tests/faucet.test.ts b/tests/faucet.test.ts deleted file mode 100644 index 0dab5cf..0000000 --- a/tests/faucet.test.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { beforeEach, describe, expect, it } from "@jest/globals"; -import { Provider, Wallet, WalletUnlocked } from "fuels"; - -import Spark, { - BETA_CONTRACT_ADDRESSES, - BN, - TESTNET_INDEXER_URL, - TESTNET_NETWORK, -} from "../src"; - -import { - FAUCET_AMOUNTS, - PRIVATE_KEY_ALICE, - TEST_TIMEOUT, - TOKENS_BY_SYMBOL, -} from "./constants"; - -describe("Faucet Tests", () => { - let wallet: WalletUnlocked; - let spark: Spark; - - beforeEach(async () => { - const provider = await Provider.create(TESTNET_NETWORK.url); - wallet = Wallet.fromPrivateKey(PRIVATE_KEY_ALICE, provider); - - spark = new Spark({ - networkUrl: TESTNET_NETWORK.url, - contractAddresses: BETA_CONTRACT_ADDRESSES, - indexerApiUrl: TESTNET_INDEXER_URL, - wallet, - }); - }); - - it( - "should get balance of USDC", - async () => { - const usdc = TOKENS_BY_SYMBOL["USDC"]; - - let initialBalanceString = ""; - try { - initialBalanceString = await spark.fetchWalletBalance(usdc); - } catch (error) { - throw new Error("Retrieving balance should not throw an error."); - } - - expect(initialBalanceString).toBeDefined(); - expect(initialBalanceString).not.toBe(""); - }, - TEST_TIMEOUT, - ); - - it( - "should mint a token successfully", - async () => { - const usdc = TOKENS_BY_SYMBOL["USDC"]; - - const initialBalanceString = await spark.fetchWalletBalance(usdc); - const initialBalance = new BN(initialBalanceString).toNumber(); - - await spark.mintToken(usdc, FAUCET_AMOUNTS.USDC); - - const newBalanceString = await spark.fetchWalletBalance(usdc); - const newBalance = new BN(newBalanceString).toNumber(); - - expect(newBalance).toBeGreaterThan(initialBalance); - }, - TEST_TIMEOUT, - ); -}); diff --git a/tests/fulfillOrderMany.test.ts b/tests/fulfillOrderMany.test.ts deleted file mode 100644 index 1f95c13..0000000 --- a/tests/fulfillOrderMany.test.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { beforeAll, beforeEach, describe, expect, it } from "@jest/globals"; -import { Provider, Wallet, WalletUnlocked } from "fuels"; - -import Spark, { - AssetType, - BETA_CONTRACT_ADDRESSES, - BETA_TOKENS, - FulfillOrderManyParams, - OrderType, - TESTNET_INDEXER_URL, - TESTNET_NETWORK, -} from "../src"; -import { IndexerApi } from "../src/IndexerApi"; - -import { PRIVATE_KEY_ALICE, TEST_TIMEOUT } from "./constants"; - -const TOKENS_LIST = Object.values(BETA_TOKENS).map( - ({ decimals, assetId, symbol, priceFeed }) => ({ - address: assetId, - symbol, - decimals, - priceFeed, - }), -); - -const TOKENS_BY_SYMBOL = TOKENS_LIST.reduce( - (acc, t) => ({ ...acc, [t.symbol]: t }), - {}, -); - -describe("Fulfill Order Many Test", () => { - let wallet: WalletUnlocked; - let spark: Spark; - - let indexer: IndexerApi; - - beforeAll(async () => { - indexer = new IndexerApi(TESTNET_INDEXER_URL); - }); - - beforeEach(async () => { - const provider = await Provider.create(TESTNET_NETWORK.url); - wallet = Wallet.fromPrivateKey(PRIVATE_KEY_ALICE, provider); - - spark = new Spark({ - networkUrl: TESTNET_NETWORK.url, - contractAddresses: BETA_CONTRACT_ADDRESSES, - indexerApiUrl: TESTNET_INDEXER_URL, - wallet, - }); - }); - - it( - "Market Order Buy Fulfillment Test", - async () => { - const usdc = TOKENS_BY_SYMBOL["USDC"]; - const buyToken = TOKENS_BY_SYMBOL["BTC"]; - const amount = "2000"; - const price = "61143285305490"; - const depositParams = { - amount: (Number(amount) * Number(price)).toString(), - asset: buyToken.address, - }; - - const orderResponse = await indexer.getOrders({ - limit: 10, - }); - - const fulfillOrderManyParams: FulfillOrderManyParams = { - amount: amount, - assetType: AssetType.Base, - orderType: OrderType.Buy, - price, - slippage: "100", - orders: orderResponse.map((order) => order.id), - }; - - try { - const result = await spark.fulfillOrderMany( - depositParams, - fulfillOrderManyParams, - ); - - console.log("MATCH ORDERS RESULT", result); - expect(result).toBeDefined(); - } catch (error) { - console.error("Error matching orders:", error); - expect(error).toBeUndefined(); - } - }, - TEST_TIMEOUT, - ); - - it( - "Market Order Sell Fulfillment Test", - async () => { - const usdc = TOKENS_BY_SYMBOL["USDC"]; - const buyToken = TOKENS_BY_SYMBOL["BTC"]; - const amount = "2000"; - const price = "61143285305490"; - const depositParams = { - amount: amount, - asset: usdc.address, - }; - - const orderResponse = await indexer.getOrders({ - limit: 10, - }); - - const fulfillOrderManyParams: FulfillOrderManyParams = { - amount: amount, - assetType: AssetType.Base, - orderType: OrderType.Buy, - price: price, - slippage: "100", - orders: orderResponse.map((order) => order.id), - }; - - try { - const result = await spark.fulfillOrderMany( - depositParams, - fulfillOrderManyParams, - ); - - console.log("MATCH ORDERS RESULT", result); - expect(result).toBeDefined(); - } catch (error) { - console.error("Error matching orders:", error); - expect(error).toBeUndefined(); - } - }, - TEST_TIMEOUT, - ); -}); diff --git a/tests/index.ts b/tests/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/tests/indexerApi.test.ts b/tests/indexerApi.test.ts index fb59f43..733477e 100644 --- a/tests/indexerApi.test.ts +++ b/tests/indexerApi.test.ts @@ -1,49 +1,326 @@ -import { beforeAll, describe, expect, it } from "@jest/globals"; +// IndexerApi.test.ts +import { ApolloQueryResult, FetchResult, Observable } from "@apollo/client"; +import { + afterEach, + beforeEach, + describe, + expect, + jest, + test, +} from "@jest/globals"; -import { BETA_TOKENS, TESTNET_INDEXER_URL } from "../src"; import { IndexerApi } from "../src/IndexerApi"; +import { + ActiveOrderReturn, + GetActiveOrdersParams, + GetOrdersParams, + GetTradeOrderEventsParams, + Order, + OrderType, + TradeOrderEvent, +} from "../src/interface"; +import { getActiveOrdersQuery, getOrdersQuery } from "../src/query"; -import { TEST_TIMEOUT } from "./constants"; - -const TOKENS_LIST = Object.values(BETA_TOKENS).map( - ({ decimals, assetId, symbol, priceFeed }) => ({ - address: assetId, - symbol, - decimals, - priceFeed, - }), -); - -const TOKENS_BY_SYMBOL = TOKENS_LIST.reduce( - (acc, t) => ({ ...acc, [t.symbol]: t }), - {}, -); - -describe("Indexer Api Tests", () => { - let indexer: IndexerApi; - - beforeAll(async () => { - indexer = new IndexerApi(TESTNET_INDEXER_URL); - }); - it( - "getOrders", - async () => { - const response = await indexer.getOrders({ - limit: 1, - }); - - expect(response).toBeDefined(); - expect(response).toHaveLength(1); - }, - TEST_TIMEOUT, - ); - it( - "getVolume", - async () => { - const response = await indexer.getVolume(); - - expect(response).toBeDefined(); - }, - TEST_TIMEOUT, - ); +const USER_ADDRESS = "0x0000"; +const ASSET_ADDRESS = "0x0001"; +const MARKET_ADDRESS = "0x0002"; +const MOCK_DATE = "2024-10-10T00:00:00Z"; +const MOCK_DATE_YESTERDAY = "2024-10-09T00:00:00Z"; + +jest.mock("@apollo/client", () => { + const actual = jest.requireActual("@apollo/client") as { gql: any }; + return { + ...actual, + ApolloClient: jest.fn().mockImplementation(() => ({ + query: jest.fn(), + subscribe: jest.fn(), + })), + gql: actual.gql, + }; +}); + +describe("IndexerApi", () => { + let indexerApi: IndexerApi; + let mockQuery: jest.Mock; + let mockSubscribe: jest.Mock; + + beforeEach(() => { + indexerApi = new IndexerApi({ + httpUrl: "http://localhost", + wsUrl: "ws://localhost", + }); + mockQuery = indexerApi.client.query as jest.Mock; + mockSubscribe = indexerApi.client.subscribe as jest.Mock; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + // Вспомогательные функции и константы + const createMockOrder = (overrides?: Partial): Order => ({ + id: "1", + asset: ASSET_ADDRESS, + amount: "100", + initial_amount: "100", + order_type: OrderType.Buy, + price: "50000", + status: "Active", + user: USER_ADDRESS, + timestamp: MOCK_DATE, + ...overrides, + }); + + const createMockOrders = ( + count: number, + overrides?: Partial, + ): Order[] => Array.from({ length: count }, () => createMockOrder(overrides)); + + const createMockResponse = (data: T): ApolloQueryResult => ({ + data, + loading: false, + networkStatus: 7, + }); + + const createMockObservable = (data: T): Observable> => + new Observable>((observer) => { + observer.next({ data }); + observer.complete(); + }); + + test("getOrders should construct the correct query and return formatted data", async () => { + const params: GetOrdersParams = { + limit: 10, + market: MARKET_ADDRESS, + orderType: OrderType.Buy, + status: ["Active"], + }; + + const mockData = { Order: createMockOrders(1) }; + const mockResponse = createMockResponse(mockData); + + mockQuery.mockResolvedValueOnce(mockResponse); + + const result = await indexerApi.getOrders(params); + + const expectedQueryOptions = getOrdersQuery("query", params); + + expect(result).toEqual(mockResponse); + expect(mockQuery).toHaveBeenCalledWith(expectedQueryOptions); + }); + + test("getOrders should throw an error when client.query fails", async () => { + const params: GetOrdersParams = { + limit: 10, + market: MARKET_ADDRESS, + orderType: OrderType.Buy, + }; + + const errorMessage = "Network error"; + + mockQuery.mockRejectedValueOnce(new Error(errorMessage)); + + await expect(indexerApi.getOrders(params)).rejects.toThrow(errorMessage); + + const expectedQueryOptions = getOrdersQuery("query", params); + expect(mockQuery).toHaveBeenCalledWith(expectedQueryOptions); + }); + + test("subscribeOrders should subscribe with correct query and variables", (done) => { + const params: GetOrdersParams = { + limit: 10, + market: MARKET_ADDRESS, + orderType: OrderType.Sell, + status: ["Active"], + user: USER_ADDRESS, + asset: ASSET_ADDRESS, + offset: 0, + }; + + const mockData = { + Order: createMockOrders(1, { order_type: OrderType.Sell }), + }; + const mockObservable = createMockObservable(mockData); + + mockSubscribe.mockReturnValueOnce(mockObservable); + + const result = indexerApi.subscribeOrders(params); + + const expectedQueryOptions = getOrdersQuery("subscription", params); + + expect(mockSubscribe).toHaveBeenCalledWith(expectedQueryOptions); + expect(result).toBe(mockObservable); + + result.subscribe({ + next: (value) => { + expect(value.data).toEqual(mockData); + }, + error: (err) => { + done(err); + }, + complete: () => { + done(); + }, + }); + }); + + test("getActiveOrders should construct the correct query and return formatted data", async () => { + const params: GetActiveOrdersParams = { + limit: 5, + market: MARKET_ADDRESS, + orderType: OrderType.Buy, + user: USER_ADDRESS, + asset: ASSET_ADDRESS, + offset: 0, + }; + + const mockOrder = createMockOrder({ + id: "2", + asset: ASSET_ADDRESS, + amount: "50", + initial_amount: "50", + price: "4000", + user: USER_ADDRESS, + timestamp: MOCK_DATE, + }); + + const mockData: ActiveOrderReturn = { + ActiveBuyOrder: [mockOrder], + }; + + const mockResponse = createMockResponse(mockData); + + mockQuery.mockResolvedValueOnce(mockResponse); + + const result = await indexerApi.getActiveOrders(params); + + const expectedQueryOptions = getActiveOrdersQuery("query", params); + + expect(result).toEqual(mockResponse); + expect(mockQuery).toHaveBeenCalledWith(expectedQueryOptions); + }); + + test("subscribeActiveOrders should subscribe with correct query and variables", (done) => { + const params: GetActiveOrdersParams = { + limit: 5, + market: MARKET_ADDRESS, + orderType: OrderType.Sell, + user: USER_ADDRESS, + asset: ASSET_ADDRESS, + offset: 0, + }; + + const mockOrder = createMockOrder({ + order_type: OrderType.Sell, + asset: ASSET_ADDRESS, + amount: "100", + initial_amount: "100", + price: "2000", + user: USER_ADDRESS, + timestamp: MOCK_DATE, + }); + + const mockData: ActiveOrderReturn = { + ActiveSellOrder: [mockOrder], + }; + + const mockObservable = createMockObservable(mockData); + + mockSubscribe.mockReturnValueOnce(mockObservable); + + const result = indexerApi.subscribeActiveOrders(params); + + const expectedQueryOptions = getActiveOrdersQuery("subscription", params); + + expect(mockSubscribe).toHaveBeenCalledWith(expectedQueryOptions); + expect(result).toBe(mockObservable); + + result.subscribe({ + next: (value) => { + expect(value.data).toEqual(mockData); + }, + error: (err) => { + done(err); + }, + complete: () => { + done(); + }, + }); + }); + + test("getVolume should calculate volume, high, and low correctly", async () => { + const params: GetTradeOrderEventsParams = { + limit: 100, + market: MARKET_ADDRESS, + }; + + const mockDateNow = new Date(MOCK_DATE); + const mockDateYesterday = new Date(MOCK_DATE_YESTERDAY); + jest.useFakeTimers().setSystemTime(mockDateNow); + + const tradeEvents: TradeOrderEvent[] = [ + { + id: "0", + trade_size: "100", + trade_price: "50000", + timestamp: MOCK_DATE_YESTERDAY, + }, + { + id: "1", + trade_size: "200", + trade_price: "51000", + timestamp: MOCK_DATE_YESTERDAY, + }, + { + id: "2", + trade_size: "150", + trade_price: "49500", + timestamp: MOCK_DATE_YESTERDAY, + }, + ]; + + const mockData = { TradeOrderEvent: tradeEvents }; + const mockResponse = createMockResponse(mockData); + + mockQuery.mockResolvedValueOnce(mockResponse); + + const result = await indexerApi.getVolume(params); + + expect(result).toEqual({ + volume24h: "450", + high24h: "51000", + low24h: "49500", + }); + + expect(mockQuery).toHaveBeenCalledWith({ + query: expect.any(Object), + variables: { + where: { + market: { _eq: MARKET_ADDRESS }, + timestamp: { _gte: mockDateYesterday.toISOString() }, + }, + }, + }); + jest.useRealTimers(); + }); + + test("getVolume should return zeros when no trade events are available", async () => { + const params: GetTradeOrderEventsParams = { + limit: 100, + market: MARKET_ADDRESS, + }; + + const mockData = { TradeOrderEvent: [] }; + const mockResponse = createMockResponse(mockData); + + mockQuery.mockResolvedValueOnce(mockResponse); + + const result = await indexerApi.getVolume(params); + + expect(result).toEqual({ + volume24h: "0", + high24h: "0", + low24h: "0", + }); + }); }); diff --git a/tests/openOrder.test.ts b/tests/openOrder.test.ts deleted file mode 100644 index b9f8268..0000000 --- a/tests/openOrder.test.ts +++ /dev/null @@ -1,156 +0,0 @@ -import { beforeEach, describe, expect, it } from "@jest/globals"; -import { Provider, Wallet, WalletUnlocked } from "fuels"; - -import Spark, { - AssetType, - BETA_CONTRACT_ADDRESSES, - BETA_TOKENS, - BN, - OrderType, - TESTNET_INDEXER_URL, - TESTNET_NETWORK, -} from "../src"; - -import { PRIVATE_KEY_ALICE, TEST_TIMEOUT } from "./constants"; - -const TOKENS_LIST = Object.values(BETA_TOKENS).map( - ({ decimals, assetId, symbol, priceFeed }) => ({ - address: assetId, - symbol, - decimals, - priceFeed, - }), -); - -const TOKENS_BY_SYMBOL = TOKENS_LIST.reduce( - (acc, t) => ({ ...acc, [t.symbol]: t }), - {}, -); - -describe("Open Order Test", () => { - let wallet: WalletUnlocked; - let spark: Spark; - - beforeEach(async () => { - const provider = await Provider.create(TESTNET_NETWORK.url); - wallet = Wallet.fromPrivateKey(PRIVATE_KEY_ALICE, provider); - - spark = new Spark({ - networkUrl: TESTNET_NETWORK.url, - contractAddresses: BETA_CONTRACT_ADDRESSES, - indexerApiUrl: TESTNET_INDEXER_URL, - wallet, - }); - }); - - it( - "Trying to deposit", - async () => { - const usdc = TOKENS_BY_SYMBOL["USDC"]; - const amount = "200000000"; // USDC - - const data = await spark.deposit(usdc, amount); - - console.log("DEPOSIT DATA", data); - - expect(data.transactionId).toBeDefined(); - }, - TEST_TIMEOUT, - ); - - it( - "Fetch user orders", - async () => { - const data = await spark.fetchOrderIdsByAddress( - wallet.address.toAddress(), - ); - - console.log("ORDERS DATA", data); - - expect(data).toBeDefined(); - }, - TEST_TIMEOUT, - ); - - it.skip( - "Fetch order", - async () => { - const data = await spark.fetchOrderById( - "0xf93343da722c95c59bc77cb085f411cf83f0912f4c60571b1fa429a3ab8f88a4", - ); - - console.log("ORDER DATA", { - ...data, - baseSize: data?.baseSize.toString(), - orderPrice: data?.orderPrice.toString(), - }); - - expect(data).toBeDefined(); - }, - TEST_TIMEOUT, - ); - - // amount: this.mode === ORDER_MODE.BUY ? this.inputTotal.toString() : this.inputAmount.toString(), - // asset: this.mode === ORDER_MODE.BUY ? market.quoteToken.assetId : market.baseToken.assetId, - - it( - "Open buy order", - async () => { - const usdc = TOKENS_BY_SYMBOL["USDC"]; - const amount = new BN("1"); // BTC - - const price = "61386860766500"; - - const createOrderParams = { - amount: "547820", - assetType: AssetType.Base, - price, - type: OrderType.Buy, - }; - - const data = await spark.createOrder( - { - amount: "336289500", - asset: - "0xfed3ee85624c79cb18a3a848092239f2e764ed6b0aa156ad10a18bfdbe74269f", - }, - createOrderParams, - ); - - console.log("CREATE ORDER DATA", data); - - expect(data.transactionId).toBeDefined(); - }, - TEST_TIMEOUT, - ); - - it( - "Open sell order", - async () => { - const btc = TOKENS_BY_SYMBOL["BTC"]; - const amount = "100"; // USDC - - const price = "61143285305490"; - - const createOrderParams = { - amount, - assetType: AssetType.Base, - price, - type: OrderType.Sell, - }; - - const data = await spark.createOrder( - { - amount, - asset: btc.address, - }, - createOrderParams, - ); - - console.log("CREATE ORDER DATA", data); - - expect(data.transactionId).toBeDefined(); - }, - TEST_TIMEOUT, - ); -}); diff --git a/tests/prepareDepositAndWithdrawals.test.ts b/tests/prepareDepositAndWithdrawals.test.ts new file mode 100644 index 0000000..ba0c4f6 --- /dev/null +++ b/tests/prepareDepositAndWithdrawals.test.ts @@ -0,0 +1,155 @@ +jest.mock("../src/types/market/SparkMarket"); + +jest.mock("../src/utils/getTotalBalance", () => ({ + getTotalBalance: jest.fn(), +})); + +const BASE_CONTRACT_ADDRESS = "0xBaseContractAddress"; +const CONTRACT_ADDRESS_2 = "0xContractAddress_2"; +const CONTRACT_ADDRESS_3 = "0xContractAddress_3"; + +jest.mock("../src/types/market/SparkMarket", () => { + return { + SparkMarket: jest.fn().mockImplementation(() => ({ + id: { + toB256: jest.fn(() => BASE_CONTRACT_ADDRESS), + }, + functions: { + withdraw_to_market: jest + .fn() + .mockImplementation((amount, type, market) => ({ + amount, + type, + market, + })), + deposit: jest.fn().mockImplementation(() => ({ + isReadOnly: jest.fn(() => false), + callParams: jest.fn().mockImplementation((forward) => forward), + })), + }, + })), + }; +}); + +import { beforeEach, describe, expect, it, jest } from "@jest/globals"; +import { WalletLocked, WalletUnlocked } from "fuels"; + +import { AssetType } from "../src/interface"; +import { SparkMarket } from "../src/types/market/SparkMarket"; +import BN from "../src/utils/BN"; +import { getTotalBalance } from "../src/utils/getTotalBalance"; +import { prepareDepositAndWithdrawals } from "../src/utils/prepareDepositAndWithdrawals"; + +const mockGetTotalBalance = getTotalBalance as jest.Mock; + +describe("prepareDepositAndWithdrawals", () => { + let baseMarketFactory: any; + let wallet: WalletLocked | WalletUnlocked; + + beforeEach(() => { + jest.clearAllMocks(); + + baseMarketFactory = new SparkMarket("", {} as any); + + wallet = { + address: { + toB256: jest.fn(() => "0xMockWalletAddress"), + }, + getBalance: jest.fn(), + } as unknown as WalletLocked | WalletUnlocked; + }); + + it("throws error when totalBalance is less than amountToSpend", async () => { + mockGetTotalBalance.mockResolvedValue({ + totalBalance: new BN("60"), + otherContractBalances: [new BN("20")], + walletFeeBalance: new BN("10"), + targetMarketBalance: new BN("30"), + }); + + await expect( + prepareDepositAndWithdrawals({ + baseMarketFactory, + wallet, + assetType: AssetType.Base, + allMarketContracts: [BASE_CONTRACT_ADDRESS, CONTRACT_ADDRESS_2], + depositAssetId: "0xDepositAssetId", + feeAssetId: "0xFeeAssetId", + amountToSpend: "100", + amountFee: "5", + }), + ).rejects.toThrow( + "Insufficient balance:\nAmount to spend: 100\nFee: 5\nBalance: 60", + ); + }); + + it("throws error when walletFeeBalance is less than amountFee", async () => { + mockGetTotalBalance.mockResolvedValue({ + totalBalance: new BN("153"), + otherContractBalances: [new BN("100")], + walletFeeBalance: new BN("3"), + targetMarketBalance: new BN("50"), + }); + + await expect( + prepareDepositAndWithdrawals({ + baseMarketFactory, + wallet, + assetType: AssetType.Base, + allMarketContracts: [BASE_CONTRACT_ADDRESS, CONTRACT_ADDRESS_2], + depositAssetId: "0xDepositAssetId", + feeAssetId: "0xFeeAssetId", + amountToSpend: "100", + amountFee: "5", + }), + ).rejects.toThrow("Insufficient fee balance:\nFee: 5\nWallet balance: 3"); + }); + + it("returns correct contract calls when balances are sufficient", async () => { + mockGetTotalBalance.mockResolvedValue({ + totalBalance: new BN("105"), + otherContractBalances: [new BN("25"), new BN("25")], + walletFeeBalance: new BN("15"), + targetMarketBalance: new BN("40"), + }); + + const contractCalls = await prepareDepositAndWithdrawals({ + baseMarketFactory, + wallet, + assetType: AssetType.Base, + allMarketContracts: [ + BASE_CONTRACT_ADDRESS, + CONTRACT_ADDRESS_2, + CONTRACT_ADDRESS_3, + ], + depositAssetId: "0xDepositAssetId", + feeAssetId: "0xFeeAssetId", + amountToSpend: "100", + amountFee: "5", + }); + + expect(contractCalls).toHaveLength(4); + }); + + it("handles remainingAmountNeeded correctly", async () => { + mockGetTotalBalance.mockResolvedValue({ + totalBalance: new BN("200"), + otherContractBalances: [new BN("100"), new BN("100")], + walletFeeBalance: new BN("10"), + targetMarketBalance: new BN("50"), + }); + + const contractCalls = await prepareDepositAndWithdrawals({ + baseMarketFactory, + wallet, + assetType: AssetType.Base, + allMarketContracts: ["0xBaseContractAddress", CONTRACT_ADDRESS_2], + depositAssetId: "0xDepositAssetId", + feeAssetId: "0xFeeAssetId", + amountToSpend: "160", + amountFee: "5", + }); + + expect(contractCalls).toHaveLength(3); + }); +}); diff --git a/tests/readActions.test.ts b/tests/readActions.test.ts deleted file mode 100644 index 5679d00..0000000 --- a/tests/readActions.test.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { beforeEach, describe, expect, it } from "@jest/globals"; -import { Provider, Wallet, WalletUnlocked } from "fuels"; - -import Spark, { - BETA_CONTRACT_ADDRESSES, - BETA_TOKENS, - TESTNET_INDEXER_URL, - TESTNET_NETWORK, -} from "../src"; - -import { PRIVATE_KEY_ALICE, TEST_TIMEOUT } from "./constants"; - -const TOKENS_LIST = Object.values(BETA_TOKENS).map( - ({ decimals, assetId, symbol, priceFeed }) => ({ - address: assetId, - symbol, - decimals, - priceFeed, - }), -); - -const TOKENS_BY_SYMBOL = TOKENS_LIST.reduce( - (acc, t) => ({ ...acc, [t.symbol]: t }), - {}, -); - -describe("Read Tests", () => { - let wallet: WalletUnlocked; - let spark: Spark; - - beforeEach(async () => { - const provider = await Provider.create(TESTNET_NETWORK.url); - wallet = Wallet.fromPrivateKey(PRIVATE_KEY_ALICE, provider); - - spark = new Spark({ - networkUrl: TESTNET_NETWORK.url, - contractAddresses: BETA_CONTRACT_ADDRESSES, - indexerApiUrl: TESTNET_INDEXER_URL, - wallet, - }); - }); - - it( - "fetchMarkets", - async () => { - const allMarkets = await spark.fetchMarkets(1); - - expect(allMarkets).toHaveLength(1); - }, - TEST_TIMEOUT, - ); - it( - "fetchMarketPrice", - async () => { - const marketPrice = await spark.fetchMarketPrice(TOKENS_BY_SYMBOL["BTC"]); - - expect(marketPrice.toString()).toBeDefined(); - }, - TEST_TIMEOUT, - ); - it( - "fetchOrders", - async () => { - const allOrders = await spark.fetchOrders({ - limit: 1, - }); - - expect(allOrders).toHaveLength(1); - }, - TEST_TIMEOUT, - ); - it( - "fetchVolume", - async () => { - const volume = await spark.fetchVolume(); - - expect(volume).toBeDefined(); - }, - TEST_TIMEOUT, - ); - it( - "fetchWalletBalance", - async () => { - const allOrders = await spark.fetchWalletBalance(TOKENS_BY_SYMBOL["BTC"]); - - expect(allOrders).toBeDefined(); - }, - TEST_TIMEOUT, - ); -});