Skip to content

Commit 68ad0d3

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

File tree

10 files changed

+860
-1
lines changed

10 files changed

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

0 commit comments

Comments
 (0)