@@ -135,25 +135,7 @@ export class ChainAdapter implements IChainAdapter<KnownChainIds.SuiMainnet> {
135135 const tokens = await Promise . all (
136136 nonZeroBalances . map ( async balance => {
137137 const symbol = balance . coinType . split ( '::' ) . pop ( ) ?? 'UNKNOWN'
138-
139- // Normalize coinType to ensure proper format with leading zeros
140- // SUI addresses should be 66 chars (0x + 64 hex chars)
141- const normalizeCoinType = ( coinType : string ) : string => {
142- const parts = coinType . split ( '::' )
143- if ( parts . length < 2 ) return coinType
144-
145- const address = parts [ 0 ]
146- if ( ! address . startsWith ( '0x' ) ) return coinType
147-
148- // Pad address to 66 characters (0x + 64 hex digits)
149- const hexPart = address . slice ( 2 )
150- const paddedHex = hexPart . padStart ( 64 , '0' )
151- parts [ 0 ] = `0x${ paddedHex } `
152-
153- return parts . join ( '::' )
154- }
155-
156- const normalizedCoinType = normalizeCoinType ( balance . coinType )
138+ const normalizedCoinType = this . normalizeCoinType ( balance . coinType )
157139
158140 const assetId = toAssetId ( {
159141 chainId : this . chainId ,
@@ -521,9 +503,102 @@ export class ChainAdapter implements IChainAdapter<KnownChainIds.SuiMainnet> {
521503 return
522504 }
523505
506+ // Normalize SUI coin type to ensure consistent AssetId generation
507+ // SUI addresses should be 66 characters (0x + 64 hex chars) with leading zeros
508+ // Example: 0x2::sui::SUI stays the same, but 0xdba3::usdc::USDC becomes 0x0000...0dba3::usdc::USDC
509+ private normalizeCoinType ( coinType : string ) : string {
510+ const parts = coinType . split ( '::' )
511+ if ( parts . length < 2 ) return coinType
512+
513+ const address = parts [ 0 ]
514+ if ( ! address . startsWith ( '0x' ) ) return coinType
515+
516+ const hexPart = address . slice ( 2 )
517+ const paddedHex = hexPart . padStart ( 64 , '0' )
518+ parts [ 0 ] = `0x${ paddedHex } `
519+
520+ return parts . join ( '::' )
521+ }
522+
523+ private parseProgrammableTransactionBlock ( tx : SuiTransactionBlockResponse ) : {
524+ transferAmount : string | undefined
525+ recipient : string | undefined
526+ coinType : string | undefined
527+ } {
528+ const ptb = tx . transaction ?. data . transaction
529+ if ( ptb ?. kind !== 'ProgrammableTransaction' ) {
530+ return { transferAmount : undefined , recipient : undefined , coinType : undefined }
531+ }
532+
533+ const inputs = ptb . inputs ?? [ ]
534+ const commands = ptb . transactions ?? [ ]
535+
536+ let transferAmount : string | undefined
537+ let recipient : string | undefined
538+ let coinObjectId : string | undefined
539+
540+ for ( const command of commands ) {
541+ if ( 'SplitCoins' in command ) {
542+ const [ coinSource , amounts ] = command . SplitCoins
543+ const firstAmount = amounts ?. [ 0 ]
544+ if ( ! firstAmount || typeof firstAmount !== 'object' || ! ( 'Input' in firstAmount ) ) continue
545+
546+ const amountInput = inputs [ firstAmount . Input ]
547+ if ( amountInput ?. type === 'pure' && amountInput . valueType === 'u64' ) {
548+ const value = amountInput . value
549+ if ( typeof value === 'string' ) {
550+ transferAmount = value
551+ }
552+ }
553+
554+ // For token transfers, coin source is an object input
555+ if ( typeof coinSource === 'object' && 'Input' in coinSource ) {
556+ const coinInput = inputs [ coinSource . Input ]
557+ if ( coinInput ?. type === 'object' ) {
558+ const objectId = coinInput . objectId
559+ if ( typeof objectId === 'string' ) {
560+ coinObjectId = objectId
561+ }
562+ }
563+ }
564+ }
565+
566+ if ( 'TransferObjects' in command ) {
567+ const [ _objects , recipientArg ] = command . TransferObjects
568+ if ( ! recipientArg || typeof recipientArg !== 'object' || ! ( 'Input' in recipientArg ) )
569+ continue
570+
571+ const recipientInput = inputs [ recipientArg . Input ]
572+ if ( recipientInput ?. type === 'pure' && recipientInput . valueType === 'address' ) {
573+ const value = recipientInput . value
574+ if ( typeof value === 'string' ) {
575+ recipient = value
576+ }
577+ }
578+ }
579+ }
580+
581+ // Extract coin type from objectChanges if we have a coin object ID
582+ const coinType = ( ( ) => {
583+ if ( ! coinObjectId ) return undefined
584+
585+ const objectChange = tx . objectChanges ?. find (
586+ change => 'objectId' in change && change . objectId === coinObjectId ,
587+ )
588+
589+ if ( ! objectChange || ! ( 'objectType' in objectChange ) ) return undefined
590+
591+ const match = objectChange . objectType . match ( / 0 x 2 : : c o i n : : C o i n < ( .+ ) > / )
592+ const extractedCoinType = match ?. [ 1 ]
593+
594+ return extractedCoinType ? this . normalizeCoinType ( extractedCoinType ) : undefined
595+ } ) ( )
596+
597+ return { transferAmount, recipient, coinType }
598+ }
599+
524600 async parseTx ( txHashOrTx : unknown , pubkey : string ) : Promise < Transaction > {
525601 try {
526- // Fetch full transaction data if only txHash was provided
527602 const tx =
528603 typeof txHashOrTx === 'string'
529604 ? await this . client . getTransactionBlock ( {
@@ -532,75 +607,168 @@ export class ChainAdapter implements IChainAdapter<KnownChainIds.SuiMainnet> {
532607 showInput : true ,
533608 showEffects : true ,
534609 showBalanceChanges : true ,
610+ showObjectChanges : true ,
535611 } ,
536612 } )
537613 : ( txHashOrTx as SuiTransactionBlockResponse )
538614
539615 const sender = tx . transaction ?. data . sender ?? ''
540-
541616 const txid = tx . digest
542617 const blockHeight = Number ( tx . checkpoint ?? 0 )
543618 const blockTime = tx . timestampMs ? Math . floor ( Number ( tx . timestampMs ) / 1000 ) : 0
544619
545620 const latestCheckpoint = await this . client . getLatestCheckpointSequenceNumber ( )
546621 const confirmations = tx . checkpoint ? Number ( latestCheckpoint ) - Number ( tx . checkpoint ) + 1 : 0
547622
548- const status =
549- tx . effects ?. status . status === 'success'
550- ? TxStatus . Confirmed
551- : tx . effects ?. status . status === 'failure'
552- ? TxStatus . Failed
553- : TxStatus . Unknown
623+ const status = ( ( ) => {
624+ const txStatus = tx . effects ?. status . status
625+ if ( txStatus === 'success' ) return TxStatus . Confirmed
626+ if ( txStatus === 'failure' ) return TxStatus . Failed
627+ return TxStatus . Unknown
628+ } ) ( )
554629
555630 const gasUsed = tx . effects ?. gasUsed
556- const fee = gasUsed
557- ? {
631+ const fee = ! gasUsed
632+ ? undefined
633+ : {
558634 assetId : this . assetId ,
559635 value : (
560636 BigInt ( gasUsed . computationCost ) +
561637 BigInt ( gasUsed . storageCost ) -
562638 BigInt ( gasUsed . storageRebate )
563639 ) . toString ( ) ,
564640 }
565- : undefined
641+
642+ const {
643+ transferAmount : ptbTransferAmount ,
644+ recipient : ptbRecipient ,
645+ coinType : ptbCoinType ,
646+ } = this . parseProgrammableTransactionBlock ( tx )
566647
567648 const balanceChanges = tx . balanceChanges ?? [ ]
568649
569- const transfers = balanceChanges . map ( change => {
570- let ownerAddress : string | null = null
571- if ( typeof change . owner === 'object' && 'AddressOwner' in change . owner ) {
572- ownerAddress = change . owner . AddressOwner
573- }
650+ // Filter out balance changes that only represent gas fees
651+ const actualTransferChanges = balanceChanges . filter ( change => {
652+ if ( ! fee || change . coinType !== '0x2::sui::SUI' ) return true
653+
654+ const changeAmount = BigInt ( change . amount )
655+ const absoluteChange = changeAmount < 0n ? - changeAmount : changeAmount
656+ const feeAmount = BigInt ( fee . value )
657+
658+ return absoluteChange !== feeAmount
659+ } )
660+
661+ const transfersFromBalanceChanges = actualTransferChanges . map ( change => {
662+ const ownerAddress = ( ( ) => {
663+ if ( typeof change . owner === 'object' && 'AddressOwner' in change . owner ) {
664+ return change . owner . AddressOwner
665+ }
666+ return null
667+ } ) ( )
574668
575669 const assetId =
576670 change . coinType === '0x2::sui::SUI'
577671 ? this . assetId
578672 : toAssetId ( {
579673 chainId : this . chainId ,
580674 assetNamespace : ASSET_NAMESPACE . suiCoin ,
581- assetReference : change . coinType ,
675+ assetReference : this . normalizeCoinType ( change . coinType ) ,
582676 } )
583677
584678 const amount = BigInt ( change . amount )
585679 const isReceive = amount > 0n
586680 const isSend = amount < 0n
587681
588- const transferType =
589- ownerAddress === pubkey
590- ? isReceive
591- ? TransferType . Receive
592- : TransferType . Send
593- : TransferType . Contract
594-
595- return {
596- assetId,
597- from : isSend ? [ sender ] : ownerAddress ? [ ownerAddress ] : [ sender ] ,
598- to : isReceive ? [ ownerAddress ?? sender ] : [ sender ] ,
599- type : transferType ,
600- value : amount < 0n ? ( - amount ) . toString ( ) : amount . toString ( ) ,
601- }
682+ const transferType = ( ( ) => {
683+ if ( ownerAddress !== pubkey ) return TransferType . Contract
684+ return isReceive ? TransferType . Receive : TransferType . Send
685+ } ) ( )
686+
687+ // For Send transfers of native SUI, use PTB amount to exclude gas
688+ const shouldUsePtbAmount =
689+ isSend && ptbTransferAmount && change . coinType === '0x2::sui::SUI'
690+ const transferValue = shouldUsePtbAmount
691+ ? ptbTransferAmount
692+ : ( amount < 0n ? - amount : amount ) . toString ( )
693+
694+ // ownerAddress is who owns the balance after the transaction
695+ // For Send: from = sender, to = recipient (from PTB if available, else ownerAddress)
696+ // For Receive: from = sender, to = ownerAddress (recipient)
697+ const from = [ sender ]
698+ const to = ( ( ) => {
699+ if ( isReceive ) return [ ownerAddress ?? sender ]
700+ return ptbRecipient ? [ ptbRecipient ] : [ sender ]
701+ } ) ( )
702+
703+ return { assetId, from, to, type : transferType , value : transferValue }
602704 } )
603705
706+ // For self-sends where balance changes were filtered out, use PTB data
707+ const transfersFromPtb = ( ( ) => {
708+ if ( actualTransferChanges . length > 0 ) return [ ]
709+ if ( ! ptbTransferAmount || ! ptbRecipient ) return [ ]
710+
711+ const isSelfSend = sender === ptbRecipient
712+ const isSender = sender === pubkey
713+ const isRecipient = ptbRecipient === pubkey
714+
715+ // Determine the correct assetId (native SUI or token)
716+ const assetId = ! ptbCoinType
717+ ? this . assetId
718+ : toAssetId ( {
719+ chainId : this . chainId ,
720+ assetNamespace : ASSET_NAMESPACE . suiCoin ,
721+ assetReference : ptbCoinType ,
722+ } )
723+
724+ if ( isSelfSend && isSender ) {
725+ return [
726+ {
727+ assetId,
728+ from : [ sender ] ,
729+ to : [ ptbRecipient ] ,
730+ type : TransferType . Send ,
731+ value : ptbTransferAmount ,
732+ } ,
733+ {
734+ assetId,
735+ from : [ sender ] ,
736+ to : [ ptbRecipient ] ,
737+ type : TransferType . Receive ,
738+ value : ptbTransferAmount ,
739+ } ,
740+ ]
741+ }
742+
743+ if ( isSender ) {
744+ return [
745+ {
746+ assetId,
747+ from : [ sender ] ,
748+ to : [ ptbRecipient ] ,
749+ type : TransferType . Send ,
750+ value : ptbTransferAmount ,
751+ } ,
752+ ]
753+ }
754+
755+ if ( isRecipient ) {
756+ return [
757+ {
758+ assetId,
759+ from : [ sender ] ,
760+ to : [ ptbRecipient ] ,
761+ type : TransferType . Receive ,
762+ value : ptbTransferAmount ,
763+ } ,
764+ ]
765+ }
766+
767+ return [ ]
768+ } ) ( )
769+
770+ const transfers = [ ...transfersFromBalanceChanges , ...transfersFromPtb ]
771+
604772 return {
605773 txid,
606774 blockHeight,
0 commit comments