@d9-network/ink 0.0.1

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/src/types.ts ADDED
@@ -0,0 +1,274 @@
1
+ /**
2
+ * Core type definitions for D9 Ink SDK
3
+ */
4
+
5
+ import type {
6
+ InkCallableDescriptor,
7
+ InkDescriptors,
8
+ InkStorageDescriptor,
9
+ InkMetadata,
10
+ Event,
11
+ } from "@polkadot-api/ink-contracts";
12
+ import type { PolkadotClient, PolkadotSigner, SS58String } from "polkadot-api";
13
+ import type { Observable } from "rxjs";
14
+ import type { ContractError } from "./errors";
15
+ import type {
16
+ DecodedContractEvent,
17
+ EventSubscriptionOptions,
18
+ } from "./event-types";
19
+
20
+ /**
21
+ * Decoder type for response decoding
22
+ */
23
+ export type ResponseDecoder = { dec: (data: Uint8Array) => unknown };
24
+
25
+ /**
26
+ * Query options for read operations (dry-run)
27
+ */
28
+ export interface QueryOptions<Args = unknown> {
29
+ /** The origin account for the call */
30
+ origin: SS58String;
31
+ /** Arguments to pass to the contract message */
32
+ args?: Args;
33
+ /** Value to transfer with the call */
34
+ value?: bigint;
35
+ /** AbortSignal for cancellation */
36
+ signal?: AbortSignal;
37
+ /** Timeout in milliseconds */
38
+ timeout?: number;
39
+ /** Block hash to query at (default: finalized) */
40
+ at?: string;
41
+ }
42
+
43
+ /**
44
+ * Send options for write operations
45
+ */
46
+ export interface SendOptions<Args = unknown> {
47
+ /** The origin account for the call */
48
+ origin: SS58String;
49
+ /** Arguments to pass to the contract message */
50
+ args?: Args;
51
+ /** Value to transfer with the call */
52
+ value?: bigint;
53
+ /** Gas limit (optional, will be estimated if not provided) */
54
+ gasLimit?: { refTime: bigint; proofSize: bigint };
55
+ /** Storage deposit limit */
56
+ storageDepositLimit?: bigint;
57
+ }
58
+
59
+ /**
60
+ * Sendable transaction that can be signed and submitted
61
+ */
62
+ export interface SendableTransaction<T> {
63
+ /** Sign and submit the transaction */
64
+ signAndSubmit(signer: PolkadotSigner): Promise<TxResult<T>>;
65
+ /** Get the encoded call data */
66
+ getEncodedData(): Uint8Array;
67
+ }
68
+
69
+ /**
70
+ * Transaction result
71
+ */
72
+ export interface TxResult<T> {
73
+ /** Whether the transaction was successful */
74
+ ok: boolean;
75
+ /** Transaction hash */
76
+ txHash: string;
77
+ /** Block information where tx was included */
78
+ block: {
79
+ hash: string;
80
+ number: number;
81
+ };
82
+ /** Decoded return value (if any) */
83
+ result?: T;
84
+ /** Events emitted by the transaction */
85
+ events: unknown[];
86
+ /** Dispatch error if failed */
87
+ dispatchError?: unknown;
88
+ }
89
+
90
+ /**
91
+ * Successful query result value
92
+ */
93
+ export interface QuerySuccessValue<T> {
94
+ /** The decoded response from the contract */
95
+ value: T;
96
+ /** Events that would be emitted */
97
+ events: unknown[];
98
+ /** Gas consumed during dry-run */
99
+ gasConsumed: { refTime: bigint; proofSize: bigint };
100
+ /** Gas required for execution */
101
+ gasRequired: { refTime: bigint; proofSize: bigint };
102
+ /** Storage deposit required */
103
+ storageDeposit: bigint;
104
+ /** Create a sendable transaction from this query result */
105
+ send(): SendableTransaction<T>;
106
+ }
107
+
108
+ /**
109
+ * Query result - discriminated union for success/failure
110
+ * On success: QuerySuccessValue fields are spread to the same level as success
111
+ * On failure: error field contains the ContractError
112
+ */
113
+ export type QueryResult<T> =
114
+ | ({ success: true } & QuerySuccessValue<T>)
115
+ | { success: false; error: ContractError };
116
+
117
+ /**
118
+ * Storage query interface
119
+ */
120
+ export interface ContractStorage {
121
+ /** Get the root storage value */
122
+ getRoot(): Promise<
123
+ { success: true; value: unknown } | { success: false; value: ContractError }
124
+ >;
125
+ /** Get a nested storage value by path */
126
+ getNested(
127
+ path: string,
128
+ ...keys: unknown[]
129
+ ): Promise<
130
+ { success: true; value: unknown } | { success: false; value: ContractError }
131
+ >;
132
+ }
133
+
134
+ /**
135
+ * Type helper to extract message args type
136
+ */
137
+ type MessageArgs<
138
+ M extends InkCallableDescriptor,
139
+ K extends keyof M,
140
+ > = M[K]["message"];
141
+
142
+ /**
143
+ * ink! LangError type pattern
144
+ * papi generates LangError variants with { type: string; value?: unknown }
145
+ */
146
+ type InkLangError = { type: string; value?: unknown };
147
+
148
+ /**
149
+ * ink! MessageResult type pattern (Result<T, LangError>)
150
+ * papi generates this as { success: true; value: T } | { success: false; value: LangError }
151
+ */
152
+ type InkMessageResult<T> =
153
+ | { success: true; value: T }
154
+ | { success: false; value: unknown };
155
+
156
+ /**
157
+ * Type helper to unwrap ink! MessageResult (Result<T, LangError>)
158
+ * 1. Extract T from { success: true; value: T } | { success: false; value: ... }
159
+ * 2. Exclude LangError variants from T
160
+ */
161
+ type UnwrapMessageResult<T> = T extends InkMessageResult<infer U>
162
+ ? Exclude<U, InkLangError>
163
+ : T;
164
+
165
+ /**
166
+ * Type helper to extract message response type (with MessageResult unwrapped)
167
+ */
168
+ type MessageResponse<
169
+ M extends InkCallableDescriptor,
170
+ K extends keyof M,
171
+ > = UnwrapMessageResult<M[K]["response"]>;
172
+
173
+ /**
174
+ * D9 Ink Contract interface
175
+ */
176
+ export interface D9InkContract<M extends InkCallableDescriptor> {
177
+ /** Contract address */
178
+ readonly address: SS58String;
179
+ /** Contract metadata */
180
+ readonly metadata: InkMetadata;
181
+
182
+ /**
183
+ * Query a contract message (dry-run)
184
+ * @param method - The message label (e.g., "PSP22::balance_of")
185
+ * @param options - Query options including origin and args
186
+ */
187
+ query<K extends keyof M & string>(
188
+ method: K,
189
+ options: QueryOptions<MessageArgs<M, K>>,
190
+ ): Promise<QueryResult<MessageResponse<M, K>>>;
191
+
192
+ /**
193
+ * Create a sendable transaction for a contract message
194
+ * @param method - The message label (e.g., "PSP22::transfer")
195
+ * @param options - Send options including origin and args
196
+ */
197
+ send<K extends keyof M & string>(
198
+ method: K,
199
+ options: SendOptions<MessageArgs<M, K>>,
200
+ ): SendableTransaction<MessageResponse<M, K>>;
201
+
202
+ /**
203
+ * Get storage query interface
204
+ */
205
+ getStorage(): ContractStorage;
206
+
207
+ /**
208
+ * Filter events from a transaction result to only include this contract's events
209
+ */
210
+ filterEvents(events: unknown[]): DecodedContractEvent[];
211
+
212
+ /**
213
+ * Subscribe to contract events as an RxJS Observable
214
+ * @param options - Event subscription options (event labels to filter, etc.)
215
+ */
216
+ subscribeToEvents(
217
+ options?: Omit<EventSubscriptionOptions, "contractAddress">,
218
+ ): Observable<DecodedContractEvent>;
219
+ }
220
+
221
+ /**
222
+ * D9 Ink SDK options
223
+ */
224
+ export interface D9InkSdkOptions {
225
+ /** Default query options */
226
+ defaultQueryOptions?: Partial<QueryOptions>;
227
+ /** Default send options */
228
+ defaultSendOptions?: Partial<SendOptions>;
229
+ }
230
+
231
+ /**
232
+ * D9 Ink SDK interface
233
+ */
234
+ export interface D9InkSdk {
235
+ /**
236
+ * Get a contract instance for interacting with a deployed contract
237
+ * @param descriptor - The contract descriptor from @polkadot-api/descriptors
238
+ * @param address - The contract address (SS58 format)
239
+ */
240
+ getContract<
241
+ S extends InkStorageDescriptor,
242
+ M extends InkCallableDescriptor,
243
+ C extends InkCallableDescriptor,
244
+ E extends Event,
245
+ >(
246
+ descriptor: InkDescriptors<S, M, C, E>,
247
+ address: SS58String,
248
+ ): D9InkContract<M>;
249
+ }
250
+
251
+ export type D9InkContractFromDescriptor<D> = D extends InkDescriptors<infer _S, infer M, infer _C, infer _E> ? D9InkContract<M> : never;
252
+
253
+ /**
254
+ * Internal options for contract creation
255
+ */
256
+ export interface CreateContractOptions {
257
+ client: PolkadotClient;
258
+ typedApi?: unknown;
259
+ defaultQueryOptions?: Partial<QueryOptions>;
260
+ defaultSendOptions?: Partial<SendOptions>;
261
+ }
262
+
263
+ /**
264
+ * Type helper to infer the Messages type from an InkDescriptors
265
+ */
266
+ export type InferMessages<D> =
267
+ D extends InkDescriptors<
268
+ InkStorageDescriptor,
269
+ infer M,
270
+ InkCallableDescriptor,
271
+ Event
272
+ >
273
+ ? M
274
+ : never;
@@ -0,0 +1,325 @@
1
+ /**
2
+ * Event parsing and subscription tests
3
+ */
4
+ import { describe, it, expect, beforeAll, afterAll } from "bun:test";
5
+ import { ss58Decode } from "@polkadot-labs/hdkd-helpers";
6
+ import { contracts, d9, getMetadata } from "@polkadot-api/descriptors";
7
+ import { createClient } from "polkadot-api";
8
+ import { getWsProvider } from "polkadot-api/ws-provider";
9
+ import { withPolkadotSdkCompat } from "polkadot-api/polkadot-sdk-compat";
10
+ import { withLegacy } from "@polkadot-api/legacy-provider";
11
+ import {
12
+ ContractEventParser,
13
+ buildEventDecoder,
14
+ buildAllEventDecoders,
15
+ getEventSignature,
16
+ createD9InkSdk,
17
+ } from "../src";
18
+
19
+ const TEST_ADDRESSES = {
20
+ testUser: "zcb3U8vYqWLmd5huhyS7nMiMnqDQfWJrmjQcpMhkWmn7krH",
21
+ usdtContract: "uLj9DRUujbpCyK7USZY5ebGbxdtKoWvdRvGyyUsoLWDsNng",
22
+ };
23
+
24
+ const ENDPOINT = process.env.TEST_ENDPOINT || "wss://mainnet.d9network.com:40300";
25
+
26
+ describe("Event Decoder Building", () => {
27
+ it("should build event decoder for Transfer event from USDT metadata", () => {
28
+ const metadata = contracts.usdt.metadata!;
29
+
30
+ // Build decoder for Transfer event
31
+ const decoder = buildEventDecoder(metadata, "Transfer");
32
+
33
+ expect(decoder).toBeDefined();
34
+ expect(typeof decoder).toBe("function");
35
+ });
36
+
37
+ it("should build all event decoders from USDT metadata", () => {
38
+ const metadata = contracts.usdt.metadata!;
39
+
40
+ const decoders = buildAllEventDecoders(metadata);
41
+
42
+ expect(decoders).toBeDefined();
43
+ expect(decoders.size).toBeGreaterThan(0);
44
+
45
+ // Check that Transfer event decoder exists
46
+ expect(decoders.has("Transfer")).toBe(true);
47
+ expect(decoders.has("Approval")).toBe(true);
48
+ });
49
+
50
+ it("should generate consistent event signatures", () => {
51
+ const sig1 = getEventSignature("Transfer");
52
+ const sig2 = getEventSignature("Transfer");
53
+
54
+ expect(sig1).toBeInstanceOf(Uint8Array);
55
+ expect(sig1.length).toBe(32); // blake2_256 produces 32 bytes
56
+ expect(sig1).toEqual(sig2); // Same label should produce same signature
57
+ });
58
+
59
+ it("should generate different signatures for different events", () => {
60
+ const transferSig = getEventSignature("Transfer");
61
+ const approvalSig = getEventSignature("Approval");
62
+
63
+ expect(transferSig).not.toEqual(approvalSig);
64
+ });
65
+ });
66
+
67
+ describe("ContractEventParser", () => {
68
+ it("should create parser instance", () => {
69
+ const metadata = contracts.usdt.metadata!;
70
+ const parser = new ContractEventParser(metadata, TEST_ADDRESSES.usdtContract);
71
+
72
+ expect(parser).toBeDefined();
73
+ });
74
+
75
+ it("should parse mock ContractEmitted event", () => {
76
+ const metadata = contracts.usdt.metadata!;
77
+ const parser = new ContractEventParser(metadata, TEST_ADDRESSES.usdtContract);
78
+
79
+ // Get contract address bytes
80
+ const [contractBytes] = ss58Decode(TEST_ADDRESSES.usdtContract);
81
+
82
+ // Get Transfer event signature
83
+ const transferSig = getEventSignature("Transfer");
84
+
85
+ // Create a mock Transfer event
86
+ // PSP22 Transfer event structure: { from: Option<AccountId>, to: Option<AccountId>, value: u128 }
87
+ // For simplicity, we'll test with minimal data
88
+ const mockEventData = new Uint8Array([
89
+ 0, // from: None (Option discriminator)
90
+ 0, // to: None
91
+ 1,
92
+ 0,
93
+ 0,
94
+ 0,
95
+ 0,
96
+ 0,
97
+ 0,
98
+ 0,
99
+ 0,
100
+ 0,
101
+ 0,
102
+ 0,
103
+ 0,
104
+ 0,
105
+ 0,
106
+ 0, // value: 1 (u128 little-endian)
107
+ ]);
108
+
109
+ const mockChainEvent = {
110
+ event: {
111
+ type: "Contracts",
112
+ value: {
113
+ type: "ContractEmitted",
114
+ value: {
115
+ contract: contractBytes,
116
+ data: mockEventData,
117
+ },
118
+ },
119
+ },
120
+ topics: [transferSig],
121
+ blockNumber: 12345,
122
+ blockHash: "0x0000000000000000000000000000000000000000000000000000000000000000",
123
+ eventIndex: 0,
124
+ };
125
+
126
+ const parsed = parser.parseEvent(mockChainEvent);
127
+
128
+ // Event should be parsed successfully
129
+ expect(parsed).toBeDefined();
130
+ if (parsed) {
131
+ expect(parsed.label).toBe("Transfer");
132
+ expect(parsed.data).toBeDefined();
133
+ expect(parsed.raw.blockNumber).toBe(12345);
134
+ expect(parsed.raw.contractAddress).toBe(TEST_ADDRESSES.usdtContract);
135
+ }
136
+ });
137
+
138
+ it("should return null for non-Contracts events", () => {
139
+ const metadata = contracts.usdt.metadata!;
140
+ const parser = new ContractEventParser(metadata, TEST_ADDRESSES.usdtContract);
141
+
142
+ const mockEvent = {
143
+ event: {
144
+ type: "Balances", // Different pallet
145
+ value: {
146
+ type: "Transfer",
147
+ value: {},
148
+ },
149
+ },
150
+ topics: [],
151
+ blockNumber: 12345,
152
+ blockHash: "0x00",
153
+ eventIndex: 0,
154
+ };
155
+
156
+ const parsed = parser.parseEvent(mockEvent);
157
+ expect(parsed).toBeNull();
158
+ });
159
+
160
+ it("should return null for different contract address", () => {
161
+ const metadata = contracts.usdt.metadata!;
162
+ const parser = new ContractEventParser(metadata, TEST_ADDRESSES.usdtContract);
163
+
164
+ // Different contract address
165
+ const [differentContract] = ss58Decode(TEST_ADDRESSES.testUser);
166
+ const transferSig = getEventSignature("Transfer");
167
+
168
+ const mockEvent = {
169
+ event: {
170
+ type: "Contracts",
171
+ value: {
172
+ type: "ContractEmitted",
173
+ value: {
174
+ contract: differentContract, // Different contract
175
+ data: new Uint8Array([0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]),
176
+ },
177
+ },
178
+ },
179
+ topics: [transferSig],
180
+ blockNumber: 12345,
181
+ blockHash: "0x00",
182
+ eventIndex: 0,
183
+ };
184
+
185
+ const parsed = parser.parseEvent(mockEvent);
186
+ expect(parsed).toBeNull(); // Should filter out different contract
187
+ });
188
+
189
+ it("should filter events by label", () => {
190
+ const metadata = contracts.usdt.metadata!;
191
+ const parser = new ContractEventParser(metadata, TEST_ADDRESSES.usdtContract);
192
+
193
+ const [contractBytes] = ss58Decode(TEST_ADDRESSES.usdtContract);
194
+ const transferSig = getEventSignature("Transfer");
195
+
196
+ const mockTransferEvent1 = {
197
+ event: {
198
+ type: "Contracts",
199
+ value: {
200
+ type: "ContractEmitted",
201
+ value: {
202
+ contract: contractBytes,
203
+ data: new Uint8Array([0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]),
204
+ },
205
+ },
206
+ },
207
+ topics: [transferSig],
208
+ blockNumber: 100,
209
+ blockHash: "0x00",
210
+ eventIndex: 0,
211
+ };
212
+
213
+ const mockTransferEvent2 = {
214
+ event: {
215
+ type: "Contracts",
216
+ value: {
217
+ type: "ContractEmitted",
218
+ value: {
219
+ contract: contractBytes,
220
+ data: new Uint8Array([0, 0, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]),
221
+ },
222
+ },
223
+ },
224
+ topics: [transferSig],
225
+ blockNumber: 101,
226
+ blockHash: "0x00",
227
+ eventIndex: 1,
228
+ };
229
+
230
+ // Non-contract event should be filtered out
231
+ const mockBalancesEvent = {
232
+ event: {
233
+ type: "Balances",
234
+ value: {
235
+ type: "Transfer",
236
+ value: {},
237
+ },
238
+ },
239
+ topics: [],
240
+ blockNumber: 102,
241
+ blockHash: "0x00",
242
+ eventIndex: 2,
243
+ };
244
+
245
+ const filtered = parser.filterEvents(
246
+ [mockTransferEvent1, mockTransferEvent2, mockBalancesEvent],
247
+ {
248
+ eventLabels: ["Transfer"],
249
+ },
250
+ );
251
+
252
+ expect(filtered.length).toBe(2);
253
+ expect(filtered[0]?.label).toBe("Transfer");
254
+ expect(filtered[1]?.label).toBe("Transfer");
255
+ });
256
+ });
257
+
258
+ describe.skipIf(!process.env.TEST_CHAIN_INTEGRATION)("Event Subscription Integration", () => {
259
+ let client: ReturnType<typeof createClient>;
260
+ let inkSdk: ReturnType<typeof createD9InkSdk>;
261
+
262
+ beforeAll(() => {
263
+ const provider = getWsProvider(ENDPOINT, {
264
+ innerEnhancer: withLegacy(),
265
+ });
266
+
267
+ client = createClient(withPolkadotSdkCompat(provider), {
268
+ async getMetadata(codeHash) {
269
+ const metadata = await getMetadata(codeHash);
270
+ return metadata ?? null;
271
+ },
272
+ });
273
+
274
+ const api = client.getTypedApi(d9);
275
+ inkSdk = createD9InkSdk(client, { typedApi: api });
276
+ });
277
+
278
+ afterAll(() => {
279
+ client.destroy();
280
+ });
281
+
282
+ it(
283
+ "should have subscribeToEvents method on contract",
284
+ () => {
285
+ const usdtContract = inkSdk.getContract(contracts.usdt, TEST_ADDRESSES.usdtContract);
286
+
287
+ expect(usdtContract.subscribeToEvents).toBeDefined();
288
+ expect(typeof usdtContract.subscribeToEvents).toBe("function");
289
+ },
290
+ { timeout: 10000 },
291
+ );
292
+
293
+ it(
294
+ "should create event subscription observable",
295
+ () => {
296
+ const usdtContract = inkSdk.getContract(contracts.usdt, TEST_ADDRESSES.usdtContract);
297
+
298
+ const api = client.getTypedApi(d9);
299
+
300
+ const subscription = usdtContract.subscribeToEvents({
301
+ eventLabels: ["Transfer"],
302
+ getEvents: async (blockHash: string) => {
303
+ return await api.query.System.Events.getValue({ at: blockHash });
304
+ },
305
+ });
306
+
307
+ expect(subscription).toBeDefined();
308
+ expect(typeof subscription.subscribe).toBe("function");
309
+
310
+ // Subscribe and immediately unsubscribe
311
+ const sub = subscription.subscribe({
312
+ next: (event) => {
313
+ console.log("Received event:", event);
314
+ },
315
+ error: (err) => {
316
+ console.error("Subscription error:", err);
317
+ },
318
+ });
319
+
320
+ // Cleanup
321
+ sub.unsubscribe();
322
+ },
323
+ { timeout: 10000 },
324
+ );
325
+ });