From 84e25d081b953259cb065a7cd12b6ff0e5cbe9a5 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Wed, 24 Dec 2025 13:17:54 +0100 Subject: [PATCH 01/12] fix: second class chains dedupe internal/value MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Native amounts are currently doubled for same-chain second-class EVM Txs (sends/swaps) in upserted Txs, when the Tx is a same-account Tx. The reason here is we parse both the `value` and the internal transfer value out of debug RPC call. This ensures deduping, so Tx value is not doubled in Tx history. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 (1M context) --- packages/chain-adapters/src/evm/SecondClassEvmAdapter.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/chain-adapters/src/evm/SecondClassEvmAdapter.ts b/packages/chain-adapters/src/evm/SecondClassEvmAdapter.ts index 27ceb3b5dfa..9bddc8a7c56 100644 --- a/packages/chain-adapters/src/evm/SecondClassEvmAdapter.ts +++ b/packages/chain-adapters/src/evm/SecondClassEvmAdapter.ts @@ -500,6 +500,14 @@ export abstract class SecondClassEvmAdapter extends EvmBas const internalFrom = getAddress(internalTx.from) const internalTo = getAddress(internalTx.to) + // Skip internal transactions that duplicate the native transaction + if ( + isAddressEqual(internalFrom, txFrom) && + isAddressEqual(internalTo, txTo) && + internalTx.value === tx.value + ) + continue + if (isAddressEqual(address, internalFrom)) { nativeTransfers.push({ assetId: this.assetId, From 025dfc7218e08296e53705d546a25a379f3a7838 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Wed, 24 Dec 2025 13:53:30 +0100 Subject: [PATCH 02/12] [skip ci] feat: skip ci From ea7b0797e3997dae6990242878f8f59d6c3ff931 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Wed, 24 Dec 2025 14:02:47 +0100 Subject: [PATCH 03/12] [skip ci] feat: clean sui transaction parsing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Extract PTB parsing into helper method - Filter gas fees from balance changes - Use PTB transfer amounts for Send transfers (excludes gas) - Handle self-sends and cross-account transfers - Use early returns and IIFEs for cleaner flow - Follow SUI terminology (PTB = Programmable Transaction Block) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 (1M context) --- .../chain-adapters/src/sui/SuiChainAdapter.ts | 175 +++++++++++++++--- 1 file changed, 145 insertions(+), 30 deletions(-) diff --git a/packages/chain-adapters/src/sui/SuiChainAdapter.ts b/packages/chain-adapters/src/sui/SuiChainAdapter.ts index a4f1c1c4fee..c36c65cb7b9 100644 --- a/packages/chain-adapters/src/sui/SuiChainAdapter.ts +++ b/packages/chain-adapters/src/sui/SuiChainAdapter.ts @@ -521,9 +521,49 @@ export class ChainAdapter implements IChainAdapter { return } + private parseProgrammableTransactionBlock(tx: SuiTransactionBlockResponse): { + transferAmount: string | undefined + recipient: string | undefined + } { + const ptb = tx.transaction?.data.transaction + if (ptb?.kind !== 'ProgrammableTransaction') { + return { transferAmount: undefined, recipient: undefined } + } + + const inputs = ptb.inputs ?? [] + const commands = ptb.transactions ?? [] + + let transferAmount: string | undefined + let recipient: string | undefined + + for (const command of commands) { + if ('SplitCoins' in command) { + const [_coinSource, amounts] = command.SplitCoins + const firstAmount = amounts?.[0] + if (!firstAmount || !('Input' in firstAmount)) continue + + const amountInput = inputs[firstAmount.Input] + if (amountInput?.type === 'pure' && amountInput.valueType === 'u64') { + transferAmount = amountInput.value + } + } + + if ('TransferObjects' in command) { + const [_objects, recipientArg] = command.TransferObjects + if (!recipientArg || !('Input' in recipientArg)) continue + + const recipientInput = inputs[recipientArg.Input] + if (recipientInput?.type === 'pure' && recipientInput.valueType === 'address') { + recipient = recipientInput.value + } + } + } + + return { transferAmount, recipient } + } + async parseTx(txHashOrTx: unknown, pubkey: string): Promise { try { - // Fetch full transaction data if only txHash was provided const tx = typeof txHashOrTx === 'string' ? await this.client.getTransactionBlock({ @@ -532,12 +572,12 @@ export class ChainAdapter implements IChainAdapter { showInput: true, showEffects: true, showBalanceChanges: true, + showObjectChanges: true, }, }) : (txHashOrTx as SuiTransactionBlockResponse) const sender = tx.transaction?.data.sender ?? '' - const txid = tx.digest const blockHeight = Number(tx.checkpoint ?? 0) const blockTime = tx.timestampMs ? Math.floor(Number(tx.timestampMs) / 1000) : 0 @@ -545,16 +585,17 @@ export class ChainAdapter implements IChainAdapter { const latestCheckpoint = await this.client.getLatestCheckpointSequenceNumber() const confirmations = tx.checkpoint ? Number(latestCheckpoint) - Number(tx.checkpoint) + 1 : 0 - const status = - tx.effects?.status.status === 'success' - ? TxStatus.Confirmed - : tx.effects?.status.status === 'failure' - ? TxStatus.Failed - : TxStatus.Unknown + const status = (() => { + const txStatus = tx.effects?.status.status + if (txStatus === 'success') return TxStatus.Confirmed + if (txStatus === 'failure') return TxStatus.Failed + return TxStatus.Unknown + })() const gasUsed = tx.effects?.gasUsed - const fee = gasUsed - ? { + const fee = !gasUsed + ? undefined + : { assetId: this.assetId, value: ( BigInt(gasUsed.computationCost) + @@ -562,15 +603,30 @@ export class ChainAdapter implements IChainAdapter { BigInt(gasUsed.storageRebate) ).toString(), } - : undefined + + const { transferAmount: ptbTransferAmount, recipient: ptbRecipient } = + this.parseProgrammableTransactionBlock(tx) const balanceChanges = tx.balanceChanges ?? [] - const transfers = balanceChanges.map(change => { - let ownerAddress: string | null = null - if (typeof change.owner === 'object' && 'AddressOwner' in change.owner) { - ownerAddress = change.owner.AddressOwner - } + // Filter out balance changes that only represent gas fees + const actualTransferChanges = balanceChanges.filter(change => { + if (!fee || change.coinType !== '0x2::sui::SUI') return true + + const changeAmount = BigInt(change.amount) + const absoluteChange = changeAmount < 0n ? -changeAmount : changeAmount + const feeAmount = BigInt(fee.value) + + return absoluteChange !== feeAmount + }) + + const transfersFromBalanceChanges = actualTransferChanges.map(change => { + const ownerAddress = (() => { + if (typeof change.owner === 'object' && 'AddressOwner' in change.owner) { + return change.owner.AddressOwner + } + return null + })() const assetId = change.coinType === '0x2::sui::SUI' @@ -585,22 +641,81 @@ export class ChainAdapter implements IChainAdapter { const isReceive = amount > 0n const isSend = amount < 0n - const transferType = - ownerAddress === pubkey - ? isReceive - ? TransferType.Receive - : TransferType.Send - : TransferType.Contract - - return { - assetId, - from: isSend ? [sender] : ownerAddress ? [ownerAddress] : [sender], - to: isReceive ? [ownerAddress ?? sender] : [sender], - type: transferType, - value: amount < 0n ? (-amount).toString() : amount.toString(), - } + const transferType = (() => { + if (ownerAddress !== pubkey) return TransferType.Contract + return isReceive ? TransferType.Receive : TransferType.Send + })() + + // For Send transfers of native SUI, use PTB amount to exclude gas + const shouldUsePtbAmount = + isSend && ptbTransferAmount && change.coinType === '0x2::sui::SUI' + const transferValue = shouldUsePtbAmount + ? ptbTransferAmount + : (amount < 0n ? -amount : amount).toString() + + const from = isSend ? [sender] : ownerAddress ? [ownerAddress] : [sender] + const to = isReceive ? [ownerAddress ?? sender] : [sender] + + return { assetId, from, to, type: transferType, value: transferValue } }) + // For self-sends where balance changes were filtered out, use PTB data + const transfersFromPtb = (() => { + if (actualTransferChanges.length > 0) return [] + if (!ptbTransferAmount || !ptbRecipient) return [] + + const isSelfSend = sender === ptbRecipient + const isSender = sender === pubkey + const isRecipient = ptbRecipient === pubkey + + if (isSelfSend && isSender) { + return [ + { + assetId: this.assetId, + from: [sender], + to: [ptbRecipient], + type: TransferType.Send, + value: ptbTransferAmount, + }, + { + assetId: this.assetId, + from: [sender], + to: [ptbRecipient], + type: TransferType.Receive, + value: ptbTransferAmount, + }, + ] + } + + if (isSender) { + return [ + { + assetId: this.assetId, + from: [sender], + to: [ptbRecipient], + type: TransferType.Send, + value: ptbTransferAmount, + }, + ] + } + + if (isRecipient) { + return [ + { + assetId: this.assetId, + from: [sender], + to: [ptbRecipient], + type: TransferType.Receive, + value: ptbTransferAmount, + }, + ] + } + + return [] + })() + + const transfers = [...transfersFromBalanceChanges, ...transfersFromPtb] + return { txid, blockHeight, From dcfa71e634b00019d29cfb05839f794d002dd7a9 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Wed, 24 Dec 2025 14:12:51 +0100 Subject: [PATCH 04/12] [skip ci] feat: sui transaction parsing fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Extract PTB parsing into helper method - Filter gas fees from balance changes - Use PTB amounts for Send transfers (excludes gas) - Parse and upsert for both sender and recipient (if held account) - Use early returns and IIFEs for cleaner flow - Follow SUI terminology (PTB = Programmable Transaction Block) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 (1M context) --- .../useSendActionSubscriber.tsx | 34 ++++++++++++++----- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/src/hooks/useActionCenterSubscribers/useSendActionSubscriber.tsx b/src/hooks/useActionCenterSubscribers/useSendActionSubscriber.tsx index fe439d99001..d0fcb7fc0db 100644 --- a/src/hooks/useActionCenterSubscribers/useSendActionSubscriber.tsx +++ b/src/hooks/useActionCenterSubscribers/useSendActionSubscriber.tsx @@ -161,19 +161,35 @@ export const useSendActionSubscriber = () => { if (isConfirmed) { // Parse and upsert Tx for second-class chains + const { accountIdsToRefetch } = action.transactionMetadata + const accountIdsToUpsert = accountIdsToRefetch ?? [accountId] + try { const adapter = getChainAdapterManager().get(chainId) - if (adapter?.parseTx) { - const parsedTx = await adapter.parseTx(txHash, accountAddress) - dispatch( - txHistory.actions.onMessage({ - message: parsedTx, - accountId, - }), - ) + if (!adapter?.parseTx) { + completeAction(action) + const intervalId = pollingIntervalsRef.current.get(pollingKey) + if (intervalId) { + clearInterval(intervalId) + pollingIntervalsRef.current.delete(pollingKey) + } + return } + + // Parse and upsert for all involved accounts (sender + recipient if held) + await Promise.all( + accountIdsToUpsert.map(async accountIdToUpsert => { + const address = fromAccountId(accountIdToUpsert).account + const parsedTx = await adapter.parseTx(txHash, address) + dispatch( + txHistory.actions.onMessage({ + message: parsedTx, + accountId: accountIdToUpsert, + }), + ) + }), + ) } catch (error) { - // Silent fail - Tx just won't show in history console.error('Failed to parse and upsert Tx:', error) } From a2256b7cb5fede7a93a8694a4da951dec1161cdd Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Wed, 24 Dec 2025 14:23:20 +0100 Subject: [PATCH 05/12] [skip ci] feat: add debug logs for token parsing --- .../chain-adapters/src/sui/SuiChainAdapter.ts | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/packages/chain-adapters/src/sui/SuiChainAdapter.ts b/packages/chain-adapters/src/sui/SuiChainAdapter.ts index c36c65cb7b9..d49584dad1c 100644 --- a/packages/chain-adapters/src/sui/SuiChainAdapter.ts +++ b/packages/chain-adapters/src/sui/SuiChainAdapter.ts @@ -578,6 +578,9 @@ export class ChainAdapter implements IChainAdapter { : (txHashOrTx as SuiTransactionBlockResponse) const sender = tx.transaction?.data.sender ?? '' + + console.log(`[SuiChainAdapter.parseTx] FULL NODE RESPONSE:`, JSON.stringify(tx, null, 2)) + const txid = tx.digest const blockHeight = Number(tx.checkpoint ?? 0) const blockTime = tx.timestampMs ? Math.floor(Number(tx.timestampMs) / 1000) : 0 @@ -607,8 +610,22 @@ export class ChainAdapter implements IChainAdapter { const { transferAmount: ptbTransferAmount, recipient: ptbRecipient } = this.parseProgrammableTransactionBlock(tx) + console.log( + `[SuiChainAdapter.parseTx] PTB parsing result:`, + JSON.stringify({ txid, ptbTransferAmount, ptbRecipient }), + ) + const balanceChanges = tx.balanceChanges ?? [] + console.log( + `[SuiChainAdapter.parseTx] Balance changes:`, + JSON.stringify({ + txid, + balanceChangesCount: balanceChanges.length, + balanceChanges, + }), + ) + // Filter out balance changes that only represent gas fees const actualTransferChanges = balanceChanges.filter(change => { if (!fee || change.coinType !== '0x2::sui::SUI') return true @@ -716,6 +733,22 @@ export class ChainAdapter implements IChainAdapter { const transfers = [...transfersFromBalanceChanges, ...transfersFromPtb] + console.log( + `[SuiChainAdapter.parseTx] FINAL:`, + JSON.stringify({ + txid, + feeValue: fee?.value, + transfersFromBalanceChanges: transfersFromBalanceChanges.length, + transfersFromPtb: transfersFromPtb.length, + totalTransfers: transfers.length, + transfers: transfers.map(t => ({ + type: t.type, + assetId: t.assetId, + value: t.value, + })), + }), + ) + return { txid, blockHeight, From dba3a7dfd742fe4a4d3d29788fad9e5bc7d09926 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Wed, 24 Dec 2025 14:26:35 +0100 Subject: [PATCH 06/12] [skip ci] feat: parse token types from object changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Extract coin type from objectChanges in PTB parsing - Match coin object ID with objectChanges to get token type - Create correct assetId for token transfers - Add logs to track token parsing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 (1M context) --- .../chain-adapters/src/sui/SuiChainAdapter.ts | 69 +++++++++++++++---- 1 file changed, 55 insertions(+), 14 deletions(-) diff --git a/packages/chain-adapters/src/sui/SuiChainAdapter.ts b/packages/chain-adapters/src/sui/SuiChainAdapter.ts index d49584dad1c..4e28a80cca0 100644 --- a/packages/chain-adapters/src/sui/SuiChainAdapter.ts +++ b/packages/chain-adapters/src/sui/SuiChainAdapter.ts @@ -524,10 +524,11 @@ export class ChainAdapter implements IChainAdapter { private parseProgrammableTransactionBlock(tx: SuiTransactionBlockResponse): { transferAmount: string | undefined recipient: string | undefined + coinType: string | undefined } { const ptb = tx.transaction?.data.transaction if (ptb?.kind !== 'ProgrammableTransaction') { - return { transferAmount: undefined, recipient: undefined } + return { transferAmount: undefined, recipient: undefined, coinType: undefined } } const inputs = ptb.inputs ?? [] @@ -535,10 +536,11 @@ export class ChainAdapter implements IChainAdapter { let transferAmount: string | undefined let recipient: string | undefined + let coinObjectId: string | undefined for (const command of commands) { if ('SplitCoins' in command) { - const [_coinSource, amounts] = command.SplitCoins + const [coinSource, amounts] = command.SplitCoins const firstAmount = amounts?.[0] if (!firstAmount || !('Input' in firstAmount)) continue @@ -546,6 +548,14 @@ export class ChainAdapter implements IChainAdapter { if (amountInput?.type === 'pure' && amountInput.valueType === 'u64') { transferAmount = amountInput.value } + + // For token transfers, coin source is an object input + if (typeof coinSource === 'object' && 'Input' in coinSource) { + const coinInput = inputs[coinSource.Input] + if (coinInput?.type === 'object') { + coinObjectId = coinInput.objectId + } + } } if ('TransferObjects' in command) { @@ -559,7 +569,26 @@ export class ChainAdapter implements IChainAdapter { } } - return { transferAmount, recipient } + // Extract coin type from objectChanges if we have a coin object ID + const coinType = (() => { + if (!coinObjectId) return undefined + + const objectChange = tx.objectChanges?.find( + change => 'objectId' in change && change.objectId === coinObjectId, + ) + + if (!objectChange || !('objectType' in objectChange)) return undefined + + const match = objectChange.objectType.match(/0x2::coin::Coin<(.+)>/) + return match?.[1] + })() + + console.log( + `[SuiChainAdapter.parseTx] PTB parsing:`, + JSON.stringify({ transferAmount, recipient, coinObjectId, coinType }), + ) + + return { transferAmount, recipient, coinType } } async parseTx(txHashOrTx: unknown, pubkey: string): Promise { @@ -607,13 +636,11 @@ export class ChainAdapter implements IChainAdapter { ).toString(), } - const { transferAmount: ptbTransferAmount, recipient: ptbRecipient } = - this.parseProgrammableTransactionBlock(tx) - - console.log( - `[SuiChainAdapter.parseTx] PTB parsing result:`, - JSON.stringify({ txid, ptbTransferAmount, ptbRecipient }), - ) + const { + transferAmount: ptbTransferAmount, + recipient: ptbRecipient, + coinType: ptbCoinType, + } = this.parseProgrammableTransactionBlock(tx) const balanceChanges = tx.balanceChanges ?? [] @@ -685,17 +712,31 @@ export class ChainAdapter implements IChainAdapter { const isSender = sender === pubkey const isRecipient = ptbRecipient === pubkey + // Determine the correct assetId (native SUI or token) + const assetId = !ptbCoinType + ? this.assetId + : toAssetId({ + chainId: this.chainId, + assetNamespace: ASSET_NAMESPACE.suiCoin, + assetReference: ptbCoinType, + }) + + console.log( + `[SuiChainAdapter.parseTx] Creating PTB transfers:`, + JSON.stringify({ ptbCoinType, assetId, isSelfSend, isSender, isRecipient }), + ) + if (isSelfSend && isSender) { return [ { - assetId: this.assetId, + assetId, from: [sender], to: [ptbRecipient], type: TransferType.Send, value: ptbTransferAmount, }, { - assetId: this.assetId, + assetId, from: [sender], to: [ptbRecipient], type: TransferType.Receive, @@ -707,7 +748,7 @@ export class ChainAdapter implements IChainAdapter { if (isSender) { return [ { - assetId: this.assetId, + assetId, from: [sender], to: [ptbRecipient], type: TransferType.Send, @@ -719,7 +760,7 @@ export class ChainAdapter implements IChainAdapter { if (isRecipient) { return [ { - assetId: this.assetId, + assetId, from: [sender], to: [ptbRecipient], type: TransferType.Receive, From f9763fd9adb176125e083f80349d198120b357a7 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Wed, 24 Dec 2025 14:36:43 +0100 Subject: [PATCH 07/12] [skip ci] feat: remove debug logs --- .../chain-adapters/src/sui/SuiChainAdapter.ts | 38 ------------------- 1 file changed, 38 deletions(-) diff --git a/packages/chain-adapters/src/sui/SuiChainAdapter.ts b/packages/chain-adapters/src/sui/SuiChainAdapter.ts index 4e28a80cca0..5cd8b6ba7e2 100644 --- a/packages/chain-adapters/src/sui/SuiChainAdapter.ts +++ b/packages/chain-adapters/src/sui/SuiChainAdapter.ts @@ -583,11 +583,6 @@ export class ChainAdapter implements IChainAdapter { return match?.[1] })() - console.log( - `[SuiChainAdapter.parseTx] PTB parsing:`, - JSON.stringify({ transferAmount, recipient, coinObjectId, coinType }), - ) - return { transferAmount, recipient, coinType } } @@ -607,9 +602,6 @@ export class ChainAdapter implements IChainAdapter { : (txHashOrTx as SuiTransactionBlockResponse) const sender = tx.transaction?.data.sender ?? '' - - console.log(`[SuiChainAdapter.parseTx] FULL NODE RESPONSE:`, JSON.stringify(tx, null, 2)) - const txid = tx.digest const blockHeight = Number(tx.checkpoint ?? 0) const blockTime = tx.timestampMs ? Math.floor(Number(tx.timestampMs) / 1000) : 0 @@ -644,15 +636,6 @@ export class ChainAdapter implements IChainAdapter { const balanceChanges = tx.balanceChanges ?? [] - console.log( - `[SuiChainAdapter.parseTx] Balance changes:`, - JSON.stringify({ - txid, - balanceChangesCount: balanceChanges.length, - balanceChanges, - }), - ) - // Filter out balance changes that only represent gas fees const actualTransferChanges = balanceChanges.filter(change => { if (!fee || change.coinType !== '0x2::sui::SUI') return true @@ -721,11 +704,6 @@ export class ChainAdapter implements IChainAdapter { assetReference: ptbCoinType, }) - console.log( - `[SuiChainAdapter.parseTx] Creating PTB transfers:`, - JSON.stringify({ ptbCoinType, assetId, isSelfSend, isSender, isRecipient }), - ) - if (isSelfSend && isSender) { return [ { @@ -774,22 +752,6 @@ export class ChainAdapter implements IChainAdapter { const transfers = [...transfersFromBalanceChanges, ...transfersFromPtb] - console.log( - `[SuiChainAdapter.parseTx] FINAL:`, - JSON.stringify({ - txid, - feeValue: fee?.value, - transfersFromBalanceChanges: transfersFromBalanceChanges.length, - transfersFromPtb: transfersFromPtb.length, - totalTransfers: transfers.length, - transfers: transfers.map(t => ({ - type: t.type, - assetId: t.assetId, - value: t.value, - })), - }), - ) - return { txid, blockHeight, From d2a50da1793a3ca51966573674e9e7a56060d91b Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Wed, 24 Dec 2025 14:44:14 +0100 Subject: [PATCH 08/12] [skip ci] feat: restore comment --- src/hooks/useActionCenterSubscribers/useSendActionSubscriber.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/hooks/useActionCenterSubscribers/useSendActionSubscriber.tsx b/src/hooks/useActionCenterSubscribers/useSendActionSubscriber.tsx index d0fcb7fc0db..a69ca814192 100644 --- a/src/hooks/useActionCenterSubscribers/useSendActionSubscriber.tsx +++ b/src/hooks/useActionCenterSubscribers/useSendActionSubscriber.tsx @@ -190,6 +190,7 @@ export const useSendActionSubscriber = () => { }), ) } catch (error) { + // Silent fail - Tx just won't show in history console.error('Failed to parse and upsert Tx:', error) } From ce0e5226046e7f27c7d9f530c3842d3d1f8512ab Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Wed, 24 Dec 2025 14:51:36 +0100 Subject: [PATCH 09/12] [skip ci] feat: normalize sui coin types consistently MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Extract normalizeCoinType to class method - Apply normalization in both getAccount and parseTx - Ensures consistent AssetId generation across methods - Prevents AssetId mismatches for token transfers 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 (1M context) --- .../chain-adapters/src/sui/SuiChainAdapter.ts | 43 ++++++++++--------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/packages/chain-adapters/src/sui/SuiChainAdapter.ts b/packages/chain-adapters/src/sui/SuiChainAdapter.ts index 5cd8b6ba7e2..82039d9af07 100644 --- a/packages/chain-adapters/src/sui/SuiChainAdapter.ts +++ b/packages/chain-adapters/src/sui/SuiChainAdapter.ts @@ -135,25 +135,7 @@ export class ChainAdapter implements IChainAdapter { const tokens = await Promise.all( nonZeroBalances.map(async balance => { const symbol = balance.coinType.split('::').pop() ?? 'UNKNOWN' - - // Normalize coinType to ensure proper format with leading zeros - // SUI addresses should be 66 chars (0x + 64 hex chars) - const normalizeCoinType = (coinType: string): string => { - const parts = coinType.split('::') - if (parts.length < 2) return coinType - - const address = parts[0] - if (!address.startsWith('0x')) return coinType - - // Pad address to 66 characters (0x + 64 hex digits) - const hexPart = address.slice(2) - const paddedHex = hexPart.padStart(64, '0') - parts[0] = `0x${paddedHex}` - - return parts.join('::') - } - - const normalizedCoinType = normalizeCoinType(balance.coinType) + const normalizedCoinType = this.normalizeCoinType(balance.coinType) const assetId = toAssetId({ chainId: this.chainId, @@ -521,6 +503,23 @@ export class ChainAdapter implements IChainAdapter { return } + // Normalize SUI coin type to ensure consistent AssetId generation + // SUI addresses should be 66 characters (0x + 64 hex chars) with leading zeros + // Example: 0x2::sui::SUI stays the same, but 0xdba3::usdc::USDC becomes 0x0000...0dba3::usdc::USDC + private normalizeCoinType(coinType: string): string { + const parts = coinType.split('::') + if (parts.length < 2) return coinType + + const address = parts[0] + if (!address.startsWith('0x')) return coinType + + const hexPart = address.slice(2) + const paddedHex = hexPart.padStart(64, '0') + parts[0] = `0x${paddedHex}` + + return parts.join('::') + } + private parseProgrammableTransactionBlock(tx: SuiTransactionBlockResponse): { transferAmount: string | undefined recipient: string | undefined @@ -580,7 +579,9 @@ export class ChainAdapter implements IChainAdapter { if (!objectChange || !('objectType' in objectChange)) return undefined const match = objectChange.objectType.match(/0x2::coin::Coin<(.+)>/) - return match?.[1] + const extractedCoinType = match?.[1] + + return extractedCoinType ? this.normalizeCoinType(extractedCoinType) : undefined })() return { transferAmount, recipient, coinType } @@ -661,7 +662,7 @@ export class ChainAdapter implements IChainAdapter { : toAssetId({ chainId: this.chainId, assetNamespace: ASSET_NAMESPACE.suiCoin, - assetReference: change.coinType, + assetReference: this.normalizeCoinType(change.coinType), }) const amount = BigInt(change.amount) From 4b2e00c52f7a9bbacbe1a7183923552231703d60 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Wed, 24 Dec 2025 14:58:15 +0100 Subject: [PATCH 10/12] chore: trigger CI From 0cdf39ea2365efe49d426425b22e21bddb8b86b9 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Wed, 24 Dec 2025 15:07:16 +0100 Subject: [PATCH 11/12] chore: trigger CI --- packages/chain-adapters/src/sui/SuiChainAdapter.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/chain-adapters/src/sui/SuiChainAdapter.ts b/packages/chain-adapters/src/sui/SuiChainAdapter.ts index 82039d9af07..e2777de72ef 100644 --- a/packages/chain-adapters/src/sui/SuiChainAdapter.ts +++ b/packages/chain-adapters/src/sui/SuiChainAdapter.ts @@ -541,29 +541,30 @@ export class ChainAdapter implements IChainAdapter { if ('SplitCoins' in command) { const [coinSource, amounts] = command.SplitCoins const firstAmount = amounts?.[0] - if (!firstAmount || !('Input' in firstAmount)) continue + if (!firstAmount || typeof firstAmount !== 'object' || !('Input' in firstAmount)) continue const amountInput = inputs[firstAmount.Input] if (amountInput?.type === 'pure' && amountInput.valueType === 'u64') { - transferAmount = amountInput.value + transferAmount = amountInput.value as string } // For token transfers, coin source is an object input if (typeof coinSource === 'object' && 'Input' in coinSource) { const coinInput = inputs[coinSource.Input] if (coinInput?.type === 'object') { - coinObjectId = coinInput.objectId + coinObjectId = coinInput.objectId as string } } } if ('TransferObjects' in command) { const [_objects, recipientArg] = command.TransferObjects - if (!recipientArg || !('Input' in recipientArg)) continue + if (!recipientArg || typeof recipientArg !== 'object' || !('Input' in recipientArg)) + continue const recipientInput = inputs[recipientArg.Input] if (recipientInput?.type === 'pure' && recipientInput.valueType === 'address') { - recipient = recipientInput.value + recipient = recipientInput.value as string } } } From 7d6bce00269cce31db4ea15584dcd9e976ff739f Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Wed, 24 Dec 2025 15:19:26 +0100 Subject: [PATCH 12/12] fix: sui transfer from/to addresses and type safety MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace type assertions with runtime type guards - Fix from/to address logic for cross-account transfers - from always = sender, to = PTB recipient for sends - Addresses CodeRabbit feedback on type safety and transfer logic 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 (1M context) --- .../chain-adapters/src/sui/SuiChainAdapter.ts | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/packages/chain-adapters/src/sui/SuiChainAdapter.ts b/packages/chain-adapters/src/sui/SuiChainAdapter.ts index e2777de72ef..270f7934c6f 100644 --- a/packages/chain-adapters/src/sui/SuiChainAdapter.ts +++ b/packages/chain-adapters/src/sui/SuiChainAdapter.ts @@ -545,14 +545,20 @@ export class ChainAdapter implements IChainAdapter { const amountInput = inputs[firstAmount.Input] if (amountInput?.type === 'pure' && amountInput.valueType === 'u64') { - transferAmount = amountInput.value as string + const value = amountInput.value + if (typeof value === 'string') { + transferAmount = value + } } // For token transfers, coin source is an object input if (typeof coinSource === 'object' && 'Input' in coinSource) { const coinInput = inputs[coinSource.Input] if (coinInput?.type === 'object') { - coinObjectId = coinInput.objectId as string + const objectId = coinInput.objectId + if (typeof objectId === 'string') { + coinObjectId = objectId + } } } } @@ -564,7 +570,10 @@ export class ChainAdapter implements IChainAdapter { const recipientInput = inputs[recipientArg.Input] if (recipientInput?.type === 'pure' && recipientInput.valueType === 'address') { - recipient = recipientInput.value as string + const value = recipientInput.value + if (typeof value === 'string') { + recipient = value + } } } } @@ -682,8 +691,14 @@ export class ChainAdapter implements IChainAdapter { ? ptbTransferAmount : (amount < 0n ? -amount : amount).toString() - const from = isSend ? [sender] : ownerAddress ? [ownerAddress] : [sender] - const to = isReceive ? [ownerAddress ?? sender] : [sender] + // ownerAddress is who owns the balance after the transaction + // For Send: from = sender, to = recipient (from PTB if available, else ownerAddress) + // For Receive: from = sender, to = ownerAddress (recipient) + const from = [sender] + const to = (() => { + if (isReceive) return [ownerAddress ?? sender] + return ptbRecipient ? [ptbRecipient] : [sender] + })() return { assetId, from, to, type: transferType, value: transferValue } })