Skip to content

Commit 1c5fd30

Browse files
authored
Merge pull request #7016 from BitGo/SC-3168
feat(sdk-coin-tao): correct validation stages order in move stake builder
2 parents 2cde6f0 + 3fe6828 commit 1c5fd30

File tree

2 files changed

+144
-2
lines changed

2 files changed

+144
-2
lines changed

modules/sdk-coin-tao/src/lib/moveStakeBuilder.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,12 +99,12 @@ export class MoveStakeBuilder extends TransactionBuilder {
9999

100100
/** @inheritdoc */
101101
protected fromImplementation(rawTransaction: string): Transaction {
102+
const tx = super.fromImplementation(rawTransaction);
102103
if (this._method?.name !== Interface.MethodNames.MoveStake) {
103104
throw new InvalidTransactionError(
104105
`Invalid Transaction Type: ${this._method?.name}. Expected ${Interface.MethodNames.MoveStake}`
105106
);
106107
}
107-
const tx = super.fromImplementation(rawTransaction);
108108
const txMethod = this._method.args as Interface.MoveStakeArgs;
109109
this.amount(txMethod.alphaAmount);
110110
this.originHotkey({ address: txMethod.originHotkey });

modules/sdk-coin-tao/test/unit/transactionBuilder/moveStakeBuilder.ts

Lines changed: 143 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import should from 'should';
33
import { assert as SinonAssert, spy } from 'sinon';
44
import { MoveStakeBuilder } from '../../../src/lib/moveStakeBuilder';
55
import utils from '../../../src/lib/utils';
6-
import { accounts, mockTssSignature, genesisHash, chainName } from '../../resources';
6+
import { accounts, mockTssSignature, genesisHash, chainName, rawTx } from '../../resources';
77
import { buildTestConfig } from './base';
88
import { testnetMaterial } from '../../../src/resources';
99
import { InvalidTransactionError } from '@bitgo/sdk-core';
@@ -498,4 +498,146 @@ describe('Tao Move Stake Builder', function () {
498498
explanation.outputAmount.should.equal('0');
499499
});
500500
});
501+
502+
describe('fromImplementation stages validation', function () {
503+
it('should call super.fromImplementation before validation to populate _method', async function () {
504+
const config = buildTestConfig();
505+
const material = utils.getMaterial(config.network.type);
506+
const validBuilder = new MoveStakeBuilder(config).material(material);
507+
validBuilder
508+
.amount('1000000000000')
509+
.originHotkey({ address: '5FCPTnjevGqAuTttetBy4a24Ej3pH9fiQ8fmvP1ZkrVsLUoT' })
510+
.destinationHotkey({ address: '5Ffp1wJCPu4hzVDTo7XaMLqZSvSadyUQmxWPDw74CBjECSoq' })
511+
.originNetuid('1')
512+
.destinationNetuid('2')
513+
.sender({ address: sender.address })
514+
.validity({ firstValid: 3933, maxDuration: 64 })
515+
.referenceBlock(referenceBlock)
516+
.sequenceId({ name: 'Nonce', keyword: 'nonce', value: 200 })
517+
.fee({ amount: 0, type: 'tip' });
518+
519+
const validTx = await validBuilder.build();
520+
const rawTxHex = validTx.toBroadcastFormat();
521+
522+
const newBuilder = new MoveStakeBuilder(config).material(material);
523+
524+
should.doesNotThrow(() => {
525+
newBuilder.from(rawTxHex);
526+
});
527+
528+
const builderMethod = (newBuilder as any)._method;
529+
builderMethod.should.not.be.undefined();
530+
builderMethod.name.should.equal('moveStake');
531+
builderMethod.args.should.have.properties([
532+
'originHotkey',
533+
'destinationHotkey',
534+
'originNetuid',
535+
'destinationNetuid',
536+
'alphaAmount',
537+
]);
538+
builderMethod.args.alphaAmount.should.equal('1000000000000');
539+
builderMethod.args.originHotkey.should.equal('5FCPTnjevGqAuTttetBy4a24Ej3pH9fiQ8fmvP1ZkrVsLUoT');
540+
builderMethod.args.destinationHotkey.should.equal('5Ffp1wJCPu4hzVDTo7XaMLqZSvSadyUQmxWPDw74CBjECSoq');
541+
builderMethod.args.originNetuid.should.equal('1');
542+
builderMethod.args.destinationNetuid.should.equal('2');
543+
});
544+
545+
it('should throw error if _method is not populated before validation', function () {
546+
const config = buildTestConfig();
547+
const material = utils.getMaterial(config.network.type);
548+
const mockBuilder = new TestMoveStakeBuilder(config).material(material);
549+
550+
assert.throws(
551+
() => {
552+
if (mockBuilder['_method']?.name !== 'moveStake') {
553+
throw new InvalidTransactionError(
554+
`Invalid Transaction Type: ${mockBuilder['_method']?.name}. Expected moveStake`
555+
);
556+
}
557+
},
558+
(e: Error) => e.message.includes('Invalid Transaction Type: undefined. Expected moveStake')
559+
);
560+
});
561+
562+
it('should properly validate transaction type after super.fromImplementation', function () {
563+
const config = buildTestConfig();
564+
const material = utils.getMaterial(config.network.type);
565+
const mockBuilder = new TestMoveStakeBuilder(config).material(material);
566+
567+
mockBuilder.setMethodForTesting({
568+
name: 'transferKeepAlive',
569+
args: { dest: { id: 'test' }, value: '1000' },
570+
pallet: 'balances',
571+
});
572+
573+
assert.throws(
574+
() => {
575+
if (mockBuilder['_method']?.name !== 'moveStake') {
576+
throw new InvalidTransactionError(
577+
`Invalid Transaction Type: ${mockBuilder['_method']?.name}. Expected moveStake`
578+
);
579+
}
580+
},
581+
(e: Error) => e.message.includes('Invalid Transaction Type: transferKeepAlive. Expected moveStake')
582+
);
583+
});
584+
585+
it('should successfully parse and validate correct moveStake transaction', function () {
586+
const config = buildTestConfig();
587+
const material = utils.getMaterial(config.network.type);
588+
const mockBuilder = new TestMoveStakeBuilder(config).material(material);
589+
590+
mockBuilder.setMethodForTesting({
591+
name: 'moveStake',
592+
args: {
593+
originHotkey: '5FCPTnjevGqAuTttetBy4a24Ej3pH9fiQ8fmvP1ZkrVsLUoT',
594+
destinationHotkey: '5Ffp1wJCPu4hzVDTo7XaMLqZSvSadyUQmxWPDw74CBjECSoq',
595+
originNetuid: '1',
596+
destinationNetuid: '2',
597+
alphaAmount: '1000000000000',
598+
},
599+
pallet: 'subtensorModule',
600+
});
601+
602+
should.doesNotThrow(() => {
603+
if (mockBuilder['_method']?.name !== 'moveStake') {
604+
throw new InvalidTransactionError(
605+
`Invalid Transaction Type: ${mockBuilder['_method']?.name}. Expected moveStake`
606+
);
607+
}
608+
});
609+
});
610+
611+
it('should fail validation when parsing wrong transaction type (transferStake instead of moveStake)', function () {
612+
const config = buildTestConfig();
613+
const material = utils.getMaterial(config.network.type);
614+
const moveStakeBuilder = new MoveStakeBuilder(config).material(material);
615+
616+
assert.throws(
617+
() => {
618+
moveStakeBuilder.from(rawTx.transferStake.signed);
619+
},
620+
(e: Error) => e.message.includes('Invalid Transaction Type: transferStake. Expected moveStake')
621+
);
622+
});
623+
624+
it('should verify _method is properly populated after super.fromImplementation with wrong transaction type', function () {
625+
const config = buildTestConfig();
626+
const material = utils.getMaterial(config.network.type);
627+
628+
const testBuilder = new TestMoveStakeBuilder(config).material(material);
629+
630+
try {
631+
testBuilder.from(rawTx.transferStake.signed);
632+
} catch (error) {
633+
const method = (testBuilder as any)._method;
634+
method.should.not.be.undefined();
635+
method.name.should.equal('transferStake'); // This proves super.fromImplementation was called
636+
method.should.have.property('args');
637+
method.should.have.property('pallet');
638+
639+
(error as Error).message.should.containEql('Invalid Transaction Type: transferStake. Expected moveStake');
640+
}
641+
});
642+
});
501643
});

0 commit comments

Comments
 (0)