@parity/product-sdk-tx 0.2.4 → 0.2.6
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/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/account-mapping.ts +2 -0
- package/src/batch.ts +2 -0
- package/src/dev-signers.ts +2 -0
- package/src/dry-run.ts +2 -0
- package/src/errors.ts +2 -0
- package/src/index.ts +2 -0
- package/src/retry.ts +2 -0
- package/src/submit.ts +2 -0
- package/src/types.ts +2 -0
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/errors.ts","../src/submit.ts","../src/batch.ts","../src/retry.ts","../src/dev-signers.ts","../src/dry-run.ts","../src/account-mapping.ts"],"names":["log","createLogger"],"mappings":"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","file":"index.js","sourcesContent":["/** Base class for all transaction errors. Use `instanceof TxError` to catch any tx-related error. */\nexport class TxError extends Error {\n constructor(message: string, options?: ErrorOptions) {\n super(message, options);\n this.name = \"TxError\";\n }\n}\n\n/** The transaction did not finalize within the configured timeout. It may still be processing on-chain. */\nexport class TxTimeoutError extends TxError {\n readonly timeoutMs: number;\n\n constructor(timeoutMs: number) {\n super(\n `Transaction timed out after ${timeoutMs / 1000}s. The transaction may still be processing on-chain.`,\n );\n this.name = \"TxTimeoutError\";\n this.timeoutMs = timeoutMs;\n }\n}\n\n/** The transaction was included on-chain but the dispatch failed. */\nexport class TxDispatchError extends TxError {\n /** Raw dispatch error from polkadot-api. */\n readonly dispatchError: unknown;\n /** Human-readable error string (e.g., \"Revive.ContractReverted\"). */\n readonly formatted: string;\n\n constructor(dispatchError: unknown, formatted: string) {\n super(`Transaction dispatch failed: ${formatted}`);\n this.name = \"TxDispatchError\";\n this.dispatchError = dispatchError;\n this.formatted = formatted;\n }\n}\n\n/** The user rejected the signing request in their wallet. */\nexport class TxSigningRejectedError extends TxError {\n constructor() {\n super(\"Transaction signing was rejected.\");\n this.name = \"TxSigningRejectedError\";\n }\n}\n\n/**\n * Extract a human-readable error from a transaction result's dispatch error.\n *\n * PAPI dispatch errors for pallet modules are nested:\n * `{ type: \"Module\", value: { type: \"Revive\", value: { type: \"ContractReverted\" } } }`\n *\n * This walks the chain to build a string like `\"Revive.ContractReverted\"`.\n *\n * @param result - A transaction result with `ok` and optional `dispatchError`.\n * @returns A human-readable error string, or `\"\"` if the result is ok, or `\"unknown error\"` if\n * the dispatch error cannot be decoded.\n */\nexport function formatDispatchError(result: { ok: boolean; dispatchError?: unknown }): string {\n if (result.ok) return \"\";\n\n try {\n const err = result.dispatchError as { type?: string; value?: unknown } | undefined;\n if (!err) return \"unknown error\";\n\n if (err.type === \"Module\" && err.value && typeof err.value === \"object\") {\n const palletErr = err.value as { type?: string; value?: unknown };\n const palletName = palletErr.type ?? \"Unknown\";\n\n if (palletErr.value && typeof palletErr.value === \"object\") {\n const innerErr = palletErr.value as { type?: string };\n if (innerErr.type) {\n return `${palletName}.${innerErr.type}`;\n }\n }\n return palletName;\n }\n\n return err.type ?? \"unknown error\";\n } catch {\n return \"unknown error\";\n }\n}\n\n/** Error specific to batch transaction construction (e.g., empty calls array). */\nexport class TxBatchError extends TxError {\n constructor(message: string) {\n super(message);\n this.name = \"TxBatchError\";\n }\n}\n\n/**\n * A dry-run simulation failed before the transaction was submitted on-chain.\n *\n * Thrown by {@link extractTransaction} when the dry-run result indicates failure.\n * Carries structured error information so callers can distinguish revert reasons\n * from dispatch errors programmatically.\n *\n * @example\n * ```ts\n * try {\n * const tx = extractTransaction(await contract.query(\"mint\", { origin, data }));\n * } catch (e) {\n * if (e instanceof TxDryRunError) {\n * console.log(e.revertReason); // \"InsufficientBalance\" (if contract provided one)\n * console.log(e.formatted); // \"Revive.StorageDepositNotEnoughFunds\"\n * }\n * }\n * ```\n */\nexport class TxDryRunError extends TxError {\n /** The raw dry-run result for programmatic inspection. */\n readonly raw: unknown;\n /** Human-readable error string derived from the dry-run result. */\n readonly formatted: string;\n /** Solidity revert reason, if the contract provided one. */\n readonly revertReason?: string;\n\n constructor(raw: unknown, formatted: string, revertReason?: string) {\n super(revertReason ? `Dry run failed: ${revertReason}` : `Dry run failed: ${formatted}`);\n this.name = \"TxDryRunError\";\n this.raw = raw;\n this.formatted = formatted;\n this.revertReason = revertReason;\n }\n}\n\n/**\n * Extract a human-readable error from a failed dry-run result.\n *\n * Handles every error shape found across the Polkadot contract ecosystem:\n *\n * 1. **Revert reason** (Ink SDK patched results / EVM contracts):\n * `{ value: { revertReason: \"InsufficientBalance\" } }`\n *\n * 2. **Nested dispatch errors** (raw Ink SDK / pallet errors):\n * `{ value: { type: \"Module\", value: { type: \"Revive\", value: { type: \"StorageDepositNotEnoughFunds\" } } } }`\n * Delegates to {@link formatDispatchError} for the Module.Pallet.Error chain.\n *\n * 3. **ReviveApi runtime messages** (`eth_transact` / `ReviveApi.call`):\n * `{ value: { type: \"Message\", value: \"Insufficient balance for gas * price + value\" } }`\n *\n * 4. **ReviveApi contract revert data**:\n * `{ value: { type: \"Data\", value: \"0x08c379a0...\" } }`\n *\n * 5. **Wrapped raw errors** (patched SDK wrappers):\n * `{ value: { raw: { type: \"Message\", value: \"...\" } } }`\n *\n * 6. **Generic error field**:\n * `{ error: { type: \"ContractTrapped\" } }` or `{ error: { name: \"...\" } }`\n *\n * @param result - A dry-run result with at least `success`, and optionally `value` / `error`.\n * @returns A human-readable error string, or `\"\"` if the result succeeded.\n */\nexport function formatDryRunError(result: {\n success?: boolean;\n value?: unknown;\n error?: unknown;\n}): string {\n if (result.success) return \"\";\n\n const formatted = extractErrorFromValue(result.value);\n if (formatted) return formatted;\n\n // Generic error field (Ink SDK)\n if (result.error != null && typeof result.error === \"object\") {\n const err = result.error as Record<string, unknown>;\n if (typeof err.type === \"string\") return err.type;\n if (typeof err.name === \"string\") return err.name;\n }\n\n return \"unknown error\";\n}\n\n/**\n * Try to extract an error string from the `value` field of a dry-run result.\n * Returns `undefined` if no known error shape is found.\n */\nfunction extractErrorFromValue(value: unknown): string | undefined {\n if (value == null || typeof value !== \"object\") return undefined;\n const v = value as Record<string, unknown>;\n\n // Explicit revert reason — most specific, from Ink SDK / EVM wrappers\n if (typeof v.revertReason === \"string\" && v.revertReason) {\n return v.revertReason;\n }\n\n if (typeof v.type === \"string\") {\n // Nested Module.Pallet.Error — reuse dispatch error formatting\n if (v.type === \"Module\") {\n const asDispatch = formatDispatchError({ ok: false, dispatchError: value });\n if (asDispatch !== \"unknown error\") return asDispatch;\n }\n\n // ReviveApi Message — runtime error string\n if (v.type === \"Message\" && typeof v.value === \"string\") {\n return v.value;\n }\n\n // ReviveApi Data — contract revert hex data\n if (v.type === \"Data\") {\n const hex =\n v.value != null &&\n typeof v.value === \"object\" &&\n typeof (v.value as { asHex?: unknown }).asHex === \"function\"\n ? String((v.value as { asHex: () => string }).asHex())\n : typeof v.value === \"string\"\n ? v.value\n : undefined;\n return hex ? `contract reverted with data: ${hex}` : \"contract reverted\";\n }\n\n // Any other typed error (e.g., \"BadOrigin\", \"ContractTrapped\")\n return v.type;\n }\n\n // Wrapped raw value — patched SDK nests the original error under `raw`\n if (\"raw\" in v && v.raw != null && typeof v.raw === \"object\") {\n return extractErrorFromValue(v.raw);\n }\n\n return undefined;\n}\n\n/**\n * Check if an error looks like a user-rejected signing request.\n *\n * Different wallets use different error messages when the user rejects signing:\n * \"Cancelled\", \"Rejected\", \"User rejected\", \"denied\". This checks for common\n * patterns as a best-effort heuristic. Non-Error values always return false.\n *\n * @param error - The error to check.\n * @returns `true` if the error message matches a known rejection pattern.\n */\nexport function isSigningRejection(error: unknown): boolean {\n if (!(error instanceof Error)) return false;\n const msg = error.message.toLowerCase();\n return (\n msg.includes(\"cancelled\") ||\n msg.includes(\"rejected\") ||\n msg.includes(\"denied\") ||\n msg.includes(\"user refused\")\n );\n}\n\nif (import.meta.vitest) {\n const { describe, test, expect } = import.meta.vitest;\n\n describe(\"TxError hierarchy\", () => {\n test(\"TxTimeoutError\", () => {\n const err = new TxTimeoutError(300_000);\n expect(err).toBeInstanceOf(TxError);\n expect(err).toBeInstanceOf(Error);\n expect(err.name).toBe(\"TxTimeoutError\");\n expect(err.timeoutMs).toBe(300_000);\n expect(err.message).toContain(\"300s\");\n });\n\n test(\"TxDispatchError\", () => {\n const raw = {\n type: \"Module\",\n value: { type: \"Balances\", value: { type: \"InsufficientBalance\" } },\n };\n const err = new TxDispatchError(raw, \"Balances.InsufficientBalance\");\n expect(err).toBeInstanceOf(TxError);\n expect(err.name).toBe(\"TxDispatchError\");\n expect(err.dispatchError).toBe(raw);\n expect(err.formatted).toBe(\"Balances.InsufficientBalance\");\n expect(err.message).toContain(\"Balances.InsufficientBalance\");\n });\n\n test(\"TxSigningRejectedError\", () => {\n const err = new TxSigningRejectedError();\n expect(err).toBeInstanceOf(TxError);\n expect(err.name).toBe(\"TxSigningRejectedError\");\n });\n\n test(\"TxBatchError\", () => {\n const err = new TxBatchError(\"Cannot batch zero calls\");\n expect(err).toBeInstanceOf(TxError);\n expect(err).toBeInstanceOf(Error);\n expect(err.name).toBe(\"TxBatchError\");\n expect(err.message).toBe(\"Cannot batch zero calls\");\n });\n });\n\n describe(\"formatDispatchError\", () => {\n test(\"returns empty string for ok result\", () => {\n expect(formatDispatchError({ ok: true })).toBe(\"\");\n });\n\n test(\"walks Module.Pallet.Error chain\", () => {\n const result = {\n ok: false,\n dispatchError: {\n type: \"Module\",\n value: { type: \"Revive\", value: { type: \"ContractReverted\" } },\n },\n };\n expect(formatDispatchError(result)).toBe(\"Revive.ContractReverted\");\n });\n\n test(\"returns pallet name when inner error has no type\", () => {\n const result = {\n ok: false,\n dispatchError: {\n type: \"Module\",\n value: { type: \"Balances\", value: {} },\n },\n };\n expect(formatDispatchError(result)).toBe(\"Balances\");\n });\n\n test(\"returns error type for non-Module errors\", () => {\n const result = {\n ok: false,\n dispatchError: { type: \"BadOrigin\" },\n };\n expect(formatDispatchError(result)).toBe(\"BadOrigin\");\n });\n\n test(\"returns unknown error when dispatchError is missing\", () => {\n expect(formatDispatchError({ ok: false })).toBe(\"unknown error\");\n });\n\n test(\"returns unknown error when dispatchError has no type\", () => {\n expect(formatDispatchError({ ok: false, dispatchError: {} })).toBe(\"unknown error\");\n });\n });\n\n describe(\"TxDryRunError\", () => {\n test(\"with revert reason\", () => {\n const err = new TxDryRunError(\n { success: false },\n \"Module.Error\",\n \"InsufficientBalance\",\n );\n expect(err).toBeInstanceOf(TxError);\n expect(err.name).toBe(\"TxDryRunError\");\n expect(err.formatted).toBe(\"Module.Error\");\n expect(err.revertReason).toBe(\"InsufficientBalance\");\n expect(err.message).toContain(\"InsufficientBalance\");\n });\n\n test(\"without revert reason uses formatted\", () => {\n const err = new TxDryRunError({ success: false }, \"BadOrigin\");\n expect(err.message).toContain(\"BadOrigin\");\n expect(err.revertReason).toBeUndefined();\n });\n\n test(\"preserves raw result for inspection\", () => {\n const raw = { success: false, value: { type: \"Module\" } };\n const err = new TxDryRunError(raw, \"Module\");\n expect(err.raw).toBe(raw);\n });\n });\n\n describe(\"formatDryRunError\", () => {\n test(\"returns empty string for successful result\", () => {\n expect(formatDryRunError({ success: true })).toBe(\"\");\n });\n\n test(\"extracts revert reason\", () => {\n expect(\n formatDryRunError({\n success: false,\n value: { revertReason: \"InsufficientBalance\" },\n }),\n ).toBe(\"InsufficientBalance\");\n });\n\n test(\"walks Module.Pallet.Error chain\", () => {\n expect(\n formatDryRunError({\n success: false,\n value: {\n type: \"Module\",\n value: { type: \"Revive\", value: { type: \"StorageDepositNotEnoughFunds\" } },\n },\n }),\n ).toBe(\"Revive.StorageDepositNotEnoughFunds\");\n });\n\n test(\"returns pallet name when inner error has no type\", () => {\n expect(\n formatDryRunError({\n success: false,\n value: { type: \"Module\", value: { type: \"Balances\", value: {} } },\n }),\n ).toBe(\"Balances\");\n });\n\n test(\"extracts ReviveApi Message string\", () => {\n expect(\n formatDryRunError({\n success: false,\n value: {\n type: \"Message\",\n value: \"Insufficient balance for gas * price + value\",\n },\n }),\n ).toBe(\"Insufficient balance for gas * price + value\");\n });\n\n test(\"handles ReviveApi Data with string hex\", () => {\n expect(\n formatDryRunError({\n success: false,\n value: { type: \"Data\", value: \"0x08c379a0\" },\n }),\n ).toBe(\"contract reverted with data: 0x08c379a0\");\n });\n\n test(\"handles ReviveApi Data with Binary-like object\", () => {\n const binary = { asHex: () => \"0xdeadbeef\" };\n expect(\n formatDryRunError({ success: false, value: { type: \"Data\", value: binary } }),\n ).toBe(\"contract reverted with data: 0xdeadbeef\");\n });\n\n test(\"handles ReviveApi Data with no extractable hex\", () => {\n expect(formatDryRunError({ success: false, value: { type: \"Data\", value: 42 } })).toBe(\n \"contract reverted\",\n );\n });\n\n test(\"returns non-Module/Message type directly\", () => {\n expect(formatDryRunError({ success: false, value: { type: \"BadOrigin\" } })).toBe(\n \"BadOrigin\",\n );\n });\n\n test(\"extracts from nested raw field (patched SDK)\", () => {\n expect(\n formatDryRunError({\n success: false,\n value: { raw: { type: \"Message\", value: \"out of gas\" } },\n }),\n ).toBe(\"out of gas\");\n });\n\n test(\"extracts revertReason from nested raw\", () => {\n expect(\n formatDryRunError({\n success: false,\n value: { raw: { revertReason: \"Unauthorized\" } },\n }),\n ).toBe(\"Unauthorized\");\n });\n\n test(\"falls back to error.type\", () => {\n expect(formatDryRunError({ success: false, error: { type: \"ContractTrapped\" } })).toBe(\n \"ContractTrapped\",\n );\n });\n\n test(\"falls back to error.name\", () => {\n expect(formatDryRunError({ success: false, error: { name: \"ExecutionFailed\" } })).toBe(\n \"ExecutionFailed\",\n );\n });\n\n test(\"returns unknown error when nothing is extractable\", () => {\n expect(formatDryRunError({ success: false })).toBe(\"unknown error\");\n });\n\n test(\"returns unknown error for empty value and error\", () => {\n expect(formatDryRunError({ success: false, value: {}, error: {} })).toBe(\n \"unknown error\",\n );\n });\n\n test(\"returns unknown error for null value\", () => {\n expect(formatDryRunError({ success: false, value: null })).toBe(\"unknown error\");\n });\n\n test(\"prefers revertReason over Module error\", () => {\n // When both are present, revertReason is more specific\n expect(\n formatDryRunError({\n success: false,\n value: {\n revertReason: \"OwnableUnauthorizedAccount\",\n type: \"Module\",\n value: {},\n },\n }),\n ).toBe(\"OwnableUnauthorizedAccount\");\n });\n });\n\n describe(\"isSigningRejection\", () => {\n test(\"detects common rejection messages\", () => {\n expect(isSigningRejection(new Error(\"Cancelled\"))).toBe(true);\n expect(isSigningRejection(new Error(\"User rejected the request\"))).toBe(true);\n expect(isSigningRejection(new Error(\"Transaction was rejected by user\"))).toBe(true);\n expect(isSigningRejection(new Error(\"User denied\"))).toBe(true);\n expect(isSigningRejection(new Error(\"Signing was denied by user\"))).toBe(true);\n expect(isSigningRejection(new Error(\"User refused to sign\"))).toBe(true);\n });\n\n test(\"returns false for non-rejection errors\", () => {\n expect(isSigningRejection(new Error(\"Network timeout\"))).toBe(false);\n expect(isSigningRejection(new Error(\"Insufficient balance\"))).toBe(false);\n });\n\n test(\"returns false for non-Error values\", () => {\n expect(isSigningRejection(\"cancelled\")).toBe(false);\n expect(isSigningRejection(null)).toBe(false);\n });\n });\n}\n","import type { PolkadotSigner } from \"polkadot-api\";\nimport { createLogger } from \"@parity/product-sdk-logger\";\n\nimport {\n TxDispatchError,\n TxSigningRejectedError,\n TxTimeoutError,\n formatDispatchError,\n isSigningRejection,\n} from \"./errors.js\";\nimport type { SubmitOptions, SubmittableTransaction, TxEvent, TxResult } from \"./types.js\";\n\nconst DEFAULT_TIMEOUT_MS = 300_000;\nconst DEFAULT_MORTALITY_PERIOD = 256;\n\nconst log = createLogger(\"tx\");\n\n/**\n * Resolve Ink SDK AsyncTransaction wrappers.\n *\n * Ink SDK's `contract.send()` returns an object with a `.waited` Promise that\n * resolves to the actual transaction. This handles that transparently.\n */\nasync function resolveTransaction(tx: SubmittableTransaction): Promise<SubmittableTransaction> {\n if (tx.waited && typeof tx.waited.then === \"function\") {\n log.debug(\"Resolving Ink SDK AsyncTransaction\");\n return tx.waited;\n }\n return tx;\n}\n\nfunction buildTxResult(\n event: TxEvent & { ok: boolean; block: TxResult[\"block\"]; events: unknown[] },\n): TxResult {\n return {\n txHash: event.txHash,\n ok: event.ok,\n block: event.block,\n events: event.events,\n dispatchError: \"dispatchError\" in event ? event.dispatchError : undefined,\n };\n}\n\n/**\n * Submit a transaction and watch its lifecycle through signing, broadcasting,\n * block inclusion, and (optionally) finalization.\n *\n * @param tx - A transaction object with `signSubmitAndWatch`. Works with raw PAPI\n * transactions and Ink SDK `AsyncTransaction` wrappers (resolved automatically).\n * @param signer - The signer to use. Can come from the Host API\n * (`getProductAccountSigner`) or {@link createDevSigner}.\n * @param options - Submission options (waitFor, timeout, mortality, status callback).\n * @returns The transaction result once included/finalized.\n *\n * @throws {TxTimeoutError} If the transaction does not reach the target state within `timeoutMs`.\n * @throws {TxDispatchError} If the on-chain dispatch fails (e.g., insufficient balance, contract revert).\n * @throws {TxSigningRejectedError} If the user rejects signing in their wallet.\n */\nexport async function submitAndWatch(\n tx: SubmittableTransaction,\n signer: PolkadotSigner,\n options?: SubmitOptions,\n): Promise<TxResult> {\n const waitFor = options?.waitFor ?? \"best-block\";\n const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const mortalityPeriod = options?.mortalityPeriod ?? DEFAULT_MORTALITY_PERIOD;\n const onStatus = options?.onStatus;\n\n const resolvedTx = await resolveTransaction(tx);\n\n return new Promise<TxResult>((resolve, reject) => {\n let settled = false;\n let subscription: { unsubscribe: () => void } | null = null;\n\n const timer = setTimeout(() => {\n subscription?.unsubscribe();\n if (!settled) {\n settled = true;\n onStatus?.(\"error\");\n reject(new TxTimeoutError(timeoutMs));\n }\n }, timeoutMs);\n\n function teardown(): void {\n clearTimeout(timer);\n subscription?.unsubscribe();\n }\n\n function settleReject(error: Error): void {\n if (settled) return;\n settled = true;\n teardown();\n onStatus?.(\"error\");\n reject(error);\n }\n\n try {\n const observable = resolvedTx.signSubmitAndWatch(signer, {\n mortality: { mortal: true, period: mortalityPeriod },\n });\n\n subscription = observable.subscribe({\n next: (event: TxEvent) => {\n switch (event.type) {\n case \"signed\": {\n log.info(\"Transaction signed\", { txHash: event.txHash });\n onStatus?.(\"signing\");\n break;\n }\n case \"broadcasted\": {\n log.info(\"Transaction broadcasted\", { txHash: event.txHash });\n onStatus?.(\"broadcasting\");\n break;\n }\n case \"txBestBlocksState\": {\n if (!event.found) break;\n\n if (event.ok === false) {\n const formatted = formatDispatchError({\n ok: false,\n dispatchError: event.dispatchError,\n });\n log.error(\"Transaction failed in best block\", {\n formatted,\n block: event.block,\n });\n settleReject(new TxDispatchError(event.dispatchError, formatted));\n return;\n }\n\n log.info(\"Transaction in best block\", { block: event.block });\n onStatus?.(\"in-block\");\n\n if (\n waitFor === \"best-block\" &&\n event.ok === true &&\n event.block &&\n event.events\n ) {\n // Resolve the Promise but keep the subscription alive so we can\n // detect reorgs (finalized event with ok=false after best-block ok=true).\n // Only clear the timer since the consumer has their result.\n settled = true;\n clearTimeout(timer);\n resolve(\n buildTxResult(\n event as TxEvent & {\n ok: boolean;\n block: TxResult[\"block\"];\n events: unknown[];\n },\n ),\n );\n }\n break;\n }\n case \"finalized\": {\n log.info(\"Transaction finalized\", { ok: event.ok, block: event.block });\n\n if (!event.ok) {\n const formatted = formatDispatchError({\n ok: false,\n dispatchError: event.dispatchError,\n });\n\n if (settled) {\n // Already resolved at best-block but finalized shows failure\n // due to a chain reorganization. We can only log since the\n // Promise is already resolved.\n log.warn(\n \"Transaction failed after being in best block (reorg). \" +\n \"The consumer received a success result that is no longer valid.\",\n { formatted, block: event.block },\n );\n } else {\n settleReject(\n new TxDispatchError(event.dispatchError, formatted),\n );\n }\n subscription?.unsubscribe();\n return;\n }\n\n onStatus?.(\"finalized\");\n\n if (!settled) {\n settled = true;\n teardown();\n resolve(buildTxResult(event));\n } else {\n // Already resolved at best-block, finalization confirmed success.\n subscription?.unsubscribe();\n }\n break;\n }\n }\n },\n error: (err: Error) => {\n log.error(\"Transaction subscription error\", { error: err.message });\n\n if (isSigningRejection(err)) {\n settleReject(new TxSigningRejectedError());\n } else {\n settleReject(err);\n }\n },\n });\n } catch (err) {\n log.error(\"Failed to start transaction\", { error: (err as Error).message });\n teardown();\n\n if (isSigningRejection(err)) {\n settleReject(new TxSigningRejectedError());\n } else {\n settleReject(err as Error);\n }\n }\n });\n}\n\nif (import.meta.vitest) {\n const { describe, test, expect, vi, beforeEach } = import.meta.vitest;\n const { configure } = await import(\"@parity/product-sdk-logger\");\n\n // Silence logger during tests\n beforeEach(() => {\n configure({ handler: () => {} });\n });\n\n type MockSubscribeHandlers = {\n next: (event: TxEvent) => void;\n error: (error: Error) => void;\n };\n\n function createMockTx(\n emitFn: (handlers: MockSubscribeHandlers) => void,\n ): SubmittableTransaction {\n return {\n signSubmitAndWatch: (_signer: PolkadotSigner, _options?: unknown) => ({\n subscribe: (handlers: MockSubscribeHandlers) => {\n const unsub = vi.fn();\n // Emit events asynchronously so the subscription is returned first\n queueMicrotask(() => emitFn(handlers));\n return { unsubscribe: unsub };\n },\n }),\n };\n }\n\n const mockSigner = {} as PolkadotSigner;\n\n const signedEvent: TxEvent = { type: \"signed\", txHash: \"0xabc\" };\n const broadcastedEvent: TxEvent = { type: \"broadcasted\", txHash: \"0xabc\" };\n const bestBlockOk: TxEvent = {\n type: \"txBestBlocksState\",\n txHash: \"0xabc\",\n found: true,\n ok: true,\n events: [{ id: 1 }],\n block: { hash: \"0xblock1\", number: 100, index: 0 },\n };\n const bestBlockFail: TxEvent = {\n type: \"txBestBlocksState\",\n txHash: \"0xabc\",\n found: true,\n ok: false,\n events: [],\n block: { hash: \"0xblock1\", number: 100, index: 0 },\n dispatchError: {\n type: \"Module\",\n value: { type: \"Balances\", value: { type: \"InsufficientBalance\" } },\n },\n };\n const finalizedOk: TxEvent = {\n type: \"finalized\",\n txHash: \"0xabc\",\n ok: true,\n events: [{ id: 1 }],\n block: { hash: \"0xblock2\", number: 101, index: 0 },\n };\n const finalizedFail: TxEvent = {\n type: \"finalized\",\n txHash: \"0xabc\",\n ok: false,\n events: [],\n block: { hash: \"0xblock2\", number: 101, index: 0 },\n dispatchError: { type: \"BadOrigin\" },\n };\n\n describe(\"submitAndWatch\", () => {\n test(\"resolves at best-block by default\", async () => {\n const tx = createMockTx((h) => {\n h.next(signedEvent);\n h.next(broadcastedEvent);\n h.next(bestBlockOk);\n h.next(finalizedOk);\n });\n const result = await submitAndWatch(tx, mockSigner);\n expect(result.ok).toBe(true);\n expect(result.block.number).toBe(100);\n });\n\n test(\"resolves at finalized when configured\", async () => {\n const tx = createMockTx((h) => {\n h.next(signedEvent);\n h.next(bestBlockOk);\n h.next(finalizedOk);\n });\n const result = await submitAndWatch(tx, mockSigner, { waitFor: \"finalized\" });\n expect(result.ok).toBe(true);\n expect(result.block.number).toBe(101);\n });\n\n test(\"rejects with TxDispatchError on best-block failure\", async () => {\n const tx = createMockTx((h) => {\n h.next(signedEvent);\n h.next(bestBlockFail);\n });\n await expect(submitAndWatch(tx, mockSigner)).rejects.toThrow(TxDispatchError);\n });\n\n test(\"rejects with TxDispatchError on finalized failure\", async () => {\n const tx = createMockTx((h) => {\n h.next(signedEvent);\n h.next(finalizedFail);\n });\n await expect(submitAndWatch(tx, mockSigner, { waitFor: \"finalized\" })).rejects.toThrow(\n TxDispatchError,\n );\n });\n\n test(\"rejects with TxTimeoutError after timeout\", async () => {\n const tx = createMockTx(() => {\n // Never emits any events - tx hangs forever\n });\n const error = await submitAndWatch(tx, mockSigner, { timeoutMs: 50 }).catch(\n (e: unknown) => e,\n );\n expect(error).toBeInstanceOf(TxTimeoutError);\n expect((error as TxTimeoutError).timeoutMs).toBe(50);\n });\n\n test(\"calls onStatus callbacks in order\", async () => {\n const statuses: string[] = [];\n const tx = createMockTx((h) => {\n h.next(signedEvent);\n h.next(broadcastedEvent);\n h.next(bestBlockOk);\n });\n await submitAndWatch(tx, mockSigner, {\n onStatus: (s) => statuses.push(s),\n });\n expect(statuses).toEqual([\"signing\", \"broadcasting\", \"in-block\"]);\n });\n\n test(\"resolves Ink SDK AsyncTransaction\", async () => {\n const innerTx = createMockTx((h) => {\n h.next(signedEvent);\n h.next(bestBlockOk);\n });\n const wrappedTx: SubmittableTransaction = {\n signSubmitAndWatch: () => {\n throw new Error(\"Should not be called on outer tx\");\n },\n waited: Promise.resolve(innerTx),\n };\n const result = await submitAndWatch(wrappedTx, mockSigner);\n expect(result.ok).toBe(true);\n });\n\n test(\"passes mortality options\", async () => {\n let capturedOptions: unknown;\n const tx: SubmittableTransaction = {\n signSubmitAndWatch: (_signer: PolkadotSigner, options?: unknown) => {\n capturedOptions = options;\n return {\n subscribe: (handlers: MockSubscribeHandlers) => {\n queueMicrotask(() => {\n handlers.next(signedEvent);\n handlers.next(bestBlockOk);\n });\n return { unsubscribe: vi.fn() };\n },\n };\n },\n };\n await submitAndWatch(tx, mockSigner, { mortalityPeriod: 512 });\n expect(capturedOptions).toEqual({ mortality: { mortal: true, period: 512 } });\n });\n\n test(\"wraps signing rejection in TxSigningRejectedError\", async () => {\n const tx = createMockTx((h) => {\n h.error(new Error(\"User rejected the request\"));\n });\n await expect(submitAndWatch(tx, mockSigner)).rejects.toThrow(TxSigningRejectedError);\n });\n\n test(\"skips txBestBlocksState with found=false\", async () => {\n const tx = createMockTx((h) => {\n h.next(signedEvent);\n h.next({\n type: \"txBestBlocksState\",\n txHash: \"0xabc\",\n found: false,\n });\n h.next(bestBlockOk);\n });\n const result = await submitAndWatch(tx, mockSigner);\n expect(result.ok).toBe(true);\n });\n\n test(\"rejects with original error for non-rejection Observable errors\", async () => {\n const tx = createMockTx((h) => {\n h.error(new Error(\"WebSocket disconnected\"));\n });\n const err = await submitAndWatch(tx, mockSigner).catch((e) => e);\n expect(err.message).toBe(\"WebSocket disconnected\");\n expect(err).not.toBeInstanceOf(TxSigningRejectedError);\n });\n\n test(\"handles synchronous throw from signSubmitAndWatch\", async () => {\n const tx: SubmittableTransaction = {\n signSubmitAndWatch: () => {\n throw new Error(\"Signer not available\");\n },\n };\n await expect(submitAndWatch(tx, mockSigner)).rejects.toThrow(\"Signer not available\");\n });\n\n test(\"calls onStatus error on dispatch failure\", async () => {\n const statuses: string[] = [];\n const tx = createMockTx((h) => {\n h.next(bestBlockFail);\n });\n await submitAndWatch(tx, mockSigner, {\n onStatus: (s) => statuses.push(s),\n }).catch(() => {});\n expect(statuses).toContain(\"error\");\n });\n\n test(\"logs warning on reorg (best-block ok, finalized fail)\", async () => {\n const warnings: unknown[] = [];\n const { configure: configureLogs } = await import(\"@parity/product-sdk-logger\");\n configureLogs({\n level: \"debug\",\n handler: (entry) => {\n if (entry.level === \"warn\") warnings.push(entry.message);\n },\n });\n\n const tx = createMockTx((h) => {\n h.next(signedEvent);\n h.next(bestBlockOk);\n // Finalized says the tx actually failed (reorg)\n h.next(finalizedFail);\n });\n\n // Should resolve at best-block (success)\n const result = await submitAndWatch(tx, mockSigner);\n expect(result.ok).toBe(true);\n\n // Give the finalized event time to fire and log\n await new Promise((r) => setTimeout(r, 10));\n\n expect(warnings.some((w) => typeof w === \"string\" && w.includes(\"reorg\"))).toBe(true);\n\n // Restore silent handler\n configureLogs({ handler: () => {} });\n });\n\n test(\"does not resolve when txBestBlocksState ok is undefined\", async () => {\n const tx = createMockTx((h) => {\n h.next(signedEvent);\n // ok is undefined (not explicitly true or false)\n h.next({\n type: \"txBestBlocksState\",\n txHash: \"0xabc\",\n found: true,\n events: [{ id: 1 }],\n block: { hash: \"0xblock1\", number: 100, index: 0 },\n // ok intentionally omitted\n } as TxEvent);\n // Should only resolve when finalized\n h.next(finalizedOk);\n });\n\n const result = await submitAndWatch(tx, mockSigner);\n // Should resolve from finalized, not best-block (since ok was undefined)\n expect(result.block.number).toBe(101);\n });\n });\n}\n","import type { PolkadotSigner } from \"polkadot-api\";\nimport { createLogger } from \"@parity/product-sdk-logger\";\n\nimport { TxBatchError } from \"./errors.js\";\nimport { submitAndWatch } from \"./submit.js\";\nimport type {\n BatchApi,\n BatchSubmitOptions,\n BatchableCall,\n SubmittableTransaction,\n TxResult,\n} from \"./types.js\";\n\nconst log = createLogger(\"tx:batch\");\n\n/**\n * Resolve a single call to its decoded call data.\n *\n * Handles three shapes:\n * 1. Ink SDK AsyncTransaction — has `.waited` Promise that resolves to a tx with `.decodedCall`\n * 2. PAPI transaction or object with `.decodedCall` — extract directly\n * 3. Raw decoded call — pass through as-is\n */\nasync function resolveDecodedCall(call: BatchableCall): Promise<unknown> {\n if (call != null && typeof call === \"object\") {\n const obj = call as Record<string, unknown>;\n\n // Handle Ink SDK AsyncTransaction: resolve .waited first\n if (\n \"waited\" in obj &&\n obj.waited &&\n typeof (obj.waited as Promise<unknown>).then === \"function\"\n ) {\n log.debug(\"Resolving Ink SDK AsyncTransaction in batch\");\n const resolved = (await obj.waited) as Record<string, unknown>;\n if (resolved.decodedCall !== undefined) return resolved.decodedCall;\n throw new TxBatchError(\"Resolved AsyncTransaction has no decodedCall property\");\n }\n\n // Handle SubmittableTransaction or object with decodedCall\n if (\"decodedCall\" in obj && obj.decodedCall !== undefined) {\n return obj.decodedCall;\n }\n }\n\n // Reject null, undefined, and primitives — they will cause cryptic codec errors on-chain\n if (call == null || typeof call !== \"object\") {\n throw new TxBatchError(\n `Invalid batch call: expected a transaction or decoded call object, got ${call === null ? \"null\" : typeof call}`,\n );\n }\n\n // Raw decoded call object — pass through\n return call;\n}\n\n/**\n * Batch multiple transactions into a single Substrate Utility batch and submit.\n *\n * Extracts `.decodedCall` from each transaction (handling Ink SDK `AsyncTransaction`\n * wrappers), wraps them in `Utility.batch_all` (or `batch`/`force_batch` via the\n * `mode` option), and submits via {@link submitAndWatch} with full lifecycle tracking.\n *\n * @param calls - Array of transactions, AsyncTransactions, or raw decoded calls to batch.\n * @param api - A typed API with `tx.Utility.batch_all/batch/force_batch`. Works with any\n * chain that has the Utility pallet — no chain-specific imports required.\n * **All calls must target the same chain as this API.** Do not mix decoded calls\n * from different chains (e.g., Asset Hub and Bulletin) in a single batch.\n * @param signer - The signer to use. Can come from the Host API\n * (`getProductAccountSigner`) or {@link createDevSigner}.\n * @param options - Optional {@link BatchSubmitOptions} (extends `SubmitOptions` with `mode`).\n * @returns The transaction result from the batch submission.\n *\n * @throws {TxBatchError} If `calls` is empty.\n * @throws {TxBatchError} If an AsyncTransaction resolves without a `.decodedCall` property.\n * @throws {TxTimeoutError} If the batch transaction does not reach the target state within `timeoutMs`.\n * @throws {TxDispatchError} If the on-chain dispatch fails.\n * @throws {TxSigningRejectedError} If the user rejects signing in their wallet.\n *\n * @example\n * ```ts\n * import { batchSubmitAndWatch } from \"@parity/product-sdk-tx\";\n *\n * const tx1 = api.tx.Balances.transfer_keep_alive({ dest: addr1, value: 1_000n });\n * const tx2 = api.tx.Balances.transfer_keep_alive({ dest: addr2, value: 2_000n });\n *\n * const result = await batchSubmitAndWatch([tx1, tx2], api, signer, {\n * onStatus: (status) => console.log(status),\n * });\n * ```\n */\nexport async function batchSubmitAndWatch(\n calls: BatchableCall[],\n api: BatchApi,\n signer: PolkadotSigner,\n options?: BatchSubmitOptions,\n): Promise<TxResult> {\n if (calls.length === 0) {\n throw new TxBatchError(\"Cannot batch zero calls\");\n }\n\n const mode = options?.mode ?? \"batch_all\";\n\n log.info(\"Resolving batch calls\", { count: calls.length, mode });\n const decodedCalls = await Promise.all(calls.map(resolveDecodedCall));\n\n log.info(\"Constructing batch transaction\", { mode, callCount: decodedCalls.length });\n const batchTx = api.tx.Utility[mode]({ calls: decodedCalls });\n\n return submitAndWatch(batchTx, signer, options);\n}\n\nif (import.meta.vitest) {\n const { describe, test, expect, vi, beforeEach } = import.meta.vitest;\n const { configure } = await import(\"@parity/product-sdk-logger\");\n const { TxDispatchError, TxSigningRejectedError } = await import(\"./errors.js\");\n type TxEvent = import(\"./types.js\").TxEvent;\n\n // Silence logger during tests\n beforeEach(() => {\n configure({ handler: () => {} });\n });\n\n type MockSubscribeHandlers = {\n next: (event: TxEvent) => void;\n error: (error: Error) => void;\n };\n\n function createMockTx(\n emitFn: (handlers: MockSubscribeHandlers) => void,\n decodedCall?: unknown,\n ): SubmittableTransaction {\n return {\n signSubmitAndWatch: (_signer: PolkadotSigner, _options?: unknown) => ({\n subscribe: (handlers: MockSubscribeHandlers) => {\n const unsub = vi.fn();\n queueMicrotask(() => emitFn(handlers));\n return { unsubscribe: unsub };\n },\n }),\n decodedCall,\n };\n }\n\n const mockSigner = {} as PolkadotSigner;\n\n const signedEvent: TxEvent = { type: \"signed\", txHash: \"0xbatch\" };\n const bestBlockOk: TxEvent = {\n type: \"txBestBlocksState\",\n txHash: \"0xbatch\",\n found: true,\n ok: true,\n events: [{ id: 1 }],\n block: { hash: \"0xblock1\", number: 100, index: 0 },\n };\n\n function createMockBatchApi(emitFn: (handlers: MockSubscribeHandlers) => void): {\n api: BatchApi;\n getCapturedCalls: () => unknown[][];\n } {\n const capturedCalls: unknown[][] = [];\n\n const api: BatchApi = {\n tx: {\n Utility: {\n batch: vi.fn((args: { calls: unknown[] }) => {\n capturedCalls.push(args.calls);\n return createMockTx(emitFn);\n }) as BatchApi[\"tx\"][\"Utility\"][\"batch\"],\n batch_all: vi.fn((args: { calls: unknown[] }) => {\n capturedCalls.push(args.calls);\n return createMockTx(emitFn);\n }) as BatchApi[\"tx\"][\"Utility\"][\"batch_all\"],\n force_batch: vi.fn((args: { calls: unknown[] }) => {\n capturedCalls.push(args.calls);\n return createMockTx(emitFn);\n }) as BatchApi[\"tx\"][\"Utility\"][\"force_batch\"],\n },\n },\n };\n\n return { api, getCapturedCalls: () => capturedCalls };\n }\n\n const successEmit = (h: MockSubscribeHandlers) => {\n h.next(signedEvent);\n h.next(bestBlockOk);\n };\n\n describe(\"batchSubmitAndWatch\", () => {\n test(\"batches multiple transactions with decodedCall\", async () => {\n const { api, getCapturedCalls } = createMockBatchApi(successEmit);\n const calls = [\n { decodedCall: { pallet: \"Balances\", method: \"transfer\", args: { value: 1 } } },\n { decodedCall: { pallet: \"Balances\", method: \"transfer\", args: { value: 2 } } },\n ];\n\n const result = await batchSubmitAndWatch(calls, api, mockSigner);\n\n expect(result.ok).toBe(true);\n expect(getCapturedCalls()).toHaveLength(1);\n expect(getCapturedCalls()[0]).toEqual([\n { pallet: \"Balances\", method: \"transfer\", args: { value: 1 } },\n { pallet: \"Balances\", method: \"transfer\", args: { value: 2 } },\n ]);\n expect(api.tx.Utility.batch_all).toHaveBeenCalledOnce();\n });\n\n test(\"handles Ink SDK AsyncTransaction wrappers\", async () => {\n const { api, getCapturedCalls } = createMockBatchApi(successEmit);\n const asyncCall = {\n waited: Promise.resolve({ decodedCall: { pallet: \"Contracts\", method: \"call\" } }),\n signSubmitAndWatch: () => {\n throw new Error(\"Should not be called\");\n },\n };\n\n const result = await batchSubmitAndWatch([asyncCall], api, mockSigner);\n\n expect(result.ok).toBe(true);\n expect(getCapturedCalls()[0]).toEqual([{ pallet: \"Contracts\", method: \"call\" }]);\n });\n\n test(\"accepts raw decoded calls (pass-through)\", async () => {\n const { api, getCapturedCalls } = createMockBatchApi(successEmit);\n const rawCall = { pallet: \"System\", method: \"remark\" };\n\n const result = await batchSubmitAndWatch([rawCall], api, mockSigner);\n\n expect(result.ok).toBe(true);\n expect(getCapturedCalls()[0]).toEqual([{ pallet: \"System\", method: \"remark\" }]);\n });\n\n test(\"mixes transaction types in a single batch\", async () => {\n const { api, getCapturedCalls } = createMockBatchApi(successEmit);\n const txWithDecoded = { decodedCall: \"call1\" };\n const asyncTx = {\n waited: Promise.resolve({ decodedCall: \"call2\" }),\n signSubmitAndWatch: () => {\n throw new Error(\"Should not be called\");\n },\n };\n const rawCall = { pallet: \"System\", method: \"remark\" };\n\n const result = await batchSubmitAndWatch(\n [txWithDecoded, asyncTx, rawCall],\n api,\n mockSigner,\n );\n\n expect(result.ok).toBe(true);\n expect(getCapturedCalls()[0]).toEqual([\n \"call1\",\n \"call2\",\n { pallet: \"System\", method: \"remark\" },\n ]);\n });\n\n test(\"throws TxBatchError for empty calls array\", async () => {\n const { api } = createMockBatchApi(successEmit);\n await expect(batchSubmitAndWatch([], api, mockSigner)).rejects.toThrow(TxBatchError);\n await expect(batchSubmitAndWatch([], api, mockSigner)).rejects.toThrow(\n \"Cannot batch zero calls\",\n );\n });\n\n test(\"throws TxBatchError when AsyncTransaction resolves without decodedCall\", async () => {\n const { api } = createMockBatchApi(successEmit);\n const badAsync = {\n waited: Promise.resolve({ noDecodedCall: true }),\n signSubmitAndWatch: () => {\n throw new Error(\"Should not be called\");\n },\n };\n\n await expect(\n batchSubmitAndWatch([badAsync as unknown as BatchableCall], api, mockSigner),\n ).rejects.toThrow(TxBatchError);\n });\n\n test(\"throws TxBatchError for null call\", async () => {\n const { api } = createMockBatchApi(successEmit);\n await expect(\n batchSubmitAndWatch([null as unknown as BatchableCall], api, mockSigner),\n ).rejects.toThrow(TxBatchError);\n await expect(\n batchSubmitAndWatch([null as unknown as BatchableCall], api, mockSigner),\n ).rejects.toThrow(\"Invalid batch call\");\n });\n\n test(\"throws TxBatchError for primitive call\", async () => {\n const { api } = createMockBatchApi(successEmit);\n await expect(\n batchSubmitAndWatch([42 as unknown as BatchableCall], api, mockSigner),\n ).rejects.toThrow(TxBatchError);\n await expect(\n batchSubmitAndWatch([\"oops\" as unknown as BatchableCall], api, mockSigner),\n ).rejects.toThrow(\"Invalid batch call\");\n });\n\n test(\"treats { decodedCall: undefined } as raw pass-through object\", async () => {\n const { api, getCapturedCalls } = createMockBatchApi(successEmit);\n const edgeCase = { decodedCall: undefined, other: \"data\" };\n\n const result = await batchSubmitAndWatch([edgeCase], api, mockSigner);\n\n expect(result.ok).toBe(true);\n // decodedCall is undefined so it falls through to raw pass-through\n expect(getCapturedCalls()[0]).toEqual([{ decodedCall: undefined, other: \"data\" }]);\n });\n\n test(\"defaults to batch_all mode\", async () => {\n const { api } = createMockBatchApi(successEmit);\n await batchSubmitAndWatch([{ decodedCall: \"call1\" }], api, mockSigner);\n\n expect(api.tx.Utility.batch_all).toHaveBeenCalledOnce();\n expect(api.tx.Utility.batch).not.toHaveBeenCalled();\n expect(api.tx.Utility.force_batch).not.toHaveBeenCalled();\n });\n\n test(\"respects mode: batch\", async () => {\n const { api } = createMockBatchApi(successEmit);\n await batchSubmitAndWatch([{ decodedCall: \"call1\" }], api, mockSigner, {\n mode: \"batch\",\n });\n\n expect(api.tx.Utility.batch).toHaveBeenCalledOnce();\n expect(api.tx.Utility.batch_all).not.toHaveBeenCalled();\n });\n\n test(\"respects mode: force_batch\", async () => {\n const { api } = createMockBatchApi(successEmit);\n await batchSubmitAndWatch([{ decodedCall: \"call1\" }], api, mockSigner, {\n mode: \"force_batch\",\n });\n\n expect(api.tx.Utility.force_batch).toHaveBeenCalledOnce();\n expect(api.tx.Utility.batch_all).not.toHaveBeenCalled();\n });\n\n test(\"forwards SubmitOptions to submitAndWatch\", async () => {\n const statuses: string[] = [];\n const { api } = createMockBatchApi(successEmit);\n\n await batchSubmitAndWatch([{ decodedCall: \"call1\" }], api, mockSigner, {\n onStatus: (s) => statuses.push(s),\n });\n\n expect(statuses).toContain(\"signing\");\n expect(statuses).toContain(\"in-block\");\n });\n\n test(\"propagates TxDispatchError\", async () => {\n const { api } = createMockBatchApi((h) => {\n h.next(signedEvent);\n h.next({\n type: \"txBestBlocksState\",\n txHash: \"0xbatch\",\n found: true,\n ok: false,\n events: [],\n block: { hash: \"0xblock1\", number: 100, index: 0 },\n dispatchError: {\n type: \"Module\",\n value: { type: \"Utility\", value: { type: \"TooManyCalls\" } },\n },\n });\n });\n\n await expect(\n batchSubmitAndWatch([{ decodedCall: \"call1\" }], api, mockSigner),\n ).rejects.toThrow(TxDispatchError);\n });\n\n test(\"propagates TxSigningRejectedError\", async () => {\n const { api } = createMockBatchApi((h) => {\n h.error(new Error(\"User rejected the request\"));\n });\n\n await expect(\n batchSubmitAndWatch([{ decodedCall: \"call1\" }], api, mockSigner),\n ).rejects.toThrow(TxSigningRejectedError);\n });\n\n test(\"resolves all calls in parallel\", async () => {\n const { api } = createMockBatchApi(successEmit);\n const resolveOrder: number[] = [];\n\n const asyncCall1 = {\n waited: new Promise<{ decodedCall: string }>((resolve) => {\n setTimeout(() => {\n resolveOrder.push(1);\n resolve({ decodedCall: \"call1\" });\n }, 10);\n }),\n signSubmitAndWatch: () => {\n throw new Error(\"Should not be called\");\n },\n };\n const asyncCall2 = {\n waited: new Promise<{ decodedCall: string }>((resolve) => {\n setTimeout(() => {\n resolveOrder.push(2);\n resolve({ decodedCall: \"call2\" });\n }, 5); // Resolves faster\n }),\n signSubmitAndWatch: () => {\n throw new Error(\"Should not be called\");\n },\n };\n\n await batchSubmitAndWatch([asyncCall1, asyncCall2], api, mockSigner);\n\n // Both should have resolved (order depends on timing, but both are present)\n expect(resolveOrder).toContain(1);\n expect(resolveOrder).toContain(2);\n });\n });\n}\n","import { createLogger } from \"@parity/product-sdk-logger\";\n\nimport { TxBatchError, TxDispatchError, TxSigningRejectedError, TxTimeoutError } from \"./errors.js\";\nimport type { RetryOptions } from \"./types.js\";\n\nconst log = createLogger(\"tx:retry\");\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/**\n * Whether an error is deterministic and should not be retried.\n *\n * - Batch errors are deterministic input validation failures (e.g., empty calls array).\n * - Dispatch errors are on-chain failures (e.g., insufficient balance) that will\n * produce the same result on retry.\n * - Signing rejections are explicit user intent.\n * - Timeouts mean we already waited the full duration; retrying would double the wait.\n */\nfunction isNonRetryable(error: unknown): boolean {\n return (\n error instanceof TxBatchError ||\n error instanceof TxDispatchError ||\n error instanceof TxSigningRejectedError ||\n error instanceof TxTimeoutError\n );\n}\n\n/**\n * Calculate delay with exponential backoff and jitter.\n *\n * Jitter prevents thundering-herd when multiple clients retry simultaneously.\n * The delay is `min(baseDelay * 2^attempt, maxDelay) * random(0.5, 1.0)`.\n */\nexport function calculateDelay(attempt: number, baseDelayMs: number, maxDelayMs: number): number {\n const exponential = Math.min(baseDelayMs * 2 ** attempt, maxDelayMs);\n const jitter = 0.5 + Math.random() * 0.5;\n return Math.round(exponential * jitter);\n}\n\n/**\n * Wrap an async function with retry logic and exponential backoff.\n *\n * Only retries transient errors (network disconnects, temporary RPC failures).\n * Deterministic errors ({@link TxDispatchError}, {@link TxBatchError}), user\n * rejections ({@link TxSigningRejectedError}), and timeouts ({@link TxTimeoutError})\n * are rethrown immediately without retry.\n *\n * @param fn - The async function to retry.\n * @param options - Retry configuration.\n * @returns The result of the first successful call.\n *\n * @example\n * ```ts\n * const result = await withRetry(\n * () => submitAndWatch(tx, signer),\n * { maxAttempts: 3, baseDelayMs: 1_000 },\n * );\n * ```\n */\nexport async function withRetry<T>(fn: () => Promise<T>, options?: RetryOptions): Promise<T> {\n const maxAttempts = options?.maxAttempts ?? 3;\n const baseDelayMs = options?.baseDelayMs ?? 1_000;\n const maxDelayMs = options?.maxDelayMs ?? 15_000;\n\n let lastError: unknown;\n\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n try {\n return await fn();\n } catch (error) {\n lastError = error;\n\n if (isNonRetryable(error)) {\n throw error;\n }\n\n if (attempt + 1 >= maxAttempts) {\n break;\n }\n\n const delay = calculateDelay(attempt, baseDelayMs, maxDelayMs);\n log.warn(`Attempt ${attempt + 1}/${maxAttempts} failed, retrying in ${delay}ms`, {\n error: error instanceof Error ? error.message : String(error),\n });\n await sleep(delay);\n }\n }\n\n throw lastError;\n}\n\nif (import.meta.vitest) {\n const { describe, test, expect, vi, beforeEach } = import.meta.vitest;\n const { configure } = await import(\"@parity/product-sdk-logger\");\n\n beforeEach(() => {\n configure({ handler: () => {} });\n vi.useRealTimers();\n });\n\n describe(\"withRetry\", () => {\n test(\"returns on first success\", async () => {\n const result = await withRetry(() => Promise.resolve(\"ok\"));\n expect(result).toBe(\"ok\");\n });\n\n test(\"retries transient error then succeeds\", async () => {\n let calls = 0;\n const result = await withRetry(\n () => {\n calls++;\n if (calls < 2) return Promise.reject(new Error(\"Network error\"));\n return Promise.resolve(\"recovered\");\n },\n { baseDelayMs: 1 },\n );\n expect(result).toBe(\"recovered\");\n expect(calls).toBe(2);\n });\n\n test(\"gives up after maxAttempts\", async () => {\n let calls = 0;\n await expect(\n withRetry(\n () => {\n calls++;\n return Promise.reject(new Error(\"Persistent failure\"));\n },\n { maxAttempts: 3, baseDelayMs: 1 },\n ),\n ).rejects.toThrow(\"Persistent failure\");\n expect(calls).toBe(3);\n });\n\n test(\"does NOT retry TxDispatchError\", async () => {\n let calls = 0;\n await expect(\n withRetry(\n () => {\n calls++;\n return Promise.reject(\n new TxDispatchError({}, \"Balances.InsufficientBalance\"),\n );\n },\n { maxAttempts: 3, baseDelayMs: 1 },\n ),\n ).rejects.toThrow(TxDispatchError);\n expect(calls).toBe(1);\n });\n\n test(\"does NOT retry TxSigningRejectedError\", async () => {\n let calls = 0;\n await expect(\n withRetry(\n () => {\n calls++;\n return Promise.reject(new TxSigningRejectedError());\n },\n { maxAttempts: 3, baseDelayMs: 1 },\n ),\n ).rejects.toThrow(TxSigningRejectedError);\n expect(calls).toBe(1);\n });\n\n test(\"does NOT retry TxBatchError\", async () => {\n let calls = 0;\n await expect(\n withRetry(\n () => {\n calls++;\n return Promise.reject(new TxBatchError(\"Cannot batch zero calls\"));\n },\n { maxAttempts: 3, baseDelayMs: 1 },\n ),\n ).rejects.toThrow(TxBatchError);\n expect(calls).toBe(1);\n });\n\n test(\"does NOT retry TxTimeoutError\", async () => {\n let calls = 0;\n await expect(\n withRetry(\n () => {\n calls++;\n return Promise.reject(new TxTimeoutError(300_000));\n },\n { maxAttempts: 3, baseDelayMs: 1 },\n ),\n ).rejects.toThrow(TxTimeoutError);\n expect(calls).toBe(1);\n });\n\n test(\"respects maxDelayMs cap\", () => {\n // attempt=10 with baseDelay=1000 would be 1024000ms uncapped\n const delay = calculateDelay(10, 1_000, 15_000);\n expect(delay).toBeLessThanOrEqual(15_000);\n expect(delay).toBeGreaterThan(0);\n });\n\n test(\"applies jitter (delay varies between calls)\", () => {\n const delays = Array.from({ length: 20 }, () => calculateDelay(2, 1_000, 15_000));\n const unique = new Set(delays);\n // With jitter, we should get multiple distinct values out of 20 samples\n expect(unique.size).toBeGreaterThan(1);\n });\n\n test(\"exponential backoff increases delay\", () => {\n const base = 1_000;\n // The minimum possible delay at each attempt (jitter factor = 0.5)\n const minDelay2 = base * 4 * 0.5; // 2000\n // attempt 2 minimum should be greater than attempt 0 maximum\n const maxDelay0 = base * 1.0; // 1000\n expect(minDelay2).toBeGreaterThan(maxDelay0);\n });\n });\n}\n","import { DEV_PHRASE } from \"@polkadot-labs/hdkd-helpers\";\nimport { seedToAccount } from \"@parity/product-sdk-keys\";\nimport type { PolkadotSigner } from \"polkadot-api\";\n\nimport type { DevAccountName } from \"./types.js\";\n\n/**\n * Create a PolkadotSigner for a standard Substrate dev account.\n *\n * Dev accounts use the well-known Substrate dev mnemonic (`DEV_PHRASE`) with\n * Sr25519 key derivation at the path `//{Name}`. These accounts have known\n * private keys and are pre-funded on dev/test chains.\n *\n * Only for local development, scripts, and testing. Never use in production.\n *\n * @param name - Dev account name (\"Alice\", \"Bob\", \"Charlie\", \"Dave\", \"Eve\", or \"Ferdie\").\n * @returns A PolkadotSigner that can sign transactions.\n *\n * @example\n * ```ts\n * import { createDevSigner } from \"@parity/product-sdk-tx\";\n *\n * const alice = createDevSigner(\"Alice\");\n * const result = await submitAndWatch(tx, alice);\n * ```\n */\nexport function createDevSigner(name: DevAccountName): PolkadotSigner {\n return seedToAccount(DEV_PHRASE, `//${name}`).signer;\n}\n\n/**\n * Get the public key bytes for a dev account.\n *\n * Useful for address derivation or identity checks in tests without\n * needing the full signer.\n *\n * @param name - Dev account name.\n * @returns 32-byte Sr25519 public key.\n */\nexport function getDevPublicKey(name: DevAccountName): Uint8Array {\n return seedToAccount(DEV_PHRASE, `//${name}`).publicKey;\n}\n\nif (import.meta.vitest) {\n const { describe, test, expect } = import.meta.vitest;\n\n // Alice's well-known sr25519 public key\n const ALICE_PUBKEY = new Uint8Array([\n 0xd4, 0x35, 0x93, 0xc7, 0x15, 0xfd, 0xd3, 0x1c, 0x61, 0x14, 0x1a, 0xbd, 0x04, 0xa9, 0x9f,\n 0xd6, 0x82, 0x2c, 0x85, 0x58, 0x85, 0x4c, 0xcd, 0xe3, 0x9a, 0x56, 0x84, 0xe7, 0xa5, 0x6d,\n 0xa2, 0x7d,\n ]);\n\n describe(\"createDevSigner\", () => {\n test(\"creates a signer for Alice with known public key\", () => {\n const signer = createDevSigner(\"Alice\");\n expect(signer).toBeDefined();\n expect(signer.publicKey).toEqual(ALICE_PUBKEY);\n });\n\n test(\"different names produce different signers\", () => {\n const alice = createDevSigner(\"Alice\");\n const bob = createDevSigner(\"Bob\");\n expect(alice.publicKey).not.toEqual(bob.publicKey);\n });\n\n test(\"all dev account names produce valid signers\", () => {\n const names: DevAccountName[] = [\"Alice\", \"Bob\", \"Charlie\", \"Dave\", \"Eve\", \"Ferdie\"];\n const keys = new Set<string>();\n\n for (const name of names) {\n const signer = createDevSigner(name);\n expect(signer).toBeDefined();\n expect(signer.publicKey).toBeInstanceOf(Uint8Array);\n expect(signer.publicKey.length).toBe(32);\n // All keys should be unique\n const hex = Array.from(signer.publicKey)\n .map((b) => b.toString(16).padStart(2, \"0\"))\n .join(\"\");\n expect(keys.has(hex)).toBe(false);\n keys.add(hex);\n }\n });\n });\n\n describe(\"getDevPublicKey\", () => {\n test(\"returns Alice's known public key\", () => {\n expect(getDevPublicKey(\"Alice\")).toEqual(ALICE_PUBKEY);\n });\n\n test(\"matches the signer's public key\", () => {\n const signer = createDevSigner(\"Bob\");\n const pubkey = getDevPublicKey(\"Bob\");\n expect(pubkey).toEqual(signer.publicKey);\n });\n });\n}\n","import { TxDryRunError, formatDryRunError } from \"./errors.js\";\nimport type { SubmittableTransaction, Weight } from \"./types.js\";\n\n/**\n * Validate an Ink SDK dry-run result and extract the submittable transaction.\n *\n * Replaces the 5-10 line boilerplate that every contract interaction repeats:\n * check `success`, parse the error, verify `send()` exists, and call it.\n *\n * Works with any object whose shape matches the Ink SDK contract query result\n * (typed structurally — no Ink SDK import required):\n *\n * - `contract.query(\"method\", { origin, data })` (Ink SDK)\n * - `contract.write(\"method\", args, origin)` (patched SDK wrappers)\n * - Any object with `{ success: boolean; value?: { send?(): ... } }`\n *\n * @param result - The dry-run result from a contract query or write simulation.\n * @returns The submittable transaction, ready to pass to {@link submitAndWatch}.\n * @throws {TxDryRunError} If the dry run failed or the result has no `send()`.\n *\n * @example\n * ```ts\n * import { extractTransaction, submitAndWatch, createDevSigner } from \"@parity/product-sdk-tx\";\n *\n * const dryRun = await contract.query(\"createItem\", { origin, data: { name, price } });\n * const tx = extractTransaction(dryRun);\n * const result = await submitAndWatch(tx, createDevSigner(\"Alice\"));\n * ```\n *\n * @example Composing with retry logic:\n * ```ts\n * const tx = extractTransaction(await contract.query(\"transfer\", { origin, data }));\n * const result = await withRetry(() => submitAndWatch(tx, signer));\n * ```\n */\nexport function extractTransaction(result: {\n success: boolean;\n value?: unknown;\n error?: unknown;\n}): SubmittableTransaction {\n if (!result.success) {\n const formatted = formatDryRunError(result);\n const revertReason = extractRevertReason(result.value);\n throw new TxDryRunError(result, formatted, revertReason);\n }\n\n const value = result.value;\n if (value == null || typeof value !== \"object\") {\n throw new TxDryRunError(result, \"dry run returned no value\");\n }\n\n const v = value as Record<string, unknown>;\n if (typeof v.send !== \"function\") {\n throw new TxDryRunError(result, \"not a write query (no send())\");\n }\n\n return v.send() as SubmittableTransaction;\n}\n\n/**\n * Try to extract a revert reason string from a dry-run result value.\n * Returns `undefined` if no revert reason is available.\n */\nfunction extractRevertReason(value: unknown): string | undefined {\n if (value == null || typeof value !== \"object\") return undefined;\n const v = value as Record<string, unknown>;\n\n if (typeof v.revertReason === \"string\" && v.revertReason) {\n return v.revertReason;\n }\n\n // Wrapped raw value (patched SDK)\n if (\"raw\" in v && v.raw != null && typeof v.raw === \"object\") {\n return extractRevertReason(v.raw);\n }\n\n return undefined;\n}\n\n/**\n * Apply a safety buffer to weight estimates from a dry-run result.\n *\n * Dry-run weight estimates reflect the exact execution cost at the time of\n * simulation. On-chain conditions can change between dry-run and actual\n * submission (storage growth, state changes by other transactions), so a\n * buffer prevents unexpected `OutOfGas` failures.\n *\n * The default 25% buffer matches the convention used across Polkadot\n * ecosystem tooling.\n *\n * @param weight - The `weight_required` from a `ReviveApi.call` or `ReviveApi.eth_transact` dry-run.\n * @param options - Override the buffer percentage (default: 25%).\n * @returns A new weight with both components scaled up.\n *\n * @example Basic usage with ReviveApi dry-run:\n * ```ts\n * const dryRun = await api.apis.ReviveApi.call(origin, dest, value, undefined, undefined, data);\n *\n * const tx = api.tx.Revive.call({\n * dest, value, data,\n * weight_limit: applyWeightBuffer(dryRun.weight_required),\n * storage_deposit_limit: dryRun.storage_deposit.value,\n * });\n * ```\n *\n * @example Custom buffer for latency-sensitive operations:\n * ```ts\n * applyWeightBuffer(dryRun.weight_required, { percent: 50 });\n * ```\n */\nexport function applyWeightBuffer(weight: Weight, options?: { percent?: number }): Weight {\n const percent = options?.percent ?? 25;\n const multiplier = 100n + BigInt(percent);\n return {\n ref_time: (weight.ref_time * multiplier) / 100n,\n proof_size: (weight.proof_size * multiplier) / 100n,\n };\n}\n\nif (import.meta.vitest) {\n const { describe, test, expect } = import.meta.vitest;\n\n describe(\"extractTransaction\", () => {\n test(\"returns tx from successful dry-run with send()\", () => {\n const mockTx = {\n signSubmitAndWatch: () => ({ subscribe: () => ({ unsubscribe: () => {} }) }),\n };\n const result = {\n success: true,\n value: { response: \"ok\", send: () => mockTx },\n };\n expect(extractTransaction(result)).toBe(mockTx);\n });\n\n test(\"throws TxDryRunError on failed dry-run\", () => {\n const result = {\n success: false,\n value: { revertReason: \"InsufficientBalance\" },\n };\n try {\n extractTransaction(result);\n expect.unreachable(\"should have thrown\");\n } catch (e) {\n expect(e).toBeInstanceOf(TxDryRunError);\n const err = e as TxDryRunError;\n expect(err.revertReason).toBe(\"InsufficientBalance\");\n expect(err.formatted).toBe(\"InsufficientBalance\");\n expect(err.message).toContain(\"InsufficientBalance\");\n expect(err.raw).toBe(result);\n }\n });\n\n test(\"throws TxDryRunError with Module error formatting\", () => {\n const result = {\n success: false,\n value: {\n type: \"Module\",\n value: { type: \"Revive\", value: { type: \"StorageDepositNotEnoughFunds\" } },\n },\n };\n try {\n extractTransaction(result);\n expect.unreachable(\"should have thrown\");\n } catch (e) {\n const err = e as TxDryRunError;\n expect(err.formatted).toBe(\"Revive.StorageDepositNotEnoughFunds\");\n expect(err.revertReason).toBeUndefined();\n }\n });\n\n test(\"throws TxDryRunError with error field\", () => {\n const result = {\n success: false,\n value: {},\n error: { type: \"ContractTrapped\" },\n };\n try {\n extractTransaction(result);\n expect.unreachable(\"should have thrown\");\n } catch (e) {\n const err = e as TxDryRunError;\n expect(err.formatted).toBe(\"ContractTrapped\");\n }\n });\n\n test(\"throws when value is missing\", () => {\n const result = { success: true };\n expect(() => extractTransaction(result)).toThrow(TxDryRunError);\n });\n\n test(\"throws when send is not a function\", () => {\n const result = { success: true, value: { response: \"ok\" } };\n expect(() => extractTransaction(result)).toThrow(\"not a write query\");\n });\n\n test(\"throws with revertReason from nested raw (patched SDK)\", () => {\n const result = {\n success: false,\n value: { raw: { revertReason: \"Unauthorized\" } },\n };\n try {\n extractTransaction(result);\n expect.unreachable(\"should have thrown\");\n } catch (e) {\n const err = e as TxDryRunError;\n expect(err.revertReason).toBe(\"Unauthorized\");\n }\n });\n\n test(\"throws with ReviveApi Message error\", () => {\n const result = {\n success: false,\n value: { type: \"Message\", value: \"Insufficient balance for gas * price + value\" },\n };\n try {\n extractTransaction(result);\n expect.unreachable(\"should have thrown\");\n } catch (e) {\n const err = e as TxDryRunError;\n expect(err.formatted).toBe(\"Insufficient balance for gas * price + value\");\n }\n });\n });\n\n describe(\"applyWeightBuffer\", () => {\n test(\"applies default 25% buffer\", () => {\n const weight: Weight = { ref_time: 1000n, proof_size: 500n };\n const buffered = applyWeightBuffer(weight);\n expect(buffered.ref_time).toBe(1250n);\n expect(buffered.proof_size).toBe(625n);\n });\n\n test(\"applies custom buffer percentage\", () => {\n const weight: Weight = { ref_time: 1000n, proof_size: 1000n };\n const buffered = applyWeightBuffer(weight, { percent: 50 });\n expect(buffered.ref_time).toBe(1500n);\n expect(buffered.proof_size).toBe(1500n);\n });\n\n test(\"zero buffer returns same values\", () => {\n const weight: Weight = { ref_time: 1000n, proof_size: 500n };\n const buffered = applyWeightBuffer(weight, { percent: 0 });\n expect(buffered.ref_time).toBe(1000n);\n expect(buffered.proof_size).toBe(500n);\n });\n\n test(\"does not mutate original weight\", () => {\n const weight: Weight = { ref_time: 1000n, proof_size: 500n };\n const buffered = applyWeightBuffer(weight);\n expect(weight.ref_time).toBe(1000n);\n expect(weight.proof_size).toBe(500n);\n expect(buffered).not.toBe(weight);\n });\n\n test(\"works with realistic weight values\", () => {\n // Values from tick3t reference repo\n const weight: Weight = { ref_time: 4_500_000_000n, proof_size: 1_000_000n };\n const buffered = applyWeightBuffer(weight);\n expect(buffered.ref_time).toBe(5_625_000_000n);\n expect(buffered.proof_size).toBe(1_250_000n);\n });\n });\n}\n","import type { PolkadotSigner } from \"polkadot-api\";\n\nimport { createLogger } from \"@parity/product-sdk-logger\";\n\nimport { submitAndWatch } from \"./submit.js\";\nimport type { SubmittableTransaction, TxResult } from \"./types.js\";\n\nconst log = createLogger(\"tx:mapping\");\n\n/**\n * Error thrown when account mapping fails.\n */\nexport class TxAccountMappingError extends Error {\n constructor(message: string, options?: ErrorOptions) {\n super(message, options);\n this.name = \"TxAccountMappingError\";\n }\n}\n\n/**\n * Minimal interface for checking if an address is mapped on-chain.\n *\n * Accepted structurally so this module stays runtime-agnostic. Build one\n * directly from the typed API of any pallet-revive-capable chain — see the\n * example on {@link ensureAccountMapped}.\n */\nexport interface MappingChecker {\n addressIsMapped(address: string): Promise<boolean>;\n}\n\n/**\n * Minimal typed API shape for `Revive.map_account()`.\n *\n * Accepted structurally so this module works with any PAPI typed API\n * that has the Revive pallet, without importing chain-specific descriptors.\n */\nexport interface ReviveApi {\n tx: {\n Revive: {\n map_account(): SubmittableTransaction;\n };\n };\n}\n\n/** Options for {@link ensureAccountMapped}. */\nexport interface EnsureAccountMappedOptions {\n /** Timeout in ms for the map_account transaction. Default: 60_000 (1 minute). */\n timeoutMs?: number;\n /** Called on mapping transaction status changes. */\n onStatus?: (status: \"checking\" | \"mapping\" | \"mapped\" | \"already-mapped\") => void;\n}\n\n/**\n * Ensure an account's SS58 address is mapped to its H160 EVM address on-chain.\n *\n * Account mapping is a prerequisite for any EVM contract interaction on Asset Hub.\n * This function checks the on-chain mapping status and, if unmapped, submits a\n * `Revive.map_account()` transaction and waits for inclusion.\n *\n * Idempotent — safe to call multiple times. Returns immediately if already mapped.\n *\n * @param address - The SS58 address to check/map.\n * @param signer - The signer for the account (must match the address).\n * @param checker - An object with `addressIsMapped()`. Easiest path: build one\n * from a pallet-revive-capable typed API by querying `Revive.OriginalAccount`.\n * @param api - A typed API with `tx.Revive.map_account()`.\n * @param options - Optional timeout and status callback.\n * @returns The transaction result if mapping was performed, or `null` if already mapped.\n *\n * @throws {TxAccountMappingError} If the mapping check or transaction fails.\n * @throws {TxDispatchError} If the map_account transaction fails on-chain.\n * @throws {TxTimeoutError} If the mapping transaction times out.\n *\n * @example\n * ```ts\n * import { ensureAccountMapped } from \"@parity/product-sdk-tx\";\n * import { ss58ToH160 } from \"@parity/product-sdk-address\";\n *\n * const api = client.getTypedApi(paseo_asset_hub);\n * const checker = {\n * addressIsMapped: async (addr: string) =>\n * (await api.query.Revive.OriginalAccount.getValue(ss58ToH160(addr))) !== undefined,\n * };\n *\n * await ensureAccountMapped(address, signer, checker, api);\n * // Account is now mapped — safe to call PolkaVM/Solidity contracts\n * ```\n */\nexport async function ensureAccountMapped(\n address: string,\n signer: PolkadotSigner,\n checker: MappingChecker,\n api: ReviveApi,\n options?: EnsureAccountMappedOptions,\n): Promise<TxResult | null> {\n const timeoutMs = options?.timeoutMs ?? 60_000;\n const onStatus = options?.onStatus;\n\n // Step 1: Check if already mapped\n onStatus?.(\"checking\");\n let isMapped: boolean;\n try {\n isMapped = await checker.addressIsMapped(address);\n } catch (cause) {\n throw new TxAccountMappingError(`Failed to check mapping status for ${address}`, { cause });\n }\n\n if (isMapped) {\n log.debug(\"account already mapped\", { address });\n onStatus?.(\"already-mapped\");\n return null;\n }\n\n // Step 2: Submit map_account transaction\n log.info(\"mapping account\", { address });\n onStatus?.(\"mapping\");\n\n const tx = api.tx.Revive.map_account();\n // submitAndWatch throws TxDispatchError on dispatch failure and\n // TxTimeoutError on timeout — both propagate to the caller as documented.\n const result = await submitAndWatch(tx, signer, {\n waitFor: \"best-block\",\n timeoutMs,\n });\n\n log.info(\"account mapped successfully\", { address, block: result.block });\n onStatus?.(\"mapped\");\n return result;\n}\n\n/**\n * Check if an address is mapped on-chain.\n *\n * Convenience wrapper around `checker.addressIsMapped()` with error handling.\n */\nexport async function isAccountMapped(address: string, checker: MappingChecker): Promise<boolean> {\n try {\n return await checker.addressIsMapped(address);\n } catch (cause) {\n throw new TxAccountMappingError(`Failed to check mapping status for ${address}`, { cause });\n }\n}\n\nif (import.meta.vitest) {\n const { describe, test, expect, vi } = import.meta.vitest;\n\n describe(\"ensureAccountMapped\", () => {\n const mockSigner = {} as PolkadotSigner;\n\n test(\"returns null when already mapped\", async () => {\n const checker: MappingChecker = {\n addressIsMapped: vi.fn().mockResolvedValue(true),\n };\n const api = {} as ReviveApi;\n\n const result = await ensureAccountMapped(\"5Alice\", mockSigner, checker, api);\n expect(result).toBeNull();\n expect(checker.addressIsMapped).toHaveBeenCalledWith(\"5Alice\");\n });\n\n test(\"calls onStatus with already-mapped when mapped\", async () => {\n const statuses: string[] = [];\n const checker: MappingChecker = {\n addressIsMapped: vi.fn().mockResolvedValue(true),\n };\n\n await ensureAccountMapped(\"5Alice\", mockSigner, checker, {} as ReviveApi, {\n onStatus: (s) => statuses.push(s),\n });\n expect(statuses).toEqual([\"checking\", \"already-mapped\"]);\n });\n\n test(\"throws TxAccountMappingError when check fails\", async () => {\n const checker: MappingChecker = {\n addressIsMapped: vi.fn().mockRejectedValue(new Error(\"network error\")),\n };\n\n await expect(\n ensureAccountMapped(\"5Alice\", mockSigner, checker, {} as ReviveApi),\n ).rejects.toThrow(TxAccountMappingError);\n });\n\n test(\"submits map_account when not mapped\", async () => {\n const checker: MappingChecker = {\n addressIsMapped: vi.fn().mockResolvedValue(false),\n };\n\n const mockTx: SubmittableTransaction = {\n signSubmitAndWatch: (_signer, _options) => ({\n subscribe: (handlers) => {\n queueMicrotask(() => {\n handlers.next({ type: \"signed\", txHash: \"0xabc\" });\n handlers.next({\n type: \"txBestBlocksState\",\n txHash: \"0xabc\",\n found: true,\n ok: true,\n events: [],\n block: { hash: \"0xblock\", number: 1, index: 0 },\n });\n });\n return { unsubscribe: () => {} };\n },\n }),\n };\n\n const api: ReviveApi = {\n tx: { Revive: { map_account: () => mockTx } },\n };\n\n const result = await ensureAccountMapped(\"5Alice\", mockSigner, checker, api);\n expect(result).not.toBeNull();\n expect(result!.ok).toBe(true);\n });\n\n test(\"calls onStatus through full mapping flow\", async () => {\n const statuses: string[] = [];\n const checker: MappingChecker = {\n addressIsMapped: vi.fn().mockResolvedValue(false),\n };\n\n const mockTx: SubmittableTransaction = {\n signSubmitAndWatch: (_signer, _options) => ({\n subscribe: (handlers) => {\n queueMicrotask(() => {\n handlers.next({ type: \"signed\", txHash: \"0xabc\" });\n handlers.next({\n type: \"txBestBlocksState\",\n txHash: \"0xabc\",\n found: true,\n ok: true,\n events: [],\n block: { hash: \"0xblock\", number: 1, index: 0 },\n });\n });\n return { unsubscribe: () => {} };\n },\n }),\n };\n\n const api: ReviveApi = {\n tx: { Revive: { map_account: () => mockTx } },\n };\n\n await ensureAccountMapped(\"5Alice\", mockSigner, checker, api, {\n onStatus: (s) => statuses.push(s),\n });\n expect(statuses).toEqual([\"checking\", \"mapping\", \"mapped\"]);\n });\n\n test(\"propagates TxDispatchError from submitAndWatch\", async () => {\n const { TxDispatchError } = await import(\"./errors.js\");\n const checker: MappingChecker = {\n addressIsMapped: vi.fn().mockResolvedValue(false),\n };\n\n const mockTx: SubmittableTransaction = {\n signSubmitAndWatch: (_signer, _options) => ({\n subscribe: (handlers) => {\n queueMicrotask(() => {\n handlers.next({\n type: \"txBestBlocksState\",\n txHash: \"0xabc\",\n found: true,\n ok: false,\n events: [],\n block: { hash: \"0xblock\", number: 1, index: 0 },\n dispatchError: { type: \"BadOrigin\" },\n });\n });\n return { unsubscribe: () => {} };\n },\n }),\n };\n\n const api: ReviveApi = {\n tx: { Revive: { map_account: () => mockTx } },\n };\n\n await expect(ensureAccountMapped(\"5Alice\", mockSigner, checker, api)).rejects.toThrow(\n TxDispatchError,\n );\n });\n\n test(\"propagates TxTimeoutError from submitAndWatch\", async () => {\n const { TxTimeoutError } = await import(\"./errors.js\");\n const checker: MappingChecker = {\n addressIsMapped: vi.fn().mockResolvedValue(false),\n };\n\n const mockTx: SubmittableTransaction = {\n signSubmitAndWatch: (_signer, _options) => ({\n subscribe: () => ({ unsubscribe: () => {} }),\n }),\n };\n\n const api: ReviveApi = {\n tx: { Revive: { map_account: () => mockTx } },\n };\n\n await expect(\n ensureAccountMapped(\"5Alice\", mockSigner, checker, api, { timeoutMs: 50 }),\n ).rejects.toThrow(TxTimeoutError);\n });\n });\n\n describe(\"isAccountMapped\", () => {\n test(\"returns true when mapped\", async () => {\n const checker: MappingChecker = {\n addressIsMapped: vi.fn().mockResolvedValue(true),\n };\n expect(await isAccountMapped(\"5Alice\", checker)).toBe(true);\n });\n\n test(\"returns false when not mapped\", async () => {\n const checker: MappingChecker = {\n addressIsMapped: vi.fn().mockResolvedValue(false),\n };\n expect(await isAccountMapped(\"5Alice\", checker)).toBe(false);\n });\n\n test(\"throws TxAccountMappingError on failure\", async () => {\n const checker: MappingChecker = {\n addressIsMapped: vi.fn().mockRejectedValue(new Error(\"timeout\")),\n };\n await expect(isAccountMapped(\"5Alice\", checker)).rejects.toThrow(TxAccountMappingError);\n });\n });\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/errors.ts","../src/submit.ts","../src/batch.ts","../src/retry.ts","../src/dev-signers.ts","../src/dry-run.ts","../src/account-mapping.ts"],"names":["log","createLogger"],"mappings":"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","file":"index.js","sourcesContent":["// Copyright 2026 Parity Technologies (UK) Ltd.\n// SPDX-License-Identifier: Apache-2.0\n/** Base class for all transaction errors. Use `instanceof TxError` to catch any tx-related error. */\nexport class TxError extends Error {\n constructor(message: string, options?: ErrorOptions) {\n super(message, options);\n this.name = \"TxError\";\n }\n}\n\n/** The transaction did not finalize within the configured timeout. It may still be processing on-chain. */\nexport class TxTimeoutError extends TxError {\n readonly timeoutMs: number;\n\n constructor(timeoutMs: number) {\n super(\n `Transaction timed out after ${timeoutMs / 1000}s. The transaction may still be processing on-chain.`,\n );\n this.name = \"TxTimeoutError\";\n this.timeoutMs = timeoutMs;\n }\n}\n\n/** The transaction was included on-chain but the dispatch failed. */\nexport class TxDispatchError extends TxError {\n /** Raw dispatch error from polkadot-api. */\n readonly dispatchError: unknown;\n /** Human-readable error string (e.g., \"Revive.ContractReverted\"). */\n readonly formatted: string;\n\n constructor(dispatchError: unknown, formatted: string) {\n super(`Transaction dispatch failed: ${formatted}`);\n this.name = \"TxDispatchError\";\n this.dispatchError = dispatchError;\n this.formatted = formatted;\n }\n}\n\n/** The user rejected the signing request in their wallet. */\nexport class TxSigningRejectedError extends TxError {\n constructor() {\n super(\"Transaction signing was rejected.\");\n this.name = \"TxSigningRejectedError\";\n }\n}\n\n/**\n * Extract a human-readable error from a transaction result's dispatch error.\n *\n * PAPI dispatch errors for pallet modules are nested:\n * `{ type: \"Module\", value: { type: \"Revive\", value: { type: \"ContractReverted\" } } }`\n *\n * This walks the chain to build a string like `\"Revive.ContractReverted\"`.\n *\n * @param result - A transaction result with `ok` and optional `dispatchError`.\n * @returns A human-readable error string, or `\"\"` if the result is ok, or `\"unknown error\"` if\n * the dispatch error cannot be decoded.\n */\nexport function formatDispatchError(result: { ok: boolean; dispatchError?: unknown }): string {\n if (result.ok) return \"\";\n\n try {\n const err = result.dispatchError as { type?: string; value?: unknown } | undefined;\n if (!err) return \"unknown error\";\n\n if (err.type === \"Module\" && err.value && typeof err.value === \"object\") {\n const palletErr = err.value as { type?: string; value?: unknown };\n const palletName = palletErr.type ?? \"Unknown\";\n\n if (palletErr.value && typeof palletErr.value === \"object\") {\n const innerErr = palletErr.value as { type?: string };\n if (innerErr.type) {\n return `${palletName}.${innerErr.type}`;\n }\n }\n return palletName;\n }\n\n return err.type ?? \"unknown error\";\n } catch {\n return \"unknown error\";\n }\n}\n\n/** Error specific to batch transaction construction (e.g., empty calls array). */\nexport class TxBatchError extends TxError {\n constructor(message: string) {\n super(message);\n this.name = \"TxBatchError\";\n }\n}\n\n/**\n * A dry-run simulation failed before the transaction was submitted on-chain.\n *\n * Thrown by {@link extractTransaction} when the dry-run result indicates failure.\n * Carries structured error information so callers can distinguish revert reasons\n * from dispatch errors programmatically.\n *\n * @example\n * ```ts\n * try {\n * const tx = extractTransaction(await contract.query(\"mint\", { origin, data }));\n * } catch (e) {\n * if (e instanceof TxDryRunError) {\n * console.log(e.revertReason); // \"InsufficientBalance\" (if contract provided one)\n * console.log(e.formatted); // \"Revive.StorageDepositNotEnoughFunds\"\n * }\n * }\n * ```\n */\nexport class TxDryRunError extends TxError {\n /** The raw dry-run result for programmatic inspection. */\n readonly raw: unknown;\n /** Human-readable error string derived from the dry-run result. */\n readonly formatted: string;\n /** Solidity revert reason, if the contract provided one. */\n readonly revertReason?: string;\n\n constructor(raw: unknown, formatted: string, revertReason?: string) {\n super(revertReason ? `Dry run failed: ${revertReason}` : `Dry run failed: ${formatted}`);\n this.name = \"TxDryRunError\";\n this.raw = raw;\n this.formatted = formatted;\n this.revertReason = revertReason;\n }\n}\n\n/**\n * Extract a human-readable error from a failed dry-run result.\n *\n * Handles every error shape found across the Polkadot contract ecosystem:\n *\n * 1. **Revert reason** (Ink SDK patched results / EVM contracts):\n * `{ value: { revertReason: \"InsufficientBalance\" } }`\n *\n * 2. **Nested dispatch errors** (raw Ink SDK / pallet errors):\n * `{ value: { type: \"Module\", value: { type: \"Revive\", value: { type: \"StorageDepositNotEnoughFunds\" } } } }`\n * Delegates to {@link formatDispatchError} for the Module.Pallet.Error chain.\n *\n * 3. **ReviveApi runtime messages** (`eth_transact` / `ReviveApi.call`):\n * `{ value: { type: \"Message\", value: \"Insufficient balance for gas * price + value\" } }`\n *\n * 4. **ReviveApi contract revert data**:\n * `{ value: { type: \"Data\", value: \"0x08c379a0...\" } }`\n *\n * 5. **Wrapped raw errors** (patched SDK wrappers):\n * `{ value: { raw: { type: \"Message\", value: \"...\" } } }`\n *\n * 6. **Generic error field**:\n * `{ error: { type: \"ContractTrapped\" } }` or `{ error: { name: \"...\" } }`\n *\n * @param result - A dry-run result with at least `success`, and optionally `value` / `error`.\n * @returns A human-readable error string, or `\"\"` if the result succeeded.\n */\nexport function formatDryRunError(result: {\n success?: boolean;\n value?: unknown;\n error?: unknown;\n}): string {\n if (result.success) return \"\";\n\n const formatted = extractErrorFromValue(result.value);\n if (formatted) return formatted;\n\n // Generic error field (Ink SDK)\n if (result.error != null && typeof result.error === \"object\") {\n const err = result.error as Record<string, unknown>;\n if (typeof err.type === \"string\") return err.type;\n if (typeof err.name === \"string\") return err.name;\n }\n\n return \"unknown error\";\n}\n\n/**\n * Try to extract an error string from the `value` field of a dry-run result.\n * Returns `undefined` if no known error shape is found.\n */\nfunction extractErrorFromValue(value: unknown): string | undefined {\n if (value == null || typeof value !== \"object\") return undefined;\n const v = value as Record<string, unknown>;\n\n // Explicit revert reason — most specific, from Ink SDK / EVM wrappers\n if (typeof v.revertReason === \"string\" && v.revertReason) {\n return v.revertReason;\n }\n\n if (typeof v.type === \"string\") {\n // Nested Module.Pallet.Error — reuse dispatch error formatting\n if (v.type === \"Module\") {\n const asDispatch = formatDispatchError({ ok: false, dispatchError: value });\n if (asDispatch !== \"unknown error\") return asDispatch;\n }\n\n // ReviveApi Message — runtime error string\n if (v.type === \"Message\" && typeof v.value === \"string\") {\n return v.value;\n }\n\n // ReviveApi Data — contract revert hex data\n if (v.type === \"Data\") {\n const hex =\n v.value != null &&\n typeof v.value === \"object\" &&\n typeof (v.value as { asHex?: unknown }).asHex === \"function\"\n ? String((v.value as { asHex: () => string }).asHex())\n : typeof v.value === \"string\"\n ? v.value\n : undefined;\n return hex ? `contract reverted with data: ${hex}` : \"contract reverted\";\n }\n\n // Any other typed error (e.g., \"BadOrigin\", \"ContractTrapped\")\n return v.type;\n }\n\n // Wrapped raw value — patched SDK nests the original error under `raw`\n if (\"raw\" in v && v.raw != null && typeof v.raw === \"object\") {\n return extractErrorFromValue(v.raw);\n }\n\n return undefined;\n}\n\n/**\n * Check if an error looks like a user-rejected signing request.\n *\n * Different wallets use different error messages when the user rejects signing:\n * \"Cancelled\", \"Rejected\", \"User rejected\", \"denied\". This checks for common\n * patterns as a best-effort heuristic. Non-Error values always return false.\n *\n * @param error - The error to check.\n * @returns `true` if the error message matches a known rejection pattern.\n */\nexport function isSigningRejection(error: unknown): boolean {\n if (!(error instanceof Error)) return false;\n const msg = error.message.toLowerCase();\n return (\n msg.includes(\"cancelled\") ||\n msg.includes(\"rejected\") ||\n msg.includes(\"denied\") ||\n msg.includes(\"user refused\")\n );\n}\n\nif (import.meta.vitest) {\n const { describe, test, expect } = import.meta.vitest;\n\n describe(\"TxError hierarchy\", () => {\n test(\"TxTimeoutError\", () => {\n const err = new TxTimeoutError(300_000);\n expect(err).toBeInstanceOf(TxError);\n expect(err).toBeInstanceOf(Error);\n expect(err.name).toBe(\"TxTimeoutError\");\n expect(err.timeoutMs).toBe(300_000);\n expect(err.message).toContain(\"300s\");\n });\n\n test(\"TxDispatchError\", () => {\n const raw = {\n type: \"Module\",\n value: { type: \"Balances\", value: { type: \"InsufficientBalance\" } },\n };\n const err = new TxDispatchError(raw, \"Balances.InsufficientBalance\");\n expect(err).toBeInstanceOf(TxError);\n expect(err.name).toBe(\"TxDispatchError\");\n expect(err.dispatchError).toBe(raw);\n expect(err.formatted).toBe(\"Balances.InsufficientBalance\");\n expect(err.message).toContain(\"Balances.InsufficientBalance\");\n });\n\n test(\"TxSigningRejectedError\", () => {\n const err = new TxSigningRejectedError();\n expect(err).toBeInstanceOf(TxError);\n expect(err.name).toBe(\"TxSigningRejectedError\");\n });\n\n test(\"TxBatchError\", () => {\n const err = new TxBatchError(\"Cannot batch zero calls\");\n expect(err).toBeInstanceOf(TxError);\n expect(err).toBeInstanceOf(Error);\n expect(err.name).toBe(\"TxBatchError\");\n expect(err.message).toBe(\"Cannot batch zero calls\");\n });\n });\n\n describe(\"formatDispatchError\", () => {\n test(\"returns empty string for ok result\", () => {\n expect(formatDispatchError({ ok: true })).toBe(\"\");\n });\n\n test(\"walks Module.Pallet.Error chain\", () => {\n const result = {\n ok: false,\n dispatchError: {\n type: \"Module\",\n value: { type: \"Revive\", value: { type: \"ContractReverted\" } },\n },\n };\n expect(formatDispatchError(result)).toBe(\"Revive.ContractReverted\");\n });\n\n test(\"returns pallet name when inner error has no type\", () => {\n const result = {\n ok: false,\n dispatchError: {\n type: \"Module\",\n value: { type: \"Balances\", value: {} },\n },\n };\n expect(formatDispatchError(result)).toBe(\"Balances\");\n });\n\n test(\"returns error type for non-Module errors\", () => {\n const result = {\n ok: false,\n dispatchError: { type: \"BadOrigin\" },\n };\n expect(formatDispatchError(result)).toBe(\"BadOrigin\");\n });\n\n test(\"returns unknown error when dispatchError is missing\", () => {\n expect(formatDispatchError({ ok: false })).toBe(\"unknown error\");\n });\n\n test(\"returns unknown error when dispatchError has no type\", () => {\n expect(formatDispatchError({ ok: false, dispatchError: {} })).toBe(\"unknown error\");\n });\n });\n\n describe(\"TxDryRunError\", () => {\n test(\"with revert reason\", () => {\n const err = new TxDryRunError(\n { success: false },\n \"Module.Error\",\n \"InsufficientBalance\",\n );\n expect(err).toBeInstanceOf(TxError);\n expect(err.name).toBe(\"TxDryRunError\");\n expect(err.formatted).toBe(\"Module.Error\");\n expect(err.revertReason).toBe(\"InsufficientBalance\");\n expect(err.message).toContain(\"InsufficientBalance\");\n });\n\n test(\"without revert reason uses formatted\", () => {\n const err = new TxDryRunError({ success: false }, \"BadOrigin\");\n expect(err.message).toContain(\"BadOrigin\");\n expect(err.revertReason).toBeUndefined();\n });\n\n test(\"preserves raw result for inspection\", () => {\n const raw = { success: false, value: { type: \"Module\" } };\n const err = new TxDryRunError(raw, \"Module\");\n expect(err.raw).toBe(raw);\n });\n });\n\n describe(\"formatDryRunError\", () => {\n test(\"returns empty string for successful result\", () => {\n expect(formatDryRunError({ success: true })).toBe(\"\");\n });\n\n test(\"extracts revert reason\", () => {\n expect(\n formatDryRunError({\n success: false,\n value: { revertReason: \"InsufficientBalance\" },\n }),\n ).toBe(\"InsufficientBalance\");\n });\n\n test(\"walks Module.Pallet.Error chain\", () => {\n expect(\n formatDryRunError({\n success: false,\n value: {\n type: \"Module\",\n value: { type: \"Revive\", value: { type: \"StorageDepositNotEnoughFunds\" } },\n },\n }),\n ).toBe(\"Revive.StorageDepositNotEnoughFunds\");\n });\n\n test(\"returns pallet name when inner error has no type\", () => {\n expect(\n formatDryRunError({\n success: false,\n value: { type: \"Module\", value: { type: \"Balances\", value: {} } },\n }),\n ).toBe(\"Balances\");\n });\n\n test(\"extracts ReviveApi Message string\", () => {\n expect(\n formatDryRunError({\n success: false,\n value: {\n type: \"Message\",\n value: \"Insufficient balance for gas * price + value\",\n },\n }),\n ).toBe(\"Insufficient balance for gas * price + value\");\n });\n\n test(\"handles ReviveApi Data with string hex\", () => {\n expect(\n formatDryRunError({\n success: false,\n value: { type: \"Data\", value: \"0x08c379a0\" },\n }),\n ).toBe(\"contract reverted with data: 0x08c379a0\");\n });\n\n test(\"handles ReviveApi Data with Binary-like object\", () => {\n const binary = { asHex: () => \"0xdeadbeef\" };\n expect(\n formatDryRunError({ success: false, value: { type: \"Data\", value: binary } }),\n ).toBe(\"contract reverted with data: 0xdeadbeef\");\n });\n\n test(\"handles ReviveApi Data with no extractable hex\", () => {\n expect(formatDryRunError({ success: false, value: { type: \"Data\", value: 42 } })).toBe(\n \"contract reverted\",\n );\n });\n\n test(\"returns non-Module/Message type directly\", () => {\n expect(formatDryRunError({ success: false, value: { type: \"BadOrigin\" } })).toBe(\n \"BadOrigin\",\n );\n });\n\n test(\"extracts from nested raw field (patched SDK)\", () => {\n expect(\n formatDryRunError({\n success: false,\n value: { raw: { type: \"Message\", value: \"out of gas\" } },\n }),\n ).toBe(\"out of gas\");\n });\n\n test(\"extracts revertReason from nested raw\", () => {\n expect(\n formatDryRunError({\n success: false,\n value: { raw: { revertReason: \"Unauthorized\" } },\n }),\n ).toBe(\"Unauthorized\");\n });\n\n test(\"falls back to error.type\", () => {\n expect(formatDryRunError({ success: false, error: { type: \"ContractTrapped\" } })).toBe(\n \"ContractTrapped\",\n );\n });\n\n test(\"falls back to error.name\", () => {\n expect(formatDryRunError({ success: false, error: { name: \"ExecutionFailed\" } })).toBe(\n \"ExecutionFailed\",\n );\n });\n\n test(\"returns unknown error when nothing is extractable\", () => {\n expect(formatDryRunError({ success: false })).toBe(\"unknown error\");\n });\n\n test(\"returns unknown error for empty value and error\", () => {\n expect(formatDryRunError({ success: false, value: {}, error: {} })).toBe(\n \"unknown error\",\n );\n });\n\n test(\"returns unknown error for null value\", () => {\n expect(formatDryRunError({ success: false, value: null })).toBe(\"unknown error\");\n });\n\n test(\"prefers revertReason over Module error\", () => {\n // When both are present, revertReason is more specific\n expect(\n formatDryRunError({\n success: false,\n value: {\n revertReason: \"OwnableUnauthorizedAccount\",\n type: \"Module\",\n value: {},\n },\n }),\n ).toBe(\"OwnableUnauthorizedAccount\");\n });\n });\n\n describe(\"isSigningRejection\", () => {\n test(\"detects common rejection messages\", () => {\n expect(isSigningRejection(new Error(\"Cancelled\"))).toBe(true);\n expect(isSigningRejection(new Error(\"User rejected the request\"))).toBe(true);\n expect(isSigningRejection(new Error(\"Transaction was rejected by user\"))).toBe(true);\n expect(isSigningRejection(new Error(\"User denied\"))).toBe(true);\n expect(isSigningRejection(new Error(\"Signing was denied by user\"))).toBe(true);\n expect(isSigningRejection(new Error(\"User refused to sign\"))).toBe(true);\n });\n\n test(\"returns false for non-rejection errors\", () => {\n expect(isSigningRejection(new Error(\"Network timeout\"))).toBe(false);\n expect(isSigningRejection(new Error(\"Insufficient balance\"))).toBe(false);\n });\n\n test(\"returns false for non-Error values\", () => {\n expect(isSigningRejection(\"cancelled\")).toBe(false);\n expect(isSigningRejection(null)).toBe(false);\n });\n });\n}\n","// Copyright 2026 Parity Technologies (UK) Ltd.\n// SPDX-License-Identifier: Apache-2.0\nimport type { PolkadotSigner } from \"polkadot-api\";\nimport { createLogger } from \"@parity/product-sdk-logger\";\n\nimport {\n TxDispatchError,\n TxSigningRejectedError,\n TxTimeoutError,\n formatDispatchError,\n isSigningRejection,\n} from \"./errors.js\";\nimport type { SubmitOptions, SubmittableTransaction, TxEvent, TxResult } from \"./types.js\";\n\nconst DEFAULT_TIMEOUT_MS = 300_000;\nconst DEFAULT_MORTALITY_PERIOD = 256;\n\nconst log = createLogger(\"tx\");\n\n/**\n * Resolve Ink SDK AsyncTransaction wrappers.\n *\n * Ink SDK's `contract.send()` returns an object with a `.waited` Promise that\n * resolves to the actual transaction. This handles that transparently.\n */\nasync function resolveTransaction(tx: SubmittableTransaction): Promise<SubmittableTransaction> {\n if (tx.waited && typeof tx.waited.then === \"function\") {\n log.debug(\"Resolving Ink SDK AsyncTransaction\");\n return tx.waited;\n }\n return tx;\n}\n\nfunction buildTxResult(\n event: TxEvent & { ok: boolean; block: TxResult[\"block\"]; events: unknown[] },\n): TxResult {\n return {\n txHash: event.txHash,\n ok: event.ok,\n block: event.block,\n events: event.events,\n dispatchError: \"dispatchError\" in event ? event.dispatchError : undefined,\n };\n}\n\n/**\n * Submit a transaction and watch its lifecycle through signing, broadcasting,\n * block inclusion, and (optionally) finalization.\n *\n * @param tx - A transaction object with `signSubmitAndWatch`. Works with raw PAPI\n * transactions and Ink SDK `AsyncTransaction` wrappers (resolved automatically).\n * @param signer - The signer to use. Can come from the Host API\n * (`getProductAccountSigner`) or {@link createDevSigner}.\n * @param options - Submission options (waitFor, timeout, mortality, status callback).\n * @returns The transaction result once included/finalized.\n *\n * @throws {TxTimeoutError} If the transaction does not reach the target state within `timeoutMs`.\n * @throws {TxDispatchError} If the on-chain dispatch fails (e.g., insufficient balance, contract revert).\n * @throws {TxSigningRejectedError} If the user rejects signing in their wallet.\n */\nexport async function submitAndWatch(\n tx: SubmittableTransaction,\n signer: PolkadotSigner,\n options?: SubmitOptions,\n): Promise<TxResult> {\n const waitFor = options?.waitFor ?? \"best-block\";\n const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const mortalityPeriod = options?.mortalityPeriod ?? DEFAULT_MORTALITY_PERIOD;\n const onStatus = options?.onStatus;\n\n const resolvedTx = await resolveTransaction(tx);\n\n return new Promise<TxResult>((resolve, reject) => {\n let settled = false;\n let subscription: { unsubscribe: () => void } | null = null;\n\n const timer = setTimeout(() => {\n subscription?.unsubscribe();\n if (!settled) {\n settled = true;\n onStatus?.(\"error\");\n reject(new TxTimeoutError(timeoutMs));\n }\n }, timeoutMs);\n\n function teardown(): void {\n clearTimeout(timer);\n subscription?.unsubscribe();\n }\n\n function settleReject(error: Error): void {\n if (settled) return;\n settled = true;\n teardown();\n onStatus?.(\"error\");\n reject(error);\n }\n\n try {\n const observable = resolvedTx.signSubmitAndWatch(signer, {\n mortality: { mortal: true, period: mortalityPeriod },\n });\n\n subscription = observable.subscribe({\n next: (event: TxEvent) => {\n switch (event.type) {\n case \"signed\": {\n log.info(\"Transaction signed\", { txHash: event.txHash });\n onStatus?.(\"signing\");\n break;\n }\n case \"broadcasted\": {\n log.info(\"Transaction broadcasted\", { txHash: event.txHash });\n onStatus?.(\"broadcasting\");\n break;\n }\n case \"txBestBlocksState\": {\n if (!event.found) break;\n\n if (event.ok === false) {\n const formatted = formatDispatchError({\n ok: false,\n dispatchError: event.dispatchError,\n });\n log.error(\"Transaction failed in best block\", {\n formatted,\n block: event.block,\n });\n settleReject(new TxDispatchError(event.dispatchError, formatted));\n return;\n }\n\n log.info(\"Transaction in best block\", { block: event.block });\n onStatus?.(\"in-block\");\n\n if (\n waitFor === \"best-block\" &&\n event.ok === true &&\n event.block &&\n event.events\n ) {\n // Resolve the Promise but keep the subscription alive so we can\n // detect reorgs (finalized event with ok=false after best-block ok=true).\n // Only clear the timer since the consumer has their result.\n settled = true;\n clearTimeout(timer);\n resolve(\n buildTxResult(\n event as TxEvent & {\n ok: boolean;\n block: TxResult[\"block\"];\n events: unknown[];\n },\n ),\n );\n }\n break;\n }\n case \"finalized\": {\n log.info(\"Transaction finalized\", { ok: event.ok, block: event.block });\n\n if (!event.ok) {\n const formatted = formatDispatchError({\n ok: false,\n dispatchError: event.dispatchError,\n });\n\n if (settled) {\n // Already resolved at best-block but finalized shows failure\n // due to a chain reorganization. We can only log since the\n // Promise is already resolved.\n log.warn(\n \"Transaction failed after being in best block (reorg). \" +\n \"The consumer received a success result that is no longer valid.\",\n { formatted, block: event.block },\n );\n } else {\n settleReject(\n new TxDispatchError(event.dispatchError, formatted),\n );\n }\n subscription?.unsubscribe();\n return;\n }\n\n onStatus?.(\"finalized\");\n\n if (!settled) {\n settled = true;\n teardown();\n resolve(buildTxResult(event));\n } else {\n // Already resolved at best-block, finalization confirmed success.\n subscription?.unsubscribe();\n }\n break;\n }\n }\n },\n error: (err: Error) => {\n log.error(\"Transaction subscription error\", { error: err.message });\n\n if (isSigningRejection(err)) {\n settleReject(new TxSigningRejectedError());\n } else {\n settleReject(err);\n }\n },\n });\n } catch (err) {\n log.error(\"Failed to start transaction\", { error: (err as Error).message });\n teardown();\n\n if (isSigningRejection(err)) {\n settleReject(new TxSigningRejectedError());\n } else {\n settleReject(err as Error);\n }\n }\n });\n}\n\nif (import.meta.vitest) {\n const { describe, test, expect, vi, beforeEach } = import.meta.vitest;\n const { configure } = await import(\"@parity/product-sdk-logger\");\n\n // Silence logger during tests\n beforeEach(() => {\n configure({ handler: () => {} });\n });\n\n type MockSubscribeHandlers = {\n next: (event: TxEvent) => void;\n error: (error: Error) => void;\n };\n\n function createMockTx(\n emitFn: (handlers: MockSubscribeHandlers) => void,\n ): SubmittableTransaction {\n return {\n signSubmitAndWatch: (_signer: PolkadotSigner, _options?: unknown) => ({\n subscribe: (handlers: MockSubscribeHandlers) => {\n const unsub = vi.fn();\n // Emit events asynchronously so the subscription is returned first\n queueMicrotask(() => emitFn(handlers));\n return { unsubscribe: unsub };\n },\n }),\n };\n }\n\n const mockSigner = {} as PolkadotSigner;\n\n const signedEvent: TxEvent = { type: \"signed\", txHash: \"0xabc\" };\n const broadcastedEvent: TxEvent = { type: \"broadcasted\", txHash: \"0xabc\" };\n const bestBlockOk: TxEvent = {\n type: \"txBestBlocksState\",\n txHash: \"0xabc\",\n found: true,\n ok: true,\n events: [{ id: 1 }],\n block: { hash: \"0xblock1\", number: 100, index: 0 },\n };\n const bestBlockFail: TxEvent = {\n type: \"txBestBlocksState\",\n txHash: \"0xabc\",\n found: true,\n ok: false,\n events: [],\n block: { hash: \"0xblock1\", number: 100, index: 0 },\n dispatchError: {\n type: \"Module\",\n value: { type: \"Balances\", value: { type: \"InsufficientBalance\" } },\n },\n };\n const finalizedOk: TxEvent = {\n type: \"finalized\",\n txHash: \"0xabc\",\n ok: true,\n events: [{ id: 1 }],\n block: { hash: \"0xblock2\", number: 101, index: 0 },\n };\n const finalizedFail: TxEvent = {\n type: \"finalized\",\n txHash: \"0xabc\",\n ok: false,\n events: [],\n block: { hash: \"0xblock2\", number: 101, index: 0 },\n dispatchError: { type: \"BadOrigin\" },\n };\n\n describe(\"submitAndWatch\", () => {\n test(\"resolves at best-block by default\", async () => {\n const tx = createMockTx((h) => {\n h.next(signedEvent);\n h.next(broadcastedEvent);\n h.next(bestBlockOk);\n h.next(finalizedOk);\n });\n const result = await submitAndWatch(tx, mockSigner);\n expect(result.ok).toBe(true);\n expect(result.block.number).toBe(100);\n });\n\n test(\"resolves at finalized when configured\", async () => {\n const tx = createMockTx((h) => {\n h.next(signedEvent);\n h.next(bestBlockOk);\n h.next(finalizedOk);\n });\n const result = await submitAndWatch(tx, mockSigner, { waitFor: \"finalized\" });\n expect(result.ok).toBe(true);\n expect(result.block.number).toBe(101);\n });\n\n test(\"rejects with TxDispatchError on best-block failure\", async () => {\n const tx = createMockTx((h) => {\n h.next(signedEvent);\n h.next(bestBlockFail);\n });\n await expect(submitAndWatch(tx, mockSigner)).rejects.toThrow(TxDispatchError);\n });\n\n test(\"rejects with TxDispatchError on finalized failure\", async () => {\n const tx = createMockTx((h) => {\n h.next(signedEvent);\n h.next(finalizedFail);\n });\n await expect(submitAndWatch(tx, mockSigner, { waitFor: \"finalized\" })).rejects.toThrow(\n TxDispatchError,\n );\n });\n\n test(\"rejects with TxTimeoutError after timeout\", async () => {\n const tx = createMockTx(() => {\n // Never emits any events - tx hangs forever\n });\n const error = await submitAndWatch(tx, mockSigner, { timeoutMs: 50 }).catch(\n (e: unknown) => e,\n );\n expect(error).toBeInstanceOf(TxTimeoutError);\n expect((error as TxTimeoutError).timeoutMs).toBe(50);\n });\n\n test(\"calls onStatus callbacks in order\", async () => {\n const statuses: string[] = [];\n const tx = createMockTx((h) => {\n h.next(signedEvent);\n h.next(broadcastedEvent);\n h.next(bestBlockOk);\n });\n await submitAndWatch(tx, mockSigner, {\n onStatus: (s) => statuses.push(s),\n });\n expect(statuses).toEqual([\"signing\", \"broadcasting\", \"in-block\"]);\n });\n\n test(\"resolves Ink SDK AsyncTransaction\", async () => {\n const innerTx = createMockTx((h) => {\n h.next(signedEvent);\n h.next(bestBlockOk);\n });\n const wrappedTx: SubmittableTransaction = {\n signSubmitAndWatch: () => {\n throw new Error(\"Should not be called on outer tx\");\n },\n waited: Promise.resolve(innerTx),\n };\n const result = await submitAndWatch(wrappedTx, mockSigner);\n expect(result.ok).toBe(true);\n });\n\n test(\"passes mortality options\", async () => {\n let capturedOptions: unknown;\n const tx: SubmittableTransaction = {\n signSubmitAndWatch: (_signer: PolkadotSigner, options?: unknown) => {\n capturedOptions = options;\n return {\n subscribe: (handlers: MockSubscribeHandlers) => {\n queueMicrotask(() => {\n handlers.next(signedEvent);\n handlers.next(bestBlockOk);\n });\n return { unsubscribe: vi.fn() };\n },\n };\n },\n };\n await submitAndWatch(tx, mockSigner, { mortalityPeriod: 512 });\n expect(capturedOptions).toEqual({ mortality: { mortal: true, period: 512 } });\n });\n\n test(\"wraps signing rejection in TxSigningRejectedError\", async () => {\n const tx = createMockTx((h) => {\n h.error(new Error(\"User rejected the request\"));\n });\n await expect(submitAndWatch(tx, mockSigner)).rejects.toThrow(TxSigningRejectedError);\n });\n\n test(\"skips txBestBlocksState with found=false\", async () => {\n const tx = createMockTx((h) => {\n h.next(signedEvent);\n h.next({\n type: \"txBestBlocksState\",\n txHash: \"0xabc\",\n found: false,\n });\n h.next(bestBlockOk);\n });\n const result = await submitAndWatch(tx, mockSigner);\n expect(result.ok).toBe(true);\n });\n\n test(\"rejects with original error for non-rejection Observable errors\", async () => {\n const tx = createMockTx((h) => {\n h.error(new Error(\"WebSocket disconnected\"));\n });\n const err = await submitAndWatch(tx, mockSigner).catch((e) => e);\n expect(err.message).toBe(\"WebSocket disconnected\");\n expect(err).not.toBeInstanceOf(TxSigningRejectedError);\n });\n\n test(\"handles synchronous throw from signSubmitAndWatch\", async () => {\n const tx: SubmittableTransaction = {\n signSubmitAndWatch: () => {\n throw new Error(\"Signer not available\");\n },\n };\n await expect(submitAndWatch(tx, mockSigner)).rejects.toThrow(\"Signer not available\");\n });\n\n test(\"calls onStatus error on dispatch failure\", async () => {\n const statuses: string[] = [];\n const tx = createMockTx((h) => {\n h.next(bestBlockFail);\n });\n await submitAndWatch(tx, mockSigner, {\n onStatus: (s) => statuses.push(s),\n }).catch(() => {});\n expect(statuses).toContain(\"error\");\n });\n\n test(\"logs warning on reorg (best-block ok, finalized fail)\", async () => {\n const warnings: unknown[] = [];\n const { configure: configureLogs } = await import(\"@parity/product-sdk-logger\");\n configureLogs({\n level: \"debug\",\n handler: (entry) => {\n if (entry.level === \"warn\") warnings.push(entry.message);\n },\n });\n\n const tx = createMockTx((h) => {\n h.next(signedEvent);\n h.next(bestBlockOk);\n // Finalized says the tx actually failed (reorg)\n h.next(finalizedFail);\n });\n\n // Should resolve at best-block (success)\n const result = await submitAndWatch(tx, mockSigner);\n expect(result.ok).toBe(true);\n\n // Give the finalized event time to fire and log\n await new Promise((r) => setTimeout(r, 10));\n\n expect(warnings.some((w) => typeof w === \"string\" && w.includes(\"reorg\"))).toBe(true);\n\n // Restore silent handler\n configureLogs({ handler: () => {} });\n });\n\n test(\"does not resolve when txBestBlocksState ok is undefined\", async () => {\n const tx = createMockTx((h) => {\n h.next(signedEvent);\n // ok is undefined (not explicitly true or false)\n h.next({\n type: \"txBestBlocksState\",\n txHash: \"0xabc\",\n found: true,\n events: [{ id: 1 }],\n block: { hash: \"0xblock1\", number: 100, index: 0 },\n // ok intentionally omitted\n } as TxEvent);\n // Should only resolve when finalized\n h.next(finalizedOk);\n });\n\n const result = await submitAndWatch(tx, mockSigner);\n // Should resolve from finalized, not best-block (since ok was undefined)\n expect(result.block.number).toBe(101);\n });\n });\n}\n","// Copyright 2026 Parity Technologies (UK) Ltd.\n// SPDX-License-Identifier: Apache-2.0\nimport type { PolkadotSigner } from \"polkadot-api\";\nimport { createLogger } from \"@parity/product-sdk-logger\";\n\nimport { TxBatchError } from \"./errors.js\";\nimport { submitAndWatch } from \"./submit.js\";\nimport type {\n BatchApi,\n BatchSubmitOptions,\n BatchableCall,\n SubmittableTransaction,\n TxResult,\n} from \"./types.js\";\n\nconst log = createLogger(\"tx:batch\");\n\n/**\n * Resolve a single call to its decoded call data.\n *\n * Handles three shapes:\n * 1. Ink SDK AsyncTransaction — has `.waited` Promise that resolves to a tx with `.decodedCall`\n * 2. PAPI transaction or object with `.decodedCall` — extract directly\n * 3. Raw decoded call — pass through as-is\n */\nasync function resolveDecodedCall(call: BatchableCall): Promise<unknown> {\n if (call != null && typeof call === \"object\") {\n const obj = call as Record<string, unknown>;\n\n // Handle Ink SDK AsyncTransaction: resolve .waited first\n if (\n \"waited\" in obj &&\n obj.waited &&\n typeof (obj.waited as Promise<unknown>).then === \"function\"\n ) {\n log.debug(\"Resolving Ink SDK AsyncTransaction in batch\");\n const resolved = (await obj.waited) as Record<string, unknown>;\n if (resolved.decodedCall !== undefined) return resolved.decodedCall;\n throw new TxBatchError(\"Resolved AsyncTransaction has no decodedCall property\");\n }\n\n // Handle SubmittableTransaction or object with decodedCall\n if (\"decodedCall\" in obj && obj.decodedCall !== undefined) {\n return obj.decodedCall;\n }\n }\n\n // Reject null, undefined, and primitives — they will cause cryptic codec errors on-chain\n if (call == null || typeof call !== \"object\") {\n throw new TxBatchError(\n `Invalid batch call: expected a transaction or decoded call object, got ${call === null ? \"null\" : typeof call}`,\n );\n }\n\n // Raw decoded call object — pass through\n return call;\n}\n\n/**\n * Batch multiple transactions into a single Substrate Utility batch and submit.\n *\n * Extracts `.decodedCall` from each transaction (handling Ink SDK `AsyncTransaction`\n * wrappers), wraps them in `Utility.batch_all` (or `batch`/`force_batch` via the\n * `mode` option), and submits via {@link submitAndWatch} with full lifecycle tracking.\n *\n * @param calls - Array of transactions, AsyncTransactions, or raw decoded calls to batch.\n * @param api - A typed API with `tx.Utility.batch_all/batch/force_batch`. Works with any\n * chain that has the Utility pallet — no chain-specific imports required.\n * **All calls must target the same chain as this API.** Do not mix decoded calls\n * from different chains (e.g., Asset Hub and Bulletin) in a single batch.\n * @param signer - The signer to use. Can come from the Host API\n * (`getProductAccountSigner`) or {@link createDevSigner}.\n * @param options - Optional {@link BatchSubmitOptions} (extends `SubmitOptions` with `mode`).\n * @returns The transaction result from the batch submission.\n *\n * @throws {TxBatchError} If `calls` is empty.\n * @throws {TxBatchError} If an AsyncTransaction resolves without a `.decodedCall` property.\n * @throws {TxTimeoutError} If the batch transaction does not reach the target state within `timeoutMs`.\n * @throws {TxDispatchError} If the on-chain dispatch fails.\n * @throws {TxSigningRejectedError} If the user rejects signing in their wallet.\n *\n * @example\n * ```ts\n * import { batchSubmitAndWatch } from \"@parity/product-sdk-tx\";\n *\n * const tx1 = api.tx.Balances.transfer_keep_alive({ dest: addr1, value: 1_000n });\n * const tx2 = api.tx.Balances.transfer_keep_alive({ dest: addr2, value: 2_000n });\n *\n * const result = await batchSubmitAndWatch([tx1, tx2], api, signer, {\n * onStatus: (status) => console.log(status),\n * });\n * ```\n */\nexport async function batchSubmitAndWatch(\n calls: BatchableCall[],\n api: BatchApi,\n signer: PolkadotSigner,\n options?: BatchSubmitOptions,\n): Promise<TxResult> {\n if (calls.length === 0) {\n throw new TxBatchError(\"Cannot batch zero calls\");\n }\n\n const mode = options?.mode ?? \"batch_all\";\n\n log.info(\"Resolving batch calls\", { count: calls.length, mode });\n const decodedCalls = await Promise.all(calls.map(resolveDecodedCall));\n\n log.info(\"Constructing batch transaction\", { mode, callCount: decodedCalls.length });\n const batchTx = api.tx.Utility[mode]({ calls: decodedCalls });\n\n return submitAndWatch(batchTx, signer, options);\n}\n\nif (import.meta.vitest) {\n const { describe, test, expect, vi, beforeEach } = import.meta.vitest;\n const { configure } = await import(\"@parity/product-sdk-logger\");\n const { TxDispatchError, TxSigningRejectedError } = await import(\"./errors.js\");\n type TxEvent = import(\"./types.js\").TxEvent;\n\n // Silence logger during tests\n beforeEach(() => {\n configure({ handler: () => {} });\n });\n\n type MockSubscribeHandlers = {\n next: (event: TxEvent) => void;\n error: (error: Error) => void;\n };\n\n function createMockTx(\n emitFn: (handlers: MockSubscribeHandlers) => void,\n decodedCall?: unknown,\n ): SubmittableTransaction {\n return {\n signSubmitAndWatch: (_signer: PolkadotSigner, _options?: unknown) => ({\n subscribe: (handlers: MockSubscribeHandlers) => {\n const unsub = vi.fn();\n queueMicrotask(() => emitFn(handlers));\n return { unsubscribe: unsub };\n },\n }),\n decodedCall,\n };\n }\n\n const mockSigner = {} as PolkadotSigner;\n\n const signedEvent: TxEvent = { type: \"signed\", txHash: \"0xbatch\" };\n const bestBlockOk: TxEvent = {\n type: \"txBestBlocksState\",\n txHash: \"0xbatch\",\n found: true,\n ok: true,\n events: [{ id: 1 }],\n block: { hash: \"0xblock1\", number: 100, index: 0 },\n };\n\n function createMockBatchApi(emitFn: (handlers: MockSubscribeHandlers) => void): {\n api: BatchApi;\n getCapturedCalls: () => unknown[][];\n } {\n const capturedCalls: unknown[][] = [];\n\n const api: BatchApi = {\n tx: {\n Utility: {\n batch: vi.fn((args: { calls: unknown[] }) => {\n capturedCalls.push(args.calls);\n return createMockTx(emitFn);\n }) as BatchApi[\"tx\"][\"Utility\"][\"batch\"],\n batch_all: vi.fn((args: { calls: unknown[] }) => {\n capturedCalls.push(args.calls);\n return createMockTx(emitFn);\n }) as BatchApi[\"tx\"][\"Utility\"][\"batch_all\"],\n force_batch: vi.fn((args: { calls: unknown[] }) => {\n capturedCalls.push(args.calls);\n return createMockTx(emitFn);\n }) as BatchApi[\"tx\"][\"Utility\"][\"force_batch\"],\n },\n },\n };\n\n return { api, getCapturedCalls: () => capturedCalls };\n }\n\n const successEmit = (h: MockSubscribeHandlers) => {\n h.next(signedEvent);\n h.next(bestBlockOk);\n };\n\n describe(\"batchSubmitAndWatch\", () => {\n test(\"batches multiple transactions with decodedCall\", async () => {\n const { api, getCapturedCalls } = createMockBatchApi(successEmit);\n const calls = [\n { decodedCall: { pallet: \"Balances\", method: \"transfer\", args: { value: 1 } } },\n { decodedCall: { pallet: \"Balances\", method: \"transfer\", args: { value: 2 } } },\n ];\n\n const result = await batchSubmitAndWatch(calls, api, mockSigner);\n\n expect(result.ok).toBe(true);\n expect(getCapturedCalls()).toHaveLength(1);\n expect(getCapturedCalls()[0]).toEqual([\n { pallet: \"Balances\", method: \"transfer\", args: { value: 1 } },\n { pallet: \"Balances\", method: \"transfer\", args: { value: 2 } },\n ]);\n expect(api.tx.Utility.batch_all).toHaveBeenCalledOnce();\n });\n\n test(\"handles Ink SDK AsyncTransaction wrappers\", async () => {\n const { api, getCapturedCalls } = createMockBatchApi(successEmit);\n const asyncCall = {\n waited: Promise.resolve({ decodedCall: { pallet: \"Contracts\", method: \"call\" } }),\n signSubmitAndWatch: () => {\n throw new Error(\"Should not be called\");\n },\n };\n\n const result = await batchSubmitAndWatch([asyncCall], api, mockSigner);\n\n expect(result.ok).toBe(true);\n expect(getCapturedCalls()[0]).toEqual([{ pallet: \"Contracts\", method: \"call\" }]);\n });\n\n test(\"accepts raw decoded calls (pass-through)\", async () => {\n const { api, getCapturedCalls } = createMockBatchApi(successEmit);\n const rawCall = { pallet: \"System\", method: \"remark\" };\n\n const result = await batchSubmitAndWatch([rawCall], api, mockSigner);\n\n expect(result.ok).toBe(true);\n expect(getCapturedCalls()[0]).toEqual([{ pallet: \"System\", method: \"remark\" }]);\n });\n\n test(\"mixes transaction types in a single batch\", async () => {\n const { api, getCapturedCalls } = createMockBatchApi(successEmit);\n const txWithDecoded = { decodedCall: \"call1\" };\n const asyncTx = {\n waited: Promise.resolve({ decodedCall: \"call2\" }),\n signSubmitAndWatch: () => {\n throw new Error(\"Should not be called\");\n },\n };\n const rawCall = { pallet: \"System\", method: \"remark\" };\n\n const result = await batchSubmitAndWatch(\n [txWithDecoded, asyncTx, rawCall],\n api,\n mockSigner,\n );\n\n expect(result.ok).toBe(true);\n expect(getCapturedCalls()[0]).toEqual([\n \"call1\",\n \"call2\",\n { pallet: \"System\", method: \"remark\" },\n ]);\n });\n\n test(\"throws TxBatchError for empty calls array\", async () => {\n const { api } = createMockBatchApi(successEmit);\n await expect(batchSubmitAndWatch([], api, mockSigner)).rejects.toThrow(TxBatchError);\n await expect(batchSubmitAndWatch([], api, mockSigner)).rejects.toThrow(\n \"Cannot batch zero calls\",\n );\n });\n\n test(\"throws TxBatchError when AsyncTransaction resolves without decodedCall\", async () => {\n const { api } = createMockBatchApi(successEmit);\n const badAsync = {\n waited: Promise.resolve({ noDecodedCall: true }),\n signSubmitAndWatch: () => {\n throw new Error(\"Should not be called\");\n },\n };\n\n await expect(\n batchSubmitAndWatch([badAsync as unknown as BatchableCall], api, mockSigner),\n ).rejects.toThrow(TxBatchError);\n });\n\n test(\"throws TxBatchError for null call\", async () => {\n const { api } = createMockBatchApi(successEmit);\n await expect(\n batchSubmitAndWatch([null as unknown as BatchableCall], api, mockSigner),\n ).rejects.toThrow(TxBatchError);\n await expect(\n batchSubmitAndWatch([null as unknown as BatchableCall], api, mockSigner),\n ).rejects.toThrow(\"Invalid batch call\");\n });\n\n test(\"throws TxBatchError for primitive call\", async () => {\n const { api } = createMockBatchApi(successEmit);\n await expect(\n batchSubmitAndWatch([42 as unknown as BatchableCall], api, mockSigner),\n ).rejects.toThrow(TxBatchError);\n await expect(\n batchSubmitAndWatch([\"oops\" as unknown as BatchableCall], api, mockSigner),\n ).rejects.toThrow(\"Invalid batch call\");\n });\n\n test(\"treats { decodedCall: undefined } as raw pass-through object\", async () => {\n const { api, getCapturedCalls } = createMockBatchApi(successEmit);\n const edgeCase = { decodedCall: undefined, other: \"data\" };\n\n const result = await batchSubmitAndWatch([edgeCase], api, mockSigner);\n\n expect(result.ok).toBe(true);\n // decodedCall is undefined so it falls through to raw pass-through\n expect(getCapturedCalls()[0]).toEqual([{ decodedCall: undefined, other: \"data\" }]);\n });\n\n test(\"defaults to batch_all mode\", async () => {\n const { api } = createMockBatchApi(successEmit);\n await batchSubmitAndWatch([{ decodedCall: \"call1\" }], api, mockSigner);\n\n expect(api.tx.Utility.batch_all).toHaveBeenCalledOnce();\n expect(api.tx.Utility.batch).not.toHaveBeenCalled();\n expect(api.tx.Utility.force_batch).not.toHaveBeenCalled();\n });\n\n test(\"respects mode: batch\", async () => {\n const { api } = createMockBatchApi(successEmit);\n await batchSubmitAndWatch([{ decodedCall: \"call1\" }], api, mockSigner, {\n mode: \"batch\",\n });\n\n expect(api.tx.Utility.batch).toHaveBeenCalledOnce();\n expect(api.tx.Utility.batch_all).not.toHaveBeenCalled();\n });\n\n test(\"respects mode: force_batch\", async () => {\n const { api } = createMockBatchApi(successEmit);\n await batchSubmitAndWatch([{ decodedCall: \"call1\" }], api, mockSigner, {\n mode: \"force_batch\",\n });\n\n expect(api.tx.Utility.force_batch).toHaveBeenCalledOnce();\n expect(api.tx.Utility.batch_all).not.toHaveBeenCalled();\n });\n\n test(\"forwards SubmitOptions to submitAndWatch\", async () => {\n const statuses: string[] = [];\n const { api } = createMockBatchApi(successEmit);\n\n await batchSubmitAndWatch([{ decodedCall: \"call1\" }], api, mockSigner, {\n onStatus: (s) => statuses.push(s),\n });\n\n expect(statuses).toContain(\"signing\");\n expect(statuses).toContain(\"in-block\");\n });\n\n test(\"propagates TxDispatchError\", async () => {\n const { api } = createMockBatchApi((h) => {\n h.next(signedEvent);\n h.next({\n type: \"txBestBlocksState\",\n txHash: \"0xbatch\",\n found: true,\n ok: false,\n events: [],\n block: { hash: \"0xblock1\", number: 100, index: 0 },\n dispatchError: {\n type: \"Module\",\n value: { type: \"Utility\", value: { type: \"TooManyCalls\" } },\n },\n });\n });\n\n await expect(\n batchSubmitAndWatch([{ decodedCall: \"call1\" }], api, mockSigner),\n ).rejects.toThrow(TxDispatchError);\n });\n\n test(\"propagates TxSigningRejectedError\", async () => {\n const { api } = createMockBatchApi((h) => {\n h.error(new Error(\"User rejected the request\"));\n });\n\n await expect(\n batchSubmitAndWatch([{ decodedCall: \"call1\" }], api, mockSigner),\n ).rejects.toThrow(TxSigningRejectedError);\n });\n\n test(\"resolves all calls in parallel\", async () => {\n const { api } = createMockBatchApi(successEmit);\n const resolveOrder: number[] = [];\n\n const asyncCall1 = {\n waited: new Promise<{ decodedCall: string }>((resolve) => {\n setTimeout(() => {\n resolveOrder.push(1);\n resolve({ decodedCall: \"call1\" });\n }, 10);\n }),\n signSubmitAndWatch: () => {\n throw new Error(\"Should not be called\");\n },\n };\n const asyncCall2 = {\n waited: new Promise<{ decodedCall: string }>((resolve) => {\n setTimeout(() => {\n resolveOrder.push(2);\n resolve({ decodedCall: \"call2\" });\n }, 5); // Resolves faster\n }),\n signSubmitAndWatch: () => {\n throw new Error(\"Should not be called\");\n },\n };\n\n await batchSubmitAndWatch([asyncCall1, asyncCall2], api, mockSigner);\n\n // Both should have resolved (order depends on timing, but both are present)\n expect(resolveOrder).toContain(1);\n expect(resolveOrder).toContain(2);\n });\n });\n}\n","// Copyright 2026 Parity Technologies (UK) Ltd.\n// SPDX-License-Identifier: Apache-2.0\nimport { createLogger } from \"@parity/product-sdk-logger\";\n\nimport { TxBatchError, TxDispatchError, TxSigningRejectedError, TxTimeoutError } from \"./errors.js\";\nimport type { RetryOptions } from \"./types.js\";\n\nconst log = createLogger(\"tx:retry\");\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/**\n * Whether an error is deterministic and should not be retried.\n *\n * - Batch errors are deterministic input validation failures (e.g., empty calls array).\n * - Dispatch errors are on-chain failures (e.g., insufficient balance) that will\n * produce the same result on retry.\n * - Signing rejections are explicit user intent.\n * - Timeouts mean we already waited the full duration; retrying would double the wait.\n */\nfunction isNonRetryable(error: unknown): boolean {\n return (\n error instanceof TxBatchError ||\n error instanceof TxDispatchError ||\n error instanceof TxSigningRejectedError ||\n error instanceof TxTimeoutError\n );\n}\n\n/**\n * Calculate delay with exponential backoff and jitter.\n *\n * Jitter prevents thundering-herd when multiple clients retry simultaneously.\n * The delay is `min(baseDelay * 2^attempt, maxDelay) * random(0.5, 1.0)`.\n */\nexport function calculateDelay(attempt: number, baseDelayMs: number, maxDelayMs: number): number {\n const exponential = Math.min(baseDelayMs * 2 ** attempt, maxDelayMs);\n const jitter = 0.5 + Math.random() * 0.5;\n return Math.round(exponential * jitter);\n}\n\n/**\n * Wrap an async function with retry logic and exponential backoff.\n *\n * Only retries transient errors (network disconnects, temporary RPC failures).\n * Deterministic errors ({@link TxDispatchError}, {@link TxBatchError}), user\n * rejections ({@link TxSigningRejectedError}), and timeouts ({@link TxTimeoutError})\n * are rethrown immediately without retry.\n *\n * @param fn - The async function to retry.\n * @param options - Retry configuration.\n * @returns The result of the first successful call.\n *\n * @example\n * ```ts\n * const result = await withRetry(\n * () => submitAndWatch(tx, signer),\n * { maxAttempts: 3, baseDelayMs: 1_000 },\n * );\n * ```\n */\nexport async function withRetry<T>(fn: () => Promise<T>, options?: RetryOptions): Promise<T> {\n const maxAttempts = options?.maxAttempts ?? 3;\n const baseDelayMs = options?.baseDelayMs ?? 1_000;\n const maxDelayMs = options?.maxDelayMs ?? 15_000;\n\n let lastError: unknown;\n\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n try {\n return await fn();\n } catch (error) {\n lastError = error;\n\n if (isNonRetryable(error)) {\n throw error;\n }\n\n if (attempt + 1 >= maxAttempts) {\n break;\n }\n\n const delay = calculateDelay(attempt, baseDelayMs, maxDelayMs);\n log.warn(`Attempt ${attempt + 1}/${maxAttempts} failed, retrying in ${delay}ms`, {\n error: error instanceof Error ? error.message : String(error),\n });\n await sleep(delay);\n }\n }\n\n throw lastError;\n}\n\nif (import.meta.vitest) {\n const { describe, test, expect, vi, beforeEach } = import.meta.vitest;\n const { configure } = await import(\"@parity/product-sdk-logger\");\n\n beforeEach(() => {\n configure({ handler: () => {} });\n vi.useRealTimers();\n });\n\n describe(\"withRetry\", () => {\n test(\"returns on first success\", async () => {\n const result = await withRetry(() => Promise.resolve(\"ok\"));\n expect(result).toBe(\"ok\");\n });\n\n test(\"retries transient error then succeeds\", async () => {\n let calls = 0;\n const result = await withRetry(\n () => {\n calls++;\n if (calls < 2) return Promise.reject(new Error(\"Network error\"));\n return Promise.resolve(\"recovered\");\n },\n { baseDelayMs: 1 },\n );\n expect(result).toBe(\"recovered\");\n expect(calls).toBe(2);\n });\n\n test(\"gives up after maxAttempts\", async () => {\n let calls = 0;\n await expect(\n withRetry(\n () => {\n calls++;\n return Promise.reject(new Error(\"Persistent failure\"));\n },\n { maxAttempts: 3, baseDelayMs: 1 },\n ),\n ).rejects.toThrow(\"Persistent failure\");\n expect(calls).toBe(3);\n });\n\n test(\"does NOT retry TxDispatchError\", async () => {\n let calls = 0;\n await expect(\n withRetry(\n () => {\n calls++;\n return Promise.reject(\n new TxDispatchError({}, \"Balances.InsufficientBalance\"),\n );\n },\n { maxAttempts: 3, baseDelayMs: 1 },\n ),\n ).rejects.toThrow(TxDispatchError);\n expect(calls).toBe(1);\n });\n\n test(\"does NOT retry TxSigningRejectedError\", async () => {\n let calls = 0;\n await expect(\n withRetry(\n () => {\n calls++;\n return Promise.reject(new TxSigningRejectedError());\n },\n { maxAttempts: 3, baseDelayMs: 1 },\n ),\n ).rejects.toThrow(TxSigningRejectedError);\n expect(calls).toBe(1);\n });\n\n test(\"does NOT retry TxBatchError\", async () => {\n let calls = 0;\n await expect(\n withRetry(\n () => {\n calls++;\n return Promise.reject(new TxBatchError(\"Cannot batch zero calls\"));\n },\n { maxAttempts: 3, baseDelayMs: 1 },\n ),\n ).rejects.toThrow(TxBatchError);\n expect(calls).toBe(1);\n });\n\n test(\"does NOT retry TxTimeoutError\", async () => {\n let calls = 0;\n await expect(\n withRetry(\n () => {\n calls++;\n return Promise.reject(new TxTimeoutError(300_000));\n },\n { maxAttempts: 3, baseDelayMs: 1 },\n ),\n ).rejects.toThrow(TxTimeoutError);\n expect(calls).toBe(1);\n });\n\n test(\"respects maxDelayMs cap\", () => {\n // attempt=10 with baseDelay=1000 would be 1024000ms uncapped\n const delay = calculateDelay(10, 1_000, 15_000);\n expect(delay).toBeLessThanOrEqual(15_000);\n expect(delay).toBeGreaterThan(0);\n });\n\n test(\"applies jitter (delay varies between calls)\", () => {\n const delays = Array.from({ length: 20 }, () => calculateDelay(2, 1_000, 15_000));\n const unique = new Set(delays);\n // With jitter, we should get multiple distinct values out of 20 samples\n expect(unique.size).toBeGreaterThan(1);\n });\n\n test(\"exponential backoff increases delay\", () => {\n const base = 1_000;\n // The minimum possible delay at each attempt (jitter factor = 0.5)\n const minDelay2 = base * 4 * 0.5; // 2000\n // attempt 2 minimum should be greater than attempt 0 maximum\n const maxDelay0 = base * 1.0; // 1000\n expect(minDelay2).toBeGreaterThan(maxDelay0);\n });\n });\n}\n","// Copyright 2026 Parity Technologies (UK) Ltd.\n// SPDX-License-Identifier: Apache-2.0\nimport { DEV_PHRASE } from \"@polkadot-labs/hdkd-helpers\";\nimport { seedToAccount } from \"@parity/product-sdk-keys\";\nimport type { PolkadotSigner } from \"polkadot-api\";\n\nimport type { DevAccountName } from \"./types.js\";\n\n/**\n * Create a PolkadotSigner for a standard Substrate dev account.\n *\n * Dev accounts use the well-known Substrate dev mnemonic (`DEV_PHRASE`) with\n * Sr25519 key derivation at the path `//{Name}`. These accounts have known\n * private keys and are pre-funded on dev/test chains.\n *\n * Only for local development, scripts, and testing. Never use in production.\n *\n * @param name - Dev account name (\"Alice\", \"Bob\", \"Charlie\", \"Dave\", \"Eve\", or \"Ferdie\").\n * @returns A PolkadotSigner that can sign transactions.\n *\n * @example\n * ```ts\n * import { createDevSigner } from \"@parity/product-sdk-tx\";\n *\n * const alice = createDevSigner(\"Alice\");\n * const result = await submitAndWatch(tx, alice);\n * ```\n */\nexport function createDevSigner(name: DevAccountName): PolkadotSigner {\n return seedToAccount(DEV_PHRASE, `//${name}`).signer;\n}\n\n/**\n * Get the public key bytes for a dev account.\n *\n * Useful for address derivation or identity checks in tests without\n * needing the full signer.\n *\n * @param name - Dev account name.\n * @returns 32-byte Sr25519 public key.\n */\nexport function getDevPublicKey(name: DevAccountName): Uint8Array {\n return seedToAccount(DEV_PHRASE, `//${name}`).publicKey;\n}\n\nif (import.meta.vitest) {\n const { describe, test, expect } = import.meta.vitest;\n\n // Alice's well-known sr25519 public key\n const ALICE_PUBKEY = new Uint8Array([\n 0xd4, 0x35, 0x93, 0xc7, 0x15, 0xfd, 0xd3, 0x1c, 0x61, 0x14, 0x1a, 0xbd, 0x04, 0xa9, 0x9f,\n 0xd6, 0x82, 0x2c, 0x85, 0x58, 0x85, 0x4c, 0xcd, 0xe3, 0x9a, 0x56, 0x84, 0xe7, 0xa5, 0x6d,\n 0xa2, 0x7d,\n ]);\n\n describe(\"createDevSigner\", () => {\n test(\"creates a signer for Alice with known public key\", () => {\n const signer = createDevSigner(\"Alice\");\n expect(signer).toBeDefined();\n expect(signer.publicKey).toEqual(ALICE_PUBKEY);\n });\n\n test(\"different names produce different signers\", () => {\n const alice = createDevSigner(\"Alice\");\n const bob = createDevSigner(\"Bob\");\n expect(alice.publicKey).not.toEqual(bob.publicKey);\n });\n\n test(\"all dev account names produce valid signers\", () => {\n const names: DevAccountName[] = [\"Alice\", \"Bob\", \"Charlie\", \"Dave\", \"Eve\", \"Ferdie\"];\n const keys = new Set<string>();\n\n for (const name of names) {\n const signer = createDevSigner(name);\n expect(signer).toBeDefined();\n expect(signer.publicKey).toBeInstanceOf(Uint8Array);\n expect(signer.publicKey.length).toBe(32);\n // All keys should be unique\n const hex = Array.from(signer.publicKey)\n .map((b) => b.toString(16).padStart(2, \"0\"))\n .join(\"\");\n expect(keys.has(hex)).toBe(false);\n keys.add(hex);\n }\n });\n });\n\n describe(\"getDevPublicKey\", () => {\n test(\"returns Alice's known public key\", () => {\n expect(getDevPublicKey(\"Alice\")).toEqual(ALICE_PUBKEY);\n });\n\n test(\"matches the signer's public key\", () => {\n const signer = createDevSigner(\"Bob\");\n const pubkey = getDevPublicKey(\"Bob\");\n expect(pubkey).toEqual(signer.publicKey);\n });\n });\n}\n","// Copyright 2026 Parity Technologies (UK) Ltd.\n// SPDX-License-Identifier: Apache-2.0\nimport { TxDryRunError, formatDryRunError } from \"./errors.js\";\nimport type { SubmittableTransaction, Weight } from \"./types.js\";\n\n/**\n * Validate an Ink SDK dry-run result and extract the submittable transaction.\n *\n * Replaces the 5-10 line boilerplate that every contract interaction repeats:\n * check `success`, parse the error, verify `send()` exists, and call it.\n *\n * Works with any object whose shape matches the Ink SDK contract query result\n * (typed structurally — no Ink SDK import required):\n *\n * - `contract.query(\"method\", { origin, data })` (Ink SDK)\n * - `contract.write(\"method\", args, origin)` (patched SDK wrappers)\n * - Any object with `{ success: boolean; value?: { send?(): ... } }`\n *\n * @param result - The dry-run result from a contract query or write simulation.\n * @returns The submittable transaction, ready to pass to {@link submitAndWatch}.\n * @throws {TxDryRunError} If the dry run failed or the result has no `send()`.\n *\n * @example\n * ```ts\n * import { extractTransaction, submitAndWatch, createDevSigner } from \"@parity/product-sdk-tx\";\n *\n * const dryRun = await contract.query(\"createItem\", { origin, data: { name, price } });\n * const tx = extractTransaction(dryRun);\n * const result = await submitAndWatch(tx, createDevSigner(\"Alice\"));\n * ```\n *\n * @example Composing with retry logic:\n * ```ts\n * const tx = extractTransaction(await contract.query(\"transfer\", { origin, data }));\n * const result = await withRetry(() => submitAndWatch(tx, signer));\n * ```\n */\nexport function extractTransaction(result: {\n success: boolean;\n value?: unknown;\n error?: unknown;\n}): SubmittableTransaction {\n if (!result.success) {\n const formatted = formatDryRunError(result);\n const revertReason = extractRevertReason(result.value);\n throw new TxDryRunError(result, formatted, revertReason);\n }\n\n const value = result.value;\n if (value == null || typeof value !== \"object\") {\n throw new TxDryRunError(result, \"dry run returned no value\");\n }\n\n const v = value as Record<string, unknown>;\n if (typeof v.send !== \"function\") {\n throw new TxDryRunError(result, \"not a write query (no send())\");\n }\n\n return v.send() as SubmittableTransaction;\n}\n\n/**\n * Try to extract a revert reason string from a dry-run result value.\n * Returns `undefined` if no revert reason is available.\n */\nfunction extractRevertReason(value: unknown): string | undefined {\n if (value == null || typeof value !== \"object\") return undefined;\n const v = value as Record<string, unknown>;\n\n if (typeof v.revertReason === \"string\" && v.revertReason) {\n return v.revertReason;\n }\n\n // Wrapped raw value (patched SDK)\n if (\"raw\" in v && v.raw != null && typeof v.raw === \"object\") {\n return extractRevertReason(v.raw);\n }\n\n return undefined;\n}\n\n/**\n * Apply a safety buffer to weight estimates from a dry-run result.\n *\n * Dry-run weight estimates reflect the exact execution cost at the time of\n * simulation. On-chain conditions can change between dry-run and actual\n * submission (storage growth, state changes by other transactions), so a\n * buffer prevents unexpected `OutOfGas` failures.\n *\n * The default 25% buffer matches the convention used across Polkadot\n * ecosystem tooling.\n *\n * @param weight - The `weight_required` from a `ReviveApi.call` or `ReviveApi.eth_transact` dry-run.\n * @param options - Override the buffer percentage (default: 25%).\n * @returns A new weight with both components scaled up.\n *\n * @example Basic usage with ReviveApi dry-run:\n * ```ts\n * const dryRun = await api.apis.ReviveApi.call(origin, dest, value, undefined, undefined, data);\n *\n * const tx = api.tx.Revive.call({\n * dest, value, data,\n * weight_limit: applyWeightBuffer(dryRun.weight_required),\n * storage_deposit_limit: dryRun.storage_deposit.value,\n * });\n * ```\n *\n * @example Custom buffer for latency-sensitive operations:\n * ```ts\n * applyWeightBuffer(dryRun.weight_required, { percent: 50 });\n * ```\n */\nexport function applyWeightBuffer(weight: Weight, options?: { percent?: number }): Weight {\n const percent = options?.percent ?? 25;\n const multiplier = 100n + BigInt(percent);\n return {\n ref_time: (weight.ref_time * multiplier) / 100n,\n proof_size: (weight.proof_size * multiplier) / 100n,\n };\n}\n\nif (import.meta.vitest) {\n const { describe, test, expect } = import.meta.vitest;\n\n describe(\"extractTransaction\", () => {\n test(\"returns tx from successful dry-run with send()\", () => {\n const mockTx = {\n signSubmitAndWatch: () => ({ subscribe: () => ({ unsubscribe: () => {} }) }),\n };\n const result = {\n success: true,\n value: { response: \"ok\", send: () => mockTx },\n };\n expect(extractTransaction(result)).toBe(mockTx);\n });\n\n test(\"throws TxDryRunError on failed dry-run\", () => {\n const result = {\n success: false,\n value: { revertReason: \"InsufficientBalance\" },\n };\n try {\n extractTransaction(result);\n expect.unreachable(\"should have thrown\");\n } catch (e) {\n expect(e).toBeInstanceOf(TxDryRunError);\n const err = e as TxDryRunError;\n expect(err.revertReason).toBe(\"InsufficientBalance\");\n expect(err.formatted).toBe(\"InsufficientBalance\");\n expect(err.message).toContain(\"InsufficientBalance\");\n expect(err.raw).toBe(result);\n }\n });\n\n test(\"throws TxDryRunError with Module error formatting\", () => {\n const result = {\n success: false,\n value: {\n type: \"Module\",\n value: { type: \"Revive\", value: { type: \"StorageDepositNotEnoughFunds\" } },\n },\n };\n try {\n extractTransaction(result);\n expect.unreachable(\"should have thrown\");\n } catch (e) {\n const err = e as TxDryRunError;\n expect(err.formatted).toBe(\"Revive.StorageDepositNotEnoughFunds\");\n expect(err.revertReason).toBeUndefined();\n }\n });\n\n test(\"throws TxDryRunError with error field\", () => {\n const result = {\n success: false,\n value: {},\n error: { type: \"ContractTrapped\" },\n };\n try {\n extractTransaction(result);\n expect.unreachable(\"should have thrown\");\n } catch (e) {\n const err = e as TxDryRunError;\n expect(err.formatted).toBe(\"ContractTrapped\");\n }\n });\n\n test(\"throws when value is missing\", () => {\n const result = { success: true };\n expect(() => extractTransaction(result)).toThrow(TxDryRunError);\n });\n\n test(\"throws when send is not a function\", () => {\n const result = { success: true, value: { response: \"ok\" } };\n expect(() => extractTransaction(result)).toThrow(\"not a write query\");\n });\n\n test(\"throws with revertReason from nested raw (patched SDK)\", () => {\n const result = {\n success: false,\n value: { raw: { revertReason: \"Unauthorized\" } },\n };\n try {\n extractTransaction(result);\n expect.unreachable(\"should have thrown\");\n } catch (e) {\n const err = e as TxDryRunError;\n expect(err.revertReason).toBe(\"Unauthorized\");\n }\n });\n\n test(\"throws with ReviveApi Message error\", () => {\n const result = {\n success: false,\n value: { type: \"Message\", value: \"Insufficient balance for gas * price + value\" },\n };\n try {\n extractTransaction(result);\n expect.unreachable(\"should have thrown\");\n } catch (e) {\n const err = e as TxDryRunError;\n expect(err.formatted).toBe(\"Insufficient balance for gas * price + value\");\n }\n });\n });\n\n describe(\"applyWeightBuffer\", () => {\n test(\"applies default 25% buffer\", () => {\n const weight: Weight = { ref_time: 1000n, proof_size: 500n };\n const buffered = applyWeightBuffer(weight);\n expect(buffered.ref_time).toBe(1250n);\n expect(buffered.proof_size).toBe(625n);\n });\n\n test(\"applies custom buffer percentage\", () => {\n const weight: Weight = { ref_time: 1000n, proof_size: 1000n };\n const buffered = applyWeightBuffer(weight, { percent: 50 });\n expect(buffered.ref_time).toBe(1500n);\n expect(buffered.proof_size).toBe(1500n);\n });\n\n test(\"zero buffer returns same values\", () => {\n const weight: Weight = { ref_time: 1000n, proof_size: 500n };\n const buffered = applyWeightBuffer(weight, { percent: 0 });\n expect(buffered.ref_time).toBe(1000n);\n expect(buffered.proof_size).toBe(500n);\n });\n\n test(\"does not mutate original weight\", () => {\n const weight: Weight = { ref_time: 1000n, proof_size: 500n };\n const buffered = applyWeightBuffer(weight);\n expect(weight.ref_time).toBe(1000n);\n expect(weight.proof_size).toBe(500n);\n expect(buffered).not.toBe(weight);\n });\n\n test(\"works with realistic weight values\", () => {\n // Values from tick3t reference repo\n const weight: Weight = { ref_time: 4_500_000_000n, proof_size: 1_000_000n };\n const buffered = applyWeightBuffer(weight);\n expect(buffered.ref_time).toBe(5_625_000_000n);\n expect(buffered.proof_size).toBe(1_250_000n);\n });\n });\n}\n","// Copyright 2026 Parity Technologies (UK) Ltd.\n// SPDX-License-Identifier: Apache-2.0\nimport type { PolkadotSigner } from \"polkadot-api\";\n\nimport { createLogger } from \"@parity/product-sdk-logger\";\n\nimport { submitAndWatch } from \"./submit.js\";\nimport type { SubmittableTransaction, TxResult } from \"./types.js\";\n\nconst log = createLogger(\"tx:mapping\");\n\n/**\n * Error thrown when account mapping fails.\n */\nexport class TxAccountMappingError extends Error {\n constructor(message: string, options?: ErrorOptions) {\n super(message, options);\n this.name = \"TxAccountMappingError\";\n }\n}\n\n/**\n * Minimal interface for checking if an address is mapped on-chain.\n *\n * Accepted structurally so this module stays runtime-agnostic. Build one\n * directly from the typed API of any pallet-revive-capable chain — see the\n * example on {@link ensureAccountMapped}.\n */\nexport interface MappingChecker {\n addressIsMapped(address: string): Promise<boolean>;\n}\n\n/**\n * Minimal typed API shape for `Revive.map_account()`.\n *\n * Accepted structurally so this module works with any PAPI typed API\n * that has the Revive pallet, without importing chain-specific descriptors.\n */\nexport interface ReviveApi {\n tx: {\n Revive: {\n map_account(): SubmittableTransaction;\n };\n };\n}\n\n/** Options for {@link ensureAccountMapped}. */\nexport interface EnsureAccountMappedOptions {\n /** Timeout in ms for the map_account transaction. Default: 60_000 (1 minute). */\n timeoutMs?: number;\n /** Called on mapping transaction status changes. */\n onStatus?: (status: \"checking\" | \"mapping\" | \"mapped\" | \"already-mapped\") => void;\n}\n\n/**\n * Ensure an account's SS58 address is mapped to its H160 EVM address on-chain.\n *\n * Account mapping is a prerequisite for any EVM contract interaction on Asset Hub.\n * This function checks the on-chain mapping status and, if unmapped, submits a\n * `Revive.map_account()` transaction and waits for inclusion.\n *\n * Idempotent — safe to call multiple times. Returns immediately if already mapped.\n *\n * @param address - The SS58 address to check/map.\n * @param signer - The signer for the account (must match the address).\n * @param checker - An object with `addressIsMapped()`. Easiest path: build one\n * from a pallet-revive-capable typed API by querying `Revive.OriginalAccount`.\n * @param api - A typed API with `tx.Revive.map_account()`.\n * @param options - Optional timeout and status callback.\n * @returns The transaction result if mapping was performed, or `null` if already mapped.\n *\n * @throws {TxAccountMappingError} If the mapping check or transaction fails.\n * @throws {TxDispatchError} If the map_account transaction fails on-chain.\n * @throws {TxTimeoutError} If the mapping transaction times out.\n *\n * @example\n * ```ts\n * import { ensureAccountMapped } from \"@parity/product-sdk-tx\";\n * import { ss58ToH160 } from \"@parity/product-sdk-address\";\n *\n * const api = client.getTypedApi(paseo_asset_hub);\n * const checker = {\n * addressIsMapped: async (addr: string) =>\n * (await api.query.Revive.OriginalAccount.getValue(ss58ToH160(addr))) !== undefined,\n * };\n *\n * await ensureAccountMapped(address, signer, checker, api);\n * // Account is now mapped — safe to call PolkaVM/Solidity contracts\n * ```\n */\nexport async function ensureAccountMapped(\n address: string,\n signer: PolkadotSigner,\n checker: MappingChecker,\n api: ReviveApi,\n options?: EnsureAccountMappedOptions,\n): Promise<TxResult | null> {\n const timeoutMs = options?.timeoutMs ?? 60_000;\n const onStatus = options?.onStatus;\n\n // Step 1: Check if already mapped\n onStatus?.(\"checking\");\n let isMapped: boolean;\n try {\n isMapped = await checker.addressIsMapped(address);\n } catch (cause) {\n throw new TxAccountMappingError(`Failed to check mapping status for ${address}`, { cause });\n }\n\n if (isMapped) {\n log.debug(\"account already mapped\", { address });\n onStatus?.(\"already-mapped\");\n return null;\n }\n\n // Step 2: Submit map_account transaction\n log.info(\"mapping account\", { address });\n onStatus?.(\"mapping\");\n\n const tx = api.tx.Revive.map_account();\n // submitAndWatch throws TxDispatchError on dispatch failure and\n // TxTimeoutError on timeout — both propagate to the caller as documented.\n const result = await submitAndWatch(tx, signer, {\n waitFor: \"best-block\",\n timeoutMs,\n });\n\n log.info(\"account mapped successfully\", { address, block: result.block });\n onStatus?.(\"mapped\");\n return result;\n}\n\n/**\n * Check if an address is mapped on-chain.\n *\n * Convenience wrapper around `checker.addressIsMapped()` with error handling.\n */\nexport async function isAccountMapped(address: string, checker: MappingChecker): Promise<boolean> {\n try {\n return await checker.addressIsMapped(address);\n } catch (cause) {\n throw new TxAccountMappingError(`Failed to check mapping status for ${address}`, { cause });\n }\n}\n\nif (import.meta.vitest) {\n const { describe, test, expect, vi } = import.meta.vitest;\n\n describe(\"ensureAccountMapped\", () => {\n const mockSigner = {} as PolkadotSigner;\n\n test(\"returns null when already mapped\", async () => {\n const checker: MappingChecker = {\n addressIsMapped: vi.fn().mockResolvedValue(true),\n };\n const api = {} as ReviveApi;\n\n const result = await ensureAccountMapped(\"5Alice\", mockSigner, checker, api);\n expect(result).toBeNull();\n expect(checker.addressIsMapped).toHaveBeenCalledWith(\"5Alice\");\n });\n\n test(\"calls onStatus with already-mapped when mapped\", async () => {\n const statuses: string[] = [];\n const checker: MappingChecker = {\n addressIsMapped: vi.fn().mockResolvedValue(true),\n };\n\n await ensureAccountMapped(\"5Alice\", mockSigner, checker, {} as ReviveApi, {\n onStatus: (s) => statuses.push(s),\n });\n expect(statuses).toEqual([\"checking\", \"already-mapped\"]);\n });\n\n test(\"throws TxAccountMappingError when check fails\", async () => {\n const checker: MappingChecker = {\n addressIsMapped: vi.fn().mockRejectedValue(new Error(\"network error\")),\n };\n\n await expect(\n ensureAccountMapped(\"5Alice\", mockSigner, checker, {} as ReviveApi),\n ).rejects.toThrow(TxAccountMappingError);\n });\n\n test(\"submits map_account when not mapped\", async () => {\n const checker: MappingChecker = {\n addressIsMapped: vi.fn().mockResolvedValue(false),\n };\n\n const mockTx: SubmittableTransaction = {\n signSubmitAndWatch: (_signer, _options) => ({\n subscribe: (handlers) => {\n queueMicrotask(() => {\n handlers.next({ type: \"signed\", txHash: \"0xabc\" });\n handlers.next({\n type: \"txBestBlocksState\",\n txHash: \"0xabc\",\n found: true,\n ok: true,\n events: [],\n block: { hash: \"0xblock\", number: 1, index: 0 },\n });\n });\n return { unsubscribe: () => {} };\n },\n }),\n };\n\n const api: ReviveApi = {\n tx: { Revive: { map_account: () => mockTx } },\n };\n\n const result = await ensureAccountMapped(\"5Alice\", mockSigner, checker, api);\n expect(result).not.toBeNull();\n expect(result!.ok).toBe(true);\n });\n\n test(\"calls onStatus through full mapping flow\", async () => {\n const statuses: string[] = [];\n const checker: MappingChecker = {\n addressIsMapped: vi.fn().mockResolvedValue(false),\n };\n\n const mockTx: SubmittableTransaction = {\n signSubmitAndWatch: (_signer, _options) => ({\n subscribe: (handlers) => {\n queueMicrotask(() => {\n handlers.next({ type: \"signed\", txHash: \"0xabc\" });\n handlers.next({\n type: \"txBestBlocksState\",\n txHash: \"0xabc\",\n found: true,\n ok: true,\n events: [],\n block: { hash: \"0xblock\", number: 1, index: 0 },\n });\n });\n return { unsubscribe: () => {} };\n },\n }),\n };\n\n const api: ReviveApi = {\n tx: { Revive: { map_account: () => mockTx } },\n };\n\n await ensureAccountMapped(\"5Alice\", mockSigner, checker, api, {\n onStatus: (s) => statuses.push(s),\n });\n expect(statuses).toEqual([\"checking\", \"mapping\", \"mapped\"]);\n });\n\n test(\"propagates TxDispatchError from submitAndWatch\", async () => {\n const { TxDispatchError } = await import(\"./errors.js\");\n const checker: MappingChecker = {\n addressIsMapped: vi.fn().mockResolvedValue(false),\n };\n\n const mockTx: SubmittableTransaction = {\n signSubmitAndWatch: (_signer, _options) => ({\n subscribe: (handlers) => {\n queueMicrotask(() => {\n handlers.next({\n type: \"txBestBlocksState\",\n txHash: \"0xabc\",\n found: true,\n ok: false,\n events: [],\n block: { hash: \"0xblock\", number: 1, index: 0 },\n dispatchError: { type: \"BadOrigin\" },\n });\n });\n return { unsubscribe: () => {} };\n },\n }),\n };\n\n const api: ReviveApi = {\n tx: { Revive: { map_account: () => mockTx } },\n };\n\n await expect(ensureAccountMapped(\"5Alice\", mockSigner, checker, api)).rejects.toThrow(\n TxDispatchError,\n );\n });\n\n test(\"propagates TxTimeoutError from submitAndWatch\", async () => {\n const { TxTimeoutError } = await import(\"./errors.js\");\n const checker: MappingChecker = {\n addressIsMapped: vi.fn().mockResolvedValue(false),\n };\n\n const mockTx: SubmittableTransaction = {\n signSubmitAndWatch: (_signer, _options) => ({\n subscribe: () => ({ unsubscribe: () => {} }),\n }),\n };\n\n const api: ReviveApi = {\n tx: { Revive: { map_account: () => mockTx } },\n };\n\n await expect(\n ensureAccountMapped(\"5Alice\", mockSigner, checker, api, { timeoutMs: 50 }),\n ).rejects.toThrow(TxTimeoutError);\n });\n });\n\n describe(\"isAccountMapped\", () => {\n test(\"returns true when mapped\", async () => {\n const checker: MappingChecker = {\n addressIsMapped: vi.fn().mockResolvedValue(true),\n };\n expect(await isAccountMapped(\"5Alice\", checker)).toBe(true);\n });\n\n test(\"returns false when not mapped\", async () => {\n const checker: MappingChecker = {\n addressIsMapped: vi.fn().mockResolvedValue(false),\n };\n expect(await isAccountMapped(\"5Alice\", checker)).toBe(false);\n });\n\n test(\"throws TxAccountMappingError on failure\", async () => {\n const checker: MappingChecker = {\n addressIsMapped: vi.fn().mockRejectedValue(new Error(\"timeout\")),\n };\n await expect(isAccountMapped(\"5Alice\", checker)).rejects.toThrow(TxAccountMappingError);\n });\n });\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@parity/product-sdk-tx",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
4
4
|
"description": "Transaction submission, lifecycle watching, and dev signers for Polkadot chains",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"@polkadot-labs/hdkd-helpers": "^0.0.10",
|
|
22
22
|
"polkadot-api": "^2.1.2",
|
|
23
|
-
"@parity/product-sdk-
|
|
24
|
-
"@parity/product-sdk-
|
|
23
|
+
"@parity/product-sdk-keys": "0.3.2",
|
|
24
|
+
"@parity/product-sdk-logger": "0.1.1"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"typescript": "^5.7.0",
|
package/src/account-mapping.ts
CHANGED
package/src/batch.ts
CHANGED
package/src/dev-signers.ts
CHANGED
package/src/dry-run.ts
CHANGED
package/src/errors.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Copyright 2026 Parity Technologies (UK) Ltd.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
1
3
|
/** Base class for all transaction errors. Use `instanceof TxError` to catch any tx-related error. */
|
|
2
4
|
export class TxError extends Error {
|
|
3
5
|
constructor(message: string, options?: ErrorOptions) {
|
package/src/index.ts
CHANGED
package/src/retry.ts
CHANGED
package/src/submit.ts
CHANGED