Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 141 additions & 1 deletion e2e/nodes/src/tests/ToTx.test.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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);
});
});
});
3 changes: 2 additions & 1 deletion packages/api/src/client/BaseSubstrateClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
GenericStorageQuery,
GenericSubstrateApi,
InjectedSigner,
IRuntimeTxCall,
ISubmittableResult,
Query,
QueryFnResult,
Expand Down Expand Up @@ -576,7 +577,7 @@ export abstract class BaseSubstrateClient<
throw new Error('Unimplemented!');
}

toTx(tx: HexString | Extrinsic): ChainSubmittableExtrinsic<ChainApi> {
toTx(tx: HexString | Uint8Array | Extrinsic | IRuntimeTxCall): ChainSubmittableExtrinsic<ChainApi> {
throw new Error('Unimplemented!');
}

Expand Down
19 changes: 11 additions & 8 deletions packages/api/src/client/DedotClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
GenericStorageQuery,
GenericSubstrateApi,
InjectedSigner,
IRuntimeTxCall,
ISubmittableResult,
Query,
QueryFnResult,
Expand Down Expand Up @@ -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<ChainApi> {
toTx(tx: HexString | Uint8Array | Extrinsic | IRuntimeTxCall): ChainSubmittableExtrinsic<ChainApi> {
return this.#client.toTx(tx);
}

Expand Down
4 changes: 2 additions & 2 deletions packages/api/src/client/LegacyClient.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -433,7 +433,7 @@ export class LegacyClient<ChainApi extends GenericSubstrateApi = SubstrateApi> /
});
}

toTx(tx: HexString | Extrinsic): ChainSubmittableExtrinsic<ChainApi> {
toTx(tx: HexString | Uint8Array | Extrinsic | IRuntimeTxCall): ChainSubmittableExtrinsic<ChainApi> {
return SubmittableExtrinsic.fromTx(this, tx);
}
}
3 changes: 2 additions & 1 deletion packages/api/src/client/V2Client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
ChainSubmittableExtrinsic,
GenericStorageQuery,
GenericSubstrateApi,
IRuntimeTxCall,
ISubmittableResult,
TxUnsub,
} from '@dedot/types';
Expand Down Expand Up @@ -411,7 +412,7 @@ export class V2Client<ChainApi extends GenericSubstrateApi = SubstrateApi> // pr
});
}

toTx(tx: HexString | Extrinsic): ChainSubmittableExtrinsic<ChainApi> {
toTx(tx: HexString | Uint8Array | Extrinsic | IRuntimeTxCall): ChainSubmittableExtrinsic<ChainApi> {
return SubmittableExtrinsicV2.fromTx(this, tx);
}
}
71 changes: 70 additions & 1 deletion packages/api/src/client/__tests__/LegacyClient.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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');
Expand Down
Loading
Loading