From 5879ef4fff3338c5d8218bf8bdc27bc470985cb2 Mon Sep 17 00:00:00 2001 From: "Thang X. Vu" Date: Sun, 15 Mar 2026 16:21:45 +0700 Subject: [PATCH] feat: to tx from runtime call --- e2e/nodes/src/tests/ToTx.test.ts | 142 +++++++++++++++++- .../api/src/client/BaseSubstrateClient.ts | 3 +- packages/api/src/client/DedotClient.ts | 19 ++- packages/api/src/client/LegacyClient.ts | 4 +- packages/api/src/client/V2Client.ts | 3 +- .../src/client/__tests__/LegacyClient.spec.ts | 71 ++++++++- .../api/src/client/__tests__/V2Client.spec.ts | 71 ++++++++- .../submittable/SubmittableExtrinsic.ts | 15 +- .../submittable/SubmittableExtrinsicV2.ts | 14 +- .../api/src/extrinsic/submittable/utils.ts | 43 +++++- packages/api/src/types.ts | 25 ++- 11 files changed, 365 insertions(+), 45 deletions(-) diff --git a/e2e/nodes/src/tests/ToTx.test.ts b/e2e/nodes/src/tests/ToTx.test.ts index e3bee13b3..afc0cf044 100644 --- a/e2e/nodes/src/tests/ToTx.test.ts +++ b/e2e/nodes/src/tests/ToTx.test.ts @@ -1,4 +1,4 @@ -import { assert } from 'dedot/utils'; +import { assert, hexToU8a } from 'dedot/utils'; import { describe, expect, it } from 'vitest'; import { devPairs } from '../utils.js'; @@ -63,6 +63,76 @@ describe('toTx', () => { const remarkedEvent = contractsClient.events.system.Remarked.find(result.events); assert(remarkedEvent, 'Remarked event should be emitted'); }); + + it('should send tx from Uint8Array via toTx', async () => { + const prevBobBalance = (await contractsClient.query.system.account(bob.address)).data.free; + + const signedTx = await contractsClient.tx.balances.transferKeepAlive(bob.address, TEN_UNIT).sign(alice); + const u8a = hexToU8a(signedTx.toHex()); + + const submittable = contractsClient.toTx(u8a); + const result = await submittable.send().untilFinalized(); + + expect(result.status.type).toBe('Finalized'); + expect(result.txHash).toBeDefined(); + + const newBobBalance = (await contractsClient.query.system.account(bob.address)).data.free; + expect(newBobBalance).toBe(prevBobBalance + TEN_UNIT); + }); + + it('should signAndSend IRuntimeTxCall via toTx', async () => { + const prevBobBalance = (await contractsClient.query.system.account(bob.address)).data.free; + + const txCall = contractsClient.tx.balances.transferKeepAlive(bob.address, TEN_UNIT).call; + + const submittable = contractsClient.toTx(txCall); + const result = await submittable.signAndSend(alice).untilFinalized(); + + expect(result.status.type).toBe('Finalized'); + expect(result.txHash).toBeDefined(); + + const newBobBalance = (await contractsClient.query.system.account(bob.address)).data.free; + expect(newBobBalance).toBe(prevBobBalance + TEN_UNIT); + }); + + it('should signAndSend runtime call hex via toTx', async () => { + const remarkMessage = 'Hello from toTx callHex test'; + const callHex = contractsClient.tx.system.remarkWithEvent(remarkMessage).callHex; + + const submittable = contractsClient.toTx(callHex); + const result = await submittable.signAndSend(alice).untilFinalized(); + + expect(result.status.type).toBe('Finalized'); + expect(result.txHash).toBeDefined(); + + const remarkedEvent = contractsClient.events.system.Remarked.find(result.events); + assert(remarkedEvent, 'Remarked event should be emitted'); + }); + + it('should signAndSend runtime call Uint8Array via toTx', async () => { + const remarkMessage = 'Hello from toTx callU8a test'; + const callU8a = contractsClient.tx.system.remarkWithEvent(remarkMessage).callU8a; + + const submittable = contractsClient.toTx(callU8a); + const result = await submittable.signAndSend(alice).untilFinalized(); + + expect(result.status.type).toBe('Finalized'); + expect(result.txHash).toBeDefined(); + + const remarkedEvent = contractsClient.events.system.Remarked.find(result.events); + assert(remarkedEvent, 'Remarked event should be emitted'); + }); + + it('should get paymentInfo from Uint8Array via toTx', async () => { + const unsignedTx = contractsClient.tx.balances.transferKeepAlive(bob.address, TEN_UNIT); + const u8a = hexToU8a(unsignedTx.toHex()); + + const submittable = contractsClient.toTx(u8a); + const paymentInfo = await submittable.paymentInfo(alice.address); + + expect(paymentInfo).toBeDefined(); + expect(paymentInfo.partialFee).toBeGreaterThan(0n); + }); }); describe('V2Client', () => { @@ -122,5 +192,75 @@ describe('toTx', () => { const remarkedEvent = reviveClient.events.system.Remarked.find(result.events); assert(remarkedEvent, 'Remarked event should be emitted'); }); + + it('should send tx from Uint8Array via toTx', async () => { + const prevBobBalance = (await reviveClient.query.system.account(bob.address)).data.free; + + const signedTx = await reviveClient.tx.balances.transferKeepAlive(bob.address, TEN_UNIT).sign(alice); + const u8a = hexToU8a(signedTx.toHex()); + + const submittable = reviveClient.toTx(u8a); + const result = await submittable.send().untilBestChainBlockIncluded(); + + expect(result.status.type).toBe('BestChainBlockIncluded'); + expect(result.txHash).toBeDefined(); + + const newBobBalance = (await reviveClient.query.system.account(bob.address)).data.free; + expect(newBobBalance).toBe(prevBobBalance + TEN_UNIT); + }); + + it('should signAndSend IRuntimeTxCall via toTx', async () => { + const prevBobBalance = (await reviveClient.query.system.account(bob.address)).data.free; + + const txCall = reviveClient.tx.balances.transferKeepAlive(bob.address, TEN_UNIT).call; + + const submittable = reviveClient.toTx(txCall); + const result = await submittable.signAndSend(alice).untilBestChainBlockIncluded(); + + expect(result.status.type).toBe('BestChainBlockIncluded'); + expect(result.txHash).toBeDefined(); + + const newBobBalance = (await reviveClient.query.system.account(bob.address)).data.free; + expect(newBobBalance).toBe(prevBobBalance + TEN_UNIT); + }); + + it('should signAndSend runtime call hex via toTx', async () => { + const remarkMessage = 'Hello from toTx callHex V2 test'; + const callHex = reviveClient.tx.system.remarkWithEvent(remarkMessage).callHex; + + const submittable = reviveClient.toTx(callHex); + const result = await submittable.signAndSend(alice).untilBestChainBlockIncluded(); + + expect(result.status.type).toBe('BestChainBlockIncluded'); + expect(result.txHash).toBeDefined(); + + const remarkedEvent = reviveClient.events.system.Remarked.find(result.events); + assert(remarkedEvent, 'Remarked event should be emitted'); + }); + + it('should signAndSend runtime call Uint8Array via toTx', async () => { + const remarkMessage = 'Hello from toTx callU8a V2 test'; + const callU8a = reviveClient.tx.system.remarkWithEvent(remarkMessage).callU8a; + + const submittable = reviveClient.toTx(callU8a); + const result = await submittable.signAndSend(alice).untilBestChainBlockIncluded(); + + expect(result.status.type).toBe('BestChainBlockIncluded'); + expect(result.txHash).toBeDefined(); + + const remarkedEvent = reviveClient.events.system.Remarked.find(result.events); + assert(remarkedEvent, 'Remarked event should be emitted'); + }); + + it('should get paymentInfo from Uint8Array via toTx', async () => { + const unsignedTx = reviveClient.tx.balances.transferKeepAlive(bob.address, TEN_UNIT); + const u8a = hexToU8a(unsignedTx.toHex()); + + const submittable = reviveClient.toTx(u8a); + const paymentInfo = await submittable.paymentInfo(alice.address); + + expect(paymentInfo).toBeDefined(); + expect(paymentInfo.partialFee).toBeGreaterThan(0n); + }); }); }); diff --git a/packages/api/src/client/BaseSubstrateClient.ts b/packages/api/src/client/BaseSubstrateClient.ts index ff821cf81..8ff55c186 100644 --- a/packages/api/src/client/BaseSubstrateClient.ts +++ b/packages/api/src/client/BaseSubstrateClient.ts @@ -16,6 +16,7 @@ import { GenericStorageQuery, GenericSubstrateApi, InjectedSigner, + IRuntimeTxCall, ISubmittableResult, Query, QueryFnResult, @@ -576,7 +577,7 @@ export abstract class BaseSubstrateClient< throw new Error('Unimplemented!'); } - toTx(tx: HexString | Extrinsic): ChainSubmittableExtrinsic { + toTx(tx: HexString | Uint8Array | Extrinsic | IRuntimeTxCall): ChainSubmittableExtrinsic { throw new Error('Unimplemented!'); } diff --git a/packages/api/src/client/DedotClient.ts b/packages/api/src/client/DedotClient.ts index 0ef662def..955c1c5df 100644 --- a/packages/api/src/client/DedotClient.ts +++ b/packages/api/src/client/DedotClient.ts @@ -6,6 +6,7 @@ import { GenericStorageQuery, GenericSubstrateApi, InjectedSigner, + IRuntimeTxCall, ISubmittableResult, Query, QueryFnResult, @@ -514,28 +515,30 @@ export class DedotClient< } /** - * Convert a raw hex-encoded transaction or an Extrinsic instance into a submittable extrinsic + * Convert a transaction input into a submittable extrinsic * with `sign`, `signAndSend`, `send`, and `paymentInfo` methods. * - * @param tx - A hex-encoded transaction string or an Extrinsic instance + * @param tx - A hex-encoded extrinsic or runtime call, a Uint8Array of encoded bytes, + * an Extrinsic instance, or an IRuntimeTxCall object. + * For HexString/Uint8Array, it first tries to decode as a full extrinsic; + * if that fails, it falls back to decoding as a raw runtime call. * @returns A submittable extrinsic instance * * @example * ```typescript - * // Convert a raw hex transaction to a submittable extrinsic + * // From a raw hex extrinsic * const submittable = client.toTx(rawTxHex); * + * // From a runtime call object + * const submittable = client.toTx({ pallet: 'Balances', palletCall: { name: 'TransferKeepAlive', params: { dest, value } } }); + * * // Sign and send * const unsub = await submittable.signAndSend(alice, (result) => { * console.log('Status:', result.status); * }); - * - * // Or query payment info - * const paymentInfo = await submittable.paymentInfo(alice); - * console.log('Estimated fee:', paymentInfo.partialFee); * ``` */ - toTx(tx: HexString | Extrinsic): ChainSubmittableExtrinsic { + toTx(tx: HexString | Uint8Array | Extrinsic | IRuntimeTxCall): ChainSubmittableExtrinsic { return this.#client.toTx(tx); } diff --git a/packages/api/src/client/LegacyClient.ts b/packages/api/src/client/LegacyClient.ts index ec7292efd..14726f1ad 100644 --- a/packages/api/src/client/LegacyClient.ts +++ b/packages/api/src/client/LegacyClient.ts @@ -1,6 +1,6 @@ import { BlockHash, type Extrinsic, Hash, Header, Metadata, PortableRegistry } from '@dedot/codecs'; import type { JsonRpcProvider } from '@dedot/providers'; -import { Callback, ChainSubmittableExtrinsic, GenericSubstrateApi, ISubmittableResult, TxUnsub } from '@dedot/types'; +import { Callback, ChainSubmittableExtrinsic, GenericSubstrateApi, IRuntimeTxCall, ISubmittableResult, TxUnsub } from '@dedot/types'; import { ChainProperties } from '@dedot/types/json-rpc'; import { assert, HashFn, HexString, noop } from '@dedot/utils'; import type { SubstrateApi } from '../chaintypes/index.js'; @@ -433,7 +433,7 @@ export class LegacyClient / }); } - toTx(tx: HexString | Extrinsic): ChainSubmittableExtrinsic { + toTx(tx: HexString | Uint8Array | Extrinsic | IRuntimeTxCall): ChainSubmittableExtrinsic { return SubmittableExtrinsic.fromTx(this, tx); } } diff --git a/packages/api/src/client/V2Client.ts b/packages/api/src/client/V2Client.ts index 165912998..b2d439b8b 100644 --- a/packages/api/src/client/V2Client.ts +++ b/packages/api/src/client/V2Client.ts @@ -15,6 +15,7 @@ import { ChainSubmittableExtrinsic, GenericStorageQuery, GenericSubstrateApi, + IRuntimeTxCall, ISubmittableResult, TxUnsub, } from '@dedot/types'; @@ -411,7 +412,7 @@ export class V2Client // pr }); } - toTx(tx: HexString | Extrinsic): ChainSubmittableExtrinsic { + toTx(tx: HexString | Uint8Array | Extrinsic | IRuntimeTxCall): ChainSubmittableExtrinsic { return SubmittableExtrinsicV2.fromTx(this, tx); } } diff --git a/packages/api/src/client/__tests__/LegacyClient.spec.ts b/packages/api/src/client/__tests__/LegacyClient.spec.ts index 60816476a..72a5ccf43 100644 --- a/packages/api/src/client/__tests__/LegacyClient.spec.ts +++ b/packages/api/src/client/__tests__/LegacyClient.spec.ts @@ -6,7 +6,7 @@ import { WsProvider } from '@dedot/providers'; import type { AnyShape } from '@dedot/shape'; import * as $ from '@dedot/shape'; import { createShape } from '@dedot/shape'; -import { blake2_256, HexString, keccak_256, stringCamelCase, stringPascalCase, u8aToHex } from '@dedot/utils'; +import { blake2_256, HexString, hexToU8a, keccak_256, stringCamelCase, stringPascalCase, u8aToHex } from '@dedot/utils'; import { afterEach, beforeEach, describe, expect, expectTypeOf, it, vi } from 'vitest'; import { LegacyClient } from '../LegacyClient.js'; import MockProvider, { MockedRuntimeVersion } from './MockProvider.js'; @@ -262,6 +262,75 @@ describe('LegacyClient', () => { }); }); + describe('toTx', () => { + it('should create submittable from unsigned extrinsic hex', () => { + const tx = api.tx.system.remarkWithEvent('hello'); + const hex = tx.toHex(); + + const submittable = api.toTx(hex); + expect(submittable).toBeDefined(); + expect(submittable.callHex).toEqual(tx.callHex); + }); + + it('should create submittable from Extrinsic instance', () => { + const tx = api.tx.system.remarkWithEvent('hello'); + + const submittable = api.toTx(tx); + expect(submittable).toBeDefined(); + expect(submittable.call).toEqual(tx.call); + }); + + it('should create submittable from Uint8Array of encoded extrinsic', () => { + const tx = api.tx.system.remarkWithEvent('hello'); + const hex = tx.toHex(); + const u8a = hexToU8a(hex); + + const submittable = api.toTx(u8a); + expect(submittable).toBeDefined(); + expect(submittable.callHex).toEqual(tx.callHex); + }); + + it('should create submittable from IRuntimeTxCall object', () => { + const tx = api.tx.system.remarkWithEvent('hello'); + const call = tx.call; + + const submittable = api.toTx(call); + expect(submittable).toBeDefined(); + expect(submittable.callHex).toEqual(tx.callHex); + }); + + it('should create submittable from runtime call hex (not full extrinsic)', () => { + const tx = api.tx.system.remarkWithEvent('hello'); + const callHex = tx.callHex; + + const submittable = api.toTx(callHex); + expect(submittable).toBeDefined(); + expect(submittable.callHex).toEqual(tx.callHex); + }); + + it('should create submittable from runtime call Uint8Array', () => { + const tx = api.tx.system.remarkWithEvent('hello'); + const callU8a = tx.callU8a; + + const submittable = api.toTx(callU8a); + expect(submittable).toBeDefined(); + expect(submittable.callHex).toEqual(tx.callHex); + }); + + it('should preserve preamble from extrinsic hex', () => { + const tx = api.tx.system.remarkWithEvent('hello'); + const submittable = api.toTx(tx.toHex()); + expect(submittable.preamble).toEqual(tx.preamble); + }); + + it('should produce same encoded extrinsic from IRuntimeTxCall as from tx builder', () => { + const tx = api.tx.system.remarkWithEvent('hello'); + + const submittable = api.toTx(tx.call); + expect(submittable.toHex()).toEqual(tx.toHex()); + }); + }); + describe('create api instance at a specific block', () => { it('should fetch runtime version for the block', async () => { const providerSend = vi.spyOn(provider, 'send'); diff --git a/packages/api/src/client/__tests__/V2Client.spec.ts b/packages/api/src/client/__tests__/V2Client.spec.ts index 2b0b0bded..cd2183712 100644 --- a/packages/api/src/client/__tests__/V2Client.spec.ts +++ b/packages/api/src/client/__tests__/V2Client.spec.ts @@ -14,7 +14,7 @@ import { OperationStorageDone, OperationStorageItems, } from '@dedot/types/json-rpc'; -import { assert, blake2_256, deferred, keccak_256, stringCamelCase, stringPascalCase, u8aToHex } from '@dedot/utils'; +import { assert, blake2_256, deferred, hexToU8a, keccak_256, stringCamelCase, stringPascalCase, u8aToHex } from '@dedot/utils'; import { MockInstance } from '@vitest/spy'; import { afterEach, beforeEach, describe, expect, expectTypeOf, it, vi } from 'vitest'; import { PinnedBlock } from '../../json-rpc/group/ChainHead/ChainHead.js'; @@ -333,6 +333,75 @@ describe( expect(() => api.tx.notFound.notFound()).toThrowError(`Pallet not found: notFound`); }); + describe('toTx', () => { + it('should create submittable from unsigned extrinsic hex', () => { + const tx = api.tx.system.remarkWithEvent('hello'); + const hex = tx.toHex(); + + const submittable = api.toTx(hex); + expect(submittable).toBeDefined(); + expect(submittable.callHex).toEqual(tx.callHex); + }); + + it('should create submittable from Extrinsic instance', () => { + const tx = api.tx.system.remarkWithEvent('hello'); + + const submittable = api.toTx(tx); + expect(submittable).toBeDefined(); + expect(submittable.call).toEqual(tx.call); + }); + + it('should create submittable from Uint8Array of encoded extrinsic', () => { + const tx = api.tx.system.remarkWithEvent('hello'); + const hex = tx.toHex(); + const u8a = hexToU8a(hex); + + const submittable = api.toTx(u8a); + expect(submittable).toBeDefined(); + expect(submittable.callHex).toEqual(tx.callHex); + }); + + it('should create submittable from IRuntimeTxCall object', () => { + const tx = api.tx.system.remarkWithEvent('hello'); + const call = tx.call; + + const submittable = api.toTx(call); + expect(submittable).toBeDefined(); + expect(submittable.callHex).toEqual(tx.callHex); + }); + + it('should create submittable from runtime call hex', () => { + const tx = api.tx.system.remarkWithEvent('hello'); + const callHex = tx.callHex; + + const submittable = api.toTx(callHex); + expect(submittable).toBeDefined(); + expect(submittable.callHex).toEqual(tx.callHex); + }); + + it('should create submittable from runtime call Uint8Array', () => { + const tx = api.tx.system.remarkWithEvent('hello'); + const callU8a = tx.callU8a; + + const submittable = api.toTx(callU8a); + expect(submittable).toBeDefined(); + expect(submittable.callHex).toEqual(tx.callHex); + }); + + it('should preserve preamble from extrinsic hex', () => { + const tx = api.tx.system.remarkWithEvent('hello'); + const submittable = api.toTx(tx.toHex()); + expect(submittable.preamble).toEqual(tx.preamble); + }); + + it('should produce same encoded extrinsic from IRuntimeTxCall as from tx builder', () => { + const tx = api.tx.system.remarkWithEvent('hello'); + + const submittable = api.toTx(tx.call); + expect(submittable.toHex()).toEqual(tx.toHex()); + }); + }); + describe('signer should works', () => { const ALICE = '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY'; let signer: InjectedSigner; diff --git a/packages/api/src/extrinsic/submittable/SubmittableExtrinsic.ts b/packages/api/src/extrinsic/submittable/SubmittableExtrinsic.ts index 0e828b468..4ca3fa936 100644 --- a/packages/api/src/extrinsic/submittable/SubmittableExtrinsic.ts +++ b/packages/api/src/extrinsic/submittable/SubmittableExtrinsic.ts @@ -3,6 +3,7 @@ import { AddressOrPair, Callback, DryRunResult, + IRuntimeTxCall, ISubmittableExtrinsicLegacy, ISubmittableResult, SignerOptions, @@ -15,22 +16,16 @@ import { assert, HexString, isHex, noop } from '@dedot/utils'; import { LegacyClient } from '../../client/LegacyClient.js'; import { BaseSubmittableExtrinsic } from './BaseSubmittableExtrinsic.js'; import { SubmittableResult } from './SubmittableResult.js'; -import { toTxStatus, txDefer } from './utils.js'; +import { resolveCallAndPreamble, toTxStatus, txDefer } from './utils.js'; /** * @name SubmittableExtrinsic * @description A wrapper around an Extrinsic that exposes methods to sign, send, and other utility around Extrinsic. */ export class SubmittableExtrinsic extends BaseSubmittableExtrinsic implements ISubmittableExtrinsicLegacy { - static fromTx(client: LegacyClient, tx: HexString | Extrinsic) { - let extrinsic: Extrinsic; - if (isHex(tx)) { - extrinsic = client.registry.$Extrinsic.tryDecode(tx); - } else { - extrinsic = tx; - } - - return new SubmittableExtrinsic(client, extrinsic.call, extrinsic.preamble); + static fromTx(client: LegacyClient, tx: HexString | Uint8Array | Extrinsic | IRuntimeTxCall) { + const { call, preamble } = resolveCallAndPreamble(client.registry, tx); + return new SubmittableExtrinsic(client, call, preamble); } async dryRun(account: AddressOrPair, optionsOrHash?: Partial | BlockHash): Promise { diff --git a/packages/api/src/extrinsic/submittable/SubmittableExtrinsicV2.ts b/packages/api/src/extrinsic/submittable/SubmittableExtrinsicV2.ts index d0f0c3c47..91b62e055 100644 --- a/packages/api/src/extrinsic/submittable/SubmittableExtrinsicV2.ts +++ b/packages/api/src/extrinsic/submittable/SubmittableExtrinsicV2.ts @@ -6,7 +6,7 @@ import { PinnedBlock } from '../../json-rpc/index.js'; import { BaseSubmittableExtrinsic } from './BaseSubmittableExtrinsic.js'; import { SubmittableResult } from './SubmittableResult.js'; import { InvalidTxError } from './errors.js'; -import { txDefer } from './utils.js'; +import { resolveCallAndPreamble, txDefer } from './utils.js'; type TxFound = { blockHash: BlockHash; blockNumber: number; index: number; events: IEventRecord[] }; @@ -23,15 +23,9 @@ export class SubmittableExtrinsicV2 extends BaseSubmittableExtrinsic { super(client, call, preamble); } - static fromTx(client: V2Client, tx: HexString | Extrinsic) { - let extrinsic: Extrinsic; - if (isHex(tx)) { - extrinsic = client.registry.$Extrinsic.tryDecode(tx); - } else { - extrinsic = tx; - } - - return new SubmittableExtrinsicV2(client, extrinsic.call, extrinsic.preamble); + static fromTx(client: V2Client, tx: HexString | Uint8Array | Extrinsic | IRuntimeTxCall) { + const { call, preamble } = resolveCallAndPreamble(client.registry, tx); + return new SubmittableExtrinsicV2(client, call, preamble); } async #send(callback: Callback): Promise { diff --git a/packages/api/src/extrinsic/submittable/utils.ts b/packages/api/src/extrinsic/submittable/utils.ts index a9baee79c..9f239ae1a 100644 --- a/packages/api/src/extrinsic/submittable/utils.ts +++ b/packages/api/src/extrinsic/submittable/utils.ts @@ -1,8 +1,47 @@ -import { Hash, TransactionStatus } from '@dedot/codecs'; -import { AddressOrPair, IKeyringPair, ISubmittableResult, TxStatus, Unsub } from '@dedot/types'; +import { Extrinsic, Hash, Preamble, PortableRegistry, TransactionStatus } from '@dedot/codecs'; +import { AddressOrPair, IKeyringPair, IRuntimeTxCall, ISubmittableResult, TxStatus, Unsub } from '@dedot/types'; import { assert, blake2AsU8a, Deferred, deferred, HexString, hexToU8a, isFunction } from '@dedot/utils'; import { RejectedTxError } from './errors.js'; +/** + * Check if a value is an IRuntimeTxCall object (has a 'pallet' property). + */ +function isRuntimeTxCall(tx: unknown): tx is IRuntimeTxCall { + return typeof tx === 'object' && tx !== null && 'pallet' in tx; +} + +/** + * Resolve a transaction input into a call and optional preamble. + * + * Supports: + * - `Extrinsic` instance: extract call + preamble + * - `IRuntimeTxCall` object: use directly as call, no preamble + * - `HexString` or `Uint8Array`: try decode as extrinsic first, fallback to runtime call + */ +export function resolveCallAndPreamble( + registry: PortableRegistry, + tx: HexString | Uint8Array | Extrinsic | IRuntimeTxCall, +): { call: IRuntimeTxCall; preamble?: Preamble } { + if (tx instanceof Extrinsic) { + return { call: tx.call, preamble: tx.preamble }; + } + + if (isRuntimeTxCall(tx)) { + return { call: tx }; + } + + // HexString or Uint8Array: try extrinsic decode first, fallback to runtime call + try { + const extrinsic = registry.$Extrinsic.tryDecode(tx); + return { call: extrinsic.call, preamble: extrinsic.preamble }; + } catch { + const { callTypeId } = registry.metadata!.extrinsic; + const $RuntimeCall = registry.findCodec(callTypeId); + const call = $RuntimeCall.tryDecode(tx) as IRuntimeTxCall; + return { call }; + } +} + export function isKeyringPair(account: AddressOrPair): account is IKeyringPair { return isFunction((account as IKeyringPair).sign); } diff --git a/packages/api/src/types.ts b/packages/api/src/types.ts index 544ebc785..3524d5022 100644 --- a/packages/api/src/types.ts +++ b/packages/api/src/types.ts @@ -8,6 +8,7 @@ import { GenericStorageQuery, GenericSubstrateApi, InjectedSigner, + IRuntimeTxCall, ISubmittableResult, Query, QueryFnResult, @@ -280,28 +281,36 @@ export interface ISubstrateClient< sendTx(tx: HexString | Extrinsic, callback?: Callback>): TxUnsub; /** - * Convert a raw hex-encoded transaction or an Extrinsic instance into a submittable extrinsic + * Convert a transaction input into a submittable extrinsic * with `sign`, `signAndSend`, `send`, and `paymentInfo` methods. * - * @param tx - A hex-encoded transaction string or an Extrinsic instance + * @param tx - A hex-encoded extrinsic or runtime call, a Uint8Array of encoded bytes, + * an Extrinsic instance, or an IRuntimeTxCall object. + * For HexString/Uint8Array, it first tries to decode as a full extrinsic; + * if that fails, it falls back to decoding as a raw runtime call. * @returns A submittable extrinsic instance * * @example * ```typescript - * // Convert a raw hex transaction to a submittable extrinsic + * // From a raw hex extrinsic * const submittable = client.toTx(rawTxHex); * + * // From a runtime call hex (encoded call data) + * const submittable = client.toTx(runtimeCallHex); + * + * // From a Uint8Array + * const submittable = client.toTx(callBytes); + * + * // From a runtime call object + * const submittable = client.toTx({ pallet: 'Balances', palletCall: { name: 'TransferKeepAlive', params: { dest, value } } }); + * * // Sign and send * const unsub = await submittable.signAndSend(alice, (result) => { * console.log('Status:', result.status); * }); - * - * // Or query payment info - * const paymentInfo = await submittable.paymentInfo(alice); - * console.log('Estimated fee:', paymentInfo.partialFee); * ``` */ - toTx(tx: HexString | Extrinsic): ChainSubmittableExtrinsic; + toTx(tx: HexString | Uint8Array | Extrinsic | IRuntimeTxCall): ChainSubmittableExtrinsic; /** * Query multiple storage items in a single call or subscribe to multiple storage items