@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/events.ts ADDED
@@ -0,0 +1,258 @@
1
+ /**
2
+ * Event parsing and filtering for ink! contracts
3
+ */
4
+ import type { InkMetadata } from "@polkadot-api/ink-contracts";
5
+ import type { SS58String } from "polkadot-api";
6
+ import { ss58Decode } from "@polkadot-labs/hdkd-helpers";
7
+ import {
8
+ buildAllEventDecoders,
9
+ buildEventDecoder,
10
+ getEventSignature,
11
+ } from "./codec-builder";
12
+ import type {
13
+ DecodedContractEvent,
14
+ EventFilterOptions,
15
+ RawContractEvent,
16
+ } from "./event-types";
17
+
18
+ /**
19
+ * Event parser for a specific contract
20
+ */
21
+ export class ContractEventParser {
22
+ private eventDecoders: Map<string, (data: Uint8Array) => unknown>;
23
+ private eventSignatures: Map<string, Uint8Array>;
24
+ private contractAddressBytes: Uint8Array;
25
+ private contractAddress: SS58String;
26
+ private metadata: InkMetadata;
27
+
28
+ constructor(metadata: InkMetadata, contractAddress: SS58String) {
29
+ this.metadata = metadata;
30
+ this.contractAddress = contractAddress;
31
+ this.eventDecoders = buildAllEventDecoders(metadata);
32
+ this.contractAddressBytes = ss58Decode(contractAddress)[0];
33
+
34
+ // Build signature map for topic filtering
35
+ this.eventSignatures = new Map();
36
+ const events = metadata.spec.events as Array<{ label: string }>;
37
+
38
+ for (const event of events) {
39
+ const sig = getEventSignature(event.label);
40
+ this.eventSignatures.set(event.label, sig);
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Parse a raw chain event into a contract event (if it matches)
46
+ * Returns null if the event is not from this contract or cannot be parsed
47
+ */
48
+ parseEvent(chainEvent: unknown): DecodedContractEvent | null {
49
+ // Extract Contracts.ContractEmitted event from chain event
50
+ const extracted = this.extractContractEmittedEvent(chainEvent);
51
+ if (!extracted) return null;
52
+
53
+ const { contract, data, topics, blockNumber, blockHash, eventIndex } =
54
+ extracted;
55
+
56
+ // Filter by contract address
57
+ if (!this.bytesEqual(contract, this.contractAddressBytes)) {
58
+ return null;
59
+ }
60
+
61
+ // Decode event based on topic[0] signature
62
+ if (topics.length === 0) return null;
63
+
64
+ const signature = topics[0];
65
+ let eventLabel: string | null = null;
66
+
67
+ for (const [label, sig] of this.eventSignatures) {
68
+ if (this.bytesEqual(signature!, sig)) {
69
+ eventLabel = label;
70
+ break;
71
+ }
72
+ }
73
+
74
+ if (!eventLabel) {
75
+ console.warn("Unknown event signature:", signature);
76
+ return null;
77
+ }
78
+
79
+ const decoder = this.eventDecoders.get(eventLabel);
80
+ if (!decoder) {
81
+ console.warn(`No decoder for event ${eventLabel}`);
82
+ return null;
83
+ }
84
+
85
+ try {
86
+ const decodedData = decoder(data);
87
+ return {
88
+ label: eventLabel,
89
+ data: decodedData,
90
+ raw: {
91
+ blockNumber,
92
+ blockHash,
93
+ eventIndex,
94
+ contractAddress: this.getContractAddress(),
95
+ data,
96
+ topics,
97
+ },
98
+ };
99
+ } catch (error) {
100
+ console.warn(`Failed to decode event ${eventLabel}:`, error);
101
+ return null;
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Filter a batch of events
107
+ */
108
+ filterEvents(
109
+ chainEvents: unknown[],
110
+ options?: EventFilterOptions,
111
+ ): DecodedContractEvent[] {
112
+ const results: DecodedContractEvent[] = [];
113
+
114
+ for (const chainEvent of chainEvents) {
115
+ const parsed = this.parseEvent(chainEvent);
116
+ if (!parsed) continue;
117
+
118
+ // Apply label filter
119
+ if (
120
+ options?.eventLabels &&
121
+ !options.eventLabels.includes(parsed.label)
122
+ ) {
123
+ continue;
124
+ }
125
+
126
+ // Apply block range filter
127
+ if (options?.fromBlock && parsed.raw.blockNumber < options.fromBlock) {
128
+ continue;
129
+ }
130
+ if (options?.toBlock && parsed.raw.blockNumber > options.toBlock) {
131
+ continue;
132
+ }
133
+
134
+ results.push(parsed);
135
+ }
136
+
137
+ return results;
138
+ }
139
+
140
+ /**
141
+ * Get the contract address as SS58 string
142
+ */
143
+ private getContractAddress(): SS58String {
144
+ return this.contractAddress;
145
+ }
146
+
147
+ /**
148
+ * Extract ContractEmitted event from chain event structure
149
+ * Based on polkadot-api event format
150
+ */
151
+ private extractContractEmittedEvent(chainEvent: unknown): {
152
+ contract: Uint8Array;
153
+ data: Uint8Array;
154
+ topics: Uint8Array[];
155
+ blockNumber: number;
156
+ blockHash: string;
157
+ eventIndex: number;
158
+ } | null {
159
+ // Type guard and extract event structure
160
+ if (!chainEvent || typeof chainEvent !== "object") {
161
+ return null;
162
+ }
163
+
164
+ const record = chainEvent as any;
165
+
166
+ // Extract event data
167
+ const event = record.event;
168
+ if (!event || typeof event !== "object") {
169
+ return null;
170
+ }
171
+
172
+ // Check if this is a Contracts pallet event
173
+ if (event.type !== "Contracts") {
174
+ return null;
175
+ }
176
+
177
+ // Check if this is a ContractEmitted event
178
+ const eventValue = event.value;
179
+ if (!eventValue || typeof eventValue !== "object") {
180
+ return null;
181
+ }
182
+
183
+ if (eventValue.type !== "ContractEmitted") {
184
+ return null;
185
+ }
186
+
187
+ // Extract contract address and data
188
+ const contractEmittedData = eventValue.value;
189
+ if (!contractEmittedData || typeof contractEmittedData !== "object") {
190
+ return null;
191
+ }
192
+
193
+ const contract = contractEmittedData.contract;
194
+ const data = contractEmittedData.data;
195
+
196
+ if (!(contract instanceof Uint8Array) || !(data instanceof Uint8Array)) {
197
+ return null;
198
+ }
199
+
200
+ // Extract topics from the event record
201
+ // Topics are typically stored in the event record, not in the event value
202
+ const topics: Uint8Array[] = [];
203
+
204
+ // Polkadot-API typically stores topics at the record level
205
+ if (record.topics && Array.isArray(record.topics)) {
206
+ for (const topic of record.topics) {
207
+ if (topic instanceof Uint8Array) {
208
+ topics.push(topic);
209
+ }
210
+ }
211
+ }
212
+
213
+ // Extract block metadata
214
+ const blockNumber =
215
+ typeof record.blockNumber === "number" ? record.blockNumber : 0;
216
+ const blockHash =
217
+ typeof record.blockHash === "string" ? record.blockHash : "";
218
+ const eventIndex =
219
+ typeof record.eventIndex === "number" ? record.eventIndex : 0;
220
+
221
+ return {
222
+ contract,
223
+ data,
224
+ topics,
225
+ blockNumber,
226
+ blockHash,
227
+ eventIndex,
228
+ };
229
+ }
230
+
231
+ /**
232
+ * Compare two Uint8Arrays for equality
233
+ */
234
+ private bytesEqual(a: Uint8Array, b: Uint8Array): boolean {
235
+ if (a.length !== b.length) return false;
236
+ for (let i = 0; i < a.length; i++) {
237
+ if (a[i] !== b[i]) return false;
238
+ }
239
+ return true;
240
+ }
241
+
242
+ /**
243
+ * Build event signature map from metadata
244
+ */
245
+ private static buildEventSignatureMap(
246
+ metadata: InkMetadata,
247
+ ): Map<string, Uint8Array> {
248
+ const signatures = new Map<string, Uint8Array>();
249
+ const events = metadata.spec.events as Array<{ label: string }>;
250
+
251
+ for (const event of events) {
252
+ const sig = getEventSignature(event.label);
253
+ signatures.set(event.label, sig);
254
+ }
255
+
256
+ return signatures;
257
+ }
258
+ }
package/src/index.ts ADDED
@@ -0,0 +1,94 @@
1
+ /**
2
+ * D9 Ink SDK
3
+ *
4
+ * A type-safe ink! smart contract SDK that uses state_call + ContractsApi_call
5
+ * instead of ReviveApi, for chains that don't support it.
6
+ */
7
+
8
+ // Main SDK entry point
9
+ export { createD9InkSdk, type CreateD9InkSdkOptions } from "./sdk";
10
+
11
+ // Contract creation
12
+ export { createD9InkContract } from "./contract";
13
+
14
+ // Types
15
+ export type {
16
+ D9InkSdk,
17
+ D9InkSdkOptions,
18
+ D9InkContract,
19
+ QueryOptions,
20
+ QueryResult,
21
+ QuerySuccessValue,
22
+ SendOptions,
23
+ SendableTransaction,
24
+ TxResult,
25
+ ContractStorage,
26
+ CreateContractOptions,
27
+ InferMessages,
28
+ ResponseDecoder,
29
+ D9InkContractFromDescriptor,
30
+ } from "./types";
31
+
32
+ // Errors
33
+ export {
34
+ ContractError,
35
+ MetadataError,
36
+ EncodeError,
37
+ DecodeError,
38
+ NetworkError,
39
+ ContractExecutionError,
40
+ LangError,
41
+ TimeoutError,
42
+ AbortedError,
43
+ SignerError,
44
+ TransactionError,
45
+ isContractError,
46
+ isErrorType,
47
+ type ContractErrorType,
48
+ } from "./errors";
49
+
50
+ // Encoding utilities
51
+ export {
52
+ encodeCall,
53
+ encodeContractCall,
54
+ encodeContractCallWithLimits,
55
+ } from "./encode";
56
+
57
+ // Decoding utilities
58
+ export {
59
+ decodeResult,
60
+ decodeContractCallResult,
61
+ unwrapInkResult,
62
+ isLangError,
63
+ decodeInkValue,
64
+ InkCodecs,
65
+ type GasInfo,
66
+ type StorageDepositInfo,
67
+ type ContractCallResult,
68
+ } from "./decode";
69
+
70
+ // Codec builder
71
+ export {
72
+ buildMessageDecoder,
73
+ buildAllMessageDecoders,
74
+ createCodecRegistry,
75
+ buildEventDecoder,
76
+ buildAllEventDecoders,
77
+ getEventSignature,
78
+ } from "./codec-builder";
79
+
80
+ // Event handling
81
+ export { ContractEventParser } from "./events";
82
+ export {
83
+ createContractEventStream,
84
+ createPSP22TransferStream,
85
+ createNativeTransferStream,
86
+ } from "./subscriptions";
87
+
88
+ // Event types
89
+ export type {
90
+ RawContractEvent,
91
+ DecodedContractEvent,
92
+ EventFilterOptions,
93
+ EventSubscriptionOptions,
94
+ } from "./event-types";
package/src/sdk.ts ADDED
@@ -0,0 +1,116 @@
1
+ /**
2
+ * D9 Ink SDK entry point
3
+ *
4
+ * Creates an ink SDK that uses state_call + ContractsApi_call
5
+ * instead of ReviveApi for chains that don't support it.
6
+ */
7
+
8
+ import type { PolkadotClient, SS58String } from "polkadot-api";
9
+ import {
10
+ type InkDescriptors,
11
+ type InkCallableDescriptor,
12
+ type InkStorageDescriptor,
13
+ type Event,
14
+ } from "@polkadot-api/ink-contracts";
15
+
16
+ import type {
17
+ D9InkSdk,
18
+ D9InkSdkOptions,
19
+ D9InkContract,
20
+ QueryOptions,
21
+ SendOptions,
22
+ } from "./types";
23
+ import { createD9InkContract } from "./contract";
24
+
25
+ /**
26
+ * Options for creating D9 Ink SDK
27
+ */
28
+ export interface CreateD9InkSdkOptions extends D9InkSdkOptions {
29
+ /**
30
+ * Typed API for transaction submission.
31
+ * Required for submitting real transactions.
32
+ */
33
+ typedApi?: unknown;
34
+ }
35
+
36
+ /**
37
+ * Create a D9 Ink SDK instance.
38
+ *
39
+ * This SDK provides a similar API to the official @polkadot-api/sdk-ink,
40
+ * but uses state_call + ContractsApi_call instead of ReviveApi.
41
+ *
42
+ * @example
43
+ * ```ts
44
+ * import { createD9InkSdk } from "@d9-network/ink";
45
+ * import { contracts } from "@polkadot-api/descriptors";
46
+ *
47
+ * const sdk = createD9InkSdk(client);
48
+ * const usdtContract = sdk.getContract(
49
+ * contracts.d9_usdt,
50
+ * "uLj9DRUujbpCyK7USZY5ebGbxdtKoWvdRvGyyUsoLWDsNng"
51
+ * );
52
+ *
53
+ * // Query balance
54
+ * const result = await usdtContract.query("PSP22::balance_of", {
55
+ * origin: aliceAddress,
56
+ * args: { owner: aliceAddress }
57
+ * });
58
+ *
59
+ * if (result.success) {
60
+ * console.log("Balance:", result.value.response);
61
+ *
62
+ * // Send transaction from the query result
63
+ * const txResult = await result.value.send().signAndSubmit(aliceSigner);
64
+ * }
65
+ *
66
+ * // Or send directly
67
+ * const txResult = await usdtContract
68
+ * .send("PSP22::transfer", {
69
+ * origin: aliceAddress,
70
+ * args: { to: bobAddress, value: 1000n, data: [] }
71
+ * })
72
+ * .signAndSubmit(aliceSigner);
73
+ * ```
74
+ *
75
+ * @param client - The PolkadotClient instance
76
+ * @param options - Optional SDK configuration
77
+ * @returns D9 Ink SDK instance
78
+ */
79
+ export function createD9InkSdk(
80
+ client: PolkadotClient,
81
+ options: CreateD9InkSdkOptions = {},
82
+ ): D9InkSdk {
83
+ const { typedApi, defaultQueryOptions, defaultSendOptions } = options;
84
+
85
+ return {
86
+ getContract<
87
+ S extends InkStorageDescriptor,
88
+ M extends InkCallableDescriptor,
89
+ C extends InkCallableDescriptor,
90
+ E extends Event,
91
+ >(
92
+ descriptor: InkDescriptors<S, M, C, E>,
93
+ address: SS58String,
94
+ ): D9InkContract<M> {
95
+ return createD9InkContract(descriptor, address, {
96
+ client,
97
+ typedApi,
98
+ defaultQueryOptions,
99
+ defaultSendOptions,
100
+ });
101
+ },
102
+ };
103
+ }
104
+
105
+ // Re-export types for convenience
106
+ export type {
107
+ D9InkSdk,
108
+ D9InkSdkOptions,
109
+ D9InkContract,
110
+ QueryOptions,
111
+ SendOptions,
112
+ QueryResult,
113
+ SendableTransaction,
114
+ TxResult,
115
+ ContractStorage,
116
+ } from "./types";
@@ -0,0 +1,207 @@
1
+ /**
2
+ * Event subscriptions using RxJS
3
+ */
4
+ import { Observable, filter, map, mergeMap, share, from, of, catchError } from "rxjs";
5
+ import type { PolkadotClient } from "polkadot-api";
6
+ import type { InkMetadata } from "@polkadot-api/ink-contracts";
7
+ import type { SS58String } from "polkadot-api";
8
+ import { ContractEventParser } from "./events";
9
+ import type { DecodedContractEvent, EventSubscriptionOptions } from "./event-types";
10
+
11
+ /**
12
+ * Create an observable stream of contract events
13
+ *
14
+ * @param client - Polkadot API client
15
+ * @param metadata - Contract metadata
16
+ * @param options - Subscription options
17
+ * @returns Observable stream of decoded contract events
18
+ */
19
+ export function createContractEventStream(
20
+ client: PolkadotClient,
21
+ metadata: InkMetadata,
22
+ options: EventSubscriptionOptions,
23
+ ): Observable<DecodedContractEvent> {
24
+ const parser = new ContractEventParser(metadata, options.contractAddress);
25
+
26
+ // Subscribe to finalized blocks
27
+ return client.finalizedBlock$.pipe(
28
+ // For each finalized block, fetch its events
29
+ mergeMap(async (block) => {
30
+ try {
31
+ // Fetch System.Events at this block using the provided callback
32
+ const events = await options.getEvents(block.hash);
33
+ return { block, events };
34
+ } catch (error: unknown) {
35
+ console.error(
36
+ "Error fetching events at block",
37
+ block.number,
38
+ ":",
39
+ error instanceof Error ? error.message : String(error),
40
+ );
41
+ // Return empty events on error to keep stream alive
42
+ return { block, events: [] };
43
+ }
44
+ }),
45
+
46
+ // Parse and filter events for this contract
47
+ map(({ block, events }) => {
48
+ const parsedEvents = events
49
+ .map((event, index) => {
50
+ // Attach block metadata to each event before parsing
51
+ const eventWithMeta = {
52
+ ...(event as object),
53
+ blockNumber: block.number,
54
+ blockHash: block.hash,
55
+ eventIndex: index,
56
+ };
57
+ return parser.parseEvent(eventWithMeta);
58
+ })
59
+ .filter((e: DecodedContractEvent | null): e is DecodedContractEvent => e !== null);
60
+
61
+ return parsedEvents;
62
+ }),
63
+
64
+ // Flatten array of events to individual emissions
65
+ mergeMap((events: DecodedContractEvent[]) => from(events)),
66
+
67
+ // Filter by event labels if specified
68
+ filter((event: DecodedContractEvent) => {
69
+ if (!options.eventLabels) return true;
70
+ return options.eventLabels.includes(event.label);
71
+ }),
72
+
73
+ // Handle errors gracefully
74
+ catchError((error: unknown) => {
75
+ console.error("Error in contract event stream:", error);
76
+ return of(); // Return empty observable on error
77
+ }),
78
+
79
+ // Share subscription among multiple subscribers
80
+ share(),
81
+ );
82
+ }
83
+
84
+ /**
85
+ * Convenience helper to create a Transfer event stream for PSP22 tokens
86
+ *
87
+ * @param client - Polkadot API client
88
+ * @param metadata - PSP22 contract metadata
89
+ * @param contractAddress - PSP22 contract address
90
+ * @param getEvents - Function to fetch System.Events at a block hash
91
+ * @param watchAddress - Optional address to filter transfers (only events involving this address)
92
+ * @returns Observable stream of Transfer events
93
+ */
94
+ export function createPSP22TransferStream(
95
+ client: PolkadotClient,
96
+ metadata: InkMetadata,
97
+ contractAddress: SS58String,
98
+ getEvents: (blockHash: string) => Promise<unknown[]>,
99
+ watchAddress?: SS58String,
100
+ ): Observable<
101
+ DecodedContractEvent<{
102
+ from?: SS58String | null;
103
+ to?: SS58String | null;
104
+ value: bigint;
105
+ }>
106
+ > {
107
+ return createContractEventStream(client, metadata, {
108
+ contractAddress,
109
+ eventLabels: ["Transfer"],
110
+ getEvents,
111
+ }).pipe(
112
+ filter((event: DecodedContractEvent) => {
113
+ if (!watchAddress) return true;
114
+
115
+ // Filter transfers where from=watchAddress or to=watchAddress
116
+ const data = event.data as {
117
+ from?: SS58String | null;
118
+ to?: SS58String | null;
119
+ value: bigint;
120
+ };
121
+
122
+ return data.from === watchAddress || data.to === watchAddress;
123
+ }),
124
+ ) as Observable<
125
+ DecodedContractEvent<{
126
+ from?: SS58String | null;
127
+ to?: SS58String | null;
128
+ value: bigint;
129
+ }>
130
+ >;
131
+ }
132
+
133
+ /**
134
+ * Create a native token (D9) transfer event stream
135
+ *
136
+ * This monitors System.Transfer events instead of contract events
137
+ *
138
+ * @param client - Polkadot API client
139
+ * @param getEvents - Function to fetch System.Events at a block hash
140
+ * @param watchAddress - Address to monitor for transfers
141
+ * @returns Observable stream of native transfer events
142
+ */
143
+ export function createNativeTransferStream(
144
+ client: PolkadotClient,
145
+ getEvents: (blockHash: string) => Promise<unknown[]>,
146
+ watchAddress: SS58String,
147
+ ): Observable<{
148
+ from: SS58String;
149
+ to: SS58String;
150
+ amount: bigint;
151
+ blockNumber: number;
152
+ blockHash: string;
153
+ }> {
154
+ return client.finalizedBlock$.pipe(
155
+ // For each block, query system events
156
+ mergeMap(async (block: any) => {
157
+ try {
158
+ const events = await getEvents(block.hash);
159
+ return { block, events };
160
+ } catch (error: unknown) {
161
+ console.error(
162
+ "Error fetching events for native transfers:",
163
+ error instanceof Error ? error.message : String(error),
164
+ );
165
+ return { block, events: [] };
166
+ }
167
+ }),
168
+
169
+ // Filter for Balances.Transfer events
170
+ map(({ block, events }) => {
171
+ const transfers = events
172
+ .map((record: any) => {
173
+ // Check if this is a Balances.Transfer event
174
+ if (record.event?.type !== "Balances") return null;
175
+ if (record.event?.value?.type !== "Transfer") return null;
176
+
177
+ const { from, to, amount } = record.event.value.value;
178
+
179
+ // Filter by watchAddress
180
+ if (from !== watchAddress && to !== watchAddress) return null;
181
+
182
+ return {
183
+ from: from as SS58String,
184
+ to: to as SS58String,
185
+ amount: amount as bigint,
186
+ blockNumber: block.number,
187
+ blockHash: block.hash,
188
+ };
189
+ })
190
+ .filter((t: any): t is NonNullable<typeof t> => t !== null);
191
+
192
+ return transfers;
193
+ }),
194
+
195
+ // Flatten array to individual emissions
196
+ mergeMap((transfers: any[]) => from(transfers)),
197
+
198
+ // Handle errors
199
+ catchError((error: unknown) => {
200
+ console.error("Error in native transfer stream:", error);
201
+ return of();
202
+ }),
203
+
204
+ // Share subscription
205
+ share(),
206
+ );
207
+ }