Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 22 additions & 4 deletions modules/sdk-coin-polyx/src/lib/transactionBuilderFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -291,18 +291,36 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
}

const methodName = decodedTxn.method?.name;
// Build a merged material that keeps the v8 metadata/registry but overrides specVersion and
// txVersion with the values decoded from the actual transaction bytes. The static v8 material
// has a placeholder specVersion (8000000); the live chain may have a higher value (e.g.
// 8000020). `createBaseTxInfo()` reads specVersion and txVersion from `this._material`, so
// using the static placeholder causes the rebuilt signable payload to have wrong additional
// bytes — verifySignature then fails even though the TSS signature is valid. Taking the
// decoded values from the transaction itself (rather than forwarding `this._material`, which
// may be v7) ensures the rebuild always matches the bytes the HSM signed. See SI-1034 for the
// analogous fix in getV8BatchStakingBuilder.
// specVersion is present in signing payloads (unsigned) but NOT in signed extrinsics
// (decodeSignedTx doesn't return it). Fall back to the static v8 value for signed txes —
// the signing payload path (used by getSignedTx/verifySignature) always decodes specVersion.
const v8MaterialWithDecodedSpec = {
...v8Material,
specVersion: decodedTxn.specVersion ?? v8Material.specVersion,
txVersion: decodedTxn.transactionVersion ?? v8Material.txVersion,
} as Interface.Material;

if (methodName === Interface.MethodNames.TransferWithMemo) {
const args = decodedTxn.method.args as Interface.TransferWithMemoArgs;
if (utils.isNewMemoEncoding(args.memo)) {
return this.getV8HexTransferBuilder();
return this.getV8HexTransferBuilder().material(v8MaterialWithDecodedSpec);
}
return this.getV8TransferBuilder();
return this.getV8TransferBuilder().material(v8MaterialWithDecodedSpec);
} else if (methodName === MethodNames.AddAndAffirmWithMediators) {
const args = decodedTxn.method.args as AddAndAffirmWithMediatorsArgs | V8AddAndAffirmWithMediatorsArgs;
if (utils.isNewMemoEncoding(args.instructionMemo)) {
return this.getV8HexTokenTransferBuilder();
return this.getV8HexTokenTransferBuilder().material(v8MaterialWithDecodedSpec);
}
return this.getV8TokenTransferBuilder();
return this.getV8TokenTransferBuilder().material(v8MaterialWithDecodedSpec);
} else if (methodName === MethodNames.RegisterDidWithCDD) {
return this.getV8RegisterDidWithCDDBuilder();
} else if (methodName === MethodNames.RegisterDid) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { coins } from '@bitgo/statics';
import { coins, NetworkType } from '@bitgo/statics';
import should from 'should';
import {
TransactionBuilderFactory,
TransferBuilder,
V8TransferBuilder,
V8HexTransferBuilder,
V8TokenTransferBuilder,
utils,
} from '../../../src/lib';
import { Interface } from '../../../src';
import { rawTx, accounts, mockTssSignature } from '../../resources';
Expand Down Expand Up @@ -203,5 +204,47 @@ describe('Tao Transaction Builder Factory', function () {
should.equal(json.toDID, TO_DID);
should.equal(rebuilt.toBroadcastFormat(), signedHex, 'signed round-trip hex must match original');
});

it('forwards factory live material specVersion to V8TokenTransferBuilder (SI-XXXX regression)', async function () {
// Regression for the bug where getV8TokenTransferBuilder() was called without
// .material(this._material), causing the builder to use the static placeholder
// specVersion (8000000) instead of the live chain specVersion. This produced a
// rebuilt signablePayload with wrong additional bytes, failing verifySignature.
const liveSpecVersion = 8000020;
const liveMaterial = {
...utils.getV8Material(NetworkType.TESTNET),
specVersion: liveSpecVersion,
} as Interface.Material;

// Build an unsigned tx with the live material — its signable payload encodes liveSpecVersion.
const originalTx = await new V8TokenTransferBuilder(buildTestConfig())
.material(liveMaterial)
.assetId(ASSET_ID)
.amount('1000000')
.fromDID(FROM_DID)
.toDID(TO_DID)
.memo('0')
.sender({ address: sender.address })
.validity(VALIDITY)
.referenceBlock(REF_BLOCK)
.sequenceId({ name: 'Nonce', keyword: 'nonce', value: 1 })
.fee({ amount: 0, type: 'tip' })
.build();
const originalSignable = originalTx.signablePayload.toString('hex');
const signingPayloadHex = originalTx.toBroadcastFormat();

// Factory with the same live material must forward it to the created V8TokenTransferBuilder
// so the rebuild uses liveSpecVersion, not the static 8000000 placeholder.
const factoryInst = new TransactionBuilderFactory(buildTestConfig()).material(liveMaterial);
const rebuiltBuilder = factoryInst.from(signingPayloadHex);
rebuiltBuilder.sender({ address: sender.address }).validity(VALIDITY).referenceBlock(REF_BLOCK);
const rebuiltTx = await rebuiltBuilder.build();

should.equal(
rebuiltTx.signablePayload.toString('hex'),
originalSignable,
'signablePayload must match: factory must forward live material to V8TokenTransferBuilder'
);
});
});
});
Loading