@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/dist/index.cjs +10131 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +750 -0
- package/dist/index.d.mts +750 -0
- package/dist/index.mjs +10098 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +46 -0
- package/src/codec-builder.ts +573 -0
- package/src/contract.ts +571 -0
- package/src/decode.ts +176 -0
- package/src/encode.ts +128 -0
- package/src/errors.ts +220 -0
- package/src/event-types.ts +75 -0
- package/src/events.ts +258 -0
- package/src/index.ts +94 -0
- package/src/sdk.ts +116 -0
- package/src/subscriptions.ts +207 -0
- package/src/types.ts +274 -0
- package/test/events.test.ts +325 -0
- package/test/ink.test.ts +404 -0
- package/tsconfig.json +12 -0
- package/tsdown.config.ts +13 -0
package/src/contract.ts
ADDED
|
@@ -0,0 +1,571 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* D9 Ink Contract implementation
|
|
3
|
+
*
|
|
4
|
+
* Provides a type-safe interface for interacting with ink! smart contracts
|
|
5
|
+
* using state_call + ContractsApi_call instead of ReviveApi.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { PolkadotSigner, SS58String } from "polkadot-api";
|
|
9
|
+
import { Binary } from "polkadot-api";
|
|
10
|
+
import { fromHex } from "polkadot-api/utils";
|
|
11
|
+
import {
|
|
12
|
+
getInkLookup,
|
|
13
|
+
getInkDynamicBuilder,
|
|
14
|
+
type InkDescriptors,
|
|
15
|
+
type InkCallableDescriptor,
|
|
16
|
+
type InkStorageDescriptor,
|
|
17
|
+
type InkMetadata,
|
|
18
|
+
type Event,
|
|
19
|
+
} from "@polkadot-api/ink-contracts";
|
|
20
|
+
import { ss58Decode } from "@polkadot-labs/hdkd-helpers";
|
|
21
|
+
|
|
22
|
+
import type {
|
|
23
|
+
D9InkContract,
|
|
24
|
+
QueryOptions,
|
|
25
|
+
QueryResult,
|
|
26
|
+
SendOptions,
|
|
27
|
+
SendableTransaction,
|
|
28
|
+
TxResult,
|
|
29
|
+
ContractStorage,
|
|
30
|
+
CreateContractOptions,
|
|
31
|
+
ResponseDecoder,
|
|
32
|
+
} from "./types";
|
|
33
|
+
import { encodeContractCall } from "./encode";
|
|
34
|
+
import {
|
|
35
|
+
decodeContractCallResult,
|
|
36
|
+
unwrapInkResult,
|
|
37
|
+
isLangError,
|
|
38
|
+
} from "./decode";
|
|
39
|
+
import { createCodecRegistry } from "./codec-builder";
|
|
40
|
+
import { ContractEventParser } from "./events";
|
|
41
|
+
import { createContractEventStream } from "./subscriptions";
|
|
42
|
+
import type {
|
|
43
|
+
DecodedContractEvent,
|
|
44
|
+
EventSubscriptionOptions,
|
|
45
|
+
} from "./event-types";
|
|
46
|
+
import {
|
|
47
|
+
ContractError,
|
|
48
|
+
MetadataError,
|
|
49
|
+
DecodeError,
|
|
50
|
+
LangError,
|
|
51
|
+
TimeoutError,
|
|
52
|
+
AbortedError,
|
|
53
|
+
TransactionError,
|
|
54
|
+
} from "./errors";
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Patch LangError type in ink metadata to fix the missing index 0 variant issue.
|
|
58
|
+
* Uses structuredClone for efficient deep cloning.
|
|
59
|
+
*/
|
|
60
|
+
function patchLangErrorInMetadata(metadata: InkMetadata): InkMetadata {
|
|
61
|
+
const patched = structuredClone(metadata);
|
|
62
|
+
|
|
63
|
+
for (const typeEntry of patched.types) {
|
|
64
|
+
const path = typeEntry.type?.path;
|
|
65
|
+
const def = typeEntry.type?.def as {
|
|
66
|
+
variant?: {
|
|
67
|
+
variants: Array<{
|
|
68
|
+
index: number;
|
|
69
|
+
name: string;
|
|
70
|
+
fields: unknown[];
|
|
71
|
+
docs: string[];
|
|
72
|
+
}>;
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
if (
|
|
76
|
+
path &&
|
|
77
|
+
Array.isArray(path) &&
|
|
78
|
+
path.includes("LangError") &&
|
|
79
|
+
def?.variant
|
|
80
|
+
) {
|
|
81
|
+
const variants = def.variant.variants;
|
|
82
|
+
if (Array.isArray(variants)) {
|
|
83
|
+
const hasIndex0 = variants.some((v) => v.index === 0);
|
|
84
|
+
if (!hasIndex0) {
|
|
85
|
+
variants.unshift({
|
|
86
|
+
index: 0,
|
|
87
|
+
name: "_Placeholder",
|
|
88
|
+
fields: [],
|
|
89
|
+
docs: [],
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return patched;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Convert SS58 address to bytes
|
|
101
|
+
*/
|
|
102
|
+
function ss58ToBytes(address: SS58String): Uint8Array {
|
|
103
|
+
const [publicKey] = ss58Decode(address);
|
|
104
|
+
return publicKey;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Create a promise that rejects after a timeout
|
|
109
|
+
*/
|
|
110
|
+
function createTimeout<T>(
|
|
111
|
+
ms: number,
|
|
112
|
+
label: string,
|
|
113
|
+
): { promise: Promise<T>; clear: () => void } {
|
|
114
|
+
let timeoutId: ReturnType<typeof setTimeout>;
|
|
115
|
+
const promise = new Promise<T>((_, reject) => {
|
|
116
|
+
timeoutId = setTimeout(() => {
|
|
117
|
+
reject(new TimeoutError(label, ms));
|
|
118
|
+
}, ms);
|
|
119
|
+
});
|
|
120
|
+
return {
|
|
121
|
+
promise,
|
|
122
|
+
clear: () => clearTimeout(timeoutId),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Check if AbortSignal is aborted and throw if so
|
|
128
|
+
*/
|
|
129
|
+
function checkAborted(signal: AbortSignal | undefined, label: string): void {
|
|
130
|
+
if (signal?.aborted) {
|
|
131
|
+
throw new AbortedError(label, signal.reason);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Create a D9 Ink Contract instance
|
|
137
|
+
*/
|
|
138
|
+
export function createD9InkContract<
|
|
139
|
+
S extends InkStorageDescriptor,
|
|
140
|
+
M extends InkCallableDescriptor,
|
|
141
|
+
C extends InkCallableDescriptor,
|
|
142
|
+
E extends Event,
|
|
143
|
+
>(
|
|
144
|
+
descriptor: InkDescriptors<S, M, C, E>,
|
|
145
|
+
address: SS58String,
|
|
146
|
+
options: CreateContractOptions,
|
|
147
|
+
): D9InkContract<M> {
|
|
148
|
+
const { client, typedApi, defaultQueryOptions = {}, defaultSendOptions = {} } = options;
|
|
149
|
+
|
|
150
|
+
if (!descriptor.metadata) {
|
|
151
|
+
throw new MetadataError("Contract descriptor must include metadata");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Patch and prepare metadata
|
|
155
|
+
const patchedMetadata = patchLangErrorInMetadata(descriptor.metadata);
|
|
156
|
+
const lookup = getInkLookup(patchedMetadata);
|
|
157
|
+
const builder = getInkDynamicBuilder(lookup);
|
|
158
|
+
|
|
159
|
+
// Build auto-generated codecs
|
|
160
|
+
let codecRegistry: Map<string, ResponseDecoder>;
|
|
161
|
+
try {
|
|
162
|
+
codecRegistry = createCodecRegistry(patchedMetadata);
|
|
163
|
+
} catch (error) {
|
|
164
|
+
console.warn("Failed to auto-generate codecs from metadata:", error);
|
|
165
|
+
codecRegistry = new Map();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Convert address to bytes
|
|
169
|
+
const addressBytes = ss58ToBytes(address);
|
|
170
|
+
|
|
171
|
+
// Cache for message codecs
|
|
172
|
+
type MessageCodec = ReturnType<typeof builder.buildMessage>;
|
|
173
|
+
const messageCodecCache = new Map<string, MessageCodec>();
|
|
174
|
+
|
|
175
|
+
function getMessageCodec(label: string): MessageCodec {
|
|
176
|
+
const cached = messageCodecCache.get(label);
|
|
177
|
+
if (cached) {
|
|
178
|
+
return cached;
|
|
179
|
+
}
|
|
180
|
+
const codec = builder.buildMessage(label);
|
|
181
|
+
messageCodecCache.set(label, codec);
|
|
182
|
+
return codec;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Get the decoder for a message
|
|
187
|
+
*/
|
|
188
|
+
function getDecoder(label: string): ResponseDecoder | null {
|
|
189
|
+
return codecRegistry.get(label) ?? null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Execute a query (dry-run)
|
|
194
|
+
*/
|
|
195
|
+
async function executeQuery<K extends keyof M & string>(
|
|
196
|
+
method: K,
|
|
197
|
+
queryOptions: QueryOptions<M[K]["message"]>,
|
|
198
|
+
): Promise<QueryResult<M[K]["response"]>> {
|
|
199
|
+
const opts = { ...defaultQueryOptions, ...queryOptions };
|
|
200
|
+
const { origin, args, value = 0n, signal, timeout, at } = opts;
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
checkAborted(signal, method);
|
|
204
|
+
|
|
205
|
+
const originBytes = ss58ToBytes(origin);
|
|
206
|
+
const codec = getMessageCodec(method);
|
|
207
|
+
|
|
208
|
+
// Encode the call
|
|
209
|
+
const callData = codec.call.enc(
|
|
210
|
+
(args ?? {}) as Parameters<typeof codec.call.enc>[0],
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
// Create the state_call message
|
|
214
|
+
const message = encodeContractCall(
|
|
215
|
+
originBytes,
|
|
216
|
+
addressBytes,
|
|
217
|
+
callData,
|
|
218
|
+
value,
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
// Get block hash
|
|
222
|
+
const blockHash = at ?? (await client.getFinalizedBlock()).hash;
|
|
223
|
+
|
|
224
|
+
checkAborted(signal, method);
|
|
225
|
+
|
|
226
|
+
// Execute state_call with optional timeout
|
|
227
|
+
const executeCall = async () => {
|
|
228
|
+
const response = (await client._request("state_call", [
|
|
229
|
+
"ContractsApi_call",
|
|
230
|
+
message,
|
|
231
|
+
blockHash,
|
|
232
|
+
])) as `0x${string}`;
|
|
233
|
+
|
|
234
|
+
return fromHex(response);
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
let rawResponse: Uint8Array;
|
|
238
|
+
if (timeout) {
|
|
239
|
+
const { promise: timeoutPromise, clear } = createTimeout<Uint8Array>(
|
|
240
|
+
timeout,
|
|
241
|
+
method,
|
|
242
|
+
);
|
|
243
|
+
try {
|
|
244
|
+
rawResponse = await Promise.race([executeCall(), timeoutPromise]);
|
|
245
|
+
} finally {
|
|
246
|
+
clear();
|
|
247
|
+
}
|
|
248
|
+
} else {
|
|
249
|
+
rawResponse = await executeCall();
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
checkAborted(signal, method);
|
|
253
|
+
|
|
254
|
+
// Decode the ContractsApi_call response
|
|
255
|
+
const callResult = decodeContractCallResult(rawResponse);
|
|
256
|
+
|
|
257
|
+
// Check for execution error
|
|
258
|
+
if (!callResult.success) {
|
|
259
|
+
return {
|
|
260
|
+
success: false,
|
|
261
|
+
error: new ContractError(
|
|
262
|
+
`Contract execution failed: ${callResult.debugMessage}`,
|
|
263
|
+
"CONTRACT_ERROR",
|
|
264
|
+
method,
|
|
265
|
+
),
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Check for LangError
|
|
270
|
+
if (isLangError(callResult.data)) {
|
|
271
|
+
return {
|
|
272
|
+
success: false,
|
|
273
|
+
error: new LangError(method, callResult.data[1] ?? 1),
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Unwrap the Result<T, LangError>
|
|
278
|
+
const innerData = unwrapInkResult(callResult.data);
|
|
279
|
+
|
|
280
|
+
// Decode the response
|
|
281
|
+
let decodedResponse: M[K]["response"];
|
|
282
|
+
const decoder = getDecoder(method);
|
|
283
|
+
|
|
284
|
+
if (decoder) {
|
|
285
|
+
try {
|
|
286
|
+
decodedResponse = decoder.dec(innerData) as M[K]["response"];
|
|
287
|
+
} catch (decodeError) {
|
|
288
|
+
console.warn("D9InkContract: Failed to decode response:", decodeError);
|
|
289
|
+
// Fall back to papi's value codec
|
|
290
|
+
const fullResult = new Uint8Array(1 + innerData.length);
|
|
291
|
+
fullResult[0] = 0; // Ok variant
|
|
292
|
+
fullResult.set(innerData, 1);
|
|
293
|
+
|
|
294
|
+
try {
|
|
295
|
+
const papiResult = codec.value.dec(fullResult);
|
|
296
|
+
if (
|
|
297
|
+
papiResult !== null &&
|
|
298
|
+
typeof papiResult === "object" &&
|
|
299
|
+
"success" in papiResult &&
|
|
300
|
+
"value" in papiResult
|
|
301
|
+
) {
|
|
302
|
+
if (papiResult.success) {
|
|
303
|
+
decodedResponse = papiResult.value as M[K]["response"];
|
|
304
|
+
} else {
|
|
305
|
+
return {
|
|
306
|
+
success: false,
|
|
307
|
+
error: new DecodeError(
|
|
308
|
+
method,
|
|
309
|
+
`Contract returned error: ${JSON.stringify(papiResult.value)}`,
|
|
310
|
+
papiResult.value,
|
|
311
|
+
),
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
} else {
|
|
315
|
+
decodedResponse = papiResult as M[K]["response"];
|
|
316
|
+
}
|
|
317
|
+
} catch (error) {
|
|
318
|
+
return {
|
|
319
|
+
success: false,
|
|
320
|
+
error: new DecodeError(
|
|
321
|
+
method,
|
|
322
|
+
`Failed to decode response: ${error instanceof Error ? error.message : String(error)}`,
|
|
323
|
+
{ error },
|
|
324
|
+
),
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
} else {
|
|
329
|
+
// No custom decoder, use papi's codec
|
|
330
|
+
const fullResult = new Uint8Array(1 + innerData.length);
|
|
331
|
+
fullResult[0] = 0; // Ok variant
|
|
332
|
+
fullResult.set(innerData, 1);
|
|
333
|
+
|
|
334
|
+
const papiResult = codec.value.dec(fullResult);
|
|
335
|
+
if (
|
|
336
|
+
papiResult !== null &&
|
|
337
|
+
typeof papiResult === "object" &&
|
|
338
|
+
"success" in papiResult &&
|
|
339
|
+
"value" in papiResult
|
|
340
|
+
) {
|
|
341
|
+
if (papiResult.success) {
|
|
342
|
+
decodedResponse = papiResult.value as M[K]["response"];
|
|
343
|
+
} else {
|
|
344
|
+
return {
|
|
345
|
+
success: false,
|
|
346
|
+
error: new DecodeError(
|
|
347
|
+
method,
|
|
348
|
+
`Contract returned error: ${JSON.stringify(papiResult.value)}`,
|
|
349
|
+
papiResult.value,
|
|
350
|
+
),
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
} else {
|
|
354
|
+
decodedResponse = papiResult as M[K]["response"];
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Return success with QuerySuccessValue fields spread to same level
|
|
359
|
+
return {
|
|
360
|
+
success: true,
|
|
361
|
+
value: decodedResponse,
|
|
362
|
+
events: [],
|
|
363
|
+
gasConsumed: callResult.gas.gasConsumed,
|
|
364
|
+
gasRequired: callResult.gas.gasRequired,
|
|
365
|
+
storageDeposit: callResult.storageDeposit.amount,
|
|
366
|
+
send: () => createSendableTransaction(method, {
|
|
367
|
+
origin,
|
|
368
|
+
args,
|
|
369
|
+
value,
|
|
370
|
+
gasLimit: callResult.gas.gasRequired,
|
|
371
|
+
}),
|
|
372
|
+
};
|
|
373
|
+
} catch (error) {
|
|
374
|
+
if (error instanceof ContractError) {
|
|
375
|
+
return { success: false, error };
|
|
376
|
+
}
|
|
377
|
+
return {
|
|
378
|
+
success: false,
|
|
379
|
+
error: new ContractError(
|
|
380
|
+
error instanceof Error ? error.message : String(error),
|
|
381
|
+
"NETWORK_ERROR",
|
|
382
|
+
method,
|
|
383
|
+
),
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Create a sendable transaction
|
|
390
|
+
*/
|
|
391
|
+
function createSendableTransaction<K extends keyof M & string>(
|
|
392
|
+
method: K,
|
|
393
|
+
sendOptions: SendOptions<M[K]["message"]>,
|
|
394
|
+
): SendableTransaction<M[K]["response"]> {
|
|
395
|
+
const opts = { ...defaultSendOptions, ...sendOptions };
|
|
396
|
+
const { origin, args, value = 0n, gasLimit, storageDepositLimit } = opts;
|
|
397
|
+
|
|
398
|
+
const originBytes = ss58ToBytes(origin);
|
|
399
|
+
const codec = getMessageCodec(method);
|
|
400
|
+
|
|
401
|
+
// Encode the call
|
|
402
|
+
const callData = codec.call.enc(
|
|
403
|
+
(args ?? {}) as Parameters<typeof codec.call.enc>[0],
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
return {
|
|
407
|
+
getEncodedData: () => callData,
|
|
408
|
+
|
|
409
|
+
async signAndSubmit(signer: PolkadotSigner): Promise<TxResult<M[K]["response"]>> {
|
|
410
|
+
if (!typedApi) {
|
|
411
|
+
throw new TransactionError(
|
|
412
|
+
method,
|
|
413
|
+
"typedApi is required for transaction submission. Pass typedApi in SDK options.",
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
try {
|
|
418
|
+
// First do a dry-run to get gas estimate if not provided
|
|
419
|
+
let gas = gasLimit;
|
|
420
|
+
if (!gas) {
|
|
421
|
+
const message = encodeContractCall(
|
|
422
|
+
originBytes,
|
|
423
|
+
addressBytes,
|
|
424
|
+
callData,
|
|
425
|
+
value,
|
|
426
|
+
);
|
|
427
|
+
const blockHash = (await client.getFinalizedBlock()).hash;
|
|
428
|
+
const response = (await client._request("state_call", [
|
|
429
|
+
"ContractsApi_call",
|
|
430
|
+
message,
|
|
431
|
+
blockHash,
|
|
432
|
+
])) as `0x${string}`;
|
|
433
|
+
const callResult = decodeContractCallResult(fromHex(response));
|
|
434
|
+
gas = callResult.gas.gasRequired;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Build the transaction using typedApi
|
|
438
|
+
const api = typedApi as {
|
|
439
|
+
tx: {
|
|
440
|
+
Contracts: {
|
|
441
|
+
call: (params: {
|
|
442
|
+
dest: { type: "Id"; value: SS58String };
|
|
443
|
+
value: bigint;
|
|
444
|
+
gas_limit: { ref_time: bigint; proof_size: bigint };
|
|
445
|
+
storage_deposit_limit: bigint | undefined;
|
|
446
|
+
data: Binary;
|
|
447
|
+
}) => {
|
|
448
|
+
signAndSubmit: (
|
|
449
|
+
signer: PolkadotSigner,
|
|
450
|
+
options?: { at?: "best" | "finalized" }
|
|
451
|
+
) => Promise<{
|
|
452
|
+
txHash: string;
|
|
453
|
+
block: { hash: string; number: number };
|
|
454
|
+
events: unknown[];
|
|
455
|
+
}>;
|
|
456
|
+
};
|
|
457
|
+
};
|
|
458
|
+
};
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
const tx = api.tx.Contracts.call({
|
|
462
|
+
dest: { type: "Id", value: address },
|
|
463
|
+
value,
|
|
464
|
+
gas_limit: {
|
|
465
|
+
ref_time: gas.refTime,
|
|
466
|
+
proof_size: gas.proofSize,
|
|
467
|
+
},
|
|
468
|
+
storage_deposit_limit: storageDepositLimit,
|
|
469
|
+
data: Binary.fromBytes(callData),
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
const txResult = await tx.signAndSubmit(signer, { at: "finalized" });
|
|
473
|
+
|
|
474
|
+
return {
|
|
475
|
+
ok: true,
|
|
476
|
+
txHash: txResult.txHash,
|
|
477
|
+
block: txResult.block,
|
|
478
|
+
events: txResult.events ?? [],
|
|
479
|
+
};
|
|
480
|
+
} catch (error) {
|
|
481
|
+
return {
|
|
482
|
+
ok: false,
|
|
483
|
+
txHash: "",
|
|
484
|
+
block: { hash: "", number: 0 },
|
|
485
|
+
events: [],
|
|
486
|
+
dispatchError: error,
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
},
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Create send method that returns a sendable transaction
|
|
495
|
+
*/
|
|
496
|
+
function send<K extends keyof M & string>(
|
|
497
|
+
method: K,
|
|
498
|
+
sendOptions: SendOptions<M[K]["message"]>,
|
|
499
|
+
): SendableTransaction<M[K]["response"]> {
|
|
500
|
+
return createSendableTransaction(method, sendOptions);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Create storage query interface
|
|
505
|
+
*/
|
|
506
|
+
function getStorage(): ContractStorage {
|
|
507
|
+
return {
|
|
508
|
+
async getRoot() {
|
|
509
|
+
// TODO: Implement storage root query
|
|
510
|
+
console.warn("D9InkContract: getRoot not implemented");
|
|
511
|
+
return {
|
|
512
|
+
success: false,
|
|
513
|
+
value: new ContractError(
|
|
514
|
+
"Storage queries not yet implemented",
|
|
515
|
+
"METADATA_ERROR",
|
|
516
|
+
),
|
|
517
|
+
};
|
|
518
|
+
},
|
|
519
|
+
|
|
520
|
+
async getNested(path: string, ..._keys: unknown[]) {
|
|
521
|
+
// TODO: Implement nested storage query
|
|
522
|
+
console.warn("D9InkContract: getNested not implemented");
|
|
523
|
+
return {
|
|
524
|
+
success: false,
|
|
525
|
+
value: new ContractError(
|
|
526
|
+
`Storage query for "${path}" not yet implemented`,
|
|
527
|
+
"METADATA_ERROR",
|
|
528
|
+
),
|
|
529
|
+
};
|
|
530
|
+
},
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Filter events for this contract
|
|
536
|
+
*/
|
|
537
|
+
function filterEvents(events: unknown[]): DecodedContractEvent[] {
|
|
538
|
+
const parser = new ContractEventParser(patchedMetadata, address);
|
|
539
|
+
return parser.filterEvents(events);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Subscribe to contract events as an RxJS Observable
|
|
544
|
+
*
|
|
545
|
+
* @param options - Subscription options (contractAddress is automatically set)
|
|
546
|
+
* @param options.getEvents - Function to fetch System.Events at a block hash
|
|
547
|
+
* @param options.eventLabels - Optional filter for specific event names
|
|
548
|
+
* @param options.fromBlock - Optional starting block number
|
|
549
|
+
*/
|
|
550
|
+
function subscribeToEvents(
|
|
551
|
+
options: Omit<EventSubscriptionOptions, "contractAddress">,
|
|
552
|
+
) {
|
|
553
|
+
return createContractEventStream(client, patchedMetadata, {
|
|
554
|
+
...options,
|
|
555
|
+
contractAddress: address,
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Type assertion needed because the runtime correctly unwraps MessageResult
|
|
560
|
+
// but TypeScript doesn't know that. The UnwrapMessageResult type in D9InkContract
|
|
561
|
+
// matches what we actually return at runtime.
|
|
562
|
+
return {
|
|
563
|
+
address,
|
|
564
|
+
metadata: patchedMetadata,
|
|
565
|
+
query: executeQuery,
|
|
566
|
+
send,
|
|
567
|
+
getStorage,
|
|
568
|
+
filterEvents,
|
|
569
|
+
subscribeToEvents,
|
|
570
|
+
} as D9InkContract<M>;
|
|
571
|
+
}
|