Skip to content

Commit 259d850

Browse files
committed
feat(sdk-coin-tao): add moveStakeBuilder
Ticket: SC-3017
1 parent 1979e1c commit 259d850

File tree

10 files changed

+854
-1
lines changed

10 files changed

+854
-1
lines changed

modules/abstract-substrate/src/lib/iface.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,10 @@ export const MethodNames = {
7979
* Transfer stake from one validator to another.
8080
*/
8181
TransferStake: 'transferStake' as const,
82+
/**
83+
* Move stake from one hotkey to another.
84+
*/
85+
MoveStake: 'moveStake' as const,
8286
} as const;
8387

8488
/**
@@ -195,6 +199,14 @@ export interface TransferStakeArgs extends Args {
195199
alphaAmount: string;
196200
}
197201

202+
export interface MoveStakeArgs extends Args {
203+
originHotkey: string;
204+
destinationHotkey: string;
205+
originNetuid: string;
206+
destinationNetuid: string;
207+
alphaAmount: string;
208+
}
209+
198210
/**
199211
* Decoded TxMethod from a transaction hex
200212
*/
@@ -211,7 +223,8 @@ export interface TxMethod {
211223
| UnbondArgs
212224
| WithdrawUnbondedArgs
213225
| BatchArgs
214-
| TransferStakeArgs;
226+
| TransferStakeArgs
227+
| MoveStakeArgs;
215228
name: MethodNamesValues;
216229
pallet: string;
217230
}

modules/abstract-substrate/src/lib/txnSchema.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,11 @@ export const TransferStakeTransactionSchema = joi.object({
6565
destinationNetuid: joi.string().required(),
6666
alphaAmount: joi.string().required(),
6767
});
68+
69+
export const MoveStakeTransactionSchema = joi.object({
70+
originHotkey: addressSchema.required(),
71+
destinationHotkey: addressSchema.required(),
72+
originNetuid: joi.string().required(),
73+
destinationNetuid: joi.string().required(),
74+
alphaAmount: joi.string().required(),
75+
});

modules/abstract-substrate/src/lib/utils.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
UnbondArgs,
2929
WithdrawUnbondedArgs,
3030
BatchArgs,
31+
MoveStakeArgs,
3132
} from './iface';
3233

