From c22ca5c6336b26978c04744666d9839242b47dd8 Mon Sep 17 00:00:00 2001 From: Abhijit Madhusudan Date: Thu, 9 Jul 2026 17:36:47 +0530 Subject: [PATCH] fix(sdk-coin-polyx): route v8 batchAll(bond,nominate) parse to v8 builder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On the decode-success path, TransactionBuilderFactory.getBuilder() routed every batchAll([bond, nominate]) to the v7 BatchStakingBuilder. When the transaction is decoded against v8 material the bond call carries no `controller` (v8 changed bond(controller, value, payee) → bond(value, payee)), so v7 validateBondArgs crashed reading `args.controller.id`. Inspect the decoded bond args: route to V8BatchStakingBuilder when no controller is present, and keep v7 bond+nominate on BatchStakingBuilder. TICKET: SI-981 --- .../src/lib/transactionBuilderFactory.ts | 10 ++++++++++ .../transactionBuilder/batchStakingBuilder.ts | 19 +++++++++++++++++++ .../v8BatchStakingBuilder.ts | 12 ++++++++++++ 3 files changed, 41 insertions(+) diff --git a/modules/sdk-coin-polyx/src/lib/transactionBuilderFactory.ts b/modules/sdk-coin-polyx/src/lib/transactionBuilderFactory.ts index bfee03ef10..c09cd94fac 100644 --- a/modules/sdk-coin-polyx/src/lib/transactionBuilderFactory.ts +++ b/modules/sdk-coin-polyx/src/lib/transactionBuilderFactory.ts @@ -220,6 +220,16 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory { // Check for batch staking pattern: bond + nominate if (firstCallMethod === 'bond' && secondCallMethod === 'nominate') { + // v8 dropped the controller leg of `staking.bond` (bond(controller, value, payee) → + // bond(value, payee)). When the transaction was decoded against v8 material the bond call + // carries no `controller`, so route it to the v8 batch staking builder — the v7 + // BatchStakingBuilder would otherwise crash reading `args.controller.id`. This runs on the + // decode-success path because, unlike v8 transfers, v8 staking batchAll can decode without + // throwing, so tryGetV8Builder never fires. See SI-981. + const bondArgs = args.calls[0].args as { controller?: unknown }; + if (bondArgs.controller === undefined) { + return this.getV8BatchStakingBuilder(); + } return this.getBatchBuilder(); } // Check for batch unstaking pattern: chill + unbond diff --git a/modules/sdk-coin-polyx/test/unit/transactionBuilder/batchStakingBuilder.ts b/modules/sdk-coin-polyx/test/unit/transactionBuilder/batchStakingBuilder.ts index 5ebb1d6d45..dd41d43a72 100644 --- a/modules/sdk-coin-polyx/test/unit/transactionBuilder/batchStakingBuilder.ts +++ b/modules/sdk-coin-polyx/test/unit/transactionBuilder/batchStakingBuilder.ts @@ -299,6 +299,25 @@ describe('Polyx Batch Builder', function () { should.equal(newBuilder.getPayee(), 'Staked'); should.deepEqual(newBuilder.getValidators(), [validatorAddress]); }); + + it('factory.from routes a v7 bond+nominate transaction to BatchStakingBuilder (SI-981 regression)', async () => { + // A v7 bond call carries a `controller`, so routing must keep it on the v7 builder and not + // fall through to the v8 batch staking builder. + const originalBuilder = factory.getBatchBuilder(); + originalBuilder + .amount(testAmount) + .controller({ address: controllerAddress }) + .payee('Staked') + .validators([validatorAddress]) + .sender({ address: senderAddress }) + .validity({ firstValid: 3933, maxDuration: 64 }) + .referenceBlock('0x149799bc9602cb5cf201f3425fb8d253b2d4e61fc119dcab3249f307f594754d') + .sequenceId({ name: 'Nonce', keyword: 'nonce', value: 100 }); + const rawTxHex = (await originalBuilder.build()).toBroadcastFormat(); + + const builder = factory.from(rawTxHex); + should.ok(builder instanceof BatchBuilder); + }); }); describe('should create properly configured transactions', function () { diff --git a/modules/sdk-coin-polyx/test/unit/transactionBuilder/v8BatchStakingBuilder.ts b/modules/sdk-coin-polyx/test/unit/transactionBuilder/v8BatchStakingBuilder.ts index c1a66d61c7..8831f5c0e7 100644 --- a/modules/sdk-coin-polyx/test/unit/transactionBuilder/v8BatchStakingBuilder.ts +++ b/modules/sdk-coin-polyx/test/unit/transactionBuilder/v8BatchStakingBuilder.ts @@ -158,6 +158,18 @@ describe('V8BatchStakingBuilder', function () { const builder = factory.from(rawTxHex); should.ok(builder instanceof V8BatchStakingBuilder); }); + + it('factory.from routes to V8BatchStakingBuilder on the v8 decode-success path (SI-981)', async () => { + // Reproduces the staging incident: when the factory carries v8 material, `decode()` succeeds + // and yields a bond call with no `controller`. Routing must detect the missing controller and + // return the v8 builder rather than the v7 BatchStakingBuilder, which crashes reading + // `args.controller.id`. + const rawTxHex = (await buildSandboxBatch().build()).toBroadcastFormat(); + const v8Material = utils.getV8Material(coins.get('tpolyx').network.type); + const v8Factory = new TransactionBuilderFactory(coins.get('tpolyx')).material(v8Material); + const builder = v8Factory.from(rawTxHex); + should.ok(builder instanceof V8BatchStakingBuilder); + }); }); });