Skip to content

Commit

Permalink
test(core): misc (#439)
Browse files Browse the repository at this point in the history
  • Loading branch information
tien authored Feb 3, 2025
1 parent fb423dd commit 439a212
Show file tree
Hide file tree
Showing 19 changed files with 1,130 additions and 233 deletions.
55 changes: 55 additions & 0 deletions packages/core/src/actions/connect-wallet.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import type { Wallet } from "../wallets/wallet.js";
import { connectWallet } from "./connect-wallet.js";
import { expect, it, vi } from "vitest";

it("should connect a single wallet", async () => {
const wallet = {
connect: vi.fn().mockResolvedValue(undefined),
} as unknown as Wallet;

await connectWallet(wallet);

expect(wallet.connect).toHaveBeenCalledTimes(1);
});

it("should connect multiple wallets", async () => {
const wallet1 = {
connect: vi.fn().mockResolvedValue(undefined),
} as unknown as Wallet;
const wallet2 = {
connect: vi.fn().mockResolvedValue(undefined),
} as unknown as Wallet;

await connectWallet([wallet1, wallet2]);

expect(wallet1.connect).toHaveBeenCalledTimes(1);
expect(wallet2.connect).toHaveBeenCalledTimes(1);
});

it("should handle an empty array of wallets", async () => {
await expect(connectWallet([])).resolves.toBeUndefined();
});

it("should handle a wallet that fails to connect", async () => {
const wallet = {
connect: vi.fn().mockRejectedValue(new Error("Failed to connect")),
} as unknown as Wallet;

await expect(connectWallet(wallet)).rejects.toThrow("Failed to connect");
});

it("should handle multiple wallets where one fails to connect", async () => {
const wallet1 = {
connect: vi.fn().mockResolvedValue(undefined),
} as unknown as Wallet;

const wallet2 = {
connect: vi.fn().mockRejectedValue(new Error("Failed to connect")),
} as unknown as Wallet;

await expect(connectWallet([wallet1, wallet2])).rejects.toThrow(
"Failed to connect",
);
expect(wallet1.connect).toHaveBeenCalledTimes(1);
expect(wallet2.connect).toHaveBeenCalledTimes(1);
});
42 changes: 42 additions & 0 deletions packages/core/src/actions/disconnect-wallet.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import type { Wallet } from "../wallets/wallet.js";
import { disconnectWallet } from "./disconnect-wallet.js";
import { expect, it, vi } from "vitest";

it("should disconnect a single wallet", async () => {
const mockWallet = {
disconnect: vi.fn().mockResolvedValue(undefined),
} as unknown as Wallet;

await disconnectWallet(mockWallet);

expect(mockWallet.disconnect).toHaveBeenCalledTimes(1);
});

it("should disconnect multiple wallets", async () => {
const mockWallet1 = {
disconnect: vi.fn().mockResolvedValue(undefined),
} as unknown as Wallet;

const mockWallet2 = {
disconnect: vi.fn().mockResolvedValue(undefined),
} as unknown as Wallet;

await disconnectWallet([mockWallet1, mockWallet2]);

expect(mockWallet1.disconnect).toHaveBeenCalledTimes(1);
expect(mockWallet2.disconnect).toHaveBeenCalledTimes(1);
});

it("should handle an empty array of wallets", async () => {
await expect(disconnectWallet([])).resolves.toBeUndefined();
});

it("should handle a wallet that fails to disconnect", async () => {
const mockWallet = {
disconnect: vi.fn().mockRejectedValue(new Error("Failed to disconnect")),
} as unknown as Wallet;

await expect(disconnectWallet(mockWallet)).rejects.toThrow(
"Failed to disconnect",
);
});
226 changes: 226 additions & 0 deletions packages/core/src/actions/get-block-extrinsics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
/* v8 ignore start */
import {
getDynamicBuilder,
getLookupFn,
} from "@polkadot-api/metadata-builders";
import {
Bytes,
enhanceCodec,
metadata as metadataCodec,
Struct,
u8,
type V15,
} from "@polkadot-api/substrate-bindings";
import type {
Binary,
ChainDefinition,
Codec,
Enum,
FixedSizeBinary,
PolkadotClient,
SS58String,
TypedApi,
} from "polkadot-api";

type MultiAddress = Enum<{
Id: SS58String;
Index: number | bigint;
Raw: Binary;
Address32: FixedSizeBinary<32>;
Address20: FixedSizeBinary<20>;
}>;

type MultiSignature = Enum<{
Ed25519: FixedSizeBinary<64>;
Sr25519: FixedSizeBinary<64>;
Ecdsa: FixedSizeBinary<65>;
}>;

type Extra = Partial<{
nonZeroSender: undefined;
specVersion: undefined;
txVersion: undefined;
genesis: undefined;
mortality: { type: `Mortal${string}`; value: number };
nonce: number;
weight: undefined;
transactionPayment: bigint;
metadataHash: Enum<{ Disabled: undefined; Enabled: undefined }>;
[key: string]: unknown;
}>;

type Call = {
module: string;
func: string;
args: unknown;
};

type Extrinsic = { version: number; call: Call } & (
| { signed: false }
| {
signed: true;
sender: MultiAddress;
signature: MultiSignature;
extra: Extra;
}
);

export async function unstable_getBlockExtrinsics(
client: PolkadotClient,
typedApi: TypedApi<ChainDefinition>,
blockHash: string,
) {
const v15MetadataBinary = await (
typedApi.apis["Metadata"]!["metadata_at_version"]! as (
version: number,
) => Promise<Binary | undefined>
)(15);

if (v15MetadataBinary === undefined) {
return;
}

const metadataResult = metadataCodec.dec(v15MetadataBinary.asBytes());

if (metadataResult.metadata.tag !== "v15") {
return;
}

const metadata = metadataResult.metadata.value;

const dynamicBuilder = await getOrCreateDynamicBuilder(client, metadata);

const version$ = enhanceCodec(
u8,
(value: { signed: boolean; version: number }) =>
(+!!value.signed << 7) | value.version,
(value) => ({
version: value & ~(1 << 7),
signed: !!(value & (1 << 7)),
}),
);

const address$ = dynamicBuilder.buildDefinition(
metadata.extrinsic.address,
) as Codec<MultiAddress>;

const signature$ = dynamicBuilder.buildDefinition(
metadata.extrinsic.signature,
) as Codec<MultiSignature>;

const rawExtra$ = dynamicBuilder.buildDefinition(
metadata.extrinsic.extra,
) as Codec<unknown[]>;

const extra$ = enhanceCodec(
rawExtra$,
(extra: Extra) =>
metadata.extrinsic.signedExtensions.map(
(signedExtension) =>
extra[
"Check" +
signedExtension.identifier.slice(0, 1).toUpperCase() +
signedExtension.identifier.slice(1, 0)
],
),
(extra) =>
Object.fromEntries(
metadata.extrinsic.signedExtensions.map((signedExtension, index) => {
const name = signedExtension.identifier.replace(/^Check/, "");
return [
name.slice(0, 1).toLowerCase() + name.slice(1),
extra[index],
] as const;
}),
) as Extra,
);

const rawCall$ = dynamicBuilder.buildDefinition(
metadata.extrinsic.call,
) as Codec<{ type: string; value: { type: string; value: unknown } }>;

const call$ = enhanceCodec(
rawCall$,
(call: Call) => ({
type: call.module,
value: { type: call.func, value: call.args },
}),
(call) => ({
module: call.type,
func: call.value.type,
args: call.value.value,
}),
);

const inherentExtrinsic$ = Struct({
version: version$ as Codec<{ version: number; signed: false }>,
body: Struct({ call: call$ }),
});

const signedExtrinsic$ = Struct({
version: version$ as Codec<{ version: number; signed: true }>,
body: Struct({
sender: address$,
signature: signature$,
extra: extra$,
call: call$,
}),
});

const simpleVersion$ = Struct({
version: version$,
});

const extrinsic$ = enhanceCodec(
Bytes(),
(extrinsic: Extrinsic) =>
extrinsic.signed
? signedExtrinsic$.enc({
version: { version: extrinsic.version, signed: extrinsic.signed },
body: {
sender: extrinsic.sender,
signature: extrinsic.signature,
extra: extrinsic.extra,
call: extrinsic.call,
},
})
: inherentExtrinsic$.enc({
version: { version: extrinsic.version, signed: extrinsic.signed },
body: { call: extrinsic.call },
}),
(extrinsicBytes) => {
const {
version: { signed },
} = simpleVersion$.dec(extrinsicBytes);

const rawExtrinsic = (
signed ? signedExtrinsic$.dec : inherentExtrinsic$.dec
)(extrinsicBytes);

return { ...rawExtrinsic.version, ...rawExtrinsic.body } as Extrinsic;
},
);

const blockBody = await client.getBlockBody(blockHash);

return blockBody.map(extrinsic$.dec);
}

const dynamicBuilders = new WeakMap<
PolkadotClient,
ReturnType<typeof getDynamicBuilder>
>();

async function getOrCreateDynamicBuilder(
client: PolkadotClient,
metadata: V15,
) {
if (dynamicBuilders.has(client)) {
return dynamicBuilders.get(client)!;
}

const lookup = getLookupFn(metadata);
const dynamicBuilder = getDynamicBuilder(lookup);

return dynamicBuilders.set(client, dynamicBuilder).get(client)!;
}
50 changes: 50 additions & 0 deletions packages/core/src/actions/get-block.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { getBlock, type GetBlockOptions } from "./get-block.js";
import type { PolkadotClient } from "polkadot-api";
import { of } from "rxjs";
import { expect, it } from "vitest";

type DummyBlock = { id: string };

it("should return the best block when options.tag is 'best'", () =>
new Promise<void>((resolve) => {
const fakeBlock: DummyBlock = { id: "best-block" };
const client = {
bestBlocks$: of([fakeBlock]),
finalizedBlock$: of({ id: "finalized-block" }),
} as unknown as PolkadotClient;

getBlock(client, { tag: "best" } as GetBlockOptions).subscribe((result) => {
expect(result).toBe(fakeBlock);
resolve();
});
}));

it("should return the finalized block when options.tag is 'finalized'", () =>
new Promise<void>((resolve) => {
const fakeBlock: DummyBlock = { id: "finalized-block" };
const client = {
bestBlocks$: of([{ id: "best-block" }]),
finalizedBlock$: of(fakeBlock),
} as unknown as PolkadotClient;

getBlock(client, { tag: "finalized" } as GetBlockOptions).subscribe(
(result) => {
expect(result).toBe(fakeBlock);
resolve();
},
);
}));

it("should return the finalized block when no options are provided", () =>
new Promise<void>((resolve) => {
const fakeBlock: DummyBlock = { id: "finalized-block" };
const client = {
bestBlocks$: of([{ id: "best-block" }]),
finalizedBlock$: of(fakeBlock),
} as unknown as PolkadotClient;

getBlock(client).subscribe((result) => {
expect(result).toBe(fakeBlock);
resolve();
});
}));
Loading

0 comments on commit 439a212

Please sign in to comment.