generated from PolymeshAssociation/typescript-boilerplate
-
Notifications
You must be signed in to change notification settings - Fork 11
/
PolymeshTransactionBatch.ts
263 lines (232 loc) · 7.58 KB
/
PolymeshTransactionBatch.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
import { SubmittableExtrinsic } from '@polkadot/api/types';
import { ISubmittableResult } from '@polkadot/types/types';
import BigNumber from 'bignumber.js';
import P from 'bluebird';
import { handleExtrinsicFailure } from '~/base/utils';
import { Context, PolymeshError, PolymeshTransaction, PolymeshTransactionBase } from '~/internal';
import { ErrorCode, MapTxData } from '~/types';
import {
BatchTransactionSpec,
isResolverFunction,
MapTxDataWithFees,
MapTxWithArgs,
TransactionConstructionData,
} from '~/types/internal';
import { transactionToTxTag, u32ToBigNumber } from '~/utils/conversion';
import { filterEventRecords, mergeReceipts } from '~/utils/internal';
/**
* Wrapper class for a batch of Polymesh Transactions
*/
export class PolymeshTransactionBatch<
ReturnValue,
TransformedReturnValue = ReturnValue,
Args extends unknown[][] = unknown[][]
> extends PolymeshTransactionBase<ReturnValue, TransformedReturnValue> {
/**
* @hidden
*/
public static override toTransactionSpec<R, A extends unknown[][], T>(
inputTransaction: PolymeshTransactionBatch<R, T, A>
): BatchTransactionSpec<R, A, T> {
const spec = PolymeshTransactionBase.toTransactionSpec(inputTransaction);
const { transactionData } = inputTransaction;
return {
...spec,
transactions: transactionData.map(({ transaction, args, fee, feeMultiplier }) => ({
transaction,
args,
fee,
feeMultiplier,
})) as MapTxWithArgs<A>,
};
}
/**
* @hidden
*
* underlying transactions to be batched, together with their arguments and other relevant data
*/
private transactionData: MapTxDataWithFees<Args>;
/**
* @hidden
*/
constructor(
transactionSpec: BatchTransactionSpec<ReturnValue, Args, TransformedReturnValue> &
TransactionConstructionData,
context: Context
) {
const { transactions, ...rest } = transactionSpec;
super(rest, context);
this.transactionData = transactions.map(({ transaction, args, feeMultiplier, fee }) => ({
tag: transactionToTxTag(transaction),
args,
feeMultiplier,
transaction,
fee,
})) as MapTxDataWithFees<Args>;
}
/**
* transactions in the batch with their respective arguments
*/
get transactions(): MapTxData<Args> {
return this.transactionData.map(({ tag, args }) => ({
tag,
args,
})) as MapTxData<Args>;
}
/**
* @hidden
*/
protected composeTx(): SubmittableExtrinsic<'promise', ISubmittableResult> {
const {
context: {
polymeshApi: {
tx: { utility },
},
},
} = this;
return utility.batchAll(
this.transactionData.map(({ transaction, args }) => transaction(...args))
);
}
/**
* @hidden
*/
public getProtocolFees(): Promise<BigNumber> {
return P.reduce(
this.transactionData,
async (total, { tag, feeMultiplier = new BigNumber(1), fee }) => {
let fees = fee;
if (!fees) {
[{ fees }] = await this.context.getProtocolFees({ tags: [tag] });
}
return total.plus(fees.multipliedBy(feeMultiplier));
},
new BigNumber(0)
);
}
/**
* @note batches can't be subsidized. If the caller is subsidized, they should use `splitTransactions` and
* run each transaction separately
*/
public supportsSubsidy(): boolean {
return false;
}
/**
* Splits this batch into its individual transactions to be run separately. This is useful if the caller is being subsidized,
* since batches cannot be run by subsidized Accounts
*
* @note the transactions returned by this method must be run in the same order they appear in the array to guarantee the same behavior. If run out of order,
* an error will be thrown. The result that would be obtained by running the batch is returned by running the last transaction in the array
*
* @example
*
* ```typescript
* const createAssetTx = await sdk.assets.createAsset(...);
*
* let ticker: string;
*
* if (isPolymeshTransactionBatch<Asset>(createAssetTx)) {
* const transactions = createAssetTx.splitTransactions();
*
* for (let i = 0; i < length; i += 1) {
* const result = await transactions[i].run();
*
* if (isAsset(result)) {
* ({ticker} = result)
* }
* }
* } else {
* ({ ticker } = await createAssetTx.run());
* }
*
* console.log(`New Asset created! Ticker: ${ticker}`);
* ```
*/
public splitTransactions(): (
| PolymeshTransaction<void>
| PolymeshTransaction<ReturnValue, TransformedReturnValue>
)[] {
const { signingAddress, signer, mortality, context } = this;
const { transactions, resolver, transformer } =
PolymeshTransactionBatch.toTransactionSpec(this);
const receipts: ISubmittableResult[] = [];
const processedIndexes: number[] = [];
return transactions.map(({ transaction, args }, index) => {
const isLast = index === transactions.length - 1;
const spec = {
signer,
signingAddress,
transaction,
args,
mortality,
};
let newTransaction;
/*
* the last transaction's resolver will pass the merged receipt with all events to the batch's original resolver.
* Other transactions will just add their receipts to the list to be merged
*/
if (isLast) {
newTransaction = new PolymeshTransaction(
{
...spec,
resolver: (receipt: ISubmittableResult): ReturnValue | Promise<ReturnValue> => {
if (isResolverFunction(resolver)) {
return resolver(mergeReceipts([...receipts, receipt], context));
}
return resolver;
},
transformer,
},
context
);
} else {
newTransaction = new PolymeshTransaction(
{
...spec,
resolver: (receipt: ISubmittableResult): void => {
processedIndexes.push(index);
receipts.push(receipt);
},
},
context
);
}
const originalRun = newTransaction.run.bind(newTransaction);
newTransaction.run = ((): Promise<TransformedReturnValue> | Promise<void> => {
const expectedIndex = index - 1;
// we throw an error if the transactions aren't being run in order
if (expectedIndex >= 0 && processedIndexes[expectedIndex] !== expectedIndex) {
throw new PolymeshError({
code: ErrorCode.General,
message: 'Transactions resulting from splitting a batch must be run in order',
});
}
return originalRun();
}) as (() => Promise<TransformedReturnValue>) | (() => Promise<void>);
return newTransaction;
});
}
/**
* @hidden
*/
protected override handleExtrinsicSuccess(
resolve: (value: ISubmittableResult | PromiseLike<ISubmittableResult>) => void,
reject: (reason?: unknown) => void,
receipt: ISubmittableResult
): void {
// If one of the transactions in the batch fails, this event gets emitted
const [failed] = filterEventRecords(receipt, 'utility', 'BatchInterrupted', true);
if (failed) {
const {
data: [, failedData],
} = failed;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const failedIndex = u32ToBigNumber((failedData as any)[0]).toNumber();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const dispatchError = (failedData as any)[1];
handleExtrinsicFailure(reject, dispatchError, { failedIndex });
} else {
resolve(receipt);
}
}
}