@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/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
|
+
}
|