From a7924cff1ecaef3df2b8207b99d54dea1332f5c5 Mon Sep 17 00:00:00 2001 From: kwoktung Date: Tue, 24 Oct 2023 17:28:30 +0800 Subject: [PATCH] feat: add thorswap quotor (#3645) * feat: add thorswap quotor * feat: update thorswap quoter * feat: update Thorswap Quoter * feat: update quoter --- packages/engine/src/vaults/impl/btc/types.ts | 2 +- packages/kit/src/views/Swap/quoter/index.ts | 137 +++++++++++++++++- .../kit/src/views/Swap/quoter/thorswap.ts | 83 +++++++++++ packages/kit/src/views/Swap/typings.ts | 7 + packages/kit/src/views/Swap/utils.ts | 3 +- 5 files changed, 225 insertions(+), 7 deletions(-) create mode 100644 packages/kit/src/views/Swap/quoter/thorswap.ts diff --git a/packages/engine/src/vaults/impl/btc/types.ts b/packages/engine/src/vaults/impl/btc/types.ts index 686bb391c64..01a30408d21 100644 --- a/packages/engine/src/vaults/impl/btc/types.ts +++ b/packages/engine/src/vaults/impl/btc/types.ts @@ -20,7 +20,7 @@ export type IEncodedTxBtc = { outputs: Array<{ address: string; value: string; - payload?: { isCharge?: boolean; bip44Path?: string }; + payload?: { isCharge?: boolean; bip44Path?: string; opReturn?: string }; inscriptions?: NFTBTCAssetModel[]; }>; totalFee: string; diff --git a/packages/kit/src/views/Swap/quoter/index.ts b/packages/kit/src/views/Swap/quoter/index.ts index 6a3d2630b8e..b15abb84f06 100644 --- a/packages/kit/src/views/Swap/quoter/index.ts +++ b/packages/kit/src/views/Swap/quoter/index.ts @@ -4,8 +4,10 @@ import BigNumber from 'bignumber.js'; import { getNetworkImpl } from '@onekeyhq/engine/src/managers/network'; import type { Token } from '@onekeyhq/engine/src/types/token'; import type { IEncodedTxAptos } from '@onekeyhq/engine/src/vaults/impl/apt/types'; +import type { IEncodedTxBtc } from '@onekeyhq/engine/src/vaults/impl/btc/types'; import type { IEncodedTxEvm } from '@onekeyhq/engine/src/vaults/impl/evm/Vault'; import { IDecodedTxStatus } from '@onekeyhq/engine/src/vaults/types'; +import { OnekeyNetwork } from '@onekeyhq/shared/src/config/networkIds'; import { IMPL_APTOS, IMPL_EVM } from '@onekeyhq/shared/src/engine/engineConsts'; import debugLogger from '@onekeyhq/shared/src/logger/debugLogger'; @@ -31,6 +33,7 @@ import { DeezyQuoter } from './deezy'; import { JupiterQuoter } from './jupiter'; import { SocketQuoter } from './socket'; import { SwftcQuoter } from './swftc'; +import { ThorSwapQuoter } from './thorswap'; import type { BuildTransactionParams, @@ -62,6 +65,21 @@ type TransactionOrder = { orderId: string; }; +type ThorswapOrder = { + fromAsset: string; + userAddress: string; + amountIn: string; + amountOut: string; + amountOutMin: string; + memo: string; + expiration: string; + tcVault: string; +}; + +type ThorSwapData = { + quoteId: string; +}; + type EVMTransaction = { to: string; value: string; @@ -84,6 +102,8 @@ type BuildTransactionHttpResponse = { order?: TransactionOrder; errMsg?: string; result?: FetchQuoteHttpResult; + thor?: ThorSwapData; + thorOrder?: ThorswapOrder; }; type FetchQuoteHttpParams = { @@ -147,6 +167,8 @@ export class SwapQuoter { private socket = new SocketQuoter(); + private thor = new ThorSwapQuoter(); + private deezy = new DeezyQuoter(); private quoters: Quoter[] = [ @@ -155,6 +177,7 @@ export class SwapQuoter { this.jupiter, this.swftc, this.deezy, + this.thor, ]; transactionReceipts: Record< @@ -209,6 +232,72 @@ export class SwapQuoter { return result; } + async convertThorswapOrderToTransaction( + params: BuildTransactionParams, + order: ThorswapOrder, + ) { + const { tokenIn, networkIn, activeAccount, sellAmount } = params; + if (!sellAmount || !tokenIn) { + return; + } + if (!order.tcVault) { + throw new Error('failed to build transaction due to invalid tcVault'); + } + const depositCoinAmt = new BigNumber(sellAmount) + .shiftedBy(-tokenIn.decimals) + .toFixed(); + let result: TransactionData | undefined; + if ( + !tokenIn.tokenIdOnNetwork && + [ + OnekeyNetwork.btc, + OnekeyNetwork.doge, + OnekeyNetwork.ltc, + OnekeyNetwork.bch, + ].includes(networkIn.id) + ) { + result = await backgroundApiProxy.engine.buildEncodedTxFromTransfer({ + networkId: networkIn.id, + accountId: activeAccount.id, + transferInfo: { + from: activeAccount.address, + to: order.tcVault, + amount: depositCoinAmt, + opReturn: order.memo, + }, + }); + if (result) { + const btcResult = result as IEncodedTxBtc; + const isOpReturnEqualOrderMemo = btcResult.outputs.some( + (output) => output.payload?.opReturn === order.memo, + ); + if (!isOpReturnEqualOrderMemo) { + throw new Error( + 'failed to build transaction due to invalid opReturn', + ); + } + } + return result; + } + if ( + !tokenIn.tokenIdOnNetwork && + [OnekeyNetwork.cosmoshub].includes(networkIn.id) + ) { + result = await backgroundApiProxy.engine.buildEncodedTxFromTransfer({ + networkId: networkIn.id, + accountId: activeAccount.id, + transferInfo: { + from: activeAccount.address, + to: order.tcVault, + amount: depositCoinAmt, + destinationTag: order.memo, + }, + }); + return result; + } + throw new Error('not support network'); + } + async fetchLimitOrderQuote(params: ILimitOrderQuoteParams) { const urlParams = convertLimitOrderParams(params) as | FetchQuoteHttpParams @@ -408,15 +497,14 @@ export class SwapQuoter { urlParams.quoterType = quoterType; urlParams.disableValidate = Boolean(params.disableValidate); - const serverEndPont = + const serverEndPoint = await backgroundApiProxy.serviceSwap.getServerEndPoint(); - const url = `${serverEndPont}/exchange/build_tx`; + const url = `${serverEndPoint}/exchange/build_tx`; const res = await this.httpClient.post(url, urlParams); const requestId = this.parseRequestId(res); const data = res.data as BuildTransactionHttpResponse; - if (data.errMsg) { throw new Error(data.errMsg); } @@ -433,16 +521,32 @@ export class SwapQuoter { requestId, }; } - return { + const result = { data: { ...data.transaction, from: params.activeAccount.address, } as IEncodedTxEvm, result: data.result, requestId, + } as BuildTransactionResponse; + if (data.thor) { + result.attachment = { + thorswapQuoteId: data.thor.quoteId, + }; + } + return result; + } + const result = { + data: data.transaction, + result: data.result, + requestId, + } as BuildTransactionResponse; + if (data.thor) { + result.attachment = { + thorswapQuoteId: data.thor.quoteId, }; } - return { data: data.transaction, result: data.result, requestId }; + return result; } if (data.order && data.result?.instantRate) { const transaction = await this.convertOrderToTransaction( @@ -463,6 +567,22 @@ export class SwapQuoter { requestId, }; } + if (data.thorOrder) { + const transaction = await this.convertThorswapOrderToTransaction( + params, + data.thorOrder, + ); + const result: BuildTransactionResponse = { + data: transaction, + result: data.result, + }; + if (data.thor) { + result.attachment = { + thorswapQuoteId: data.thor.quoteId, + }; + } + return result; + } return undefined; } @@ -631,6 +751,13 @@ export class SwapQuoter { if (orderInfo) { return orderInfo.receiveCoinAmt; } + } else if (tx?.quoterType === 'Thorswap') { + const info = await this.thor.getTransactionInfo(tx); + if (info) { + const { legs } = info.result; + const lastLegs = legs[legs.length - 1]; + return lastLegs.toAmount; + } } else { const historyTx = await this.getHistoryTx(tx); const txid = historyTx?.decodedTx.txid || tx.hash; diff --git a/packages/kit/src/views/Swap/quoter/thorswap.ts b/packages/kit/src/views/Swap/quoter/thorswap.ts new file mode 100644 index 00000000000..aef93c3c547 --- /dev/null +++ b/packages/kit/src/views/Swap/quoter/thorswap.ts @@ -0,0 +1,83 @@ +import axios from 'axios'; + +import backgroundApiProxy from '../../../background/instance/backgroundApiProxy'; +import { QuoterType } from '../typings'; + +import type { + Quoter, + TransactionDetails, + TransactionProgress, +} from '../typings'; + +export class ThorSwapQuoter implements Quoter { + type: QuoterType = QuoterType.thorswap; + + async getBaseUrl() { + const baseUrl = await backgroundApiProxy.serviceSwap.getServerEndPoint(); + return `${baseUrl}/thorswap`; + } + + async queryTransactionProgress( + tx: TransactionDetails, + ): Promise { + const { networkId, accountId, nonce, hash, attachment } = tx; + if (nonce !== undefined) { + const status = + await backgroundApiProxy.serviceHistory.queryTransactionNonceStatus({ + networkId, + accountId, + nonce, + }); + if (status === 'failed' || status === 'canceled') { + return { status }; + } + } + if (hash && attachment?.thorswapQuoteId) { + const data = await this.getTransactionInfo(tx); + if (data && data.status === 'success') { + const { legs } = data.result; + const lastLegs = legs[legs.length - 1]; + return { status: 'sucesss', destinationTransactionHash: lastLegs.hash }; + } + return { status: 'pending' }; + } + if (Date.now() - tx.addedTime > 60 * 60 * 1000 * 24) { + return { status: 'failed' }; + } + return undefined; + } + + async getTransactionInfo(tx: TransactionDetails) { + const { hash, attachment } = tx; + if (hash && attachment?.thorswapQuoteId) { + const baseUrl = await this.getBaseUrl(); + const url = `${baseUrl}/transaction_status`; + const res = await axios.get(url, { + params: { + hash, + quoteId: attachment?.thorswapQuoteId, + }, + }); + const data = res.data as { + status: string; + done: boolean; + result: { + quoteId: string; + firstTransactionHash: string; + status: string; + isLending: boolean; + isStreamingSwap: false; + legs: { + chain: string; + hash: string; + fromAsset: string; + fromAmount: string; + toAsset: string; + toAmount: string; + }[]; + }; + }; + return data; + } + } +} diff --git a/packages/kit/src/views/Swap/typings.ts b/packages/kit/src/views/Swap/typings.ts index f3824af7054..fa32ded815d 100644 --- a/packages/kit/src/views/Swap/typings.ts +++ b/packages/kit/src/views/Swap/typings.ts @@ -120,6 +120,7 @@ export enum QuoterType { jupiter = 'jupiter', onekey = 'onekey', deezy = 'Deezy', + thorswap = 'Thorswap', } export type FieldType = 'INPUT' | 'OUTPUT'; @@ -237,6 +238,12 @@ export interface TransactionAttachment { swftcReceiveCoinAmt?: string; swftcReceiveCoinCode?: string; socketUsedBridgeNames?: string[]; + + thorswapQuoteId?: string; +} + +export interface ThorswapOrderReceipt { + quoteId: string; } export type BuildTransactionParams = FetchQuoteParams & { diff --git a/packages/kit/src/views/Swap/utils.ts b/packages/kit/src/views/Swap/utils.ts index ecea66a88c7..8df13b858dc 100644 --- a/packages/kit/src/views/Swap/utils.ts +++ b/packages/kit/src/views/Swap/utils.ts @@ -300,7 +300,8 @@ export function convertParams(params: FetchQuoteParams) { if (params.onChainSatsPerVbyte) { urlParams.onChainSatsPerVbyte = params.onChainSatsPerVbyte; } - urlParams.includes = '0x,1inch,jupiter,openocean,swftc,socket,mdex,Deezy'; + urlParams.includes = + '0x,1inch,jupiter,openocean,swftc,socket,mdex,Deezy,Thorswap'; urlParams.noFilter = true; return urlParams; }