From c139f7f4917b2224121718c25831a3307fc3dc73 Mon Sep 17 00:00:00 2001 From: Abhishek Agrawal Date: Tue, 7 Jul 2026 14:05:22 +0530 Subject: [PATCH] fix(sdk-coin-sui): keep token coin type on addr-balance rebuild TICKET: CSHLD-1146 --- .../src/lib/tokenTransferBuilder.ts | 28 ++++++++++++++++++- .../tokenTransferBuilder.ts | 26 +++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/modules/sdk-coin-sui/src/lib/tokenTransferBuilder.ts b/modules/sdk-coin-sui/src/lib/tokenTransferBuilder.ts index ee45697964..5c7b36d06a 100644 --- a/modules/sdk-coin-sui/src/lib/tokenTransferBuilder.ts +++ b/modules/sdk-coin-sui/src/lib/tokenTransferBuilder.ts @@ -5,13 +5,14 @@ import { SuiTransaction, SuiTransactionType, TokenTransferProgrammableTransactio import { Transaction } from './transaction'; import { TransactionBuilder } from './transactionBuilder'; import { TokenTransferTransaction } from './tokenTransferTransaction'; -import { SuiObjectRef } from './mystenlab/types'; +import { normalizeSuiAddress, SuiObjectRef } from './mystenlab/types'; import utils from './utils'; import { Inputs, TransactionBlock as ProgrammingTransactionBlockBuilder, TransactionArgument, } from './mystenlab/builder'; +import { TypeTagSerializer } from './mystenlab/txn-data-serializers/type-tag-serializer'; import BigNumber from 'bignumber.js'; export class TokenTransferBuilder extends TransactionBuilder { @@ -25,6 +26,15 @@ export class TokenTransferBuilder extends TransactionBuilder) { super(_coinConfig); this._transaction = new TokenTransferTransaction(_coinConfig); @@ -38,6 +48,9 @@ export class TokenTransferBuilder extends TransactionBuilder::module::name`). The serialized + // bytes are identical either way since parseFromStr re-normalizes the address on encode. + const [address, ...rest] = TypeTagSerializer.tagToString(withdrawalTypeTag).split('::'); + this._tokenCoinTypeOverride = [normalizeSuiAddress(address), ...rest].join('::'); + } } if (txData.inputObjects && txData.inputObjects.length > 0) { diff --git a/modules/sdk-coin-sui/test/unit/transactionBuilder/tokenTransferBuilder.ts b/modules/sdk-coin-sui/test/unit/transactionBuilder/tokenTransferBuilder.ts index d79e0702a4..0eca2828c7 100644 --- a/modules/sdk-coin-sui/test/unit/transactionBuilder/tokenTransferBuilder.ts +++ b/modules/sdk-coin-sui/test/unit/transactionBuilder/tokenTransferBuilder.ts @@ -180,6 +180,32 @@ describe('Sui Token Transfer Builder', () => { rebuiltTx.toBroadcastFormat().should.equal(rawTx); }); + it('should preserve the token coin type when rebuilt with the parent chain config', async function () { + // Reproduces the production broadcast path: the transaction is created with the token config + // (tsui:deep) but re-assembled for broadcast by a builder constructed with the parent CHAIN + // config (tsui, no packageId/module/symbol). Before the fix, buildSuiTransaction re-derived + // the coin type from the chain config and produced `undefined::undefined::undefined`, changing + // the tx bytes so the already-computed signature failed on-chain verification. + const txBuilder = factory.getTokenTransferBuilder(); + txBuilder.type(SuiTransactionType.TokenTransfer); + txBuilder.sender(testData.sender.address); + txBuilder.send([{ address: testData.recipients[0].address, amount: '1000' }]); + txBuilder.gasData(testData.gasData); + txBuilder.fundsInAddressBalance(FUNDS_IN_ADDRESS_BALANCE); + const rawTx = (await txBuilder.build()).toBroadcastFormat(); + // Rebuild using the CHAIN config, not the token config. + const chainFactory = getBuilderFactory('tsui'); + const rebuilder = chainFactory.from(rawTx); + rebuilder.addSignature({ pub: testData.sender.publicKey }, Buffer.from(testData.sender.signatureHex)); + const rebuiltTx = await rebuilder.build(); + // Bytes must be unchanged so the signature still verifies. + rebuiltTx.toBroadcastFormat().should.equal(rawTx); + const rebuiltProgrammableTx = (rebuiltTx as SuiTransaction).suiTransaction + .tx; + (rebuiltProgrammableTx.transactions[0] as any).target.should.equal('0x2::coin::redeem_funds'); + (rebuiltProgrammableTx.transactions[0] as any).typeArguments[0].should.equal(TOKEN_COIN_TYPE); + }); + it('should build a token transfer with coin objects + address balance', async function () { const numberOfInputObjects = 3; const inputObjects = testData.generateObjects(numberOfInputObjects);