Skip to content

Commit 43fc839

Browse files
committed
feat(sdk-coin-tao): add tokenTransferBuilder
Ticket: WIN-6426 TICKET: WIN-6426
1 parent ff8f7f6 commit 43fc839

File tree

6 files changed

+297
-1
lines changed

6 files changed

+297
-1
lines changed

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

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,11 @@ export const MethodNames = {
7474
* Send a batch of dispatch calls and atomically execute them.
7575
*/
7676
BatchAll: 'batchAll' as const,
77+
78+
/**
79+
* Transfer stake from one validator to another.
80+
*/
81+
TransferStake: 'transferStake' as const,
7782
} as const;
7883

7984
/**
@@ -182,6 +187,14 @@ export interface BatchArgs {
182187
calls: BatchCallObject[];
183188
}
184189

190+
export interface TransferStakeArgs extends Args {
191+
destinationColdkey: string;
192+
hotkey: string;
193+
originNetuid: string;
194+
destinationNetuid: string;
195+
alphaAmount: string;
196+
}
197+
185198
/**
186199
* Decoded TxMethod from a transaction hex
187200
*/
@@ -197,7 +210,8 @@ export interface TxMethod {
197210
| ChillArgs
198211
| UnbondArgs
199212
| WithdrawUnbondedArgs
200-
| BatchArgs;
213+
| BatchArgs
214+
| TransferStakeArgs;
201215
name: MethodNamesValues;
202216
pallet: string;
203217
}

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,11 @@ export const UnstakeTransactionSchema = joi.object({
5757
hotkey: joi.string().required(),
5858
netuid: joi.string().required(),
5959
});
60+
61+
export const TransferStakeTransactionSchema = joi.object({
62+
destinationColdkey: addressSchema.required(),
63+
hotkey: addressSchema.required(),
64+
originNetuid: joi.string().required(),
65+
destinationNetuid: joi.string().required(),
66+
alphaAmount: joi.string().required(),
67+
});
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
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 } from '@bitgo/sdk-core';
5+
import { TransactionBuilder, Interface, Schema, Transaction } from '@bitgo/abstract-substrate';
6+
7+
export class TokenTransferBuilder extends TransactionBuilder {
8+
protected _destinationColdkey: string;
9+
protected _hotkey: string;
10+
protected _originNetuid: string;
11+
protected _destinationNetuid: string;
12+
protected _alphaAmount: string;
13+
14+
constructor(_coinConfig: Readonly<CoinConfig>) {
15+
super(_coinConfig);
16+
}
17+
18+
/**
19+
* Construct a transaction to transfer stake
20+
* @returns {UnsignedTransaction} an unsigned transfer stake transaction
21+
*/
22+
protected buildTransaction(): UnsignedTransaction {
23+
const baseTxInfo = this.createBaseTxInfo();
24+
return this.transferStake(
25+
{
26+
destinationColdkey: this._destinationColdkey,
27+
hotkey: this._hotkey,
28+
originNetuid: this._originNetuid,
29+
destinationNetuid: this._destinationNetuid,
30+
alphaAmount: this._alphaAmount,
31+
},
32+
baseTxInfo
33+
);
34+
}
35+
36+
/** @inheritdoc */
37+
protected get transactionType(): TransactionType {
38+
return TransactionType.SendToken;
39+
}
40+
41+
/**
42+
* Set the amount to transfer
43+
* @param {string} amount to transfer
44+
* @returns {TokenTransferBuilder} This builder.
45+
*/
46+
amount(amount: string): this {
47+
this.validateValue(new BigNumber(amount));
48+
this._alphaAmount = amount;
49+
return this;
50+
}
51+
52+
/**
53+
* Set the validator hot key address
54+
* @param {string} hotkey address of validator
55+
* @returns {TokenTransferBuilder} This builder.
56+
*/
57+
hotkey(address: string): this {
58+
this.validateAddress({ address });
59+
this._hotkey = address;
60+
return this;
61+
}
62+
63+
/**
64+
* Set the destination cold key address
65+
* @param {string} address of the destination cold key
66+
* @returns {TokenTransferBuilder} This builder.
67+
*/
68+
69+
destinationColdkey(address: string): this {
70+
this.validateAddress({ address });
71+
this._destinationColdkey = address;
72+
return this;
73+
}
74+
75+
/**
76+
* Set the origin netuid of the subnet (root network is 0)
77+
* @param {string} netuid of subnet
78+
* @returns {TokenTransferBuilder} This builder.
79+
*/
80+
originNetuid(netuid: string): this {
81+
this._originNetuid = netuid;
82+
return this;
83+
}
84+
85+
/**
86+
* Set the destination netuid of the subnet (root network is 0)
87+
* @param {string} netuid of subnet
88+
* @returns {TokenTransferBuilder} This builder.
89+
*/
90+
destinationNetuid(netuid: string): this {
91+
this._destinationNetuid = netuid;
92+
return this;
93+
}
94+
95+
/** @inheritdoc */
96+
protected fromImplementation(rawTransaction: string): Transaction {
97+
const tx = super.fromImplementation(rawTransaction);
98+
if (this._method?.name === Interface.MethodNames.TransferStake) {
99+
const txMethod = this._method.args as Interface.TransferStakeArgs;
100+
this.amount(txMethod.alphaAmount);
101+
this.hotkey(txMethod.hotkey);
102+
this.destinationColdkey(txMethod.destinationColdkey);
103+
this.originNetuid(txMethod.originNetuid);
104+
this.destinationNetuid(txMethod.destinationNetuid);
105+
} else {
106+
throw new InvalidTransactionError(
107+
`Invalid Transaction Type: ${this._method?.name}. Expected ${Interface.MethodNames.TransferStake}`
108+
);
109+
}
110+
return tx;
111+
}
112+
113+
/** @inheritdoc */
114+
validateTransaction(_: Transaction): void {
115+
super.validateTransaction(_);
116+
this.validateFields(
117+
this._destinationColdkey,
118+
this._hotkey,
119+
this._originNetuid,
120+
this._destinationNetuid,
121+
this._alphaAmount
122+
);
123+
}
124+
125+
/**
126+
* Helper method to validate whether tx params have the correct type and format
127+
* @param {string} destinationColdkey destination cold key address
128+
* @param {string} hotkey hotkey address of the validator
129+
* @param {string} originNetuid netuid of the origin subnet
130+
* @param {string} destinationNetuid netuid of the destination subnet
131+
* @param {string} alphaAmount amount to transfer
132+
* @throws {InvalidTransactionError} if validation fails
133+
*/
134+
private validateFields(
135+
destinationColdkey: string,
136+
hotkey: string,
137+
originNetuid: string,
138+
destinationNetuid: string,
139+
alphaAmount: string
140+
): void {
141+
const validationResult = Schema.TransferStakeTransactionSchema.validate({
142+
destinationColdkey,
143+
hotkey,
144+
originNetuid,
145+
destinationNetuid,
146+
alphaAmount,
147+
});
148+
149+
if (validationResult.error) {
150+
throw new InvalidTransactionError(`Transaction validation failed: ${validationResult.error.message}`);
151+
}
152+
}
153+
154+
/** @inheritdoc */
155+
validateDecodedTransaction(decodedTxn: DecodedSigningPayload | DecodedSignedTx, rawTransaction: string): void {
156+
if (decodedTxn.method?.name === Interface.MethodNames.TransferStake) {
157+
const txMethod = decodedTxn.method.args as unknown as Interface.TransferStakeArgs;
158+
159+
const validationResult = Schema.TransferStakeTransactionSchema.validate(txMethod);
160+
if (validationResult.error) {
161+
throw new InvalidTransactionError(`Transfer Transaction validation failed: ${validationResult.error.message}`);
162+
}
163+
}
164+
}
165+
166+
/**
167+
* Construct a transaction to transfer stake
168+
*
169+
* @param {Interface.TransferStakeArgs} args arguments to be passed to the transferStake method
170+
* @param {Interface.CreateBaseTxInfo} info txn info required to construct the transferStake txn
171+
* @returns {UnsignedTransaction} an unsigned stake transaction
172+
*/
173+
174+
private transferStake(args: Interface.TransferStakeArgs, info: Interface.CreateBaseTxInfo): UnsignedTransaction {
175+
return defineMethod(
176+
{
177+
method: {
178+
args,
179+
name: 'transferStake',
180+
pallet: 'subtensorModule',
181+
},
182+
...info.baseTxInfo,
183+
},
184+
info.options
185+
);
186+
}
187+
}

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { TransferBuilder } from './transferBuilder';
66
import utils from './utils';
77
import { StakingBuilder } from './stakingBuilder';
88
import { UnstakeBuilder } from './unstakeBuilder';
9+
import { TokenTransferBuilder } from './tokenTransferBuilder';
910

