Skip to content

Commit 1a9ffdf

Browse files
Merge pull request #5586 from BitGo/sakshi_bitgo
Add unstaking builder for TAO
2 parents ac95c73 + fe568b5 commit 1a9ffdf

File tree

4 files changed

+279
-1
lines changed

4 files changed

+279
-1
lines changed

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { SingletonRegistry, TransactionBuilder, Interface } from './';
55
import { TransferBuilder } from './transferBuilder';
66
import utils from './utils';
77
import { StakingBuilder } from './stakingBuilder';
8+
import { UnstakeBuilder } from './unstakeBuilder';
89

910
export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
1011
protected _material: Interface.Material;
@@ -22,6 +23,10 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
2223
return new StakingBuilder(this._coinConfig).material(this._material);
2324
}
2425

26+
getUnstakingBuilder(): UnstakeBuilder {
27+
return new UnstakeBuilder(this._coinConfig).material(this._material);
28+
}
29+
2530
getWalletInitializationBuilder(): void {
2631
throw new NotImplementedError(`walletInitialization for ${this._coinConfig.name} not implemented`);
2732
}
@@ -49,6 +54,8 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
4954
return this.getTransferBuilder();
5055
} else if (methodName === Interface.MethodNames.AddStake) {
5156
return this.getStakingBuilder();
57+
} else if (methodName === Interface.MethodNames.RemoveStake) {
58+
return this.getUnstakingBuilder();
5259
} else {
5360
throw new NotSupported('Transaction cannot be parsed or has an unsupported transaction type');
5461
}
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import { BaseCoin as CoinConfig } from '@bitgo/statics';
2+
import { defineMethod, UnsignedTransaction, DecodedSignedTx, DecodedSigningPayload } from '@substrate/txwrapper-core';
3+
import BigNumber from 'bignumber.js';
4+
import { InvalidTransactionError, TransactionType, BaseAddress } from '@bitgo/sdk-core';
5+
import { Transaction, TransactionBuilder, Interface, Schema } from '@bitgo/abstract-substrate';
6+
7+
export class UnstakeBuilder extends TransactionBuilder {
8+
protected _amount: number;
9+
protected _hotkey: string;
10+
protected _netuid: number;
11+
12+
constructor(_coinConfig: Readonly<CoinConfig>) {
13+
super(_coinConfig);
14+
}
15+
16+
/**
17+
* Construct a transaction to unstake
18+
*
19+
* @returns {UnsignedTransaction} an unsigned unstake TAO transaction
20+
*
21+
* @see https://polkadot.js.org/docs/substrate/extrinsics/#staking
22+
*/
23+
protected buildTransaction(): UnsignedTransaction {
24+
const baseTxInfo = this.createBaseTxInfo();
25+
return this.removeStake(
26+
{
27+
amountUnstaked: this._amount,
28+
hotkey: this._hotkey,
29+
netuid: this._netuid,
30+
},
31+
baseTxInfo
32+
);
33+
}
34+
35+
/** @inheritdoc */
36+
protected get transactionType(): TransactionType {
37+
return TransactionType.StakingDeactivate;
38+
}
39+
40+
/**
41+
* The amount to unstake.
42+
*
43+
* @param {number} amount to unstake
44+
* @returns {UnstakeBuilder} This unstaking builder.
45+
*
46+
* @see https://wiki.polkadot.network/docs/learn-nominator#required-minimum-stake
47+
*/
48+
amount(amount: number): this {
49+
this.validateValue(new BigNumber(amount));
50+
this._amount = amount;
51+
return this;
52+
}
53+
54+
/**
55+
* The controller of the staked amount.
56+
*
57+
* @param {string} hotkey address of validator
58+
* @returns {UnstakeBuilder} This unstaking builder.
59+
*
60+
* @see https://wiki.polkadot.network/docs/learn-staking#accounts
61+
*/
62+
hotkey({ address }: BaseAddress): this {
63+
this.validateAddress({ address });
64+
this._hotkey = address;
65+
return this;
66+
}
67+
68+
/**
69+
* Netuid of the subnet (root network is 0)
70+
* @param {number} netuid
71+
* @returns {UnstakeBuilder} This unstaking builder
72+
*/
73+
netuid(netuid: number): this {
74+
this._netuid = netuid;
75+
return this;
76+
}
77+
78+
/** @inheritdoc */
79+
protected fromImplementation(rawTransaction: string): Transaction {
80+
const tx = super.fromImplementation(rawTransaction);
81+
if (this._method?.name === Interface.MethodNames.RemoveStake) {
82+
const txMethod = this._method.args as Interface.RemoveStakeArgs;
83+
this.amount(txMethod.amountUnstaked);
84+
this.hotkey({ address: txMethod.hotkey });
85+
this.netuid(txMethod.netuid);
86+
} else {
87+
throw new InvalidTransactionError(`Invalid Transaction Type: ${this._method?.name}. Expected addStake`);
88+
}
89+
return tx;
90+
}
91+
92+
/** @inheritdoc */
93+
validateTransaction(_: Transaction): void {
94+
super.validateTransaction(_);
95+
this.validateFields(this._amount, this._hotkey, this._netuid);
96+
}
97+
98+
/**
99+
* Helper method to validate whether unstake params have the correct type and format
100+
* @param {number} amountUnstaked amount to unstake
101+
* @param {string} hotkey hotkey address of the validator
102+
* @param {number} netuid netuid of the subnet
103+
*/
104+
private validateFields(amountUnstaked: number, hotkey: string, netuid: number): void {
105+
const validationResult = Schema.UnstakeTransactionSchema.validate({
106+
amountUnstaked,
107+
hotkey,
108+
netuid,
109+
});
110+
111+
if (validationResult.error) {
112+
throw new InvalidTransactionError(
113+
`UnStake Builder Transaction validation failed: ${validationResult.error.message}`
114+
);
115+
}
116+
}
117+
118+
/** @inheritdoc */
119+
validateDecodedTransaction(decodedTxn: DecodedSigningPayload | DecodedSignedTx, rawTransaction: string): void {
120+
if (decodedTxn.method?.name === Interface.MethodNames.RemoveStake) {
121+
const txMethod = decodedTxn.method.args as unknown as Interface.RemoveStakeArgs;
122+
const amountUnstaked = txMethod.amountUnstaked;
123+
const hotkey = txMethod.hotkey;
124+
const netuid = txMethod.netuid;
125+
const validationResult = Schema.UnstakeTransactionSchema.validate({ amountUnstaked, hotkey, netuid });
126+
if (validationResult.error) {
127+
throw new InvalidTransactionError(`Transfer Transaction validation failed: ${validationResult.error.message}`);
128+
}
129+
}
130+
}
131+
132+
/**
133+
* Construct a transaction to unstake
134+
*
135+
* @param {Interface.RemoveStakeArgs} RemoveStake arguments to be passed to the addStake method
136+
* @param {Interface.CreateBaseTxInfo} Base txn info required to construct the removeStake txn
137+
* @returns {UnsignedTransaction} an unsigned unstake TAO transaction
138+
*/
139+
private removeStake(args: Interface.RemoveStakeArgs, info: Interface.CreateBaseTxInfo): UnsignedTransaction {
140+
return defineMethod(
141+
{
142+
method: {
143+
args,
144+
name: 'removeStake',
145+
pallet: 'subtensorModule',
146+
},
147+
...info.baseTxInfo,
148+
},
149+
info.options
150+
);
151+
}
152+
}

