From b27f2eb738bf888711cfbd694e29ac50906ed4a2 Mon Sep 17 00:00:00 2001 From: Support Bot Date: Thu, 16 Jul 2026 14:41:44 +0000 Subject: [PATCH] fix(sdk-coin-trx): allow non-TSS TRC20 consolidation without recipients The non-TSS verifyTransaction path in TrxToken threw 'missing required property recipients' when consolidating USDT:TRX balances. Consolidation transactions are server-determined and carry no txParams.recipients, so the missing-recipients check blocked all non-TSS TRC20 consolidations. Mirror the fix already applied to the TSS path (COINS-392): when recipients is absent or empty, the transaction is treated as a server-determined transfer and structural validation (builder round- trip) is sufficient. Also align the non-TSS txInfo cast to use optional chaining (same defensive style as the TSS path) and add tests covering both the empty-array and fully-absent recipients scenarios. Ticket: SPT-100 Session-Id: 5be4c6ff-9bea-40ab-860a-1fe74a3ab82a Task-Id: 992da874-8f85-4d61-9a44-e1b35c09ea87 --- modules/sdk-coin-trx/src/trxToken.ts | 7 +++--- modules/sdk-coin-trx/test/unit/trxToken.ts | 28 ++++++++++++++++++---- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/modules/sdk-coin-trx/src/trxToken.ts b/modules/sdk-coin-trx/src/trxToken.ts index 75bfa659cc..e0c1cd537f 100644 --- a/modules/sdk-coin-trx/src/trxToken.ts +++ b/modules/sdk-coin-trx/src/trxToken.ts @@ -162,9 +162,10 @@ export class TrxToken extends Trx { const txBuilder = getBuilder(this.getChain()).from(rawTx); const tx = await txBuilder.build(); - const recipients = txParams.recipients || (txPrebuild.txInfo as TronTxInfo).recipients; - if (!recipients) { - throw new Error('missing required property recipients'); + const recipients = txParams.recipients || (txPrebuild.txInfo as TronTxInfo | undefined)?.recipients; + if (!recipients || recipients.length === 0) { + // No recipients — server-determined transfer (e.g. consolidation); structural check above is sufficient. + return true; } if (recipients[0].address === tx.outputs[0].address && recipients[0].amount === tx.outputs[0].value) { diff --git a/modules/sdk-coin-trx/test/unit/trxToken.ts b/modules/sdk-coin-trx/test/unit/trxToken.ts index 7238289ef1..a706685091 100644 --- a/modules/sdk-coin-trx/test/unit/trxToken.ts +++ b/modules/sdk-coin-trx/test/unit/trxToken.ts @@ -127,19 +127,37 @@ describe('TrxToken verifyTransaction:', function () { }); describe('non-TSS wallet — builder-based validation (existing path)', () => { + // Full JSON tx format that the builder understands. + const NON_TSS_TX_HEX = + '{"raw_data":{"contractType":2,"contract":[{"parameter":{"value":{"data":"a9059cbb0000000000000000000000008483618ca85c35a9b923d98bebca718f5a1db2790000000000000000000000000000000000000000000000000000000005f5e100","owner_address":"41c51fbeea78910b15b1d3e8a9b62914ca94d1a4ac","contract_address":"4142a1e39aefa49290f2b3f9ed688d7cecf86cd6e0"},"type_url":"type.googleapis.com/protocol.TriggerSmartContract"},"type":"TriggerSmartContract"}],"expiration":1674581767432,"timestamp":1674578167432,"ref_block_bytes":"578b","ref_block_hash":"6113bb9ac351432b","fee_limit":15000000},"raw_data_hex":"0a02578b22086113bb9ac351432b4088eae7a6de305aae01081f12a9010a31747970652e676f6f676c65617069732e636f6d2f70726f746f636f6c2e54726967676572536d617274436f6e747261637412740a1541c51fbeea78910b15b1d3e8a9b62914ca94d1a4ac12154142a1e39aefa49290f2b3f9ed688d7cecf86cd6e02244a9059cbb0000000000000000000000008483618ca85c35a9b923d98bebca718f5a1db2790000000000000000000000000000000000000000000000000000000005f5e10070888d8ca5de309001c0c39307","txID":"fe21c49f4febd9089125e3a006943c145721d8fcb7ab84136f8c6663ff92f8ed","signature":["0775cde302689eb8293883c66a89b31e80d608bfc3ad3c283b64a490ea4cc712c55a2fd2e62c75843dd7e77d8c4cb52e0f371fbb29b332c259f8cb63c2e6195301"]}'; + it('should validate a correct non-TSS TRC20 transfer using txBuilder', async function () { - // The non-TSS path uses getBuilder().from(rawTx).build() and checks tx.outputs[0] - // This test uses the full JSON tx format that the builder understands. - const txHex = - '{"raw_data":{"contractType":2,"contract":[{"parameter":{"value":{"data":"a9059cbb0000000000000000000000008483618ca85c35a9b923d98bebca718f5a1db2790000000000000000000000000000000000000000000000000000000005f5e100","owner_address":"41c51fbeea78910b15b1d3e8a9b62914ca94d1a4ac","contract_address":"4142a1e39aefa49290f2b3f9ed688d7cecf86cd6e0"},"type_url":"type.googleapis.com/protocol.TriggerSmartContract"},"type":"TriggerSmartContract"}],"expiration":1674581767432,"timestamp":1674578167432,"ref_block_bytes":"578b","ref_block_hash":"6113bb9ac351432b","fee_limit":15000000},"raw_data_hex":"0a02578b22086113bb9ac351432b4088eae7a6de305aae01081f12a9010a31747970652e676f6f676c65617069732e636f6d2f70726f746f636f6c2e54726967676572536d617274436f6e747261637412740a1541c51fbeea78910b15b1d3e8a9b62914ca94d1a4ac12154142a1e39aefa49290f2b3f9ed688d7cecf86cd6e02244a9059cbb0000000000000000000000008483618ca85c35a9b923d98bebca718f5a1db2790000000000000000000000000000000000000000000000000000000005f5e10070888d8ca5de309001c0c39307","txID":"fe21c49f4febd9089125e3a006943c145721d8fcb7ab84136f8c6663ff92f8ed","signature":["0775cde302689eb8293883c66a89b31e80d608bfc3ad3c283b64a490ea4cc712c55a2fd2e62c75843dd7e77d8c4cb52e0f371fbb29b332c259f8cb63c2e6195301"]}'; const recipientBase58 = Utils.getBase58AddressFromHex(TRC20_RECIPIENT_HEX); const result = await tokenCoin.verifyTransaction({ - txPrebuild: { txHex }, + txPrebuild: { txHex: NON_TSS_TX_HEX }, txParams: { recipients: [{ address: recipientBase58, amount: TRC20_AMOUNT }] }, } as any); assert.strictEqual(result, true); }); + + it('should return true when recipients is empty array (consolidation path)', async function () { + const result = await tokenCoin.verifyTransaction({ + txPrebuild: { txHex: NON_TSS_TX_HEX }, + txParams: { recipients: [] }, + } as any); + + assert.strictEqual(result, true); + }); + + it('should return true when recipients is absent (consolidation path via txInfo)', async function () { + const result = await tokenCoin.verifyTransaction({ + txPrebuild: { txHex: NON_TSS_TX_HEX, txInfo: {} }, + txParams: {}, + } as any); + + assert.strictEqual(result, true); + }); }); });