1011
export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
1112
protected _material: Interface.Material;
@@ -27,6 +28,10 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
2728
return new UnstakeBuilder(this._coinConfig).material(this._material);
2829
}
2930

31+
getTokenTransferBuilder(): TransactionBuilder {
32+
return new TokenTransferBuilder(this._coinConfig).material(this._material);
33+
}
34+
3035
getWalletInitializationBuilder(): void {
3136
throw new NotImplementedError(`walletInitialization for ${this._coinConfig.name} not implemented`);
3237
}
@@ -56,6 +61,8 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
5661
return this.getStakingBuilder();
5762
} else if (methodName === Interface.MethodNames.RemoveStake) {
5863
return this.getUnstakingBuilder();
64+
} else if (methodName === Interface.MethodNames.TransferStake) {
65+
return this.getTokenTransferBuilder();
5966
} else {
6067
throw new NotSupported('Transaction cannot be parsed or has an unsupported transaction type');
6168
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,10 @@ export const rawTx = {
175175
unsigned:
176176
'0x9806129f7b0675db59d19b4bd9c8c72eaabba75a9863d02b30115b8b3c3ca5c20f025422130000d501210300c823000009000000e143f23803ac50e8f6f8e62695d1ce9e4e1d68aa36c1cd2cfd15340213f3423e149799bc9602cb5cf201f3425fb8d253b2d4e61fc119dcab3249f307f594754d',
177177
},
178+
transferStake: {
179+
unsigned:
180+
'0xdd02840061b18c6dc02ddcabdeac56cb4f21a971cc41cc97640f6f85b073480008c53a0d00aadae7fa1f53e7a5c900b330ff71bee6782cf3c29a2c6f9599162381cd021ad581c74ded89f49ec79adefed64af8ff16649553523dda9cb4f017cbf15681e50ed5012103000007569f7b0675db59d19b4bd9c8c72eaabba75a9863d02b30115b8b3c3ca5c20f02548a90be061598f4b592afbd546bcb6beadb3c02f5c129df2e11b698f9543dbd41010002000300000000002000',
181+
},
178182
};
179183