modules/sdk-coin-tao/test/resources/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ export const rawTx = {
142142
},
143143
unstake: {
144144
signed:
145-
'0xc501840061b18c6dc02ddcabdeac56cb4f21a971cc41cc97640f6f85b073480008c53a0d008cdb9178570210988436b9e274cd40c2e4dd5a7a36c4b4ca3026d331b27ce4d6f01c4757473559d3af5edbc4aa4d78abfbe4e42b36828fdeb8e6780353b2b009d50121030006020b00203d88792d',
145+
'0x55028400aaa34f9f3c1f685e2bac444a4e2d50d302a16f0550f732dd799f854dda7ec77201a4e5222aea1ea19ae4b8f7a542c891e5d8372d9aaafabc4616ecc89fac429a5793b29f79b709c46b41a88634ab2002e69d7777fd095fe014ddad858900155f897401a505000007038a90be061598f4b592afbd546bcb6beadb3c02f5c129df2e11b698f9543dbd41000000e1f50500000000',
146146
unsigned:
147147
'0x2406020b00203d88792dd501210300be23000008000000e143f23803ac50e8f6f8e62695d1ce9e4e1d68aa36c1cd2cfd15340213f3423e149799bc9602cb5cf201f3425fb8d253b2d4e61fc119dcab3249f307f594754d',
148148
batchAll: {
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import assert from 'assert';
2+
import should from 'should';
3+
import { spy, assert as SinonAssert } from 'sinon';
4+
import { UnstakeBuilder } from '../../../src/lib/unstakeBuilder';
5+
import { accounts, mockTssSignature, genesisHash, chainName, rawTx } from '../../resources';
6+
import { buildTestConfig } from './base';
7+
import utils from '../../../src/lib/utils';
8+
import { testnetMaterial } from '../../../src/resources';
9+
10+
describe('Tao Unstake Builder', function () {
11+
const referenceBlock = '0x149799bc9602cb5cf201f3425fb8d253b2d4e61fc119dcab3249f307f594754d';
12+
let builder: UnstakeBuilder;
13+
const sender = accounts.account1;
14+
15+
beforeEach(function () {
16+
const config = buildTestConfig();
17+
const material = utils.getMaterial(config.network.type);
18+
builder = new UnstakeBuilder(config).material(material);
19+
});
20+
21+
describe('setter validation', function () {
22+
it('should validate stake amount', function () {
23+
const spyValidateValue = spy(builder, 'validateValue');
24+
assert.throws(
25+
() => builder.amount(-1),
26+
(e: Error) => e.message === 'Value cannot be less than zero'
27+
);
28+
should.doesNotThrow(() => builder.amount(1000));
29+
SinonAssert.calledTwice(spyValidateValue);
30+
});
31+
it('should validate hotkey address', function () {
32+
const spyValidateAddress = spy(builder, 'validateAddress');
33+
assert.throws(
34+
() => builder.hotkey({ address: 'abc' }),
35+
(e: Error) => e.message === `The address 'abc' is not a well-formed dot address`
36+
);
37+
should.doesNotThrow(() => builder.hotkey({ address: '5FCPTnjevGqAuTttetBy4a24Ej3pH9fiQ8fmvP1ZkrVsLUoT' }));
38+
SinonAssert.calledTwice(spyValidateAddress);
39+
});
40+
});
41+
42+
describe('build unstake transaction', function () {
43+
it('should build a unstake transaction', async function () {
44+
builder
45+
.amount(50000000000000)
46+
.hotkey({ address: '5FCPTnjevGqAuTttetBy4a24Ej3pH9fiQ8fmvP1ZkrVsLUoT' })
47+
.netuid(0)
48+
.sender({ address: sender.address })
49+
.validity({ firstValid: 3933, maxDuration: 64 })
50+
.referenceBlock(referenceBlock)
51+
.sequenceId({ name: 'Nonce', keyword: 'nonce', value: 200 })
52+
.fee({ amount: 0, type: 'tip' })
53+
.addSignature({ pub: sender.publicKey }, Buffer.from(mockTssSignature, 'hex'));
54+
55+
const tx = await builder.build();
56+
const txJson = tx.toJson();
57+
should.deepEqual(txJson.amount, '50000000000000');
58+
should.deepEqual(txJson.to, '5FCPTnjevGqAuTttetBy4a24Ej3pH9fiQ8fmvP1ZkrVsLUoT');
59+
should.deepEqual(txJson.netuid, '0');
60+
should.deepEqual(txJson.sender, sender.address);
61+
should.deepEqual(txJson.blockNumber, 3933);
62+
should.deepEqual(txJson.referenceBlock, referenceBlock);
63+
should.deepEqual(txJson.genesisHash, genesisHash);
64+
should.deepEqual(txJson.specVersion, Number(testnetMaterial.specVersion));
65+
should.deepEqual(txJson.nonce, 200);
66+
should.deepEqual(txJson.tip, 0);
67+
should.deepEqual(txJson.transactionVersion, Number(testnetMaterial.txVersion));
68+
should.deepEqual(txJson.chainName.toLowerCase(), chainName);
69+
should.deepEqual(txJson.eraPeriod, 64);
70+
});
71+
72+
it('should build an unsigned unstake transaction', async function () {
73+
builder
74+
.amount(50000000000000)
75+
.hotkey({ address: '5FCPTnjevGqAuTttetBy4a24Ej3pH9fiQ8fmvP1ZkrVsLUoT' })
76+
.netuid(0)
77+
.sender({ address: sender.address })
78+
.validity({ firstValid: 3933, maxDuration: 64 })
79+
.referenceBlock(referenceBlock)
80+
.sequenceId({ name: 'Nonce', keyword: 'nonce', value: 200 })
81+
.fee({ amount: 0, type: 'tip' });
82+
const tx = await builder.build();
83+
const txJson = tx.toJson();
84+
should.deepEqual(txJson.amount, '50000000000000');
85+
should.deepEqual(txJson.to, '5FCPTnjevGqAuTttetBy4a24Ej3pH9fiQ8fmvP1ZkrVsLUoT');
86+
should.deepEqual(txJson.netuid, '0');
87+
should.deepEqual(txJson.sender, sender.address);
88+
should.deepEqual(txJson.blockNumber, 3933);
89+
should.deepEqual(txJson.referenceBlock, referenceBlock);
90+
should.deepEqual(txJson.genesisHash, genesisHash);
91+
should.deepEqual(txJson.specVersion, Number(testnetMaterial.specVersion));
92+
should.deepEqual(txJson.nonce, 200);
93+
should.deepEqual(txJson.tip, 0);
94+
should.deepEqual(txJson.transactionVersion, Number(testnetMaterial.txVersion));
95+
should.deepEqual(txJson.chainName.toLowerCase(), chainName);
96+
should.deepEqual(txJson.eraPeriod, 64);
97+
});
98+
99+
it('should build from raw signed tx', async function () {
100+
builder.from(rawTx.unstake.signed);
101+
builder.validity({ firstValid: 3933, maxDuration: 64 }).referenceBlock(referenceBlock);
102+
const tx = await builder.build();
103+
const txJson = tx.toJson();
104+
should.deepEqual(txJson.amount, '100000000');
105+
should.deepEqual(txJson.to, '5FCPTnjevGqAuTttetBy4a24Ej3pH9fiQ8fmvP1ZkrVsLUoT');
106+
should.deepEqual(txJson.netuid, '0');
107+
should.deepEqual(txJson.sender, '5FvSWbV4hGC7GvXQKKtiVmmHSH3JELK8R3JS8Z5adnACFBwh');
108+
should.deepEqual(txJson.blockNumber, 3933);
109+
should.deepEqual(txJson.referenceBlock, referenceBlock);
110+
should.deepEqual(txJson.genesisHash, genesisHash);
111+
should.deepEqual(txJson.specVersion, Number(testnetMaterial.specVersion));
112+
should.deepEqual(txJson.nonce, 361);
113+
should.deepEqual(txJson.tip, 0);
114+
should.deepEqual(txJson.transactionVersion, Number(testnetMaterial.txVersion));
115+
should.deepEqual(txJson.chainName.toLowerCase(), chainName);
116+
should.deepEqual(txJson.eraPeriod, 64);
117+
});
118+
});
119+
});

0 commit comments

Comments
 (0)