import { SuiCallArg, SuiTransactionBlockResponse, SuiArgument, SuiClient } from '@mysten/sui.js/client'; import { normalizeStructTag } from '@mysten/sui.js/utils'; import { ParsedCoinTransfer, ParsedObjectTransfer, ParsedTransaction } from '@/transactions/tx-types'; import { isCoinObjectType } from '@/utils'; // TODO: TxParser can be made into a more generalized framework export class TxParser { static async fetchAndParse(suiClient: SuiClient, digest: string): Promise { const resp = await suiClient.getTransactionBlock({ digest, options: { showBalanceChanges: true, showInput: true, showEffects: true, showEvents: true, showObjectChanges: true, showRawInput: true, }, }); return this.parse(resp); } static parse(resp: SuiTransactionBlockResponse): ParsedTransaction | undefined { const coinPayload = this.parseCoinTransfer(resp); if (coinPayload) { return coinPayload; } return this.parseObjectTransfer(resp); } static parseCoinTransfer(resp: SuiTransactionBlockResponse): ParsedCoinTransfer | undefined { const transactionData = resp.transaction?.data; if (!transactionData || transactionData.transaction.kind !== 'ProgrammableTransaction') { return undefined; } const { transactions, inputs } = transactionData.transaction; const isCoinTransfer = transactions.reduce( (res, tx) => res && ('TransferObjects' in tx || 'SplitCoins' in tx || 'MergeCoins' in tx), true, ); if (!isCoinTransfer) { return undefined; } const recipients = ( transactions.filter((tx) => 'TransferObjects' in tx) as { TransferObjects: [SuiArgument[], SuiArgument] }[] ).map((tx) => { const [, recipient] = tx.TransferObjects; return this.getPureAddress(inputs, recipient); }); if (new Set(recipients).size !== 1) { return undefined; } const recipient = recipients[0]; if (!resp.balanceChanges) { return undefined; } const recBalChange = resp.balanceChanges.filter( (change) => typeof change.owner !== 'string' && 'AddressOwner' in change.owner && change.owner.AddressOwner === recipient, ); if (recBalChange.length !== 1) { return undefined; } return { type: 'coin_transfer', from: transactionData.sender, to: recipient, amount: recBalChange[0].amount, coinType: normalizeStructTag(recBalChange[0].coinType), }; } static parseObjectTransfer(resp: SuiTransactionBlockResponse): ParsedObjectTransfer | undefined { const transactionData = resp.transaction?.data; if (!transactionData || transactionData.transaction.kind !== 'ProgrammableTransaction') { return undefined; } const { transactions, inputs } = transactionData.transaction; if (transactions.length !== 1 || !('TransferObjects' in transactions[0])) { return undefined; } const transferArgs = transactions[0].TransferObjects; const toAddress = this.getPureAddress(inputs, transferArgs[1]); const objIds = transferArgs[0].map((arg) => this.getOwnedObject(inputs, arg)); const from = transactionData.sender; if (!toAddress || objIds.indexOf(undefined) !== -1) { return undefined; } const objects = objIds .map((id) => { const found = resp.objectChanges.find((change) => change.type === 'mutated' && change.objectId === id); if (found && found.type === 'mutated' && !!found.objectType && !isCoinObjectType(found.objectType)) { return { objectId: id, version: found.version, type: normalizeStructTag(found.objectType), }; } return undefined; }) .filter((object) => !!object); return objects ? { type: 'object_transfer', from, to: toAddress, objects, } : undefined; } private static getOwnedObject(inputs: SuiCallArg[], arg: SuiArgument): string { const input = this.getInput(inputs, arg); if (!input || input.type !== 'object' || input.objectType !== 'immOrOwnedObject') { return undefined; } return input.objectId; } private static getPureAddress(inputs: SuiCallArg[], arg: SuiArgument): string { const input = this.getInput(inputs, arg); if (!input || input.type !== 'pure' || input.valueType !== 'address') { return undefined; } return input.value as string; } private static getInput(inputs: SuiCallArg[], arg: SuiArgument) { if (typeof arg === 'string' || !('Input' in arg)) { return undefined; } return inputs[arg.Input]; } }