Skip to content

Commit b4f30be

Browse files
authored
Merge pull request #6282 from BitGo/SC-2108
feat: add decoding support for polyx staking related txns
2 parents f1a1d61 + f680b01 commit b4f30be

File tree

14 files changed

+776
-344
lines changed

14 files changed

+776
-344
lines changed

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

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,39 @@ export enum MethodNames {
4141
AddStake = 'addStake',
4242
RemoveStake = 'removeStake',
4343

44+
/**
45+
* Take the origin account as a stash and lock up value of its balance.
46+
*/
47+
Bond = 'bond',
48+
/**
49+
* Add some extra amount that have appeared in the stash free_balance into the balance up for staking.
50+
*/
51+
BondExtra = 'bondExtra',
52+
/**
53+
* Declare the desire to nominate targets for the origin controller.
54+
*/
55+
Nominate = 'nominate',
56+
/**
57+
* Declare no desire to either validate or nominate.
58+
*/
59+
Chill = 'chill',
60+
/**
61+
* Schedule a portion of the stash to be unlocked ready for transfer out after the bond period ends.
62+
*/
63+
Unbond = 'unbond',
64+
/**
65+
* Remove any unlocked chunks from the unlocking queue from our management.
66+
*/
67+
WithdrawUnbonded = 'withdrawUnbonded',
68+
/**
69+
* Send a batch of dispatch calls.
70+
*/
71+
Batch = 'batch',
72+
/**
73+
* Send a batch of dispatch calls and atomically execute them.
74+
*/
75+
BatchAll = 'batchAll',
76+
4477
/**
4578
* Registers a Decentralized Identifier (DID) along with Customer Due Diligence (CDD) information.
4679
*
@@ -71,6 +104,8 @@ export interface TxData {
71104
payee?: string;
72105
keepAlive?: boolean;
73106
netuid?: string;
107+
numSlashingSpans?: number;
108+
batchCalls?: BatchCallObject[];
74109
}
75110

76111
/**
@@ -107,11 +142,57 @@ export interface RemoveStakeArgs extends Args {
107142
netuid: string;
108143
}
109144

145+
export interface BondArgs extends Args {
146+
value: string;
147+
controller: string;
148+
payee: string | { Account: string };
149+
}
150+
151+
export interface BondExtraArgs extends Args {
152+
maxAdditional: string;
153+
}
154+
155+
export interface NominateArgs extends Args {
156+
targets: string[];
157+
}
158+
159+
export interface ChillArgs extends Args {
160+
[key: string]: never; // Chill has no arguments
161+
}
162+
163+
export interface UnbondArgs extends Args {
164+
value: string;
165+
}
166+
167+
export interface WithdrawUnbondedArgs extends Args {
168+
numSlashingSpans: number;
169+
}
170+
171+
export interface BatchCallObject {
172+
method: string;
173+
args: Record<string, unknown>;
174+
}
175+
176+
export interface BatchArgs {
177+
calls: BatchCallObject[];
178+
}
179+
110180
/**
111181
* Decoded TxMethod from a transaction hex
112182
*/
113183
export interface TxMethod {
114-
args: TransferArgs | TransferAllArgs | AddStakeArgs | RemoveStakeArgs;
184+
args:
185+
| TransferArgs
186+
| TransferAllArgs
187+
| AddStakeArgs
188+
| RemoveStakeArgs
189+
| BondArgs
190+
| BondExtraArgs
191+
| NominateArgs
192+
| ChillArgs
193+
| UnbondArgs
194+
| WithdrawUnbondedArgs
195+
| BatchArgs;
115196
name: MethodNames;
116197
pallet: string;
117198
}

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

Lines changed: 124 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export class Transaction extends BaseTransaction {
2626
protected _chainName: string;
2727
protected _sender: string;
2828

29-
private static FAKE_SIGNATURE = `0x${Buffer.from(new Uint8Array(256).fill(1)).toString('hex')}`;
29+
private static FAKE_SIGNATURE = `0x${Buffer.from(new Uint8Array(256).fill(1)).toString('hex')}` as HexString;
3030

3131
constructor(coinConfig: Readonly<CoinConfig>) {
3232
super(coinConfig);
@@ -77,7 +77,7 @@ export class Transaction extends BaseTransaction {
7777
addSignature(signature: string): void {
7878
this._signedTransaction = utils.serializeSignedTransaction(
7979
this._substrateTransaction,
80-
signature,
80+
signature as HexString,
8181
this._substrateTransaction.metadataRpc,
8282
this._registry
8383
);
@@ -177,6 +177,11 @@ export class Transaction extends BaseTransaction {
177177
result.to = keypairDest.getAddress(this.getAddressFormat());
178178
result.amount = txMethod.amountStaked.toString();
179179
result.netuid = txMethod.netuid;
180+
} else if (utils.isBond(txMethod)) {
181+
result.amount = txMethod.value;
182+
result.payee = typeof txMethod.payee === 'string' ? txMethod.payee : txMethod.payee.Account;
183+
} else if (utils.isBondExtra(txMethod)) {
184+
result.amount = txMethod.maxAdditional;
180185
}
181186
} else if (this.type === TransactionType.StakingDeactivate) {
182187
if (utils.isRemoveStake(txMethod)) {
@@ -187,6 +192,28 @@ export class Transaction extends BaseTransaction {
187192
result.to = keypairDest.getAddress(this.getAddressFormat());
188193
result.amount = txMethod.amountUnstaked.toString();
189194
result.netuid = txMethod.netuid;
195+
} else if (utils.isUnbond(txMethod)) {
196+
result.amount = txMethod.value;
197+
} else if (utils.isWithdrawUnbonded(txMethod)) {
198+
result.numSlashingSpans = txMethod.numSlashingSpans;
199+
}
200+
} else if (this.type === TransactionType.Batch) {
201+
if (utils.isBatch(txMethod)) {
202+
result.batchCalls = txMethod.calls;
203+
// Extract amount from batch calls for display
204+
if (txMethod.calls && txMethod.calls.length === 2) {
205+
const firstCall = txMethod.calls[0];
206+
const secondCall = txMethod.calls[1];
207+
if (firstCall.method === 'bond' && secondCall.method === 'nominate') {
208+
// Staking batch: bond + nominate
209+
const bondArgs = firstCall.args as Record<string, unknown>;
210+
result.amount = bondArgs.value as string;
211+
} else if (firstCall.method === 'chill' && secondCall.method === 'unbond') {
212+
// Unstaking batch: chill + unbond
213+
const unbondArgs = secondCall.args as Record<string, unknown>;
214+
result.amount = unbondArgs.value as string;
215+
}
216+
}
190217
}
191218
}
192219

@@ -277,6 +304,8 @@ export class Transaction extends BaseTransaction {
277304
this.decodeInputsAndOutputsForStakingActivate(decodedTx);
278305
} else if (this.type === TransactionType.StakingDeactivate) {
279306
this.decodeInputsAndOutputsForStakingDeactivate(decodedTx);
307+
} else if (this.type === TransactionType.Batch) {
308+
this.decodeInputsAndOutputsForBatch(decodedTx);
280309
}
281310
}
282311

@@ -331,6 +360,14 @@ export class Transaction extends BaseTransaction {
331360
to = keypairDest.getAddress(this.getAddressFormat());
332361
value = txMethod.amountStaked.toString();
333362
from = decodedTx.address;
363+
} else if (utils.isBond(txMethod)) {
364+
to = decodedTx.address; // For bond, funds are locked in the same account
365+
value = txMethod.value;
366+
from = decodedTx.address;
367+
} else if (utils.isBondExtra(txMethod)) {
368+
to = decodedTx.address; // For bond extra, funds are locked in the same account
369+
value = txMethod.maxAdditional;
370+
from = decodedTx.address;
334371
} else {
335372
throw new ParseTransactionError(`Loading inputs of unknown StakingActivate type parameters`);
336373
}
@@ -363,6 +400,14 @@ export class Transaction extends BaseTransaction {
363400
to = keypairDest.getAddress(this.getAddressFormat());
364401
value = txMethod.amountUnstaked.toString();
365402
from = decodedTx.address;
403+
} else if (utils.isUnbond(txMethod)) {
404+
to = decodedTx.address; // For unbond, funds are unlocked from the same account
405+
value = txMethod.value;
406+
from = decodedTx.address;
407+
} else if (utils.isWithdrawUnbonded(txMethod)) {
408+
to = decodedTx.address; // For withdraw unbonded, funds are returned to the same account
409+
value = '0'; // Amount is not specified in withdraw unbonded
410+
from = decodedTx.address;
366411
} else {
367412
throw new ParseTransactionError(`Loading inputs of unknown StakingDeactivate type parameters`);
368413
}
@@ -383,6 +428,83 @@ export class Transaction extends BaseTransaction {
383428
];
384429
}
385430

431+
private decodeInputsAndOutputsForBatch(decodedTx: DecodedTx) {
432+
const txMethod = decodedTx.method.args;
433+
const sender = decodedTx.address;
434+
this._inputs = [];
435+
this._outputs = [];
436+
437+
if (utils.isBatch(txMethod)) {
438+
if (!txMethod.calls) {
439+
throw new InvalidTransactionError('failed to decode calls from batch transaction');
440+
}
441+
// Handle different types of batch operations
442+
let totalStakingValue = '0';
443+
let hasStakingOperations = false;
444+
let hasUnstakingOperations = false;
445+
446+
for (const call of txMethod.calls) {
447+
// Handle both possible formats: simple method names or callIndex with registry lookup
448+
let methodName: string;
449+
450+
if (typeof call.method === 'string') {
451+
methodName = call.method;
452+
} else {
453+
try {
454+
const callIndex = call.method as string;
455+
const decodedCall = this._registry.findMetaCall(
456+
new Uint8Array(Buffer.from(callIndex.replace('0x', ''), 'hex'))
457+
);
458+
methodName = decodedCall.method;
459+
} catch (e) {
460+
methodName = call.method as string;
461+
}
462+
}
463+
464+
if (methodName === 'bond') {
465+
const args = call.args as Record<string, unknown>;
466+
const value = (args.value as string) || '0';
467+
totalStakingValue = value;
468+
hasStakingOperations = true;
469+
} else if (methodName === 'chill') {
470+
hasUnstakingOperations = true;
471+
} else if (methodName === 'unbond') {
472+
const args = call.args as Record<string, unknown>;
473+
const value = (args.value as string) || '0';
474+
totalStakingValue = value;
475+
hasUnstakingOperations = true;
476+
}
477+
}
478+
479+
// For staking batch operations (bond + nominate or bondExtra + nominate)
480+
if (hasStakingOperations && !hasUnstakingOperations) {
481+
this._inputs.push({
482+
address: sender,
483+
value: totalStakingValue,
484+
coin: this._coinConfig.name,
485+
});
486+
this._outputs.push({
487+
address: sender, // For staking, funds are locked in the same account
488+
value: totalStakingValue,
489+
coin: this._coinConfig.name,
490+
});
491+
}
492+
// For unstaking batch operations (chill + unbond)
493+
else if (hasUnstakingOperations && !hasStakingOperations) {
494+
this._inputs.push({
495+
address: sender,
496+
value: totalStakingValue,
497+
coin: this._coinConfig.name,
498+
});
499+
this._outputs.push({
500+
address: sender, // For unstaking, funds are unlocked from the same account
501+
value: totalStakingValue,
502+
coin: this._coinConfig.name,
503+
});
504+
}
505+
}
506+
}
507+
386508
/**
387509
* Constructs a signed payload using construct.signTx
388510
* This method will be called during the build step if a TSS signature

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

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,22 @@ import bs58 from 'bs58';
1313
import base32 from 'hi-base32';
1414
import nacl from 'tweetnacl';
1515
import { KeyPair } from '.';
16-
import { HexString, Material, TransferAllArgs, TransferArgs, TxMethod, AddStakeArgs, RemoveStakeArgs } from './iface';
16+
import {
17+
HexString,
18+
Material,
19+
TransferAllArgs,
20+
TransferArgs,
21+
TxMethod,
22+
AddStakeArgs,
23+
RemoveStakeArgs,
24+
BondArgs,
25+
BondExtraArgs,
26+
NominateArgs,
27+
ChillArgs,
28+
UnbondArgs,
29+
WithdrawUnbondedArgs,
30+
BatchArgs,
31+
} from './iface';
1732

1833
export class Utils implements BaseUtils {
1934
/** @inheritdoc */
@@ -143,7 +158,12 @@ export class Utils implements BaseUtils {
143158
* @param registry Transaction registry
144159
* @returns string Serialized transaction
145160
*/
146-
serializeSignedTransaction(transaction, signature, metadataRpc: `0x${string}`, registry): string {
161+
serializeSignedTransaction(
162+
transaction: UnsignedTransaction,
163+
signature: HexString,
164+
metadataRpc: `0x${string}`,
165+
registry: TypeRegistry
166+
): string {
147167
return construct.signedTx(transaction, signature, {
148168
metadataRpc,
149169
registry,
@@ -211,6 +231,38 @@ export class Utils implements BaseUtils {
211231
);
212232
}
213233

234+
isBond(arg: TxMethod['args']): arg is BondArgs {
235+
return (
236+
(arg as BondArgs).value !== undefined &&
237+
(arg as BondArgs).controller !== undefined &&
238+
(arg as BondArgs).payee !== undefined
239+
);
240+
}
241+
242+
isBondExtra(arg: TxMethod['args']): arg is BondExtraArgs {
243+
return (arg as BondExtraArgs).maxAdditional !== undefined;
244+
}
245+
246+
isNominate(arg: TxMethod['args']): arg is NominateArgs {
247+
return (arg as NominateArgs).targets !== undefined;
248+
}
249+
250+
isChill(arg: TxMethod['args']): arg is ChillArgs {
251+
return true; // Chill has no arguments, so any object can be considered ChillArgs
252+
}
253+
254+
isUnbond(arg: TxMethod['args']): arg is UnbondArgs {
255+
return (arg as UnbondArgs).value !== undefined;
256+
}
257+
258+
isWithdrawUnbonded(arg: TxMethod['args']): arg is WithdrawUnbondedArgs {
259+
return (arg as WithdrawUnbondedArgs).numSlashingSpans !== undefined;
260+
}
261+
262+
isBatch(arg: TxMethod['args']): arg is BatchArgs {
263+
return (arg as BatchArgs).calls !== undefined && Array.isArray((arg as BatchArgs).calls);
264+
}
265+
214266
/**
215267
* extracts and returns the signature in hex format given a raw signed transaction
216268
*

0 commit comments

Comments
 (0)