@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/decode.ts ADDED
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Decoding utilities for ContractsApi_call response
3
+ */
4
+
5
+ import { Src } from "@subsquid/scale-codec";
6
+ import { type Codec, u128, Tuple } from "@polkadot-api/substrate-bindings";
7
+
8
+ /**
9
+ * Gas information from contract execution
10
+ */
11
+ export interface GasInfo {
12
+ /** Gas consumed during execution */
13
+ gasConsumed: { refTime: bigint; proofSize: bigint };
14
+ /** Gas required for execution */
15
+ gasRequired: { refTime: bigint; proofSize: bigint };
16
+ }
17
+
18
+ /**
19
+ * Storage deposit information
20
+ */
21
+ export interface StorageDepositInfo {
22
+ /** Storage deposit type: 0 = Refund, 1 = Charge */
23
+ type: "Refund" | "Charge";
24
+ /** Amount of storage deposit */
25
+ amount: bigint;
26
+ }
27
+
28
+ /**
29
+ * Full decoded result from ContractsApi_call
30
+ */
31
+ export interface ContractCallResult {
32
+ /** Gas information */
33
+ gas: GasInfo;
34
+ /** Storage deposit information */
35
+ storageDeposit: StorageDepositInfo;
36
+ /** Debug message (if any) */
37
+ debugMessage: string;
38
+ /** Whether the execution was successful */
39
+ success: boolean;
40
+ /** Execution flags */
41
+ flags: number;
42
+ /** The raw execution result bytes (still wrapped in Result<T, LangError>) */
43
+ data: Uint8Array;
44
+ }
45
+
46
+ /**
47
+ * Decode the raw ContractsApi_call response.
48
+ *
49
+ * The response format is:
50
+ * - gasConsumed: Weight { ref_time: Compact<u64>, proof_size: Compact<u64> }
51
+ * - gasRequired: Weight
52
+ * - storageDeposit: StorageDeposit { variant: u8, amount: u128 }
53
+ * - debugMessage: String
54
+ * - result: Result<ExecReturnValue, DispatchError>
55
+ * - ExecReturnValue: { flags: u32, data: Vec<u8> }
56
+ *
57
+ * @param result - The raw response bytes from state_call ContractsApi_call
58
+ * @returns Decoded contract call result
59
+ */
60
+ export function decodeContractCallResult(result: Uint8Array): ContractCallResult {
61
+ const src = new Src(result);
62
+
63
+ // gasConsumed: Weight
64
+ const gasConsumedRefTime = BigInt(src.compact());
65
+ const gasConsumedProofSize = BigInt(src.compact());
66
+
67
+ // gasRequired: Weight
68
+ const gasRequiredRefTime = BigInt(src.compact());
69
+ const gasRequiredProofSize = BigInt(src.compact());
70
+
71
+ // storageDeposit: StorageDeposit
72
+ const storageDepositVariant = src.u8();
73
+ const storageDepositAmount = src.u128();
74
+
75
+ // debugMessage: String
76
+ const debugMessage = src.str();
77
+
78
+ // result: Result<ExecReturnValue, DispatchError>
79
+ const resultVariant = src.u8(); // 0 = Ok, 1 = Err
80
+ const success = resultVariant === 0;
81
+
82
+ // ExecReturnValue: { flags: u32, data: Vec<u8> }
83
+ const flags = src.u32();
84
+ const data = src.bytes(src.compactLength());
85
+
86
+ return {
87
+ gas: {
88
+ gasConsumed: {
89
+ refTime: gasConsumedRefTime,
90
+ proofSize: gasConsumedProofSize,
91
+ },
92
+ gasRequired: {
93
+ refTime: gasRequiredRefTime,
94
+ proofSize: gasRequiredProofSize,
95
+ },
96
+ },
97
+ storageDeposit: {
98
+ type: storageDepositVariant === 0 ? "Refund" : "Charge",
99
+ amount: storageDepositAmount,
100
+ },
101
+ debugMessage,
102
+ success,
103
+ flags,
104
+ data,
105
+ };
106
+ }
107
+
108
+ /**
109
+ * Legacy function - decode and return just the exec result data.
110
+ * @deprecated Use decodeContractCallResult for full information
111
+ */
112
+ export function decodeResult(result: Uint8Array): Uint8Array {
113
+ const decoded = decodeContractCallResult(result);
114
+ return decoded.data;
115
+ }
116
+
117
+ /**
118
+ * Unwrap the inner value from Result<T, LangError> by checking the variant byte.
119
+ *
120
+ * Ink contracts wrap their return values in Result<T, LangError>.
121
+ * This function handles the unwrapping and throws an error for LangError.
122
+ *
123
+ * @param data - The exec result bytes (Result<T, LangError> encoded)
124
+ * @returns The inner value bytes (T encoded)
125
+ * @throws Error if the result is Err variant (LangError)
126
+ */
127
+ export function unwrapInkResult(data: Uint8Array): Uint8Array {
128
+ if (data.length === 0) {
129
+ throw new Error("Empty result data");
130
+ }
131
+
132
+ const variant = data[0];
133
+
134
+ if (variant === 0) {
135
+ // Ok variant - return the inner data (skip the variant byte)
136
+ return data.slice(1);
137
+ } else if (variant === 1) {
138
+ // Err variant - LangError
139
+ throw new Error("Contract call returned LangError");
140
+ } else {
141
+ throw new Error(`Unknown result variant: ${variant}`);
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Check if the result indicates a LangError
147
+ *
148
+ * @param data - The exec result bytes
149
+ * @returns True if it's a LangError (Err variant)
150
+ */
151
+ export function isLangError(data: Uint8Array): boolean {
152
+ return data.length > 0 && data[0] === 1;
153
+ }
154
+
155
+ /**
156
+ * Decode ink contract message result using a custom SCALE codec.
157
+ * This bypasses polkadot-api's ink decoder which has issues with LangError type.
158
+ *
159
+ * @param data - The exec result bytes (Result<T, LangError> encoded)
160
+ * @param codec - The SCALE codec for the inner value type T
161
+ * @returns The decoded value
162
+ */
163
+ export function decodeInkValue<T>(data: Uint8Array, codec: Codec<T>): T {
164
+ const innerData = unwrapInkResult(data);
165
+ return codec.dec(innerData);
166
+ }
167
+
168
+ /**
169
+ * Pre-defined codecs for common types
170
+ */
171
+ export const InkCodecs = {
172
+ /** u128 codec for Balance type */
173
+ u128,
174
+ /** Tuple of two u128 values, commonly used for (Balance, Balance) */
175
+ balancePair: Tuple(u128, u128),
176
+ };
package/src/encode.ts ADDED
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Encoding utilities for ContractsApi_call state_call
3
+ */
4
+
5
+ import type { Bytes } from "@subsquid/scale-codec";
6
+ import { HexSink } from "@subsquid/scale-codec";
7
+ import type { Binary } from "polkadot-api";
8
+ import { fromHex } from "polkadot-api/utils";
9
+
10
+ /**
11
+ * Encode a contract call for ContractsApi_call state_call.
12
+ *
13
+ * The encoded format matches the ContractsApi::call runtime API:
14
+ * - origin: AccountId (32 bytes)
15
+ * - dest: AccountId (32 bytes)
16
+ * - value: Balance (u128)
17
+ * - gas_limit: Option<Weight> (1 byte for None)
18
+ * - storage_deposit_limit: Option<Balance> (1 byte for None)
19
+ * - input_data: Vec<u8> (compact length + bytes)
20
+ *
21
+ * @param origin - The origin account (as Uint8Array or hex string)
22
+ * @param dest - The contract address (as Uint8Array or hex string)
23
+ * @param input - The encoded call data (selector + arguments)
24
+ * @param value - Optional value to transfer (default: 0)
25
+ * @returns Hex-encoded bytes for state_call
26
+ */
27
+ export function encodeContractCall(
28
+ origin: Uint8Array | string,
29
+ dest: Uint8Array | string,
30
+ input: Binary | Uint8Array,
31
+ value: bigint = 0n,
32
+ ): Bytes {
33
+ const sink = new HexSink();
34
+
35
+ const originBytes = typeof origin === "string" ? fromHex(origin) : origin;
36
+ const destBytes = typeof dest === "string" ? fromHex(dest) : dest;
37
+ const inputBytes = "asBytes" in input ? input.asBytes() : input;
38
+
39
+ // origin: AccountId
40
+ sink.bytes(originBytes);
41
+ // dest: AccountId
42
+ sink.bytes(destBytes);
43
+ // value: Balance (u128)
44
+ sink.u128(value);
45
+ // gas_limit: Option<Weight> - None
46
+ sink.u8(0);
47
+ // storage_deposit_limit: Option<Balance> - None
48
+ sink.u8(0);
49
+ // input_data: Vec<u8>
50
+ sink.compact(inputBytes.length);
51
+ sink.bytes(inputBytes);
52
+
53
+ return sink.toHex();
54
+ }
55
+
56
+ /**
57
+ * Encode a contract call using the same address for origin and dest.
58
+ * This is a simplified version for query operations where the origin
59
+ * doesn't matter much.
60
+ *
61
+ * @param address - The contract address
62
+ * @param input - The encoded call data
63
+ * @param value - Optional value to transfer
64
+ * @returns Hex-encoded bytes for state_call
65
+ */
66
+ export function encodeCall(
67
+ address: Uint8Array | string,
68
+ input: Binary | Uint8Array,
69
+ value: bigint = 0n,
70
+ ): Bytes {
71
+ return encodeContractCall(address, address, input, value);
72
+ }
73
+
74
+ /**
75
+ * Encode a contract call with specific gas limit and storage deposit limit.
76
+ *
77
+ * @param origin - The origin account
78
+ * @param dest - The contract address
79
+ * @param input - The encoded call data
80
+ * @param options - Call options including value, gas limit, storage deposit limit
81
+ * @returns Hex-encoded bytes for state_call
82
+ */
83
+ export function encodeContractCallWithLimits(
84
+ origin: Uint8Array | string,
85
+ dest: Uint8Array | string,
86
+ input: Binary | Uint8Array,
87
+ options: {
88
+ value?: bigint;
89
+ gasLimit?: { refTime: bigint; proofSize: bigint };
90
+ storageDepositLimit?: bigint;
91
+ } = {},
92
+ ): Bytes {
93
+ const sink = new HexSink();
94
+
95
+ const originBytes = typeof origin === "string" ? fromHex(origin) : origin;
96
+ const destBytes = typeof dest === "string" ? fromHex(dest) : dest;
97
+ const inputBytes = "asBytes" in input ? input.asBytes() : input;
98
+
99
+ // origin: AccountId
100
+ sink.bytes(originBytes);
101
+ // dest: AccountId
102
+ sink.bytes(destBytes);
103
+ // value: Balance (u128)
104
+ sink.u128(options.value ?? 0n);
105
+
106
+ // gas_limit: Option<Weight>
107
+ if (options.gasLimit) {
108
+ sink.u8(1); // Some
109
+ sink.compact(options.gasLimit.refTime);
110
+ sink.compact(options.gasLimit.proofSize);
111
+ } else {
112
+ sink.u8(0); // None
113
+ }
114
+
115
+ // storage_deposit_limit: Option<Balance>
116
+ if (options.storageDepositLimit !== undefined) {
117
+ sink.u8(1); // Some
118
+ sink.u128(options.storageDepositLimit);
119
+ } else {
120
+ sink.u8(0); // None
121
+ }
122
+
123
+ // input_data: Vec<u8>
124
+ sink.compact(inputBytes.length);
125
+ sink.bytes(inputBytes);
126
+
127
+ return sink.toHex();
128
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,220 @@
1
+ /**
2
+ * Structured error types for contract operations.
3
+ * Provides detailed error information for debugging and handling.
4
+ */
5
+
6
+ /**
7
+ * Error types for contract operations
8
+ */
9
+ export type ContractErrorType =
10
+ | "METADATA_ERROR" // Missing or invalid contract metadata
11
+ | "ENCODE_ERROR" // Failed to encode call data
12
+ | "DECODE_ERROR" // Failed to decode response
13
+ | "NETWORK_ERROR" // Network or RPC error
14
+ | "CONTRACT_ERROR" // Contract returned an error
15
+ | "LANG_ERROR" // Ink LangError (CouldNotReadInput, etc.)
16
+ | "TIMEOUT_ERROR" // Request timeout
17
+ | "ABORTED" // Request was aborted
18
+ | "SIGNER_ERROR" // Signer related error
19
+ | "TX_ERROR"; // Transaction submission error
20
+
21
+ /**
22
+ * Base error class for all contract-related errors
23
+ */
24
+ export class ContractError extends Error {
25
+ public readonly timestamp: Date;
26
+
27
+ public override readonly cause?: Error;
28
+
29
+ constructor(
30
+ message: string,
31
+ public readonly type: ContractErrorType,
32
+ public readonly label?: string,
33
+ public readonly details?: unknown,
34
+ cause?: Error,
35
+ ) {
36
+ super(message, { cause });
37
+ this.name = "ContractError";
38
+ this.timestamp = new Date();
39
+
40
+ // Maintains proper stack trace for where error was thrown
41
+ if (Error.captureStackTrace) {
42
+ Error.captureStackTrace(this, ContractError);
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Create a formatted error message for logging
48
+ */
49
+ toLogString(): string {
50
+ const parts = [
51
+ `[${this.type}]`,
52
+ this.label ? `${this.label}:` : "",
53
+ this.message,
54
+ ].filter(Boolean);
55
+
56
+ if (this.details) {
57
+ parts.push(`Details: ${JSON.stringify(this.details)}`);
58
+ }
59
+
60
+ return parts.join(" ");
61
+ }
62
+
63
+ /**
64
+ * Convert to a plain object for serialization
65
+ */
66
+ toJSON(): Record<string, unknown> {
67
+ return {
68
+ name: this.name,
69
+ type: this.type,
70
+ message: this.message,
71
+ label: this.label,
72
+ details: this.details,
73
+ timestamp: this.timestamp.toISOString(),
74
+ stack: this.stack,
75
+ };
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Error thrown when contract metadata is missing or invalid
81
+ */
82
+ export class MetadataError extends ContractError {
83
+ constructor(message: string, details?: unknown) {
84
+ super(message, "METADATA_ERROR", undefined, details);
85
+ this.name = "MetadataError";
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Error thrown when encoding call data fails
91
+ */
92
+ export class EncodeError extends ContractError {
93
+ constructor(label: string, message: string, details?: unknown) {
94
+ super(message, "ENCODE_ERROR", label, details);
95
+ this.name = "EncodeError";
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Error thrown when decoding response fails
101
+ */
102
+ export class DecodeError extends ContractError {
103
+ constructor(label: string, message: string, details?: unknown) {
104
+ super(message, "DECODE_ERROR", label, details);
105
+ this.name = "DecodeError";
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Error thrown for network/RPC errors
111
+ */
112
+ export class NetworkError extends ContractError {
113
+ constructor(message: string, cause?: Error, details?: unknown) {
114
+ super(message, "NETWORK_ERROR", undefined, details, cause);
115
+ this.name = "NetworkError";
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Error thrown when contract returns an error result
121
+ */
122
+ export class ContractExecutionError extends ContractError {
123
+ constructor(
124
+ label: string,
125
+ public readonly errorValue: unknown,
126
+ ) {
127
+ super(
128
+ `Contract execution failed: ${JSON.stringify(errorValue)}`,
129
+ "CONTRACT_ERROR",
130
+ label,
131
+ errorValue,
132
+ );
133
+ this.name = "ContractExecutionError";
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Error thrown for Ink LangError
139
+ */
140
+ export class LangError extends ContractError {
141
+ constructor(
142
+ label: string,
143
+ public readonly variant: number,
144
+ ) {
145
+ const variantName =
146
+ variant === 1 ? "CouldNotReadInput" : `Unknown(${variant})`;
147
+ super(`Ink LangError: ${variantName}`, "LANG_ERROR", label, {
148
+ variant,
149
+ variantName,
150
+ });
151
+ this.name = "LangError";
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Error thrown when request times out
157
+ */
158
+ export class TimeoutError extends ContractError {
159
+ constructor(
160
+ label: string,
161
+ public readonly timeoutMs: number,
162
+ ) {
163
+ super(`Request timed out after ${timeoutMs}ms`, "TIMEOUT_ERROR", label, {
164
+ timeoutMs,
165
+ });
166
+ this.name = "TimeoutError";
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Error thrown when request is aborted
172
+ */
173
+ export class AbortedError extends ContractError {
174
+ constructor(label: string, reason?: string) {
175
+ super(reason || "Request was aborted", "ABORTED", label, { reason });
176
+ this.name = "AbortedError";
177
+ }
178
+ }
179
+
180
+ /**
181
+ * Error thrown for signer-related issues
182
+ */
183
+ export class SignerError extends ContractError {
184
+ constructor(message: string, details?: unknown) {
185
+ super(message, "SIGNER_ERROR", undefined, details);
186
+ this.name = "SignerError";
187
+ }
188
+ }
189
+
190
+ /**
191
+ * Error thrown when transaction submission fails
192
+ */
193
+ export class TransactionError extends ContractError {
194
+ constructor(
195
+ label: string,
196
+ message: string,
197
+ public readonly txHash?: string,
198
+ details?: unknown,
199
+ ) {
200
+ super(message, "TX_ERROR", label, { ...(details as object), txHash });
201
+ this.name = "TransactionError";
202
+ }
203
+ }
204
+
205
+ /**
206
+ * Type guard to check if an error is a ContractError
207
+ */
208
+ export function isContractError(error: unknown): error is ContractError {
209
+ return error instanceof ContractError;
210
+ }
211
+
212
+ /**
213
+ * Type guard to check for specific error types
214
+ */
215
+ export function isErrorType<T extends ContractErrorType>(
216
+ error: unknown,
217
+ type: T,
218
+ ): error is ContractError & { type: T } {
219
+ return isContractError(error) && error.type === type;
220
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Event type definitions for ink! contracts
3
+ */
4
+ import type { SS58String } from "polkadot-api";
5
+
6
+ /**
7
+ * Raw event data from chain
8
+ */
9
+ export interface RawContractEvent {
10
+ /** Block number where event was emitted */
11
+ blockNumber: number;
12
+ /** Block hash */
13
+ blockHash: string;
14
+ /** Event index in block */
15
+ eventIndex: number;
16
+ /** Contract address that emitted the event */
17
+ contractAddress: SS58String;
18
+ /** Event data (SCALE encoded) */
19
+ data: Uint8Array;
20
+ /** Event topics (indexed fields) */
21
+ topics: Uint8Array[];
22
+ }
23
+
24
+ /**
25
+ * Decoded contract event
26
+ */
27
+ export interface DecodedContractEvent<T = unknown> {
28
+ /** Event label from metadata (e.g., "Transfer", "Approval") */
29
+ label: string;
30
+ /** Decoded event data */
31
+ data: T;
32
+ /** Original raw event */
33
+ raw: RawContractEvent;
34
+ }
35
+
36
+ /**
37
+ * Event filter options
38
+ */
39
+ export interface EventFilterOptions {
40
+ /** Contract address to filter by */
41
+ contractAddress?: SS58String;
42
+ /** Event labels to include (e.g., ["Transfer", "Approval"]) */
43
+ eventLabels?: string[];
44
+ /** From block number (inclusive) */
45
+ fromBlock?: number;
46
+ /** To block number (inclusive) */
47
+ toBlock?: number;
48
+ }
49
+
50
+ /**
51
+ * Event subscription options
52
+ */
53
+ export interface EventSubscriptionOptions {
54
+ /** Contract address to subscribe to */
55
+ contractAddress: SS58String;
56
+ /** Event labels to subscribe to (undefined = all events) */
57
+ eventLabels?: string[];
58
+ /** Include historical events from this block */
59
+ fromBlock?: number;
60
+ /**
61
+ * Function to fetch System.Events at a given block hash
62
+ * This is required because PolkadotClient doesn't expose typed API directly
63
+ *
64
+ * @example
65
+ * ```ts
66
+ * const options = {
67
+ * contractAddress: usdtAddress,
68
+ * getEvents: async (blockHash) => {
69
+ * return await api.query.System.Events.getValue({ at: blockHash });
70
+ * }
71
+ * };
72
+ * ```
73
+ */
74
+ getEvents: (blockHash: string) => Promise<unknown[]>;
75
+ }