diff --git a/packages/chain-adapters/src/ton/TonChainAdapter.ts b/packages/chain-adapters/src/ton/TonChainAdapter.ts index cdffb065677..3ec399eba92 100644 --- a/packages/chain-adapters/src/ton/TonChainAdapter.ts +++ b/packages/chain-adapters/src/ton/TonChainAdapter.ts @@ -20,11 +20,13 @@ import type { SignAndBroadcastTransactionInput, SignTxInput, Transaction, + TxHistoryInput, + TxHistoryResponse, ValidAddressResult, } from '../types' import { ChainAdapterDisplayName, ValidAddressResultType } from '../types' import { toAddressNList, verifyLedgerAppOpen } from '../utils' -import type { TonFeeData, TonSignTx, TonToken } from './types' +import type { TonFeeData, TonSignTx, TonToken, TonTx } from './types' const supportsTon = (wallet: HDWallet): wallet is TonWallet => { return '_supportsTon' in wallet && (wallet as TonWallet)._supportsTon === true @@ -436,8 +438,96 @@ export class ChainAdapter implements IChainAdapter { return Promise.resolve(invalid) } - getTxHistory(): Promise { - throw new Error('TON transaction history not yet implemented') + async getTxHistory(input: TxHistoryInput): Promise { + try { + const { pubkey, cursor, pageSize = 25, requestQueue, knownTxIds } = input + + const offset = cursor ? parseInt(cursor, 10) : 0 + + const fetchTxHistory = async () => { + const response = await this.httpApiRequest<{ + transactions?: TonTx[] + address_book?: Record + }>( + `/api/v3/transactions?account=${encodeURIComponent( + pubkey, + )}&limit=${pageSize}&offset=${offset}&sort=desc`, + ) + return response + } + + const data = requestQueue ? await requestQueue.add(fetchTxHistory) : await fetchTxHistory() + + if (!data?.transactions || data.transactions.length === 0) { + return { + cursor: '', + pubkey, + transactions: [], + txIds: [], + } + } + + const addressBook = data.address_book ?? {} + + const transactions: Transaction[] = [] + const txIds: string[] = [] + + for (const tx of data.transactions) { + const txid = tx.hash + + if (knownTxIds?.has(txid)) continue + + txIds.push(txid) + + const normalizedTx: TonTx = { + ...tx, + in_msg: tx.in_msg + ? { + ...tx.in_msg, + source: tx.in_msg.source + ? addressBook[tx.in_msg.source]?.user_friendly ?? tx.in_msg.source + : tx.in_msg.source, + destination: tx.in_msg.destination + ? addressBook[tx.in_msg.destination]?.user_friendly ?? tx.in_msg.destination + : tx.in_msg.destination, + } + : tx.in_msg, + out_msgs: tx.out_msgs?.map(msg => ({ + ...msg, + source: msg.source ? addressBook[msg.source]?.user_friendly ?? msg.source : msg.source, + destination: msg.destination + ? addressBook[msg.destination]?.user_friendly ?? msg.destination + : msg.destination, + })), + } + + const parsedTx = this.parse(normalizedTx, pubkey, txid) + + const fetchJettonTransfers = () => this.parseJettonTransfers(txid, pubkey) + + const jettonTransfers = requestQueue + ? await requestQueue.add(fetchJettonTransfers) + : await fetchJettonTransfers() + + transactions.push({ + ...parsedTx, + transfers: [...parsedTx.transfers, ...jettonTransfers], + }) + } + + const nextCursor = data.transactions.length === pageSize ? String(offset + pageSize) : '' + + return { + cursor: nextCursor, + pubkey, + transactions, + txIds, + } + } catch (err) { + return ErrorHandler(err, { + translation: 'chainAdapters.errors.getTxHistory', + }) + } } async getSeqno(address: string): Promise { @@ -691,8 +781,186 @@ export class ChainAdapter implements IChainAdapter { return this.normalizeAddress(addr1) === this.normalizeAddress(addr2) } - async parseTx(msgHash: string, pubkey: string): Promise { + private async parseJettonTransfers( + txHash: string, + pubkey: string, + ): Promise< + { + assetId: string + from: string[] + to: string[] + type: TransferType + value: string + }[] + > { + try { + const response = await this.httpApiRequest<{ + jetton_transfers?: { + source?: string + destination?: string + amount?: string + jetton_master?: string + }[] + address_book?: Record + }>(`/api/v3/jetton/transfers?tx_hash=${encodeURIComponent(txHash)}`) + + if (!response.jetton_transfers || response.jetton_transfers.length === 0) { + return [] + } + + const transfers: { + assetId: string + from: string[] + to: string[] + type: TransferType + value: string + }[] = [] + + const addressBook = response.address_book ?? {} + + for (const transfer of response.jetton_transfers) { + if ( + !transfer.source || + !transfer.destination || + !transfer.amount || + !transfer.jetton_master + ) { + continue + } + + const sourceUserFriendly = addressBook[transfer.source]?.user_friendly ?? transfer.source + const destUserFriendly = + addressBook[transfer.destination]?.user_friendly ?? transfer.destination + const jettonUserFriendly = + addressBook[transfer.jetton_master]?.user_friendly ?? transfer.jetton_master + + const isSend = this.addressesMatch(sourceUserFriendly, pubkey) + const isReceive = this.addressesMatch(destUserFriendly, pubkey) + + if (!isSend && !isReceive) continue + + const assetId = toAssetId({ + chainId: this.chainId, + assetNamespace: 'jetton', + assetReference: jettonUserFriendly, + }) + + if (isSend) { + transfers.push({ + assetId, + from: [sourceUserFriendly], + to: [destUserFriendly], + type: TransferType.Send, + value: transfer.amount, + }) + } + + if (isReceive) { + transfers.push({ + assetId, + from: [sourceUserFriendly], + to: [destUserFriendly], + type: TransferType.Receive, + value: transfer.amount, + }) + } + } + + return transfers + } catch { + return [] + } + } + + private parse(tx: TonTx, pubkey: string, txid: string): Transaction { + const isAborted = tx.description?.aborted ?? false + const actionSuccess = tx.description?.action?.success ?? true + const status = isAborted || !actionSuccess ? TxStatus.Failed : TxStatus.Confirmed + + const transfers: { + assetId: string + from: string[] + to: string[] + type: TransferType + value: string + }[] = [] + + if (tx.in_msg?.value && tx.in_msg.source && tx.in_msg.destination) { + const value = tx.in_msg.value + if (BigInt(value) > 0n) { + const isReceive = this.addressesMatch(tx.in_msg.destination, pubkey) + const isSend = this.addressesMatch(tx.in_msg.source, pubkey) + + if (isReceive) { + transfers.push({ + assetId: this.assetId, + from: [tx.in_msg.source], + to: [tx.in_msg.destination], + type: TransferType.Receive, + value, + }) + } else if (isSend) { + transfers.push({ + assetId: this.assetId, + from: [tx.in_msg.source], + to: [tx.in_msg.destination], + type: TransferType.Send, + value, + }) + } + } + } + + if (tx.out_msgs) { + for (const outMsg of tx.out_msgs) { + if (outMsg.value && outMsg.source && outMsg.destination) { + const value = outMsg.value + if (BigInt(value) > 0n) { + const isSend = this.addressesMatch(outMsg.source, pubkey) + const isReceive = this.addressesMatch(outMsg.destination, pubkey) + + if (isSend && !isReceive) { + transfers.push({ + assetId: this.assetId, + from: [outMsg.source], + to: [outMsg.destination], + type: TransferType.Send, + value, + }) + } else if (isReceive && !isSend) { + transfers.push({ + assetId: this.assetId, + from: [outMsg.source], + to: [outMsg.destination], + type: TransferType.Receive, + value, + }) + } + } + } + } + } + + const isSend = transfers.some(transfer => transfer.type === TransferType.Send) + + return { + txid, + blockHeight: parseInt(tx.lt, 10) || 0, + blockTime: tx.now || 0, + blockHash: undefined, + chainId: this.chainId, + confirmations: status === TxStatus.Confirmed ? 1 : 0, + status, + transfers, + pubkey, + ...(isSend && tx.total_fees && { fee: { assetId: this.assetId, value: tx.total_fees } }), + } + } + + async parseTx(txHashOrTx: unknown, pubkey: string): Promise { try { + const msgHash = typeof txHashOrTx === 'string' ? txHashOrTx : String(txHashOrTx) + const msgResult = await this.httpApiRequest<{ messages?: { hash: string @@ -725,28 +993,9 @@ export class ChainAdapter implements IChainAdapter { } } - type TonTxMessage = { - source?: string - destination?: string - value?: string - } - type TonTxResponse = { - transactions?: { - account: string - hash: string - lt: string - now: number - total_fees: string - description?: { - aborted?: boolean - action?: { - success?: boolean - } - } - in_msg?: TonTxMessage - out_msgs?: TonTxMessage[] - }[] + transactions?: TonTx[] + address_book?: Record } const txResult = await this.httpApiRequest( @@ -754,90 +1003,42 @@ export class ChainAdapter implements IChainAdapter { ) const tx = txResult.transactions?.[0] - const isAborted = tx?.description?.aborted ?? false - const actionSuccess = tx?.description?.action?.success ?? true - const status = isAborted || !actionSuccess ? TxStatus.Failed : TxStatus.Confirmed - const transfers: { - assetId: string - from: string[] - to: string[] - type: TransferType - value: string - }[] = [] - - if (tx?.in_msg?.value && tx.in_msg.source && tx.in_msg.destination) { - const value = tx.in_msg.value - if (BigInt(value) > 0n) { - const isReceive = this.addressesMatch(tx.in_msg.destination, pubkey) - const isSend = this.addressesMatch(tx.in_msg.source, pubkey) - - if (isReceive) { - transfers.push({ - assetId: this.assetId, - from: [tx.in_msg.source], - to: [tx.in_msg.destination], - type: TransferType.Receive, - value, - }) - } else if (isSend) { - transfers.push({ - assetId: this.assetId, - from: [tx.in_msg.source], - to: [tx.in_msg.destination], - type: TransferType.Send, - value, - }) - } - } + if (!tx) { + throw new Error(`Transaction not found: ${txHash}`) } - if (tx?.out_msgs) { - for (const outMsg of tx.out_msgs) { - if (outMsg.value && outMsg.source && outMsg.destination) { - const value = outMsg.value - if (BigInt(value) > 0n) { - const isSend = this.addressesMatch(outMsg.source, pubkey) - const isReceive = this.addressesMatch(outMsg.destination, pubkey) - - if (isSend && !isReceive) { - transfers.push({ - assetId: this.assetId, - from: [outMsg.source], - to: [outMsg.destination], - type: TransferType.Send, - value, - }) - } else if (isReceive && !isSend) { - transfers.push({ - assetId: this.assetId, - from: [outMsg.source], - to: [outMsg.destination], - type: TransferType.Receive, - value, - }) - } + const addressBook = txResult.address_book ?? {} + + const normalizedTx: TonTx = { + ...tx, + in_msg: tx.in_msg + ? { + ...tx.in_msg, + source: tx.in_msg.source + ? addressBook[tx.in_msg.source]?.user_friendly ?? tx.in_msg.source + : tx.in_msg.source, + destination: tx.in_msg.destination + ? addressBook[tx.in_msg.destination]?.user_friendly ?? tx.in_msg.destination + : tx.in_msg.destination, } - } - } + : tx.in_msg, + out_msgs: tx.out_msgs?.map(msg => ({ + ...msg, + source: msg.source ? addressBook[msg.source]?.user_friendly ?? msg.source : msg.source, + destination: msg.destination + ? addressBook[msg.destination]?.user_friendly ?? msg.destination + : msg.destination, + })), } + const parsedTx = this.parse(normalizedTx, pubkey, msgHash) + + const jettonTransfers = await this.parseJettonTransfers(txHash, pubkey) + return { - txid: msgHash, - blockHeight: tx ? parseInt(tx.lt, 10) || 0 : 0, - blockTime: tx?.now || 0, - blockHash: undefined, - chainId: this.chainId, - confirmations: status === TxStatus.Confirmed ? 1 : 0, - status, - fee: tx?.total_fees - ? { - assetId: this.assetId, - value: tx.total_fees, - } - : undefined, - transfers, - pubkey, + ...parsedTx, + transfers: [...parsedTx.transfers, ...jettonTransfers], } } catch (err) { return ErrorHandler(err, { diff --git a/packages/chain-adapters/src/ton/types.ts b/packages/chain-adapters/src/ton/types.ts index dd639778384..5885e54a48c 100644 --- a/packages/chain-adapters/src/ton/types.ts +++ b/packages/chain-adapters/src/ton/types.ts @@ -46,6 +46,28 @@ export type TonSignTx = { expireAt?: number } +export type TonTxMessage = { + source?: string + destination?: string + value?: string +} + +export type TonTx = { + account: string + hash: string + lt: string + now: number + total_fees: string + description?: { + aborted?: boolean + action?: { + success?: boolean + } + } + in_msg?: TonTxMessage + out_msgs?: TonTxMessage[] +} + export type Account = TonAccount export type FeeData = TonFeeData export type GetFeeDataInput = TonGetFeeDataInput