@polymeshassociation/polymesh-sdk 31.0.0 → 31.1.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.yarn/install-state.gz +0 -0
- package/CHANGELOG.md +2 -136
- package/api/client/AccountManagement.d.ts +48 -0
- package/api/client/AccountManagement.d.ts.map +1 -1
- package/api/client/AccountManagement.js +38 -0
- package/api/client/AccountManagement.js.map +1 -1
- package/api/client/Network.d.ts +79 -2
- package/api/client/Network.d.ts.map +1 -1
- package/api/client/Network.js +143 -12
- package/api/client/Network.js.map +1 -1
- package/api/client/types.d.ts +6 -0
- package/api/client/types.d.ts.map +1 -1
- package/api/client/types.js +6 -0
- package/api/client/types.js.map +1 -1
- package/api/entities/Account/index.d.ts +35 -1
- package/api/entities/Account/index.d.ts.map +1 -1
- package/api/entities/Account/index.js +89 -0
- package/api/entities/Account/index.js.map +1 -1
- package/api/entities/Account/types.d.ts +60 -1
- package/api/entities/Account/types.d.ts.map +1 -1
- package/api/entities/Account/types.js +25 -1
- package/api/entities/Account/types.js.map +1 -1
- package/api/entities/Asset/NonFungible/NftCollection.js +2 -2
- package/api/entities/Asset/NonFungible/NftCollection.js.map +1 -1
- package/api/procedures/issueNft.d.ts +1 -0
- package/api/procedures/issueNft.d.ts.map +1 -1
- package/api/procedures/issueNft.js +24 -6
- package/api/procedures/issueNft.js.map +1 -1
- package/api/procedures/toggleEvmAccountMapping.d.ts +18 -0
- package/api/procedures/toggleEvmAccountMapping.d.ts.map +1 -0
- package/api/procedures/toggleEvmAccountMapping.js +80 -0
- package/api/procedures/toggleEvmAccountMapping.js.map +1 -0
- package/api/procedures/types.d.ts +44 -0
- package/api/procedures/types.d.ts.map +1 -1
- package/api/procedures/types.js.map +1 -1
- package/base/Context.d.ts +33 -0
- package/base/Context.d.ts.map +1 -1
- package/base/Context.js +59 -1
- package/base/Context.js.map +1 -1
- package/base/PolymeshTransactionBase.d.ts +199 -3
- package/base/PolymeshTransactionBase.d.ts.map +1 -1
- package/base/PolymeshTransactionBase.js +595 -137
- package/base/PolymeshTransactionBase.js.map +1 -1
- package/base/Procedure.d.ts.map +1 -1
- package/base/Procedure.js +2 -0
- package/base/Procedure.js.map +1 -1
- package/base/ethTransaction.d.ts +222 -0
- package/base/ethTransaction.d.ts.map +1 -0
- package/base/ethTransaction.js +317 -0
- package/base/ethTransaction.js.map +1 -0
- package/base/types.d.ts +66 -1
- package/base/types.d.ts.map +1 -1
- package/base/types.js.map +1 -1
- package/base/utils.d.ts +60 -11
- package/base/utils.d.ts.map +1 -1
- package/base/utils.js +328 -45
- package/base/utils.js.map +1 -1
- package/internal.d.ts +1 -0
- package/internal.d.ts.map +1 -1
- package/internal.js +4 -2
- package/internal.js.map +1 -1
- package/npm-package/polymeshassociation-polymesh-sdk-31.1.0-beta.2.tgz +0 -0
- package/package.json +2 -2
- package/types/internal.d.ts +38 -1
- package/types/internal.d.ts.map +1 -1
- package/types/internal.js.map +1 -1
- package/utils/eth.d.ts +63 -0
- package/utils/eth.d.ts.map +1 -0
- package/utils/eth.js +209 -0
- package/utils/eth.js.map +1 -0
- package/utils/internal.d.ts +19 -1
- package/utils/internal.d.ts.map +1 -1
- package/utils/internal.js +34 -0
- package/utils/internal.js.map +1 -1
- package/utils/typeguards.d.ts +10 -0
- package/utils/typeguards.d.ts.map +1 -1
- package/utils/typeguards.js +11 -0
- package/utils/typeguards.js.map +1 -1
- package/npm-package/polymeshassociation-polymesh-sdk-31.0.0.tgz +0 -0
|
@@ -16,6 +16,7 @@ exports.PolymeshTransactionBase = void 0;
|
|
|
16
16
|
/* eslint-disable simple-import-sort/imports */
|
|
17
17
|
const events_1 = require("events");
|
|
18
18
|
const bignumber_js_1 = __importDefault(require("bignumber.js"));
|
|
19
|
+
const ethTransaction_1 = require("../base/ethTransaction");
|
|
19
20
|
const utils_1 = require("../base/utils");
|
|
20
21
|
const internal_1 = require("../internal");
|
|
21
22
|
const common_1 = require("../middleware/queries/common");
|
|
@@ -23,6 +24,7 @@ const types_1 = require("../types");
|
|
|
23
24
|
const internal_2 = require("../types/internal");
|
|
24
25
|
const constants_1 = require("../utils/constants");
|
|
25
26
|
const conversion_1 = require("../utils/conversion");
|
|
27
|
+
const eth_1 = require("../utils/eth");
|
|
26
28
|
const internal_3 = require("../utils/internal");
|
|
27
29
|
/**
|
|
28
30
|
* @hidden
|
|
@@ -68,11 +70,12 @@ class PolymeshTransactionBase {
|
|
|
68
70
|
* whether the queue has run or not (prevents re-running)
|
|
69
71
|
*/
|
|
70
72
|
this.hasRun = false;
|
|
71
|
-
const { resolver, transformer, signingAddress, signer, paidForBy, mortality, multiSig, multiSigOpts, preRunValidation, } = transactionSpec;
|
|
73
|
+
const { resolver, transformer, signingAddress, signer, paidForBy, mortality, multiSig, multiSigOpts, submission, preRunValidation, } = transactionSpec;
|
|
72
74
|
this.signingAddress = signingAddress;
|
|
73
75
|
this.multiSig = multiSig !== null && multiSig !== void 0 ? multiSig : null;
|
|
74
76
|
this.mortality = mortality;
|
|
75
77
|
this.multiSigOpts = multiSigOpts !== null && multiSigOpts !== void 0 ? multiSigOpts : {};
|
|
78
|
+
this.submissionOpts = submission !== null && submission !== void 0 ? submission : {};
|
|
76
79
|
this.signer = signer;
|
|
77
80
|
this.context = context;
|
|
78
81
|
this.paidForBy = paidForBy;
|
|
@@ -145,18 +148,7 @@ class PolymeshTransactionBase {
|
|
|
145
148
|
}
|
|
146
149
|
yield this.assertFeesCovered();
|
|
147
150
|
const receipt = yield this.internalRun();
|
|
148
|
-
this.receipt
|
|
149
|
-
const { resolver, transformer = (val) => Promise.resolve(val), } = this;
|
|
150
|
-
let value;
|
|
151
|
-
if ((0, internal_2.isResolverFunction)(resolver)) {
|
|
152
|
-
value = yield resolver(receipt);
|
|
153
|
-
}
|
|
154
|
-
else {
|
|
155
|
-
value = resolver;
|
|
156
|
-
}
|
|
157
|
-
this._result = yield transformer(value);
|
|
158
|
-
this.updateStatus(types_1.TransactionStatus.Succeeded);
|
|
159
|
-
return this._result;
|
|
151
|
+
return yield this.resolveResult(receipt);
|
|
160
152
|
}
|
|
161
153
|
catch (err) {
|
|
162
154
|
this.handleRunError(err);
|
|
@@ -166,6 +158,149 @@ class PolymeshTransactionBase {
|
|
|
166
158
|
}
|
|
167
159
|
});
|
|
168
160
|
}
|
|
161
|
+
/**
|
|
162
|
+
* Broadcast the transaction and return as soon as it has been accepted, without waiting for it
|
|
163
|
+
* to be included in a block. This is {@link run} split at its natural seam — the returned
|
|
164
|
+
* handle's `watch` performs the second half and yields the exact value `run` would have — so
|
|
165
|
+
* `await (await tx.broadcast()).watch()` is equivalent to `await tx.run()`
|
|
166
|
+
*
|
|
167
|
+
* Use it when waiting is not wanted, or not safe to depend on: an Ethereum wallet that
|
|
168
|
+
* broadcasts on the user's behalf, a long finalization window a UI should not block on, or a
|
|
169
|
+
* flow that persists the hash and resumes tracking later via
|
|
170
|
+
* {@link api/client/Network!Network.watchTransaction | network.watchTransaction}
|
|
171
|
+
*
|
|
172
|
+
* @note this always submits without an extrinsic status subscription, since that channel cannot
|
|
173
|
+
* report a hash without also waiting for the result. The trade-off is that `Future` status
|
|
174
|
+
* reporting and in-pool `Aborted` detection are unavailable — those come from the node's
|
|
175
|
+
* status stream, which only {@link run} attaches to
|
|
176
|
+
*
|
|
177
|
+
* @throws if the transaction has already been run or broadcast
|
|
178
|
+
* @throws `NotSupported` if the signing Account is a MultiSig signer. Use `runAsProposal`, which
|
|
179
|
+
* needs the finalized receipt to report the resulting proposal
|
|
180
|
+
*/
|
|
181
|
+
broadcast() {
|
|
182
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
183
|
+
const { context } = this;
|
|
184
|
+
if (this.hasRun) {
|
|
185
|
+
throw new internal_1.PolymeshError({
|
|
186
|
+
code: types_1.ErrorCode.General,
|
|
187
|
+
message: 'Cannot re-run a Transaction',
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
if (this.multiSig) {
|
|
191
|
+
throw new internal_1.PolymeshError({
|
|
192
|
+
code: types_1.ErrorCode.NotSupported,
|
|
193
|
+
message: '`.broadcast` cannot be used with a MultiSig signer, since the resulting proposal can only be read from the finalized transaction. `.runAsProposal` should be called instead',
|
|
194
|
+
data: { signingAddress: this.signingAddress, multiSigAddress: this.multiSig.address },
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
try {
|
|
198
|
+
if (this.preRunValidation) {
|
|
199
|
+
yield this.preRunValidation({ asProposal: false });
|
|
200
|
+
}
|
|
201
|
+
yield this.assertFeesCovered();
|
|
202
|
+
const startingBlock = yield context.getLatestBlock();
|
|
203
|
+
const { matcher } = yield this.broadcastToChain(yield this.buildPollingSubmission());
|
|
204
|
+
return Object.assign(Object.assign({
|
|
205
|
+
// set by `broadcastToChain`, which cannot succeed without it
|
|
206
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
207
|
+
txHash: this.txHash }, (this.ethTxHash !== undefined ? { ethTxHash: this.ethTxHash } : {})), { startingBlock, watch: opts => this.watchBroadcast(matcher, startingBlock, opts === null || opts === void 0 ? void 0 : opts.timeout) });
|
|
208
|
+
}
|
|
209
|
+
catch (err) {
|
|
210
|
+
this.handleRunError(err);
|
|
211
|
+
}
|
|
212
|
+
finally {
|
|
213
|
+
/*
|
|
214
|
+
* marked here rather than via `markAsRan`, which would also start the middleware sync. There
|
|
215
|
+
* is nothing for the middleware to have synced until `watch` sees a block, so that emit
|
|
216
|
+
* waits until then
|
|
217
|
+
*/
|
|
218
|
+
this.hasRun = true;
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* @hidden
|
|
224
|
+
*
|
|
225
|
+
* The second half of {@link broadcast}: wait for inclusion, then produce the same value `run`
|
|
226
|
+
* would have. Retryable after a timeout, since nothing is resubmitted — the block scan simply
|
|
227
|
+
* starts over from the same point
|
|
228
|
+
*/
|
|
229
|
+
watchBroadcast(matcher, startingBlock, timeout) {
|
|
230
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
231
|
+
if (this.isSuccess) {
|
|
232
|
+
// already watched to completion; scanning again would only rediscover the same block
|
|
233
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
234
|
+
return this._result;
|
|
235
|
+
}
|
|
236
|
+
if (this.watching) {
|
|
237
|
+
throw new internal_1.PolymeshError({
|
|
238
|
+
code: types_1.ErrorCode.General,
|
|
239
|
+
message: 'This transaction is already being watched. Await the promise returned by the previous `watch` call instead of starting another',
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
const watching = (() => __awaiter(this, void 0, void 0, function* () {
|
|
243
|
+
try {
|
|
244
|
+
const receipt = yield this.watchForInclusion(matcher, startingBlock, timeout);
|
|
245
|
+
const result = yield this.resolveResult(receipt);
|
|
246
|
+
this.startMiddlewareSync();
|
|
247
|
+
return result;
|
|
248
|
+
}
|
|
249
|
+
catch (err) {
|
|
250
|
+
this.handleRunError(err);
|
|
251
|
+
}
|
|
252
|
+
}))();
|
|
253
|
+
this.watching = watching;
|
|
254
|
+
try {
|
|
255
|
+
return yield watching;
|
|
256
|
+
}
|
|
257
|
+
finally {
|
|
258
|
+
this.watching = undefined;
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* @hidden
|
|
264
|
+
*
|
|
265
|
+
* Route to the polling-style submission for whichever signing path this transaction uses. The
|
|
266
|
+
* returned function is what actually touches the signer, so that {@link broadcastToChain} can
|
|
267
|
+
* bound it
|
|
268
|
+
*/
|
|
269
|
+
buildPollingSubmission() {
|
|
270
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
271
|
+
const { signingAddress, context } = this;
|
|
272
|
+
yield context.assertHasSigningAddress(signingAddress);
|
|
273
|
+
if ((0, eth_1.isEthDerivedAddress)(signingAddress, context.ss58Format)) {
|
|
274
|
+
const { ethSigner, request } = yield this.buildEthRequest();
|
|
275
|
+
return this.buildEthPollingSubmission((0, ethTransaction_1.getEthSubmissionStrategy)(ethSigner), request);
|
|
276
|
+
}
|
|
277
|
+
const { txWithArgs, signerOptions } = this.buildNativeSignerData();
|
|
278
|
+
return () => Promise.resolve(this.buildNativePollingSubmission(txWithArgs, signerOptions));
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* @hidden
|
|
283
|
+
*
|
|
284
|
+
* Turn a finalized receipt into the value the caller gets back, recording it and moving the
|
|
285
|
+
* transaction to `Succeeded`. Shared by `run` and by the `watch` returned from
|
|
286
|
+
* {@link broadcast}, so that both produce the same value for the same transaction
|
|
287
|
+
*/
|
|
288
|
+
resolveResult(receipt) {
|
|
289
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
290
|
+
this.receipt = receipt;
|
|
291
|
+
const { resolver, transformer = (val) => Promise.resolve(val), } = this;
|
|
292
|
+
let value;
|
|
293
|
+
if ((0, internal_2.isResolverFunction)(resolver)) {
|
|
294
|
+
value = yield resolver(receipt);
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
value = resolver;
|
|
298
|
+
}
|
|
299
|
+
this._result = yield transformer(value);
|
|
300
|
+
this.updateStatus(types_1.TransactionStatus.Succeeded);
|
|
301
|
+
return this._result;
|
|
302
|
+
});
|
|
303
|
+
}
|
|
169
304
|
/**
|
|
170
305
|
* @hidden
|
|
171
306
|
*/
|
|
@@ -181,6 +316,14 @@ class PolymeshTransactionBase {
|
|
|
181
316
|
this.updateStatus(types_1.TransactionStatus.Rejected);
|
|
182
317
|
break;
|
|
183
318
|
}
|
|
319
|
+
case types_1.ErrorCode.TransactionTimeout: {
|
|
320
|
+
/*
|
|
321
|
+
* the transaction was not cancelled, the SDK merely stopped waiting for it. Moving to
|
|
322
|
+
* `Failed` would assert something untrue — it may still be included in a block — so the
|
|
323
|
+
* last observed status (`Running`, or `InBlock` if it was already seen) stands
|
|
324
|
+
*/
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
184
327
|
case types_1.ErrorCode.TransactionReverted:
|
|
185
328
|
case types_1.ErrorCode.FatalError:
|
|
186
329
|
default: {
|
|
@@ -195,6 +338,17 @@ class PolymeshTransactionBase {
|
|
|
195
338
|
*/
|
|
196
339
|
markAsRan() {
|
|
197
340
|
this.hasRun = true;
|
|
341
|
+
this.startMiddlewareSync();
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* @hidden
|
|
345
|
+
*
|
|
346
|
+
* Kick off the middleware sync notification. Separate from {@link markAsRan} because
|
|
347
|
+
* {@link broadcast} marks the transaction as ran without having waited for a block — there is
|
|
348
|
+
* nothing for the middleware to have synced yet at that point, so the emit waits until `watch`
|
|
349
|
+
* completes
|
|
350
|
+
*/
|
|
351
|
+
startMiddlewareSync() {
|
|
198
352
|
/*
|
|
199
353
|
* We do not await this promise because it is supposed to run in the background, and
|
|
200
354
|
* any errors encountered are emitted. If the user isn't listening, they shouldn't
|
|
@@ -211,145 +365,373 @@ class PolymeshTransactionBase {
|
|
|
211
365
|
*/
|
|
212
366
|
internalRun() {
|
|
213
367
|
return __awaiter(this, void 0, void 0, function* () {
|
|
214
|
-
|
|
215
|
-
const { signingAddress, signer, mortality, context } = this;
|
|
368
|
+
const { signingAddress, context } = this;
|
|
216
369
|
yield context.assertHasSigningAddress(signingAddress);
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
this.
|
|
221
|
-
const txWithArgs = this.composeTx();
|
|
222
|
-
const signerOptions = Object.assign(Object.assign({ nonce,
|
|
223
|
-
/*
|
|
224
|
-
* allows signers (e.g. Ledger devices signing via the generic app) to return a modified
|
|
225
|
-
* `signedTransaction`, which is required when signed extensions like `CheckMetadataHash` alter the payload
|
|
226
|
-
*/
|
|
227
|
-
withSignedTransaction: true,
|
|
228
|
-
/*
|
|
229
|
-
* a returned `signedTransaction` may only alter signed extensions (e.g. `mode`, `metadataHash`).
|
|
230
|
-
* The API will reject any submission where the signer changed the call data itself
|
|
231
|
-
*/
|
|
232
|
-
allowCallDataAlteration: false }, (signer && { signer })), (era !== undefined ? { era } : {}));
|
|
370
|
+
if ((0, eth_1.isEthDerivedAddress)(signingAddress, context.ss58Format)) {
|
|
371
|
+
return this.internalRunEth();
|
|
372
|
+
}
|
|
373
|
+
const { txWithArgs, signerOptions } = this.buildNativeSignerData();
|
|
233
374
|
if (context.supportsSubscription()) {
|
|
234
|
-
return
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
const { status } = receipt;
|
|
238
|
-
let isLastCallback = false;
|
|
239
|
-
let unsubscribing = Promise.resolve();
|
|
240
|
-
let extrinsicFailedEvent;
|
|
241
|
-
if (status.isFuture) {
|
|
242
|
-
this.updateStatus(types_1.TransactionStatus.Future);
|
|
243
|
-
}
|
|
244
|
-
else if (receipt.isCompleted) {
|
|
245
|
-
// isCompleted implies status is one of: isFinalized, isInBlock or isError
|
|
246
|
-
if (receipt.isInBlock) {
|
|
247
|
-
const inBlockHash = status.asInBlock;
|
|
248
|
-
/*
|
|
249
|
-
* this must be done to ensure that the block hash and number are set before the success event
|
|
250
|
-
* is emitted, and at the same time. We do not resolve or reject the containing promise until this
|
|
251
|
-
* one resolves
|
|
252
|
-
*/
|
|
253
|
-
settingBlockData = (0, internal_3.defusePromise)(this.context.polymeshApi.rpc.chain.getBlock(inBlockHash).then(({ block }) => {
|
|
254
|
-
this.blockHash = (0, conversion_1.hashToString)(inBlockHash);
|
|
255
|
-
this.blockNumber = (0, conversion_1.u32ToBigNumber)(block.header.number.unwrap());
|
|
256
|
-
// we know that the index has to be set by the time the transaction is included in a block
|
|
257
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
258
|
-
this.txIndex = new bignumber_js_1.default(receipt.txIndex);
|
|
259
|
-
this.updateStatus(types_1.TransactionStatus.InBlock);
|
|
260
|
-
}));
|
|
261
|
-
// if the extrinsic failed due to an on-chain error, we should handle it in a special way
|
|
262
|
-
[extrinsicFailedEvent] = (0, internal_3.filterEventRecords)(receipt, 'system', 'ExtrinsicFailed', true);
|
|
263
|
-
// extrinsic failed so we can unsubscribe
|
|
264
|
-
isLastCallback = !!extrinsicFailedEvent;
|
|
265
|
-
}
|
|
266
|
-
else {
|
|
267
|
-
// isFinalized || isError so we know we can unsubscribe
|
|
268
|
-
isLastCallback = true;
|
|
269
|
-
}
|
|
270
|
-
if (isLastCallback) {
|
|
271
|
-
unsubscribing = gettingUnsub.then((unsub) => {
|
|
272
|
-
unsub();
|
|
273
|
-
});
|
|
274
|
-
}
|
|
275
|
-
/*
|
|
276
|
-
* Promise chain that handles all sub-promises in this pass through the signAndSend callback.
|
|
277
|
-
* Primarily for consistent error handling
|
|
278
|
-
*/
|
|
279
|
-
let finishing = Promise.resolve();
|
|
280
|
-
if (extrinsicFailedEvent) {
|
|
281
|
-
const { data } = extrinsicFailedEvent;
|
|
282
|
-
finishing = Promise.all([settingBlockData, unsubscribing]).then(() => {
|
|
283
|
-
const error = (0, utils_1.handleExtrinsicFailure)(data[0]);
|
|
284
|
-
reject(error);
|
|
285
|
-
});
|
|
286
|
-
}
|
|
287
|
-
else if (receipt.isFinalized) {
|
|
288
|
-
finishing = Promise.all([settingBlockData, unsubscribing]).then(() => {
|
|
289
|
-
this.handleExtrinsicSuccess(resolve, reject, receipt);
|
|
290
|
-
});
|
|
291
|
-
}
|
|
292
|
-
else if (receipt.isError) {
|
|
293
|
-
reject(new internal_1.PolymeshError({ code: types_1.ErrorCode.TransactionAborted }));
|
|
294
|
-
}
|
|
295
|
-
finishing.catch((err) => reject(err));
|
|
296
|
-
}
|
|
297
|
-
});
|
|
298
|
-
gettingUnsub
|
|
299
|
-
.then(() => {
|
|
300
|
-
// tx approved by signer
|
|
301
|
-
this.setIsRunningStatus(txWithArgs.hash.toString());
|
|
302
|
-
})
|
|
303
|
-
.catch((err) => {
|
|
304
|
-
const error = (0, utils_1.handleTransactionSubmissionError)(err);
|
|
305
|
-
reject(new internal_1.PolymeshError(error));
|
|
306
|
-
});
|
|
375
|
+
return this.runViaSubscription({
|
|
376
|
+
subscribe: callback => txWithArgs.signAndSend(signingAddress, signerOptions, callback),
|
|
377
|
+
getTxHash: () => txWithArgs.hash.toString(),
|
|
307
378
|
});
|
|
308
379
|
}
|
|
309
|
-
|
|
310
|
-
return this.runViaPolling(txWithArgs, signerOptions);
|
|
311
|
-
}
|
|
380
|
+
return this.runViaPolling(() => Promise.resolve(this.buildNativePollingSubmission(txWithArgs, signerOptions)));
|
|
312
381
|
});
|
|
313
382
|
}
|
|
314
383
|
/**
|
|
315
384
|
* @hidden
|
|
316
385
|
*
|
|
317
|
-
*
|
|
318
|
-
*
|
|
386
|
+
* Compose the transaction and assemble the options the native signer is called with. Also moves
|
|
387
|
+
* the transaction into `Unapproved`, since from here on the signer is what everything waits on
|
|
319
388
|
*/
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
389
|
+
buildNativeSignerData() {
|
|
390
|
+
var _a;
|
|
391
|
+
const { signer, mortality, context } = this;
|
|
392
|
+
// era is how many blocks the transaction remains valid for, `undefined` for default
|
|
393
|
+
const era = mortality.immortal ? 0 : (_a = mortality.lifetime) === null || _a === void 0 ? void 0 : _a.toNumber();
|
|
394
|
+
const nonce = context.getNonce().toNumber();
|
|
395
|
+
this.updateStatus(types_1.TransactionStatus.Unapproved);
|
|
396
|
+
const txWithArgs = this.composeTx();
|
|
397
|
+
const signerOptions = Object.assign(Object.assign({ nonce,
|
|
324
398
|
/*
|
|
325
|
-
*
|
|
326
|
-
* `signedTransaction
|
|
327
|
-
* submitted extrinsic's hash can differ from the locally composed one
|
|
399
|
+
* allows signers (e.g. Ledger devices signing via the generic app) to return a modified
|
|
400
|
+
* `signedTransaction`, which is required when signed extensions like `CheckMetadataHash` alter the payload
|
|
328
401
|
*/
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
402
|
+
withSignedTransaction: true,
|
|
403
|
+
/*
|
|
404
|
+
* a returned `signedTransaction` may only alter signed extensions (e.g. `mode`, `metadataHash`).
|
|
405
|
+
* The API will reject any submission where the signer changed the call data itself
|
|
406
|
+
*/
|
|
407
|
+
allowCallDataAlteration: false }, (signer && { signer })), (era !== undefined ? { era } : {}));
|
|
408
|
+
return { txWithArgs, signerOptions };
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* @hidden
|
|
412
|
+
*
|
|
413
|
+
* The native "send once and correlate by hash" submission. Used when the connection has no
|
|
414
|
+
* subscription support, and by {@link broadcast}, which cannot use the callback-driven
|
|
415
|
+
* subscription at all since that never yields control back before the transaction resolves
|
|
416
|
+
*/
|
|
417
|
+
buildNativePollingSubmission(txWithArgs, signerOptions) {
|
|
418
|
+
const { signingAddress } = this;
|
|
419
|
+
return {
|
|
420
|
+
send: () => __awaiter(this, void 0, void 0, function* () {
|
|
421
|
+
/*
|
|
422
|
+
* the resolved hash is used instead of `txWithArgs.hash` because a signer may return a modified
|
|
423
|
+
* `signedTransaction` (e.g. Ledger devices signing via the generic app), in which case the
|
|
424
|
+
* submitted extrinsic's hash can differ from the locally composed one
|
|
425
|
+
*/
|
|
426
|
+
const submittedTxHash = yield txWithArgs.signAndSend(signingAddress, signerOptions);
|
|
427
|
+
return {
|
|
428
|
+
txHash: submittedTxHash.toString(),
|
|
429
|
+
matcher: (0, utils_1.extrinsicHashMatcher)(submittedTxHash),
|
|
430
|
+
};
|
|
431
|
+
}),
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* @hidden
|
|
436
|
+
*
|
|
437
|
+
* @throws `ValidationError` if the caller explicitly requested a mortality setting.
|
|
438
|
+
* Ethereum transactions have no era; replay protection comes from the nonce and chain id, so
|
|
439
|
+
* `ProcedureOpts.mortality` is meaningless on this path and silently ignoring a request for
|
|
440
|
+
* immortality that the transport cannot honour would be worse than a clear error
|
|
441
|
+
*/
|
|
442
|
+
assertMortalitySupportedForEth() {
|
|
443
|
+
const { mortality } = this;
|
|
444
|
+
const wasExplicitlySet = mortality.immortal || mortality.lifetime !== undefined;
|
|
445
|
+
if (wasExplicitlySet) {
|
|
446
|
+
throw new internal_1.PolymeshError({
|
|
447
|
+
code: types_1.ErrorCode.ValidationError,
|
|
448
|
+
message: 'Mortality cannot be set for a transaction signed by an Ethereum key. Ethereum transactions have no era; replay protection comes from the nonce and chain id, so they are effectively immortal until the nonce is consumed',
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* @hidden
|
|
454
|
+
*
|
|
455
|
+
* Ethereum submission strategy: build the request (performing the mandatory dry run
|
|
456
|
+
* pre-flight), then either broadcast a raw signed transaction from the SDK, or let the
|
|
457
|
+
* wallet broadcast and correlate the result by scanning blocks for a matching
|
|
458
|
+
* `revive.ethTransact` extrinsic
|
|
459
|
+
*/
|
|
460
|
+
internalRunEth() {
|
|
461
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
462
|
+
const { context } = this;
|
|
463
|
+
const { ethSigner, request } = yield this.buildEthRequest();
|
|
464
|
+
const strategy = (0, ethTransaction_1.getEthSubmissionStrategy)(ethSigner);
|
|
465
|
+
if (strategy.mode === 'sdkBroadcast' && context.supportsSubscription()) {
|
|
466
|
+
const rawSignedTx = yield strategy.signTransaction(request);
|
|
467
|
+
const { subscription } = (0, ethTransaction_1.buildSdkBroadcastSubmission)(context, rawSignedTx);
|
|
468
|
+
return this.runViaSubscription(subscription);
|
|
469
|
+
}
|
|
470
|
+
return this.runViaPolling(this.buildEthPollingSubmission(strategy, request));
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* @hidden
|
|
475
|
+
*
|
|
476
|
+
* The eager half of the Ethereum path: validate, compose the call and perform the mandatory dry
|
|
477
|
+
* run pre-flight. Deliberately kept out of the broadcast phase — it is chain RPC work, not a
|
|
478
|
+
* signer interaction, so a failure here is a real error rather than something a timeout should
|
|
479
|
+
* describe as "state unknown"
|
|
480
|
+
*/
|
|
481
|
+
buildEthRequest() {
|
|
482
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
483
|
+
const { context, signingAddress } = this;
|
|
484
|
+
this.assertMortalitySupportedForEth();
|
|
485
|
+
const ethSigner = context.getEthSigner();
|
|
486
|
+
if (!ethSigner) {
|
|
487
|
+
throw new internal_1.PolymeshError({
|
|
488
|
+
code: types_1.ErrorCode.General,
|
|
489
|
+
message: 'There is no Ethereum signer associated with the SDK instance. Please report this to the Polymesh team',
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
this.updateStatus(types_1.TransactionStatus.Unapproved);
|
|
493
|
+
const composedTx = this.composeTx();
|
|
494
|
+
const nonceValue = context.getNonce();
|
|
495
|
+
const nonceOverride = nonceValue.isNegative() ? undefined : nonceValue;
|
|
496
|
+
const { request } = yield (0, ethTransaction_1.buildEthTransactionRequest)(Object.assign({ context,
|
|
497
|
+
signingAddress,
|
|
498
|
+
composedTx,
|
|
499
|
+
ethSigner }, (nonceOverride ? { nonce: nonceOverride } : {})));
|
|
500
|
+
return { ethSigner, request };
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* @hidden
|
|
505
|
+
*
|
|
506
|
+
* The deferred half of the Ethereum path. Everything that touches the signer lives in here, so
|
|
507
|
+
* that the broadcast timeout covers the wallet confirmation prompt in both modes: the raw
|
|
508
|
+
* signing in `sdkBroadcast`, and the sign-and-send in `walletBroadcast`
|
|
509
|
+
*/
|
|
510
|
+
buildEthPollingSubmission(strategy, request) {
|
|
511
|
+
const { context } = this;
|
|
512
|
+
if (strategy.mode === 'sdkBroadcast') {
|
|
513
|
+
const { signTransaction } = strategy;
|
|
514
|
+
return () => __awaiter(this, void 0, void 0, function* () {
|
|
515
|
+
const rawSignedTx = yield signTransaction(request);
|
|
516
|
+
return (0, ethTransaction_1.buildSdkBroadcastSubmission)(context, rawSignedTx).polling;
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
/*
|
|
520
|
+
* The wallet broadcasts and returns the Ethereum transaction hash. The SDK submitted
|
|
521
|
+
* nothing, so there is no extrinsic-status subscription to attach to and the result has to
|
|
522
|
+
* be located by scanning blocks for the matching `revive.ethTransact` extrinsic
|
|
523
|
+
*/
|
|
524
|
+
const walletBroadcastSubmission = (0, ethTransaction_1.buildWalletBroadcastSubmission)(strategy.sendTransaction, request);
|
|
525
|
+
return () => Promise.resolve({
|
|
526
|
+
send: () => __awaiter(this, void 0, void 0, function* () {
|
|
527
|
+
const result = yield walletBroadcastSubmission.send();
|
|
528
|
+
// set as soon as the hash is known, even if the transaction later reverts or is not found
|
|
529
|
+
this.ethTxHash = result.txHash;
|
|
530
|
+
return result;
|
|
531
|
+
}),
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* @hidden
|
|
536
|
+
*
|
|
537
|
+
* Submit the transaction and track it through a subscription, updating the transaction's
|
|
538
|
+
* status and block data as the node reports progress. All lifecycle bookkeeping lives here,
|
|
539
|
+
* so the only thing a submission strategy has to provide is *how* the transaction reaches
|
|
540
|
+
* the chain
|
|
541
|
+
*/
|
|
542
|
+
runViaSubscription(submission) {
|
|
543
|
+
const { subscribe, getTxHash } = submission;
|
|
544
|
+
return new Promise((resolve, reject) => {
|
|
545
|
+
let settingBlockData = Promise.resolve();
|
|
546
|
+
const gettingUnsub = subscribe(receipt => {
|
|
547
|
+
const { status } = receipt;
|
|
548
|
+
let isLastCallback = false;
|
|
549
|
+
let unsubscribing = Promise.resolve();
|
|
550
|
+
let failureError;
|
|
551
|
+
if (status.isFuture) {
|
|
552
|
+
this.updateStatus(types_1.TransactionStatus.Future);
|
|
553
|
+
}
|
|
554
|
+
else if (receipt.isCompleted) {
|
|
555
|
+
// isCompleted implies status is one of: isFinalized, isInBlock or isError
|
|
556
|
+
if (receipt.isInBlock) {
|
|
557
|
+
const inBlockHash = status.asInBlock;
|
|
558
|
+
/*
|
|
559
|
+
* this must be done to ensure that the block hash and number are set before the success event
|
|
560
|
+
* is emitted, and at the same time. We do not resolve or reject the containing promise until this
|
|
561
|
+
* one resolves
|
|
562
|
+
*/
|
|
563
|
+
settingBlockData = (0, internal_3.defusePromise)(this.context.polymeshApi.rpc.chain.getBlock(inBlockHash).then(({ block }) => {
|
|
564
|
+
this.blockHash = (0, conversion_1.hashToString)(inBlockHash);
|
|
565
|
+
this.blockNumber = (0, conversion_1.u32ToBigNumber)(block.header.number.unwrap());
|
|
566
|
+
// we know that the index has to be set by the time the transaction is included in a block
|
|
567
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
568
|
+
this.txIndex = new bignumber_js_1.default(receipt.txIndex);
|
|
569
|
+
this.updateStatus(types_1.TransactionStatus.InBlock);
|
|
570
|
+
}));
|
|
571
|
+
// if the extrinsic failed due to an on-chain error, we should handle it in a special way
|
|
572
|
+
failureError = this.getReceiptFailure(receipt);
|
|
573
|
+
// extrinsic failed so we can unsubscribe
|
|
574
|
+
isLastCallback = !!failureError;
|
|
575
|
+
}
|
|
576
|
+
else {
|
|
577
|
+
// isFinalized || isError so we know we can unsubscribe
|
|
578
|
+
isLastCallback = true;
|
|
579
|
+
}
|
|
580
|
+
if (isLastCallback) {
|
|
581
|
+
unsubscribing = gettingUnsub.then((unsub) => {
|
|
582
|
+
unsub();
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
/*
|
|
586
|
+
* Promise chain that handles all sub-promises in this pass through the signAndSend callback.
|
|
587
|
+
* Primarily for consistent error handling
|
|
588
|
+
*/
|
|
589
|
+
let finishing = Promise.resolve();
|
|
590
|
+
if (failureError) {
|
|
591
|
+
const error = failureError;
|
|
592
|
+
finishing = Promise.all([settingBlockData, unsubscribing]).then(() => {
|
|
593
|
+
reject(error);
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
else if (receipt.isFinalized) {
|
|
597
|
+
finishing = Promise.all([settingBlockData, unsubscribing]).then(() => {
|
|
598
|
+
this.handleExtrinsicSuccess(resolve, reject, receipt);
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
else if (receipt.isError) {
|
|
602
|
+
reject(new internal_1.PolymeshError({ code: types_1.ErrorCode.TransactionAborted }));
|
|
603
|
+
}
|
|
604
|
+
finishing.catch((err) => reject(err));
|
|
605
|
+
}
|
|
606
|
+
});
|
|
607
|
+
gettingUnsub
|
|
608
|
+
.then(() => {
|
|
609
|
+
// tx approved by signer
|
|
610
|
+
this.setIsRunningStatus(getTxHash());
|
|
334
611
|
})
|
|
335
612
|
.catch((err) => {
|
|
336
613
|
const error = (0, utils_1.handleTransactionSubmissionError)(err);
|
|
337
|
-
|
|
614
|
+
reject(new internal_1.PolymeshError(error));
|
|
338
615
|
});
|
|
339
|
-
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* @hidden
|
|
620
|
+
*
|
|
621
|
+
* Submit the transaction, then locate it by scanning the chain's blocks. Used when there is no
|
|
622
|
+
* extrinsic status subscription to attach to — either because the connection does not support
|
|
623
|
+
* subscriptions, or because the transaction was broadcast by an Ethereum wallet rather than
|
|
624
|
+
* by the SDK
|
|
625
|
+
*/
|
|
626
|
+
runViaPolling(getSubmission) {
|
|
627
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
628
|
+
const startingBlock = yield this.context.getLatestBlock();
|
|
629
|
+
const { matcher } = yield this.broadcastToChain(getSubmission);
|
|
630
|
+
return this.watchForInclusion(matcher, startingBlock);
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* @hidden
|
|
635
|
+
*
|
|
636
|
+
* Hand the transaction to its signer and get it broadcast, yielding the hash it can be looked up
|
|
637
|
+
* by and the predicate that recognizes it in a block. This is the phase a wallet confirmation
|
|
638
|
+
* prompt happens in, so it is what `submission.broadcastTimeout` bounds
|
|
639
|
+
*
|
|
640
|
+
* @throws `TransactionTimeout` if the signer does not broadcast in time. Note this says nothing
|
|
641
|
+
* about whether the transaction was broadcast — only that the SDK gave up waiting to hear
|
|
642
|
+
*/
|
|
643
|
+
broadcastToChain(getSubmission) {
|
|
644
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
645
|
+
const { submissionOpts: { broadcastTimeout }, } = this;
|
|
646
|
+
const sending = (() => __awaiter(this, void 0, void 0, function* () {
|
|
647
|
+
const submission = yield getSubmission();
|
|
648
|
+
return submission.send();
|
|
649
|
+
}))();
|
|
650
|
+
const result = yield (0, internal_3.withTimeout)(sending, broadcastTimeout, () => new internal_1.PolymeshError({
|
|
651
|
+
code: types_1.ErrorCode.TransactionTimeout,
|
|
652
|
+
message: 'The signer did not broadcast the transaction within the allotted time. It was not cancelled — whether it ends up being broadcast is unknown, so check before submitting again, or the same transaction may be submitted twice',
|
|
653
|
+
data: { broadcastTimeout },
|
|
654
|
+
})).catch((err) => {
|
|
655
|
+
/*
|
|
656
|
+
* a timeout is already a well formed error describing exactly what happened. Only genuine
|
|
657
|
+
* submission failures need translating (e.g. a signer cancellation)
|
|
658
|
+
*/
|
|
659
|
+
if (err instanceof internal_1.PolymeshError && err.code === types_1.ErrorCode.TransactionTimeout) {
|
|
660
|
+
throw err;
|
|
661
|
+
}
|
|
662
|
+
throw (0, utils_1.handleTransactionSubmissionError)(err);
|
|
663
|
+
});
|
|
664
|
+
this.setIsRunningStatus(result.txHash);
|
|
665
|
+
return result;
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* @hidden
|
|
670
|
+
*
|
|
671
|
+
* Locate the broadcast transaction by scanning blocks, updating this transaction's block data as
|
|
672
|
+
* it goes. Bounded by `submission.watchTimeout`, or by the `timeout` passed to the `watch`
|
|
673
|
+
* returned from {@link broadcast}, which takes precedence
|
|
674
|
+
*
|
|
675
|
+
* @throws `TransactionTimeout` if the transaction is not found in time. The transaction is
|
|
676
|
+
* unaffected by this and may still be included in a block afterwards
|
|
677
|
+
*/
|
|
678
|
+
watchForInclusion(matcher, startingBlock, timeout) {
|
|
679
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
680
|
+
const { context, submissionOpts } = this;
|
|
681
|
+
const watchTimeout = timeout !== null && timeout !== void 0 ? timeout : submissionOpts.watchTimeout;
|
|
682
|
+
/*
|
|
683
|
+
* report inclusion as soon as the transaction lands in a block, rather than leaving the caller
|
|
684
|
+
* with no feedback for the whole finalization window. This matters most for an Ethereum
|
|
685
|
+
* transaction the wallet broadcast, where block scanning is the only signal
|
|
686
|
+
* available
|
|
687
|
+
*/
|
|
688
|
+
const onInBlock = ({ blockHash, blockNumber, txIndex }) => {
|
|
689
|
+
this.blockHash = blockHash;
|
|
690
|
+
this.blockNumber = blockNumber;
|
|
691
|
+
this.txIndex = new bignumber_js_1.default(txIndex);
|
|
692
|
+
this.updateStatus(types_1.TransactionStatus.InBlock);
|
|
693
|
+
};
|
|
694
|
+
/*
|
|
695
|
+
* subscribing is strictly better where the connection allows it: blocks arrive as they are
|
|
696
|
+
* produced instead of on a poll tick, and it removes a repeating `getHeader`/`getBlock`
|
|
697
|
+
* cycle per in-flight transaction. Polling remains the fallback for HTTP connections, which
|
|
698
|
+
* is what it was always intended for
|
|
699
|
+
*/
|
|
700
|
+
const scanning = context.supportsSubscription()
|
|
701
|
+
? (0, utils_1.subscribeForTransactionFinalization)(matcher, startingBlock, context, onInBlock)
|
|
702
|
+
: (0, utils_1.pollForTransactionFinalization)(matcher, startingBlock, context, undefined, onInBlock);
|
|
703
|
+
const finalizedReceipt = yield (0, internal_3.withTimeout)(scanning, watchTimeout, () => new internal_1.PolymeshError({
|
|
704
|
+
code: types_1.ErrorCode.TransactionTimeout,
|
|
705
|
+
message: 'The transaction was broadcast but was not found in a finalized block within the allotted time. It has not been cancelled and may still be included — it can be tracked by hash with `network.watchTransaction`',
|
|
706
|
+
data: {
|
|
707
|
+
txHash: this.txHash,
|
|
708
|
+
ethTxHash: this.ethTxHash,
|
|
709
|
+
startingBlock,
|
|
710
|
+
watchTimeout,
|
|
711
|
+
},
|
|
712
|
+
}));
|
|
340
713
|
this.blockHash = (0, conversion_1.hashToString)(finalizedReceipt.status.asFinalized);
|
|
341
714
|
this.blockNumber = (0, conversion_1.u32ToBigNumber)(finalizedReceipt.blockNumber);
|
|
342
715
|
this.txIndex = new bignumber_js_1.default(finalizedReceipt.txIndex);
|
|
343
716
|
// if the extrinsic failed due to an on-chain error, we should handle it in a special way
|
|
344
|
-
const
|
|
345
|
-
if (
|
|
346
|
-
|
|
347
|
-
const error = (0, utils_1.handleExtrinsicFailure)(data[0]);
|
|
348
|
-
throw error;
|
|
717
|
+
const failureError = this.getReceiptFailure(finalizedReceipt);
|
|
718
|
+
if (failureError) {
|
|
719
|
+
throw failureError;
|
|
349
720
|
}
|
|
350
721
|
return finalizedReceipt;
|
|
351
722
|
});
|
|
352
723
|
}
|
|
724
|
+
/**
|
|
725
|
+
* @hidden
|
|
726
|
+
*
|
|
727
|
+
* Inspect a receipt for an on-chain failure, returning the corresponding error if the
|
|
728
|
+
* transaction did not succeed. Checks both `system.ExtrinsicFailed` (the native path) and
|
|
729
|
+
* `revive.EthExtrinsicRevert` (the Ethereum signing path, where the outer extrinsic reports
|
|
730
|
+
* success even when the inner dispatch failed)
|
|
731
|
+
*/
|
|
732
|
+
getReceiptFailure(receipt) {
|
|
733
|
+
return (0, utils_1.getExtrinsicFailure)(receipt);
|
|
734
|
+
}
|
|
353
735
|
/**
|
|
354
736
|
* Subscribe to status changes
|
|
355
737
|
*
|
|
@@ -372,19 +754,26 @@ class PolymeshTransactionBase {
|
|
|
372
754
|
*
|
|
373
755
|
* @note these values might be inaccurate if the transaction is run at a later time. This can be due to a governance vote or other
|
|
374
756
|
* chain related factors (like modifications to a specific subsidizer relationship or a chain upgrade)
|
|
757
|
+
* @note for a transaction signed by an Ethereum key, the `gas` component is derived from
|
|
758
|
+
* `ethGas * gasPrice / nativeToEthRatio` (a dry run over the `revive` pallet) rather than
|
|
759
|
+
* `payment_queryInfo`, and so may differ slightly from the equivalent native call
|
|
375
760
|
*/
|
|
376
761
|
getTotalFees() {
|
|
377
762
|
return __awaiter(this, arguments, void 0, function* (asProposal = true) {
|
|
378
|
-
const { signingAddress } = this;
|
|
763
|
+
const { signingAddress, context } = this;
|
|
379
764
|
const composedTx = this.composeTxForFees(asProposal);
|
|
380
|
-
const
|
|
765
|
+
const isEthSigner = (0, eth_1.isEthDerivedAddress)(signingAddress, context.ss58Format);
|
|
766
|
+
const gasPromise = isEthSigner
|
|
767
|
+
? (0, ethTransaction_1.dryRunEthTransaction)(context, signingAddress, composedTx).then(({ rawEthGas, gasPrice }) => (0, ethTransaction_1.calculateEthGasFee)(rawEthGas, gasPrice, (0, ethTransaction_1.getNativeToEthRatio)(context)))
|
|
768
|
+
: composedTx
|
|
769
|
+
.paymentInfo(signingAddress)
|
|
770
|
+
.then(({ partialFee }) => (0, conversion_1.balanceToBigNumber)(partialFee));
|
|
381
771
|
const protocol = yield this.getProtocolFees();
|
|
382
772
|
const payingAccount = yield this.getPayingAccount(asProposal);
|
|
383
|
-
const [
|
|
384
|
-
|
|
773
|
+
const [gas, { free: balance }] = yield Promise.all([
|
|
774
|
+
gasPromise,
|
|
385
775
|
payingAccount.account.getBalance(),
|
|
386
776
|
]);
|
|
387
|
-
const gas = (0, conversion_1.balanceToBigNumber)(partialFee);
|
|
388
777
|
return {
|
|
389
778
|
fees: {
|
|
390
779
|
protocol,
|
|
@@ -601,6 +990,13 @@ class PolymeshTransactionBase {
|
|
|
601
990
|
return __awaiter(this, arguments, void 0, function* (metadata = {}, asProposal = true) {
|
|
602
991
|
var _a, _b, _c, _d;
|
|
603
992
|
const { mortality, signingAddress, context, context: { polymeshApi }, } = this;
|
|
993
|
+
if ((0, eth_1.isEthDerivedAddress)(signingAddress, context.ss58Format)) {
|
|
994
|
+
throw new internal_1.PolymeshError({
|
|
995
|
+
code: types_1.ErrorCode.NotSupported,
|
|
996
|
+
message: 'A SCALE `SignerPayload` cannot be produced for a transaction signed by an Ethereum key, since an Ethereum key cannot sign it. Use `toEthSignablePayload` instead',
|
|
997
|
+
data: { signingAddress },
|
|
998
|
+
});
|
|
999
|
+
}
|
|
604
1000
|
const tx = this.composeTxForFees(asProposal);
|
|
605
1001
|
const [tipHash, latestBlockNumber] = yield Promise.all([
|
|
606
1002
|
polymeshApi.rpc.chain.getFinalizedHead(),
|
|
@@ -646,6 +1042,47 @@ class PolymeshTransactionBase {
|
|
|
646
1042
|
};
|
|
647
1043
|
});
|
|
648
1044
|
}
|
|
1045
|
+
/**
|
|
1046
|
+
* Returns a representation intended for offline/detached signing of a transaction signed by an
|
|
1047
|
+
* Ethereum-derived Account. Unlocks Fireblocks / KMS / HSM / air-gapped custody flows: the
|
|
1048
|
+
* caller signs the returned `transaction` and submits the raw signed bytes via
|
|
1049
|
+
* {@link api/client/Network!Network.submitEthTransaction | sdk.network.submitEthTransaction}
|
|
1050
|
+
*
|
|
1051
|
+
* @param metadata - Additional information attached to the payload, such as IDs or memos about the transaction
|
|
1052
|
+
* @param opts.eip1559 - whether to build an EIP-1559 (type 2) transaction. Defaults to `true`;
|
|
1053
|
+
* pass `false` for a custody service or hardware signer that can only encode legacy (type 0)
|
|
1054
|
+
* transactions
|
|
1055
|
+
*
|
|
1056
|
+
* @note no Signing Manager is required. The whole point of this method is detached signing, so
|
|
1057
|
+
* it must work on an SDK instance connected without one — the payload is built entirely from
|
|
1058
|
+
* chain state and the signing address
|
|
1059
|
+
* @note every field of `transaction` is 0x-prefixed hex, never a number or bigint — the only
|
|
1060
|
+
* encoding that survives being serialized and sent to a remote signer. ethers and viem both
|
|
1061
|
+
* need it converted first; `@polymeshassociation/eth-signing-manager` exports
|
|
1062
|
+
* `toEthersTransaction` / `toViemTransaction` for that, usable without the manager itself
|
|
1063
|
+
*
|
|
1064
|
+
* @throws `ValidationError` if the signing Account is not Ethereum-derived
|
|
1065
|
+
*/
|
|
1066
|
+
toEthSignablePayload() {
|
|
1067
|
+
return __awaiter(this, arguments, void 0, function* (metadata = {}, opts = {}) {
|
|
1068
|
+
const { signingAddress, context } = this;
|
|
1069
|
+
const { eip1559 = true } = opts;
|
|
1070
|
+
if (!(0, eth_1.isEthDerivedAddress)(signingAddress, context.ss58Format)) {
|
|
1071
|
+
throw new internal_1.PolymeshError({
|
|
1072
|
+
code: types_1.ErrorCode.ValidationError,
|
|
1073
|
+
message: '`toEthSignablePayload` can only be used for a transaction signed by an Ethereum-derived Account. Use `toSignablePayload` instead',
|
|
1074
|
+
data: { signingAddress },
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
const composedTx = this.composeTxForFees(true);
|
|
1078
|
+
const nonceValue = context.getNonce();
|
|
1079
|
+
const nonceOverride = nonceValue.isNegative() ? undefined : nonceValue;
|
|
1080
|
+
const transaction = yield (0, ethTransaction_1.buildDetachedEthTransactionRequest)(context, signingAddress, composedTx, eip1559, nonceOverride);
|
|
1081
|
+
const tag = (0, conversion_1.transactionHexToTxTag)(transaction.data, context);
|
|
1082
|
+
const args = context.getTransactionArguments({ tag });
|
|
1083
|
+
return { transaction, tag, args, metadata };
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
649
1086
|
/**
|
|
650
1087
|
* returns true if transaction has completed successfully
|
|
651
1088
|
*/
|
|
@@ -667,15 +1104,24 @@ class PolymeshTransactionBase {
|
|
|
667
1104
|
*/
|
|
668
1105
|
getPayingAccount(asProposal) {
|
|
669
1106
|
return __awaiter(this, void 0, void 0, function* () {
|
|
670
|
-
const { paidForBy, multiSig, context } = this;
|
|
671
|
-
|
|
1107
|
+
const { paidForBy, multiSig, context, signingAddress } = this;
|
|
1108
|
+
const isEthSigner = (0, eth_1.isEthDerivedAddress)(signingAddress, context.ss58Format);
|
|
1109
|
+
/*
|
|
1110
|
+
* An Ethereum-derived Account always pays its own fees. `revive.ethTransact` is a bare
|
|
1111
|
+
* extrinsic, so neither `paidForBy` nor a subsidy reaches the chain's fee pipeline — gas is
|
|
1112
|
+
* charged to the signing Account.
|
|
1113
|
+
*
|
|
1114
|
+
* Both branches below would need revisiting if the chain ever routes third-party payment
|
|
1115
|
+
* through this transport
|
|
1116
|
+
*/
|
|
1117
|
+
if (paidForBy && !isEthSigner) {
|
|
672
1118
|
const { account: primaryAccount } = yield paidForBy.getPrimaryAccount();
|
|
673
1119
|
return {
|
|
674
1120
|
type: types_1.PayingAccountType.Other,
|
|
675
1121
|
account: primaryAccount,
|
|
676
1122
|
};
|
|
677
1123
|
}
|
|
678
|
-
const subsidyWithAllowance = yield context.accountSubsidy();
|
|
1124
|
+
const subsidyWithAllowance = isEthSigner ? null : yield context.accountSubsidy();
|
|
679
1125
|
if (subsidyWithAllowance && !this.ignoresSubsidy()) {
|
|
680
1126
|
const { subsidy: { subsidizer: account }, allowance, } = subsidyWithAllowance;
|
|
681
1127
|
return {
|
|
@@ -723,8 +1169,20 @@ class PolymeshTransactionBase {
|
|
|
723
1169
|
* Wrap a transaction with a multiSig proposal if the signer is a multiSig signer
|
|
724
1170
|
*/
|
|
725
1171
|
wrapProposalIfNeeded(tx) {
|
|
726
|
-
const { context, context: { polymeshApi: { tx: { multiSig }, }, }, multiSig: actingMultiSig, multiSigOpts, } = this;
|
|
1172
|
+
const { context, context: { polymeshApi: { tx: { multiSig }, }, }, multiSig: actingMultiSig, multiSigOpts, signingAddress, } = this;
|
|
727
1173
|
if (actingMultiSig) {
|
|
1174
|
+
/*
|
|
1175
|
+
* MultiSig + Ethereum is a narrow, untested intersection: `getPayingAccount` resolves fees
|
|
1176
|
+
* to a *native* primary key for a MultiSig proposal, while the gas-derived fee arithmetic
|
|
1177
|
+
* describes the transaction the Ethereum key itself submits. Out of scope for now,
|
|
1178
|
+
* rather than shipping an untested combination of two fee models
|
|
1179
|
+
*/
|
|
1180
|
+
if ((0, eth_1.isEthDerivedAddress)(signingAddress, context.ss58Format)) {
|
|
1181
|
+
throw new internal_1.PolymeshError({
|
|
1182
|
+
code: types_1.ErrorCode.NotSupported,
|
|
1183
|
+
message: 'Using an Ethereum-derived Account as a MultiSig signer is not currently supported',
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
728
1186
|
const rawMultiSigId = (0, conversion_1.stringToAccountId)(actingMultiSig.address, context);
|
|
729
1187
|
const rawExpiry = (0, internal_3.optionize)(conversion_1.dateToMoment)(multiSigOpts.expiry, context);
|
|
730
1188
|
return multiSig.createProposal(rawMultiSigId, tx, rawExpiry);
|