3334
export class Utils implements BaseUtils {
@@ -263,6 +264,16 @@ export class Utils implements BaseUtils {
263264
return (arg as BatchArgs).calls !== undefined && Array.isArray((arg as BatchArgs).calls);
264265
}
265266

267+
isMoveStake(arg: TxMethod['args']): arg is MoveStakeArgs {
268+
return (
269+
(arg as MoveStakeArgs).originHotkey !== undefined &&
270+
(arg as MoveStakeArgs).destinationHotkey !== undefined &&
271+
(arg as MoveStakeArgs).originNetuid !== undefined &&
272+
(arg as MoveStakeArgs).destinationNetuid !== undefined &&
273+
(arg as MoveStakeArgs).alphaAmount !== undefined
274+
);
275+
}
276+
266277
/**
267278
* extracts and returns the signature in hex format given a raw signed transaction
268279
*
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/**
2+
* Constants for TAO network operations
3+
*/
4+
export const TAO_CONSTANTS = {
5+
/**
6+
* Maximum supported netuid value for TAO subnets
7+
* Valid range is 0 to MAX_NETUID (inclusive)
8+
*/
9+
MAX_NETUID: 128,
10+
} as const;

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,11 @@ export interface TransferStakeTxData extends Interface.TxData {
77
destinationNetuid: string;
88
alphaAmount: string;
99
}
10+
11+
export interface MoveStakeTxData extends Interface.TxData {
12+
originHotkey: string;
13+
destinationHotkey: string;
14+
originNetuid: string;
15+
destinationNetuid: string;
16+
alphaAmount: string;
17+
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,8 @@ export { TransactionBuilderFactory } from './transactionBuilderFactory';
1212
export { TransferBuilder } from './transferBuilder';
1313
export { StakingBuilder } from './stakingBuilder';
1414
export { UnstakeBuilder } from './unstakeBuilder';
15+
export { TokenTransferBuilder } from './tokenTransferBuilder';
16+
export { TokenTransferTransaction } from './tokenTransferTransaction';
17+
export { MoveStakeBuilder } from './moveStakeBuilder';
18+
export { MoveStakeTransaction } from './moveStakeTransaction';
1519
export { Utils, default as utils } from './utils';
Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
import { Interface, Schema, Transaction, TransactionBuilder } from '@bitgo/abstract-substrate';
2+
import { InvalidTransactionError, TransactionType } from '@bitgo/sdk-core';
3+
import { BaseCoin as CoinConfig } from '@bitgo/statics';
4+
import { DecodedSignedTx, DecodedSigningPayload, defineMethod, UnsignedTransaction } from '@substrate/txwrapper-core';
5+
import BigNumber from 'bignumber.js';
6+
import { TAO_CONSTANTS } from './constants';
7+
import { MoveStakeTransaction } from './moveStakeTransaction';
8+
9+
export class MoveStakeBuilder extends TransactionBuilder {
10+
protected _originHotkey: string;
11+
protected _destinationHotkey: string;
12+
protected _originNetuid: string;
13+
protected _destinationNetuid: string;
14+
protected _alphaAmount: string;
15+
16+
constructor(_coinConfig: Readonly<CoinConfig>) {
17+
super(_coinConfig);
18+
this._transaction = new MoveStakeTransaction(_coinConfig);
19+
}
20+
21+
/**
22+
* Construct a transaction to move stake
23+
* @returns {UnsignedTransaction} an unsigned move stake transaction
24+
*/
25+
protected buildTransaction(): UnsignedTransaction {
26+
const baseTxInfo = this.createBaseTxInfo();
27+
return this.moveStake(
28+
{
29+
originHotkey: this._originHotkey,
30+
destinationHotkey: this._destinationHotkey,
31+
originNetuid: this._originNetuid,
32+
destinationNetuid: this._destinationNetuid,
33+
alphaAmount: this._alphaAmount,
34+
},
35+
baseTxInfo
36+
);
37+
}
38+
39+
/** @inheritdoc */
40+
protected get transactionType(): TransactionType {
41+
return TransactionType.StakingRedelegate;
42+
}
43+
44+
/**
45+
* Set the amount to move
46+
* @param {string} amount to move
47+
* @returns {MoveStakeBuilder} This builder.
48+
*/
49+
amount(amount: string): this {
50+
const value = new BigNumber(amount);
51+
this.validateAmount(value);
52+
this._alphaAmount = amount;
53+
return this;
54+
}
55+
56+
/**
57+
* Set the origin hot key address
58+
* @param {string} address of origin hotkey
59+
* @returns {MoveStakeBuilder} This builder.
60+
*/
61+
originHotkey(address: string): this {
62+
this.validateAddress({ address });
63+
this._originHotkey = address;
64+
return this;
65+
}
66+
67+
/**
68+
* Set the destination hot key address
69+
* @param {string} address of destination hotkey
70+
* @returns {MoveStakeBuilder} This builder.
71+
*/
72+
destinationHotkey(address: string): this {
73+
this.validateAddress({ address });
74+
this._destinationHotkey = address;
75+
return this;
76+
}
77+
78+
/**
79+
* Set the origin netuid of the subnet (root network is 0)
80+
* @param {string} netuid of subnet
81+
* @returns {MoveStakeBuilder} This builder.
82+
*/
83+
originNetuid(netuid: string): this {
84+
this.validateNetuid(netuid);
85+
this._originNetuid = netuid;
86+
return this;
87+
}
88+
89+
/**
90+
* Set the destination netuid of the subnet (root network is 0)
91+
* @param {string} netuid of subnet
92+
* @returns {MoveStakeBuilder} This builder.
93+
*/
94+
destinationNetuid(netuid: string): this {
95+
this.validateNetuid(netuid);
96+
this._destinationNetuid = netuid;
97+
return this;
98+
}
99+
100+
/** @inheritdoc */
101+
protected fromImplementation(rawTransaction: string): Transaction {
102+
const tx = super.fromImplementation(rawTransaction);
103+
if (this._method?.name === Interface.MethodNames.MoveStake) {
104+
const txMethod = this._method.args as Interface.MoveStakeArgs;
105+
this.amount(txMethod.alphaAmount);
106+
this.originHotkey(txMethod.originHotkey);
107+
this.destinationHotkey(txMethod.destinationHotkey);
108+
this.originNetuid(txMethod.originNetuid);
109+
this.destinationNetuid(txMethod.destinationNetuid);
110+
} else {
111+
throw new InvalidTransactionError(
112+
`Invalid Transaction Type: ${this._method?.name}. Expected ${Interface.MethodNames.MoveStake}`
113+
);
114+
}
115+
return tx;
116+
}
117+
118+
/** @inheritdoc */
119+
validateTransaction(_: Transaction): void {
120+
super.validateTransaction(_);
121+
this.validateFields(
122+
this._originHotkey,
123+
this._destinationHotkey,
124+
this._originNetuid,
125+
this._destinationNetuid,
126+
this._alphaAmount
127+
);
128+
}
129+
130+
/**
131+
* @param {BigNumber} amount amount to validate
132+
* @throws {InvalidTransactionError} if amount is less than or equal to zero
133+
*/
134+
private validateAmount(amount: BigNumber): void {
135+
if (amount.isLessThanOrEqualTo(0)) {
136+
throw new InvalidTransactionError('Amount must be greater than zero');
137+
}
138+
}
139+
140+
/**
141+
* @param {string} netuid netuid to validate
142+
* @throws {InvalidTransactionError} if netuid is out of range
143+
*/
144+
private validateNetuid(netuid: string): void {
145+
const netuidNum = Number(netuid);
146+
if (isNaN(netuidNum) || !Number.isInteger(netuidNum) || netuidNum < 0 || netuidNum > TAO_CONSTANTS.MAX_NETUID) {
147+
throw new InvalidTransactionError(
148+
`Invalid netuid: ${netuid}. Netuid must be between 0 and ${TAO_CONSTANTS.MAX_NETUID}.`
149+
);
150+
}
151+
}
152+
153+
/**
154+
* Helper method to validate whether tx params have the correct type and format
155+
* @param {string} originHotkey origin hotkey address
156+
* @param {string} destinationHotkey destination hotkey address
157+
* @param {string} originNetuid netuid of the origin subnet
158+
* @param {string} destinationNetuid netuid of the destination subnet
159+
* @param {string} alphaAmount amount to move
160+
* @throws {InvalidTransactionError} if validation fails
161+
*/
162+
private validateFields(
163+
originHotkey: string,
164+
destinationHotkey: string,
165+
originNetuid: string,
166+
destinationNetuid: string,
167+
alphaAmount: string
168+
): void {
169+
// Validate netuid ranges
170+
this.validateNetuid(originNetuid);
171+
this.validateNetuid(destinationNetuid);
172+
173+
const validationResult = Schema.MoveStakeTransactionSchema.validate({
174+
originHotkey,
175+
destinationHotkey,
176+
originNetuid,
177+
destinationNetuid,
178+
alphaAmount,
179+
});
180+
181+
if (validationResult.error) {
182+
throw new InvalidTransactionError(`Transaction validation failed: ${validationResult.error.message}`);
183+
}
184+
}
185+
186+
/** @inheritdoc */
187+
validateDecodedTransaction(decodedTxn: DecodedSigningPayload | DecodedSignedTx, rawTransaction: string): void {
188+
if (decodedTxn.method?.name === Interface.MethodNames.MoveStake) {
189+
const txMethod = decodedTxn.method.args as unknown as Interface.MoveStakeArgs;
190+
191+
const validationResult = Schema.MoveStakeTransactionSchema.validate(txMethod);
192+
if (validationResult.error) {
193+
throw new InvalidTransactionError(
194+
`Move Stake Transaction validation failed: ${validationResult.error.message}`
195+
);
196+
}
197+
}
198+
}
199+
200+
/**
201+
* Construct a transaction to move stake
202+
*
203+
* @param {Interface.MoveStakeArgs} args arguments to be passed to the moveStake method
204+
* @param {Interface.CreateBaseTxInfo} info txn info required to construct the moveStake txn
205+
* @returns {UnsignedTransaction} an unsigned move stake transaction
206+
*/
207+
private moveStake(args: Interface.MoveStakeArgs, info: Interface.CreateBaseTxInfo): UnsignedTransaction {
208+
return defineMethod(
209+
{
210+
method: {
211+
args,
212+
name: 'moveStake',
213+
pallet: 'subtensorModule',
214+
},
215+
...info.baseTxInfo,
216+
},
217+
info.options
218+
);
219+
}
220+
}

0 commit comments

Comments
 (0)