@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.
Files changed (79) hide show
  1. package/.yarn/install-state.gz +0 -0
  2. package/CHANGELOG.md +2 -136
  3. package/api/client/AccountManagement.d.ts +48 -0
  4. package/api/client/AccountManagement.d.ts.map +1 -1
  5. package/api/client/AccountManagement.js +38 -0
  6. package/api/client/AccountManagement.js.map +1 -1
  7. package/api/client/Network.d.ts +79 -2
  8. package/api/client/Network.d.ts.map +1 -1
  9. package/api/client/Network.js +143 -12
  10. package/api/client/Network.js.map +1 -1
  11. package/api/client/types.d.ts +6 -0
  12. package/api/client/types.d.ts.map +1 -1
  13. package/api/client/types.js +6 -0
  14. package/api/client/types.js.map +1 -1
  15. package/api/entities/Account/index.d.ts +35 -1
  16. package/api/entities/Account/index.d.ts.map +1 -1
  17. package/api/entities/Account/index.js +89 -0
  18. package/api/entities/Account/index.js.map +1 -1
  19. package/api/entities/Account/types.d.ts +60 -1
  20. package/api/entities/Account/types.d.ts.map +1 -1
  21. package/api/entities/Account/types.js +25 -1
  22. package/api/entities/Account/types.js.map +1 -1
  23. package/api/entities/Asset/NonFungible/NftCollection.js +2 -2
  24. package/api/entities/Asset/NonFungible/NftCollection.js.map +1 -1
  25. package/api/procedures/issueNft.d.ts +1 -0
  26. package/api/procedures/issueNft.d.ts.map +1 -1
  27. package/api/procedures/issueNft.js +24 -6
  28. package/api/procedures/issueNft.js.map +1 -1
  29. package/api/procedures/toggleEvmAccountMapping.d.ts +18 -0
  30. package/api/procedures/toggleEvmAccountMapping.d.ts.map +1 -0
  31. package/api/procedures/toggleEvmAccountMapping.js +80 -0
  32. package/api/procedures/toggleEvmAccountMapping.js.map +1 -0
  33. package/api/procedures/types.d.ts +44 -0
  34. package/api/procedures/types.d.ts.map +1 -1
  35. package/api/procedures/types.js.map +1 -1
  36. package/base/Context.d.ts +33 -0
  37. package/base/Context.d.ts.map +1 -1
  38. package/base/Context.js +59 -1
  39. package/base/Context.js.map +1 -1
  40. package/base/PolymeshTransactionBase.d.ts +199 -3
  41. package/base/PolymeshTransactionBase.d.ts.map +1 -1
  42. package/base/PolymeshTransactionBase.js +595 -137
  43. package/base/PolymeshTransactionBase.js.map +1 -1
  44. package/base/Procedure.d.ts.map +1 -1
  45. package/base/Procedure.js +2 -0
  46. package/base/Procedure.js.map +1 -1
  47. package/base/ethTransaction.d.ts +222 -0
  48. package/base/ethTransaction.d.ts.map +1 -0
  49. package/base/ethTransaction.js +317 -0
  50. package/base/ethTransaction.js.map +1 -0
  51. package/base/types.d.ts +66 -1
  52. package/base/types.d.ts.map +1 -1
  53. package/base/types.js.map +1 -1
  54. package/base/utils.d.ts +60 -11
  55. package/base/utils.d.ts.map +1 -1
  56. package/base/utils.js +328 -45
  57. package/base/utils.js.map +1 -1
  58. package/internal.d.ts +1 -0
  59. package/internal.d.ts.map +1 -1
  60. package/internal.js +4 -2
  61. package/internal.js.map +1 -1
  62. package/npm-package/polymeshassociation-polymesh-sdk-31.1.0-beta.2.tgz +0 -0
  63. package/package.json +2 -2
  64. package/types/internal.d.ts +38 -1
  65. package/types/internal.d.ts.map +1 -1
  66. package/types/internal.js.map +1 -1
  67. package/utils/eth.d.ts +63 -0
  68. package/utils/eth.d.ts.map +1 -0
  69. package/utils/eth.js +209 -0
  70. package/utils/eth.js.map +1 -0
  71. package/utils/internal.d.ts +19 -1
  72. package/utils/internal.d.ts.map +1 -1
  73. package/utils/internal.js +34 -0
  74. package/utils/internal.js.map +1 -1
  75. package/utils/typeguards.d.ts +10 -0
  76. package/utils/typeguards.d.ts.map +1 -1
  77. package/utils/typeguards.js +11 -0
  78. package/utils/typeguards.js.map +1 -1
  79. 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 = 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
- var _a;
215
- const { signingAddress, signer, mortality, context } = this;
368
+ const { signingAddress, context } = this;
216
369
  yield context.assertHasSigningAddress(signingAddress);
217
- // era is how many blocks the transaction remains valid for, `undefined` for default
218
- const era = mortality.immortal ? 0 : (_a = mortality.lifetime) === null || _a === void 0 ? void 0 : _a.toNumber();
219
- const nonce = context.getNonce().toNumber();
220
- this.updateStatus(types_1.TransactionStatus.Unapproved);
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 new Promise((resolve, reject) => {
235
- let settingBlockData = Promise.resolve();
236
- const gettingUnsub = txWithArgs.signAndSend(signingAddress, signerOptions, receipt => {
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
- else {
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
- * Sign and submit the transaction, then poll for its finalization. Used when the
318
- * node connection does not support subscriptions
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
- runViaPolling(txWithArgs, signerOptions) {
321
- return __awaiter(this, void 0, void 0, function* () {
322
- const { signingAddress, context } = this;
323
- const startingBlock = yield context.getLatestBlock();
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
- * the resolved hash is used instead of `txWithArgs.hash` because a signer may return a modified
326
- * `signedTransaction` (e.g. Ledger devices signing via the generic app), in which case the
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
- const submittedTxHash = yield txWithArgs
330
- .signAndSend(signingAddress, signerOptions)
331
- .then(txHash => {
332
- this.setIsRunningStatus(txHash.toString());
333
- return txHash;
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
- throw new internal_1.PolymeshError(error);
614
+ reject(new internal_1.PolymeshError(error));
338
615
  });
339
- const finalizedReceipt = yield (0, utils_1.pollForTransactionFinalization)(submittedTxHash, startingBlock, context);
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 [extrinsicFailedEvent] = (0, internal_3.filterEventRecords)(finalizedReceipt, 'system', 'ExtrinsicFailed', true);
345
- if (extrinsicFailedEvent) {
346
- const { data } = extrinsicFailedEvent;
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 paymentInfoPromise = composedTx.paymentInfo(signingAddress);
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 [{ partialFee }, { free: balance }] = yield Promise.all([
384
- paymentInfoPromise,
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
- if (paidForBy) {
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);