@@ -297,28 +297,94 @@ export class Xrp extends BaseCoin {
297297 mptIssuanceId : transaction . MPTokenIssuanceID ,
298298 ...( transaction . MPTHolder !== undefined && { mptHolder : transaction . MPTHolder } ) ,
299299 } ;
300+ } else if ( transaction . TransactionType === 'AccountDelete' ) {
301+ // AccountDelete sweeps the full account balance (minus fee) to Destination; the exact
302+ // amount is unknown at build time, so we record '0' as a placeholder (matches the
303+ // Transaction-class explainer in lib/transaction.ts). Without this branch the method
304+ // previously fell through to the Payment shape and returned undefined outputAmount/amount,
305+ // since AccountDelete carries no Amount field.
306+ const address =
307+ transaction . Destination + ( transaction . DestinationTag >= 0 ? '?dt=' + transaction . DestinationTag : '' ) ;
308+ return {
309+ displayOrder : [ 'id' , 'outputAmount' , 'changeAmount' , 'outputs' , 'changeOutputs' , 'fee' ] ,
310+ id : id ,
311+ changeOutputs : [ ] ,
312+ outputAmount : '0' ,
313+ changeAmount : 0 ,
314+ outputs : [
315+ {
316+ address,
317+ amount : '0' ,
318+ } ,
319+ ] ,
320+ fee : {
321+ fee : transaction . Fee ,
322+ feeRate : undefined ,
323+ size : txHex . length / 2 ,
324+ } ,
325+ } ;
326+ } else if ( transaction . TransactionType === 'SignerListSet' ) {
327+ return {
328+ displayOrder : [ 'id' , 'outputAmount' , 'changeAmount' , 'outputs' , 'changeOutputs' , 'fee' , 'signerListSet' ] ,
329+ id : id ,
330+ changeOutputs : [ ] ,
331+ outputAmount : 0 ,
332+ changeAmount : 0 ,
333+ outputs : [ ] ,
334+ fee : {
335+ fee : transaction . Fee ,
336+ feeRate : undefined ,
337+ size : txHex . length / 2 ,
338+ } ,
339+ signerListSet : {
340+ signerQuorum : transaction . SignerQuorum ,
341+ signerEntries : transaction . SignerEntries ,
342+ } ,
343+ } ;
344+ } else if ( transaction . TransactionType === 'Payment' ) {
345+ const address =
346+ transaction . Destination + ( transaction . DestinationTag >= 0 ? '?dt=' + transaction . DestinationTag : '' ) ;
347+ // When tfPartialPayment is set, tx.Amount is the *requested* amount, not the delivered
348+ // one. Prefer meta.delivered_amount when metadata was supplied; otherwise keep Amount
349+ // and surface partialPayment: true so consumers know the value is not the settlement.
350+ const partialPayment = utils . isPartialPayment ( transaction . Flags as number ) ;
351+ const deliveredValue = partialPayment ? utils . getDeliveredAmountValue ( params . meta ) : undefined ;
352+ const outputAmount = deliveredValue !== undefined ? deliveredValue : transaction . Amount ;
353+ return {
354+ displayOrder : [
355+ 'id' ,
356+ 'outputAmount' ,
357+ 'changeAmount' ,
358+ 'outputs' ,
359+ 'changeOutputs' ,
360+ 'fee' ,
361+ ...( partialPayment ? [ 'partialPayment' ] : [ ] ) ,
362+ ] ,
363+ id : id ,
364+ changeOutputs : [ ] ,
365+ outputAmount : outputAmount ,
366+ changeAmount : 0 ,
367+ outputs : [
368+ {
369+ address,
370+ amount : outputAmount ,
371+ } ,
372+ ] ,
373+ fee : {
374+ fee : transaction . Fee ,
375+ feeRate : undefined ,
376+ size : txHex . length / 2 ,
377+ } ,
378+ ...( partialPayment ? { partialPayment : true } : { } ) ,
379+ } ;
300380 }
301381
302- const address =
303- transaction . Destination + ( transaction . DestinationTag >= 0 ? '?dt=' + transaction . DestinationTag : '' ) ;
304- return {
305- displayOrder : [ 'id' , 'outputAmount' , 'changeAmount' , 'outputs' , 'changeOutputs' , 'fee' ] ,
306- id : id ,
307- changeOutputs : [ ] ,
308- outputAmount : transaction . Amount ,
309- changeAmount : 0 ,
310- outputs : [
311- {
312- address,
313- amount : transaction . Amount ,
314- } ,
315- ] ,
316- fee : {
317- fee : transaction . Fee ,
318- feeRate : undefined ,
319- size : txHex . length / 2 ,
320- } ,
321- } ;
382+ // No silent fallthrough: every other TransactionType (AMM*, Offer*, Escrow*, NFToken*,
383+ // Check*, pseudo-tx, etc.) is unsupported by this explainer. Throwing here mirrors the
384+ // safe switch in lib/transaction.ts:196-211 and prevents callers (verifyTransaction,
385+ // recover) from receiving a Payment-shaped object with undefined fields for types that
386+ // have no Amount/Destination.
387+ throw new Error ( `Unsupported XRP transaction type: ${ transaction . TransactionType } ` ) ;
322388 }
323389
324390 getTransactionTypeRawTxHex ( txHex : string ) : XrpTransactionType | undefined {
@@ -474,20 +540,41 @@ export class Xrp extends BaseCoin {
474540 const output = [ ...explanation . outputs , ...explanation . changeOutputs ] [ 0 ] ;
475541 const expectedOutput = txParams . recipients && txParams . recipients [ 0 ] ;
476542
543+ // A Payment carrying the tfPartialPayment flag may deliver less than its Amount field.
544+ // BitGo never builds partial payments, so such a prebuild cannot match a send intent —
545+ // reject it rather than risk verifying a transaction that under-delivers.
546+ if ( 'partialPayment' in explanation && explanation . partialPayment === true ) {
547+ throw new Error ( 'Partial payment (tfPartialPayment) is not permitted for verified send transactions' ) ;
548+ }
549+
550+ // XRP Payment amounts arrive in two shapes:
551+ // - string (XRP drops, base units) — recipient amount is also base units, compare directly.
552+ // - object (IssuedCurrencyAmount / MPTAmount) — `value` is in display units, while the
553+ // recipient amount from txParams is in base units. Convert the display value via the
554+ // coin's base factor (getBaseFactor() returns 10^decimalPlaces and is overridden by
555+ // XrpToken to use the *token's* decimals, not the base coin's). Previously the object
556+ // case skipped the comparison entirely, leaving every cross-currency / token transfer
557+ // unverified.
558+ const toBaseUnits = ( amount : any ) : string => {
559+ if ( amount === undefined || amount === null ) {
560+ return '' ;
561+ }
562+ if ( typeof amount === 'object' && 'value' in amount ) {
563+ return new BigNumber ( amount . value ) . times ( this . getBaseFactor ( ) ) . toFixed ( ) ;
564+ }
565+ return new BigNumber ( amount ) . toFixed ( ) ;
566+ } ;
567+
477568 const comparator = ( recipient1 , recipient2 ) => {
478569 if ( utils . getAddressDetails ( recipient1 . address ) . address !== utils . getAddressDetails ( recipient2 . address ) . address ) {
479570 return false ;
480571 }
481- const amount1 = new BigNumber ( recipient1 . amount ) ;
482- const amount2 = new BigNumber ( recipient2 . amount ) ;
572+ const amount1 = new BigNumber ( toBaseUnits ( recipient1 . amount ) ) ;
573+ const amount2 = new BigNumber ( toBaseUnits ( recipient2 . amount ) ) ;
483574 return amount1 . toFixed ( ) === amount2 . toFixed ( ) ;
484575 } ;
485576
486- if (
487- ( txParams . type === undefined || txParams . type === 'payment' ) &&
488- typeof output . amount !== 'object' &&
489- ! comparator ( output , expectedOutput )
490- ) {
577+ if ( ( txParams . type === undefined || txParams . type === 'payment' ) && ! comparator ( output , expectedOutput ) ) {
491578 throw new Error ( 'transaction prebuild does not match expected output' ) ;
492579 }
493580
0 commit comments