180184
export const jsonTransactions = {
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import assert from 'assert';
2+
import should from 'should';
3+
import { spy, assert as SinonAssert } from 'sinon';
4+
import { accounts, mockTssSignature, rawTx } from '../../resources';
5+
import { buildTestConfig } from './base';
6+
import utils from '../../../src/lib/utils';
7+
import { TokenTransferBuilder } from '../../../src/lib/tokenTransferBuilder';
8+
9+
describe('Tao Token Transfer Builder', function () {
10+
const referenceBlock = '0x149799bc9602cb5cf201f3425fb8d253b2d4e61fc119dcab3249f307f594754d';
11+
let builder: TokenTransferBuilder;
12+
const sender = accounts.account1;
13+
14+
beforeEach(function () {
15+
const config = buildTestConfig();
16+
const material = utils.getMaterial(config.network.type);
17+
builder = new TokenTransferBuilder(config).material(material);
18+
});
19+
20+
describe('setter validation', function () {
21+
it('should validate amount', function () {
22+
const spyValidateValue = spy(builder, 'validateValue');
23+
assert.throws(
24+
() => builder.amount('-1'),
25+
(e: Error) => e.message === 'Value cannot be less than zero'
26+
);
27+
should.doesNotThrow(() => builder.amount('1000'));
28+
SinonAssert.calledTwice(spyValidateValue);
29+
});
30+
31+
it('should validate address', function () {
32+
const spyValidateAddress = spy(builder, 'validateAddress');
33+
assert.throws(
34+
() => builder.hotkey('abc'),
35+
(e: Error) => e.message === `The address 'abc' is not a well-formed dot address`
36+
);
37+
assert.throws(
38+
() => builder.destinationColdkey('abc'),
39+
(e: Error) => e.message === `The address 'abc' is not a well-formed dot address`
40+
);
41+
should.doesNotThrow(() => builder.hotkey('5FCPTnjevGqAuTttetBy4a24Ej3pH9fiQ8fmvP1ZkrVsLUoT'));
42+
should.doesNotThrow(() => builder.destinationColdkey('5FCPTnjevGqAuTttetBy4a24Ej3pH9fiQ8fmvP1ZkrVsLUoT'));
43+
44+
SinonAssert.callCount(spyValidateAddress, 4);
45+
});
46+
});
47+
48+
describe('build transfer stake transaction', function () {
49+
it('should build a transfer stake transaction', async function () {
50+
builder
51+
.amount('9007199254740995')
52+
.destinationColdkey('5Ffp1wJCPu4hzVDTo7XaMLqZSvSadyUQmxWPDw74CBjECSoq')
53+
.hotkey('5FCPTnjevGqAuTttetBy4a24Ej3pH9fiQ8fmvP1ZkrVsLUoT')
54+
.originNetuid('1')
55+
.destinationNetuid('2')
56+
.sender({ address: sender.address })
57+
.validity({ firstValid: 3933, maxDuration: 64 })
58+
.referenceBlock(referenceBlock)
59+
.sequenceId({ name: 'Nonce', keyword: 'nonce', value: 200 })
60+
.fee({ amount: 0, type: 'tip' })
61+
.addSignature({ pub: sender.publicKey }, Buffer.from(mockTssSignature, 'hex'));
62+
63+
const tx = await builder.build();
64+
const serializedTx = tx.toBroadcastFormat();
65+
serializedTx.should.equal(rawTx.transferStake.unsigned);
66+
});
67+
68+
it('should re-build from raw signed tx', async function () {
69+
builder.from(rawTx.transferStake.unsigned);
70+
builder.validity({ firstValid: 3933, maxDuration: 64 }).referenceBlock(referenceBlock);
71+
const tx = await builder.build();
72+
const serializedTx = tx.toBroadcastFormat();
73+
serializedTx.should.equal(rawTx.transferStake.unsigned);
74+
});
75+
});
76+
});

0 commit comments

Comments
 (0)