Skip to content

Commit 648f74d

Browse files
committed
feat: add batchBuilder (bond and nominate) and bondExtraBuilder for polyx
TICKET: SC-1826
1 parent 9bf6627 commit 648f74d

File tree

9 files changed

+962
-0
lines changed

9 files changed

+962
-0
lines changed
Lines changed: 294 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
1+
import { TransactionBuilder, Transaction } from '@bitgo/abstract-substrate';
2+
import { DecodedSignedTx, DecodedSigningPayload, UnsignedTransaction } from '@substrate/txwrapper-core';
3+
import { methods } from '@substrate/txwrapper-polkadot';
4+
import { BaseCoin as CoinConfig } from '@bitgo/statics';
5+
import { BaseAddress, InvalidTransactionError, TransactionType } from '@bitgo/sdk-core';
6+
import { BatchTransactionSchema } from './txnSchema';
7+
import utils from './utils';
8+
import { BatchArgs, BondArgs, NominateArgs } from './iface';
9+
import BigNumber from 'bignumber.js';
10+
11+
export class BatchBuilder extends TransactionBuilder {
12+
// For bond operation
13+
protected _amount: string;
14+
protected _controller: string;
15+
protected _payee: string | { Account: string };
16+
17+
// For nominate operation
18+
protected _validators: string[] = [];
19+
20+
// Batch control
21+
protected _atomic = true; // Default to batchAll (atomic)
22+
23+
constructor(_coinConfig: Readonly<CoinConfig>) {
24+
super(_coinConfig);
25+
this.material(utils.getMaterial(_coinConfig.network.type));
26+
}
27+
28+
protected get transactionType(): TransactionType {
29+
return TransactionType.Batch;
30+
}
31+
32+
/**
33+
* Build a batch transaction that combines bond and nominate operations
34+
*/
35+
protected buildTransaction(): UnsignedTransaction {
36+
const baseTxInfo = this.createBaseTxInfo();
37+
38+
// Create the individual calls
39+
const calls: string[] = [];
40+
41+
// Add bond call if amount is set
42+
if (this._amount) {
43+
const bondCall = methods.staking.bond(
44+
{
45+
controller: this._controller || this._sender,
46+
value: this._amount,
47+
payee: this._payee || 'Staked',
48+
},
49+
baseTxInfo.baseTxInfo,
50+
baseTxInfo.options
51+
);
52+
calls.push(bondCall.method);
53+
}
54+
55+
// Add nominate call if validators are set
56+
if (this._validators.length > 0) {
57+
const nominateCall = methods.staking.nominate(
58+
{
59+
targets: this._validators,
60+
},
61+
baseTxInfo.baseTxInfo,
62+
baseTxInfo.options
63+
);
64+
calls.push(nominateCall.method);
65+
}
66+
67+
// Create batch transaction
68+
if (this._atomic) {
69+
return methods.utility.batchAll(
70+
{
71+
calls,
72+
},
73+
baseTxInfo.baseTxInfo,
74+
baseTxInfo.options
75+
);
76+
} else {
77+
return methods.utility.batch(
78+
{
79+
calls,
80+
},
81+
baseTxInfo.baseTxInfo,
82+
baseTxInfo.options
83+
);
84+
}
85+
}
86+
87+
/**
88+
* Set the staking amount for bond
89+
*/
90+
amount(amount: string): this {
91+
this.validateValue(new BigNumber(amount));
92+
this._amount = amount;
93+
return this;
94+
}
95+
96+
/**
97+
* Get the staking amount
98+
*/
99+
getAmount(): string {
100+
return this._amount;
101+
}
102+
103+
/**
104+
* Set the controller account for bond
105+
*/
106+
controller(controller: BaseAddress): this {
107+
this.validateAddress(controller);
108+
this._controller = controller.address;
109+
return this;
110+
}
111+
112+
/**
113+
* Get the controller address
114+
*/
115+
getController(): string {
116+
return this._controller;
117+
}
118+
119+
/**
120+
* Set the rewards destination for bond ('Staked', 'Stash','Controller', or { Account: string })
121+
*/
122+
payee(payee: string | { Account: string }): this {
123+
if (typeof payee === 'object' && payee.Account) {
124+
this._payee = payee;
125+
} else {
126+
this._payee = payee;
127+
}
128+
return this;
129+
}
130+
131+
/**
132+
* Get the payee
133+
*/
134+
getPayee(): string | { Account: string } {
135+
return this._payee;
136+
}
137+
138+
/**
139+
* Set the validators to nominate
140+
*/
141+
validators(validators: string[]): this {
142+
for (const address of validators) {
143+
this.validateAddress({ address });
144+
}
145+
this._validators = validators;
146+
return this;
147+
}
148+
149+
/**
150+
* Get the validators to nominate
151+
*/
152+
getValidators(): string[] {
153+
return this._validators;
154+
}
155+
156+
/**
157+
* Set whether batch should be atomic (use batchAll) or not (use batch)
158+
* Default is true (batchAll)
159+
*/
160+
atomic(isAtomic: boolean): this {
161+
this._atomic = isAtomic;
162+
return this;
163+
}
164+
165+
/**
166+
* Get whether batch is atomic
167+
*/
168+
isAtomic(): boolean {
169+
return this._atomic;
170+
}
171+
172+
/** @inheritdoc */
173+
validateDecodedTransaction(decodedTxn: DecodedSigningPayload | DecodedSignedTx): void {
174+
const methodName = decodedTxn.method?.name as string;
175+
176+
if (methodName === 'utility.batch' || methodName === 'utility.batchAll') {
177+
const txMethod = decodedTxn.method.args as unknown as BatchArgs;
178+
const calls = txMethod.calls;
179+
180+
for (const call of calls) {
181+
const callMethod = call.method;
182+
183+
if (callMethod === 'staking.bond') {
184+
const bondArgs = call.args as unknown as BondArgs;
185+
this.validateBondArgs(bondArgs);
186+
} else if (callMethod === 'staking.nominate') {
187+
const nominateArgs = call.args as unknown as NominateArgs;
188+
this.validateNominateArgs(nominateArgs);
189+
} else {
190+
throw new InvalidTransactionError(`Invalid call in batch: ${callMethod}`);
191+
}
192+
}
193+
} else {
194+
throw new InvalidTransactionError(`Invalid transaction type: ${methodName}`);
195+
}
196+
}
197+
198+
/**
199+
* Validate bond arguments
200+
*/
201+
private validateBondArgs(args: BondArgs): void {
202+
if (!utils.isValidAddress(args.controller)) {
203+
throw new InvalidTransactionError(
204+
`Invalid bond args: controller address ${args.controller} is not a well-formed address`
205+
);
206+
}
207+
208+
const validationResult = BatchTransactionSchema.validateBond({
209+
value: args.value,
210+
controller: args.controller,
211+
payee: args.payee,
212+
});
213+
214+
if (validationResult.error) {
215+
throw new InvalidTransactionError(`Invalid bond args: ${validationResult.error.message}`);
216+
}
217+
}
218+
219+
/**
220+
* Validate nominate arguments
221+
*/
222+
private validateNominateArgs(args: NominateArgs): void {
223+
const validationResult = BatchTransactionSchema.validateNominate({
224+
validators: args.targets,
225+
});
226+
227+
if (validationResult.error) {
228+
throw new InvalidTransactionError(`Invalid nominate args: ${validationResult.error.message}`);
229+
}
230+
}
231+
232+
/** @inheritdoc */
233+
protected fromImplementation(rawTransaction: string): Transaction {
234+
const tx = super.fromImplementation(rawTransaction);
235+
236+
if ((this._method?.name as string) === 'utility.batchAll') {
237+
this.atomic(true);
238+
} else if ((this._method?.name as string) === 'utility.batch') {
239+
this.atomic(false);
240+
} else {
241+
throw new InvalidTransactionError(
242+
`Invalid Transaction Type: ${this._method?.name}. Expected utility.batch or utility.batchAll`
243+
);
244+
}
245+
246+
if (this._method) {
247+
const txMethod = this._method.args as unknown as BatchArgs;
248+
249+
for (const call of txMethod.calls) {
250+
if (call.method === 'staking.bond') {
251+
const bondArgs = call.args as unknown as BondArgs;
252+
this.amount(bondArgs.value);
253+
this.controller({ address: bondArgs.controller });
254+
this.payee(bondArgs.payee);
255+
} else if (call.method === 'staking.nominate') {
256+
const nominateArgs = call.args as unknown as NominateArgs;
257+
this.validators(nominateArgs.targets);
258+
}
259+
}
260+
}
261+
262+
return tx;
263+
}
264+
265+
/** @inheritdoc */
266+
validateTransaction(tx: Transaction): void {
267+
super.validateTransaction(tx);
268+
this.validateFields();
269+
}
270+
271+
/**
272+
* Validate the builder fields
273+
*/
274+
private validateFields(): void {
275+
const validationResult = BatchTransactionSchema.validate({
276+
amount: this._amount,
277+
controller: this._controller,
278+
payee: this._payee,
279+
validators: this._validators,
280+
});
281+
282+
if (validationResult.error) {
283+
throw new InvalidTransactionError(`Invalid transaction: ${validationResult.error.message}`);
284+
}
285+
}
286+
287+
public testValidateBondArgs(args: BondArgs): void {
288+
return this.validateBondArgs(args);
289+
}
290+
291+
public testValidateNominateArgs(args: NominateArgs): void {
292+
return this.validateNominateArgs(args);
293+
}
294+
}

0 commit comments

Comments
 (0)