11import BigNumber from 'bignumber.js' ;
2+ import QuickCrypto from 'react-native-quick-crypto' ;
23import utxolib from '@runonflux/utxo-lib' ;
34import { decodeFunctionData , erc20Abi } from 'viem' ;
45import * as abi from '@runonflux/aa-schnorr-multisig-sdk/dist/abi' ;
@@ -267,7 +268,21 @@ export async function decodeTransactionForApproval(
267268 return decodedTx ;
268269 }
269270 if ( blockchains [ chain ] . chainType === 'sol' ) {
270- const decodedTx = await decodeSOLTransactionForApproval ( rawTx , chain ) ;
271+ // Single-roundtrip flow: payload is JSON wrapping the unsigned tx
272+ // plus init metadata. Older callers pass a bare base64 tx string.
273+ let serializedForDecode = rawTx ;
274+ try {
275+ const parsed = JSON . parse ( rawTx ) as { unsignedTxBase64 ?: string } ;
276+ if ( parsed && typeof parsed . unsignedTxBase64 === 'string' ) {
277+ serializedForDecode = parsed . unsignedTxBase64 ;
278+ }
279+ } catch {
280+ // Not JSON — fall through to decode rawTx directly.
281+ }
282+ const decodedTx = await decodeSOLTransactionForApproval (
283+ serializedForDecode ,
284+ chain ,
285+ ) ;
271286 return decodedTx ;
272287 }
273288 const libID = getLibId ( chain ) ;
@@ -525,34 +540,17 @@ export async function decodeEVMTransactionForApproval(
525540 }
526541}
527542
528- /**
529- * Decode an SSP Solana Multisig transaction for the key device's approval
530- * screen. The wallet hands the key a base64-encoded outer Solana Transaction
531- * containing a `create_transaction` ix; that ix's data carries the proposal
532- * `TransactionMessage` inline (borsh-encoded) describing the user's intended
533- * transfer.
534- *
535- * Two transfers are typically present in the proposal:
536- * 1. user transfer: vault → recipient (this is what the user is sending)
537- * 2. paymaster reimbursement: vault → feePayer (this is the fee — the
538- * paymaster signs the outer tx's feePayer slot and gets reimbursed
539- * in-tx so its balance stays flat)
540- *
541- * We identify the fee transfer by matching destination = outer tx feePayer.
542- * Anything else SystemProgram.transfer'd from the vault is the user's send.
543- *
544- * For SPL token transfers (TOKEN_PROGRAM ix), the recipient shown is the
545- * destination ATA — Solana explorers resolve ATAs to their owner so the
546- * user can verify on-chain that the ATA belongs to who they intended.
547- */
543+ // Decode an SSP Solana proposal for the approval screen. Splits the
544+ // proposal's transfers into the user's send (vault → recipient) and the
545+ // fee (vault → outer feePayer = paymaster). SPL transfers show the
546+ // destination ATA as the receiver; explorers resolve ATAs to owners.
548547async function decodeSOLTransactionForApproval (
549548 rawTxBase64 : string ,
550549 chain : keyof cryptos ,
551550) : Promise < tokenInfo > {
552551 try {
553552 const { Transaction, PublicKey, SystemProgram } =
554553 await import ( '@solana/web3.js' ) ;
555- const { createHash } = require ( 'crypto' ) as typeof import ( 'crypto' ) ;
556554 const decimals = blockchains [ chain ] . decimals ;
557555 const tokenSymbol = blockchains [ chain ] . symbol ;
558556
@@ -562,25 +560,35 @@ async function decodeSOLTransactionForApproval(
562560 }
563561 const paymasterPubkey = tx . feePayer ;
564562
565- // Find the create_transaction ix (matches Anchor discriminator
566- // sha256("global:create_transaction")[:8]).
567- const createIxDiscriminator : Buffer = createHash ( 'sha256' )
568- . update ( 'global:create_transaction' )
569- . digest ( )
570- . subarray ( 0 , 8 ) ;
571- const createIx = tx . instructions . find (
572- ( ix ) =>
573- ix . data . length >= 8 &&
574- ix . data . subarray ( 0 , 8 ) . equals ( createIxDiscriminator ) ,
575- ) ;
563+ // Compute the create_transaction discriminator (first 8 bytes of
564+ // sha256("global:create_transaction")) using react-native-quick-crypto
565+ // — Node's `crypto.createHash` isn't available in RN, and utxolib's
566+ // sha256 wasn't reachable from this RN bundle either.
567+ const createIxDiscriminator : Buffer = Buffer . from (
568+ QuickCrypto . createHash ( 'sha256' )
569+ . update ( 'global:create_transaction' )
570+ . digest ( ) ,
571+ ) . subarray ( 0 , 8 ) ;
572+ console . log ( createIxDiscriminator ) ;
573+ console . log ( tx . instructions ) ;
574+ // `ix.data` from `Transaction.from(...)` is typed as Buffer but the
575+ // RN runtime can deliver a plain JS array of numbers (no `.subarray`,
576+ // no `.readUInt32LE`, no `.equals`). Compare byte-by-byte using only
577+ // index access, then `Buffer.from(...)` the matched ix's data so all
578+ // the parser helpers below have a real Buffer to work with.
579+ const createIx = tx . instructions . find ( ( ix ) => {
580+ if ( ! ix . data || ix . data . length < 8 ) return false ;
581+ for ( let i = 0 ; i < 8 ; i ++ ) {
582+ if ( ( ix . data as ArrayLike < number > ) [ i ] !== createIxDiscriminator [ i ] )
583+ return false ;
584+ }
585+ return true ;
586+ } ) ;
576587 if ( ! createIx ) {
577588 throw new Error ( 'Solana tx does not contain a create_transaction ix' ) ;
578589 }
579-
580- // Borsh-decode the proposal message inline from ix data:
581- // 8 bytes discriminator + 1 byte vault_index + TransactionMessage
582- const data = createIx . data ;
583- let off = 8 + 1 + 3 ; // skip disc + vault_index + 3-byte header
590+ const data = Buffer . from ( createIx . data as ArrayLike < number > ) ;
591+ let off = 8 + 1 + 3 ; // skip discriminator + vault_index + 3-byte header
584592 const accountKeysLen = data . readUInt32LE ( off ) ;
585593 off += 4 ;
586594 const accountKeys : InstanceType < typeof PublicKey > [ ] = [ ] ;
@@ -605,71 +613,51 @@ async function decodeSOLTransactionForApproval(
605613 off += 1 ;
606614 const aiLen = data . readUInt32LE ( off ) ;
607615 off += 4 ;
608- const accountIdxs = data . subarray ( off , off + aiLen ) ;
616+ const accountIdxs = Buffer . from ( data . subarray ( off , off + aiLen ) ) ;
609617 off += aiLen ;
610618 const ixDataLen = data . readUInt32LE ( off ) ;
611619 off += 4 ;
612- const ixData = data . subarray ( off , off + ixDataLen ) ;
620+ const ixData = Buffer . from ( data . subarray ( off , off + ixDataLen ) ) ;
613621 off += ixDataLen ;
614622
615623 const ixProgram = accountKeys [ programIdIdx ] ;
616624 if ( ! ixProgram ) continue ;
617625
618- // SystemProgram.transfer (native SOL).
619626 if ( ixProgram . equals ( SystemProgram . programId ) ) {
620627 if ( ixData . length < 12 || ixData . readUInt32LE ( 0 ) !== 2 ) continue ;
621628 if ( accountIdxs . length < 2 ) continue ;
622629 const fromIdx = accountIdxs [ 0 ] ;
623- const toIdx = accountIdxs [ 1 ] ;
624- const toPubkey = accountKeys [ toIdx ] ;
630+ const toPubkey = accountKeys [ accountIdxs [ 1 ] ] ;
625631 if ( ! toPubkey ) continue ;
626632 const amountLamports = ixData . readBigUInt64LE ( 4 ) . toString ( ) ;
627633 if ( toPubkey . equals ( paymasterPubkey ) ) {
628- // Reimbursement to paymaster — this is the fee.
629634 feeBase = new BigNumber ( feeBase ) . plus ( amountLamports ) . toFixed ( ) ;
630635 } else {
631- // User's actual transfer.
632636 vaultPubkey = accountKeys [ fromIdx ] ;
633637 userReceiver = toPubkey . toBase58 ( ) ;
634638 userAmountBase = amountLamports ;
635639 }
636640 continue ;
637641 }
638642
639- // SPL token transfer (TOKEN_PROGRAM):
640- // accountIndexes: [source_ata, dest_ata, authority]
641- // data tag 3 = Transfer (legacy), tag 12 = TransferChecked
643+ // SPL Transfer (tag 3) or TransferChecked (tag 12); accountIndexes
644+ // are [source_ata, dest_ata, authority]. Mint isn't in the proposal,
645+ // so symbol stays generic — explorer resolves ATA → mint → symbol.
642646 if ( ixProgram . toBase58 ( ) === TOKEN_PROGRAM ) {
643647 const tag = ixData . readUInt8 ( 0 ) ;
644- let tokenAmount : string | null = null ;
645- if ( tag === 3 && ixData . length >= 9 ) {
646- tokenAmount = ixData . readBigUInt64LE ( 1 ) . toString ( ) ;
647- } else if ( tag === 12 && ixData . length >= 9 ) {
648- tokenAmount = ixData . readBigUInt64LE ( 1 ) . toString ( ) ;
649- } else {
650- continue ;
651- }
648+ if ( ( tag !== 3 && tag !== 12 ) || ixData . length < 9 ) continue ;
652649 if ( accountIdxs . length < 2 ) continue ;
653- const destAtaIdx = accountIdxs [ 1 ] ;
654- const destAta = accountKeys [ destAtaIdx ] ;
650+ const destAta = accountKeys [ accountIdxs [ 1 ] ] ;
655651 if ( ! destAta ) continue ;
656652 userReceiver = destAta . toBase58 ( ) ;
657- userAmountBase = tokenAmount ;
658-
659- // Look up token metadata from chain spec for symbol/decimals/contract.
660- // The proposal doesn't carry the mint address directly in the ix
661- // accountIndexes for legacy Transfer (only source ATA, dest ATA,
662- // authority). Inferring the mint requires either an RPC call or
663- // off-chain knowledge from chain spec. For now: leave userTokenSymbol
664- // generic; explorer link resolves ATA → mint → symbol.
653+ userAmountBase = ixData . readBigUInt64LE ( 1 ) . toString ( ) ;
665654 userTokenSymbol = '(token)' ;
666655 continue ;
667656 }
668657 }
669658
670- // Convert base units to display units. For native SOL the chain decimals
671- // apply; for SPL we'd need the mint's decimals (not retrievable from the
672- // proposal alone — show raw base units in that case).
659+ // SPL amounts stay in base units since the proposal doesn't carry the
660+ // mint's decimals; native SOL converts via the chain's decimals.
673661 const isNative = userTokenSymbol === tokenSymbol ;
674662 const displayAmount = isNative
675663 ? new BigNumber ( userAmountBase ) . dividedBy ( 10 ** decimals ) . toFixed ( )
0 commit comments