Skip to content

Commit

Permalink
feat(sdk-coin-tao): add unstaking builder for TAO
Browse files Browse the repository at this point in the history
Ticket: SC-1059
  • Loading branch information
joshisakshi authored and Vijay-Jagannathan committed Feb 25, 2025
1 parent 6801518 commit 1f1e8d0
Show file tree
Hide file tree
Showing 4 changed files with 279 additions and 1 deletion.
7 changes: 7 additions & 0 deletions modules/sdk-coin-tao/src/lib/transactionBuilderFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { SingletonRegistry, TransactionBuilder, Interface } from './';
import { TransferBuilder } from './transferBuilder';
import utils from './utils';
import { StakingBuilder } from './stakingBuilder';
import { UnstakeBuilder } from './unstakeBuilder';

export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
protected _material: Interface.Material;
Expand All @@ -21,6 +22,10 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
getStakingBuilder(): StakingBuilder {
return new StakingBuilder(this._coinConfig).material(this._material);
}

getUnstakingBuilder(): UnstakeBuilder {
return new UnstakeBuilder(this._coinConfig).material(this._material);
}

getWalletInitializationBuilder(): void {
throw new NotImplementedError(`walletInitialization for ${this._coinConfig.name} not implemented`);
Expand Down Expand Up @@ -49,6 +54,8 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
return this.getTransferBuilder();
} else if (methodName === Interface.MethodNames.AddStake) {
return this.getStakingBuilder();
} else if (methodName === Interface.MethodNames.RemoveStake) {
return this.getUnstakingBuilder();
} else {
throw new NotSupported('Transaction cannot be parsed or has an unsupported transaction type');
}
Expand Down
152 changes: 152 additions & 0 deletions modules/sdk-coin-tao/src/lib/unstakeBuilder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import { BaseCoin as CoinConfig } from '@bitgo/statics';
import { defineMethod, UnsignedTransaction, DecodedSignedTx, DecodedSigningPayload } from '@substrate/txwrapper-core';
import BigNumber from 'bignumber.js';
import { InvalidTransactionError, TransactionType, BaseAddress } from '@bitgo/sdk-core';
import { Transaction, TransactionBuilder, Interface, Schema } from '@bitgo/abstract-substrate';

