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
10 changes: 10 additions & 0 deletions modules/sdk-coin-polyx/src/lib/transactionBuilderFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
});

Expand Down
Loading