export class UnstakeBuilder extends TransactionBuilder {
protected _amount: number;
protected _hotkey: string;
protected _netuid: number;

constructor(_coinConfig: Readonly<CoinConfig>) {
super(_coinConfig);
}

/**
* Construct a transaction to unstake
*
* @returns {UnsignedTransaction} an unsigned unstake TAO transaction
*
* @see https://polkadot.js.org/docs/substrate/extrinsics/#staking
*/
protected buildTransaction(): UnsignedTransaction {
const baseTxInfo = this.createBaseTxInfo();
return this.removeStake(
{
amountUnstaked: this._amount,
hotkey: this._hotkey,
netuid: this._netuid,
},
baseTxInfo
);
}

/** @inheritdoc */
protected get transactionType(): TransactionType {
return TransactionType.StakingDeactivate;
}

/**
* The amount to unstake.
*
* @param {number} amount to unstake
* @returns {UnstakeBuilder} This unstaking builder.
*
* @see https://wiki.polkadot.network/docs/learn-nominator#required-minimum-stake
*/
amount(amount: number): this {
this.validateValue(new BigNumber(amount));
this._amount = amount;
return this;
}

/**
* The controller of the staked amount.
*
* @param {string} hotkey address of validator
* @returns {UnstakeBuilder} This unstaking builder.
*
* @see https://wiki.polkadot.network/docs/learn-staking#accounts
*/
hotkey({ address }: BaseAddress): this {
this.validateAddress({ address });
this._hotkey = address;
return this;
}

/**
* Netuid of the subnet (root network is 0)
* @param {number} netuid
* @returns {UnstakeBuilder} This unstaking builder
*/
netuid(netuid: number): this {
this._netuid = netuid;
return this;
}

/** @inheritdoc */
protected fromImplementation(rawTransaction: string): Transaction {
const tx = super.fromImplementation(rawTransaction);
if (this._method?.name === Interface.MethodNames.RemoveStake) {
const txMethod = this._method.args as Interface.RemoveStakeArgs;
this.amount(txMethod.amountUnstaked);
this.hotkey({ address: txMethod.hotkey });
this.netuid(txMethod.netuid);
} else {
throw new InvalidTransactionError(`Invalid Transaction Type: ${this._method?.name}. Expected addStake`);
}
return tx;
}

/** @inheritdoc */
validateTransaction(_: Transaction): void {
super.validateTransaction(_);
this.validateFields(this._amount, this._hotkey, this._netuid);
}

/**
* Helper method to validate whether unstake params have the correct type and format
* @param {number} amountUnstaked amount to unstake
* @param {string} hotkey hotkey address of the validator
* @param {number} netuid netuid of the subnet
*/
private validateFields(amountUnstaked: number, hotkey: string, netuid: number): void {
const validationResult = Schema.UnstakeTransactionSchema.validate({
amountUnstaked,
hotkey,
netuid,
});

if (validationResult.error) {
throw new InvalidTransactionError(
`UnStake Builder Transaction validation failed: ${validationResult.error.message}`
);
}
}

/** @inheritdoc */
validateDecodedTransaction(decodedTxn: DecodedSigningPayload | DecodedSignedTx, rawTransaction: string): void {
if (decodedTxn.method?.name === Interface.MethodNames.RemoveStake) {
const txMethod = decodedTxn.method.args as unknown as Interface.RemoveStakeArgs;
const amountUnstaked = txMethod.amountUnstaked;
const hotkey = txMethod.hotkey;
const netuid = txMethod.netuid;
const validationResult = Schema.UnstakeTransactionSchema.validate({ amountUnstaked, hotkey, netuid });
if (validationResult.error) {
throw new InvalidTransactionError(`Transfer Transaction validation failed: ${validationResult.error.message}`);
}
}
}

/**
* Construct a transaction to unstake
*
* @param {Interface.RemoveStakeArgs} RemoveStake arguments to be passed to the addStake method
* @param {Interface.CreateBaseTxInfo} Base txn info required to construct the removeStake txn
* @returns {UnsignedTransaction} an unsigned unstake TAO transaction
*/
private removeStake(args: Interface.RemoveStakeArgs, info: Interface.CreateBaseTxInfo): UnsignedTransaction {
return defineMethod(
{
method: {
args,
name: 'removeStake',
pallet: 'subtensorModule',
},
...info.baseTxInfo,
},
info.options
);
}
}
2 changes: 1 addition & 1 deletion modules/sdk-coin-tao/test/resources/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ export const rawTx = {
},
unstake: {
signed:
'0xc501840061b18c6dc02ddcabdeac56cb4f21a971cc41cc97640f6f85b073480008c53a0d008cdb9178570210988436b9e274cd40c2e4dd5a7a36c4b4ca3026d331b27ce4d6f01c4757473559d3af5edbc4aa4d78abfbe4e42b36828fdeb8e6780353b2b009d50121030006020b00203d88792d',
'0x55028400aaa34f9f3c1f685e2bac444a4e2d50d302a16f0550f732dd799f854dda7ec77201a4e5222aea1ea19ae4b8f7a542c891e5d8372d9aaafabc4616ecc89fac429a5793b29f79b709c46b41a88634ab2002e69d7777fd095fe014ddad858900155f897401a505000007038a90be061598f4b592afbd546bcb6beadb3c02f5c129df2e11b698f9543dbd41000000e1f50500000000',
unsigned:
'0x2406020b00203d88792dd501210300be23000008000000e143f23803ac50e8f6f8e62695d1ce9e4e1d68aa36c1cd2cfd15340213f3423e149799bc9602cb5cf201f3425fb8d253b2d4e61fc119dcab3249f307f594754d',
batchAll: {
Expand Down
119 changes: 119 additions & 0 deletions modules/sdk-coin-tao/test/unit/transactionBuilder/unstakeBuilder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import assert from 'assert';
import should from 'should';
import { spy, assert as SinonAssert } from 'sinon';
import { UnstakeBuilder } from '../../../src/lib/unstakeBuilder';
import { accounts, mockTssSignature, genesisHash, chainName, rawTx } from '../../resources';
import { buildTestConfig } from './base';
import utils from '../../../src/lib/utils';
import { testnetMaterial } from '../../../src/resources';

describe('Tao Unstake Builder', function () {
const referenceBlock = '0x149799bc9602cb5cf201f3425fb8d253b2d4e61fc119dcab3249f307f594754d';
let builder: UnstakeBuilder;
const sender = accounts.account1;

beforeEach(function () {
const config = buildTestConfig();
const material = utils.getMaterial(config.network.type);
builder = new UnstakeBuilder(config).material(material);
});

describe('setter validation', function () {
it('should validate stake amount', function () {
const spyValidateValue = spy(builder, 'validateValue');
assert.throws(
() => builder.amount(-1),
(e: Error) => e.message === 'Value cannot be less than zero'
);
should.doesNotThrow(() => builder.amount(1000));
SinonAssert.calledTwice(spyValidateValue);
});
it('should validate hotkey address', function () {
const spyValidateAddress = spy(builder, 'validateAddress');
assert.throws(
() => builder.hotkey({ address: 'abc' }),
(e: Error) => e.message === `The address 'abc' is not a well-formed dot address`
);
should.doesNotThrow(() => builder.hotkey({ address: '5FCPTnjevGqAuTttetBy4a24Ej3pH9fiQ8fmvP1ZkrVsLUoT' }));
SinonAssert.calledTwice(spyValidateAddress);
});
});

describe('build unstake transaction', function () {
it('should build a unstake transaction', async function () {
builder
.amount(50000000000000)
.hotkey({ address: '5FCPTnjevGqAuTttetBy4a24Ej3pH9fiQ8fmvP1ZkrVsLUoT' })
.netuid(0)
.sender({ address: sender.address })
.validity({ firstValid: 3933, maxDuration: 64 })
.referenceBlock(referenceBlock)
.sequenceId({ name: 'Nonce', keyword: 'nonce', value: 200 })
.fee({ amount: 0, type: 'tip' })
.addSignature({ pub: sender.publicKey }, Buffer.from(mockTssSignature, 'hex'));

const tx = await builder.build();
const txJson = tx.toJson();
should.deepEqual(txJson.amount, '50000000000000');
should.deepEqual(txJson.to, '5FCPTnjevGqAuTttetBy4a24Ej3pH9fiQ8fmvP1ZkrVsLUoT');
should.deepEqual(txJson.netuid, '0');
should.deepEqual(txJson.sender, sender.address);
should.deepEqual(txJson.blockNumber, 3933);
should.deepEqual(txJson.referenceBlock, referenceBlock);
should.deepEqual(txJson.genesisHash, genesisHash);
should.deepEqual(txJson.specVersion, Number(testnetMaterial.specVersion));
should.deepEqual(txJson.nonce, 200);
should.deepEqual(txJson.tip, 0);
should.deepEqual(txJson.transactionVersion, Number(testnetMaterial.txVersion));
should.deepEqual(txJson.chainName, chainName);
should.deepEqual(txJson.eraPeriod, 64);
});

it('should build an unsigned unstake transaction', async function () {
builder
.amount(50000000000000)
.hotkey({ address: '5FCPTnjevGqAuTttetBy4a24Ej3pH9fiQ8fmvP1ZkrVsLUoT' })
.netuid(0)
.sender({ address: sender.address })
.validity({ firstValid: 3933, maxDuration: 64 })
.referenceBlock(referenceBlock)
.sequenceId({ name: 'Nonce', keyword: 'nonce', value: 200 })
.fee({ amount: 0, type: 'tip' });
const tx = await builder.build();
const txJson = tx.toJson();
should.deepEqual(txJson.amount, '50000000000000');
should.deepEqual(txJson.to, '5FCPTnjevGqAuTttetBy4a24Ej3pH9fiQ8fmvP1ZkrVsLUoT');
should.deepEqual(txJson.netuid, '0');
should.deepEqual(txJson.sender, sender.address);
should.deepEqual(txJson.blockNumber, 3933);
should.deepEqual(txJson.referenceBlock, referenceBlock);
should.deepEqual(txJson.genesisHash, genesisHash);
should.deepEqual(txJson.specVersion, Number(testnetMaterial.specVersion));
should.deepEqual(txJson.nonce, 200);
should.deepEqual(txJson.tip, 0);
should.deepEqual(txJson.transactionVersion, Number(testnetMaterial.txVersion));
should.deepEqual(txJson.chainName, chainName);
should.deepEqual(txJson.eraPeriod, 64);
});

it('should build from raw signed tx', async function () {
builder.from(rawTx.unstake.signed);
builder.validity({ firstValid: 3933, maxDuration: 64 }).referenceBlock(referenceBlock);
const tx = await builder.build();
const txJson = tx.toJson();
should.deepEqual(txJson.amount, '100000000');
should.deepEqual(txJson.to, '5FCPTnjevGqAuTttetBy4a24Ej3pH9fiQ8fmvP1ZkrVsLUoT');
should.deepEqual(txJson.netuid, '0');
should.deepEqual(txJson.sender, '5FvSWbV4hGC7GvXQKKtiVmmHSH3JELK8R3JS8Z5adnACFBwh');
should.deepEqual(txJson.blockNumber, 3933);
should.deepEqual(txJson.referenceBlock, referenceBlock);
should.deepEqual(txJson.genesisHash, genesisHash);
should.deepEqual(txJson.specVersion, Number(testnetMaterial.specVersion));
should.deepEqual(txJson.nonce, 361);
should.deepEqual(txJson.tip, 0);
should.deepEqual(txJson.transactionVersion, Number(testnetMaterial.txVersion));
should.deepEqual(txJson.chainName, chainName);
should.deepEqual(txJson.eraPeriod, 64);
});
});
});

0 comments on commit 1f1e8d0

Please sign in to comment.