@xdc.org/interaction-detector 1.0.0
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/README.md +767 -0
- package/dist/checkpoint/checkpoint.d.ts +32 -0
- package/dist/checkpoint/checkpoint.d.ts.map +1 -0
- package/dist/checkpoint/checkpoint.js +68 -0
- package/dist/checkpoint/checkpoint.js.map +1 -0
- package/dist/decoder/abi-registry.d.ts +49 -0
- package/dist/decoder/abi-registry.d.ts.map +1 -0
- package/dist/decoder/abi-registry.js +88 -0
- package/dist/decoder/abi-registry.js.map +1 -0
- package/dist/decoder/event-decoder.d.ts +31 -0
- package/dist/decoder/event-decoder.d.ts.map +1 -0
- package/dist/decoder/event-decoder.js +142 -0
- package/dist/decoder/event-decoder.js.map +1 -0
- package/dist/explorer/explorer-client.d.ts +65 -0
- package/dist/explorer/explorer-client.d.ts.map +1 -0
- package/dist/explorer/explorer-client.js +164 -0
- package/dist/explorer/explorer-client.js.map +1 -0
- package/dist/explorer/rate-limiter.d.ts +31 -0
- package/dist/explorer/rate-limiter.d.ts.map +1 -0
- package/dist/explorer/rate-limiter.js +79 -0
- package/dist/explorer/rate-limiter.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -0
- package/dist/rpc/rpc-client.d.ts +70 -0
- package/dist/rpc/rpc-client.d.ts.map +1 -0
- package/dist/rpc/rpc-client.js +136 -0
- package/dist/rpc/rpc-client.js.map +1 -0
- package/dist/rpc/ws-manager.d.ts +27 -0
- package/dist/rpc/ws-manager.d.ts.map +1 -0
- package/dist/rpc/ws-manager.js +161 -0
- package/dist/rpc/ws-manager.js.map +1 -0
- package/dist/scanner/block-scanner.d.ts +45 -0
- package/dist/scanner/block-scanner.d.ts.map +1 -0
- package/dist/scanner/block-scanner.js +180 -0
- package/dist/scanner/block-scanner.js.map +1 -0
- package/dist/tracer/call-tree-parser.d.ts +25 -0
- package/dist/tracer/call-tree-parser.d.ts.map +1 -0
- package/dist/tracer/call-tree-parser.js +80 -0
- package/dist/tracer/call-tree-parser.js.map +1 -0
- package/dist/tracer/state-diff-parser.d.ts +13 -0
- package/dist/tracer/state-diff-parser.d.ts.map +1 -0
- package/dist/tracer/state-diff-parser.js +70 -0
- package/dist/tracer/state-diff-parser.js.map +1 -0
- package/dist/tracer/transaction-tracer.d.ts +52 -0
- package/dist/tracer/transaction-tracer.d.ts.map +1 -0
- package/dist/tracer/transaction-tracer.js +107 -0
- package/dist/tracer/transaction-tracer.js.map +1 -0
- package/dist/types/index.d.ts +262 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +8 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/address.d.ts +29 -0
- package/dist/utils/address.d.ts.map +1 -0
- package/dist/utils/address.js +49 -0
- package/dist/utils/address.js.map +1 -0
- package/dist/utils/format.d.ts +20 -0
- package/dist/utils/format.d.ts.map +1 -0
- package/dist/utils/format.js +53 -0
- package/dist/utils/format.js.map +1 -0
- package/dist/utils/logger.d.ts +16 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +58 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/watcher/contract-watcher.d.ts +56 -0
- package/dist/watcher/contract-watcher.d.ts.map +1 -0
- package/dist/watcher/contract-watcher.js +353 -0
- package/dist/watcher/contract-watcher.js.map +1 -0
- package/dist/watcher/log-poller.d.ts +24 -0
- package/dist/watcher/log-poller.d.ts.map +1 -0
- package/dist/watcher/log-poller.js +82 -0
- package/dist/watcher/log-poller.js.map +1 -0
- package/package.json +57 -0
package/README.md
ADDED
|
@@ -0,0 +1,767 @@
|
|
|
1
|
+
# XDC Interaction Detector
|
|
2
|
+
|
|
3
|
+
Standalone TypeScript library for detecting **all on-chain contract interactions** on XDC and EVM-compatible chains.
|
|
4
|
+
|
|
5
|
+
Combines: **events** + **direct calls** + **internal calls** + **transaction tracing** into a single unified detection engine.
|
|
6
|
+
|
|
7
|
+
**Framework-agnostic. Zero runtime dependencies on any specific backend.** Just detection — you decide what to do with the results.
|
|
8
|
+
|
|
9
|
+
## Table of Contents
|
|
10
|
+
|
|
11
|
+
- [XDC Interaction Detector](#xdc-interaction-detector)
|
|
12
|
+
- [Table of Contents](#table-of-contents)
|
|
13
|
+
- [Features](#features)
|
|
14
|
+
- [Installation](#installation)
|
|
15
|
+
- [Quick Start](#quick-start)
|
|
16
|
+
- [Core Classes](#core-classes)
|
|
17
|
+
- [1. ContractWatcher — Real-Time Monitoring](#1-contractwatcher--real-time-monitoring)
|
|
18
|
+
- [2. BlockScanner — Historical Queries](#2-blockscanner--historical-queries)
|
|
19
|
+
- [3. TransactionTracer — Deep Transaction Analysis](#3-transactiontracer--deep-transaction-analysis)
|
|
20
|
+
- [Explorer API Client](#explorer-api-client)
|
|
21
|
+
- [Event Decoder \& ABI Registry](#event-decoder--abi-registry)
|
|
22
|
+
- [Checkpoint Persistence](#checkpoint-persistence)
|
|
23
|
+
- [Utility Functions](#utility-functions)
|
|
24
|
+
- [Configuration Reference](#configuration-reference)
|
|
25
|
+
- [InteractionDetectorConfig (ContractWatcher)](#interactiondetectorconfig-contractwatcher)
|
|
26
|
+
- [ContractConfig](#contractconfig)
|
|
27
|
+
- [ExplorerConfig](#explorerconfig)
|
|
28
|
+
- [PollingConfig](#pollingconfig)
|
|
29
|
+
- [WsConfig](#wsconfig)
|
|
30
|
+
- [CheckpointConfig](#checkpointconfig)
|
|
31
|
+
- [XDC-Specific Notes](#xdc-specific-notes)
|
|
32
|
+
- [Architecture](#architecture)
|
|
33
|
+
- [Examples](#examples)
|
|
34
|
+
- [Development](#development)
|
|
35
|
+
- [License](#license)
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Features
|
|
40
|
+
|
|
41
|
+
- **Real-time event monitoring** — WebSocket push + HTTP polling with automatic deduplication
|
|
42
|
+
- **Historical block scanning** — Query any block range with auto-chunked fetching
|
|
43
|
+
- **Transaction tracing** — Full call trees, state diffs, and balance changes via `debug_traceTransaction`
|
|
44
|
+
- **Explorer API integration** — Direct & internal transaction collection via XDCScan / Etherscan-compatible APIs
|
|
45
|
+
- **XDC-first, EVM-compatible** — Handles XDC's non-standard ABI encoding, `xdc` address prefix, and 100-block range limit
|
|
46
|
+
- **Pluggable checkpoints** — Memory, file, or custom backends for restart persistence
|
|
47
|
+
- **Zero framework dependency** — Works in any Node.js environment: Express, Fastify, serverless functions, CLI scripts, background workers
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Installation
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
npm install xdc-interaction-detector
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
**Dependencies:** `ethers` v6, `axios`, `ws` — all installed automatically.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Quick Start
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
import { ContractWatcher } from 'xdc-interaction-detector';
|
|
65
|
+
|
|
66
|
+
const watcher = new ContractWatcher({
|
|
67
|
+
// RPC endpoints
|
|
68
|
+
rpcUrl: 'https://rpc.xinfin.network',
|
|
69
|
+
wsUrl: 'wss://ws.xinfin.network', // optional — enables real-time push
|
|
70
|
+
chainId: 50, // XDC Mainnet
|
|
71
|
+
|
|
72
|
+
// Contracts to monitor
|
|
73
|
+
contracts: [
|
|
74
|
+
{
|
|
75
|
+
address: '0x0000000000000000000000000000000000000088',
|
|
76
|
+
abi: ['event Vote(address indexed _voter, address indexed _candidate, uint256 _cap)'],
|
|
77
|
+
name: 'XDCValidator',
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
|
|
81
|
+
// Explorer API — enables direct & internal call detection
|
|
82
|
+
explorer: {
|
|
83
|
+
apiUrl: 'https://xdc.blocksscan.io/api',
|
|
84
|
+
apiKey: 'YOUR_API_KEY', // optional — higher rate limits
|
|
85
|
+
rateLimitPerSec: 5,
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
// Checkpoint — survive restarts
|
|
89
|
+
checkpoint: { backend: 'file', path: './checkpoints' },
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Decoded contract events (Transfer, Swap, Vote, etc.)
|
|
93
|
+
watcher.on('event', event => {
|
|
94
|
+
console.log(`${event.name} from ${event.contractName} at block ${event.blockNumber}`);
|
|
95
|
+
console.log(' Args:', event.args);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// ALL interactions (events + direct + internal + delegate + static calls)
|
|
99
|
+
watcher.on('interaction', interaction => {
|
|
100
|
+
console.log(`[${interaction.type}] tx ${interaction.txHash} (source: ${interaction.source})`);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
await watcher.start();
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## Core Classes
|
|
109
|
+
|
|
110
|
+
### 1. ContractWatcher — Real-Time Monitoring
|
|
111
|
+
|
|
112
|
+
Watches one or more contract addresses for all interactions in real-time using three complementary detection paths:
|
|
113
|
+
|
|
114
|
+
| Path | Method | What it catches | Latency |
|
|
115
|
+
| ------------ | --------------------------- | ----------------------------------- | ------------- |
|
|
116
|
+
| WebSocket | `eth_subscribe("logs")` | Contract events | ~2 seconds |
|
|
117
|
+
| HTTP Polling | `eth_getLogs` | Contract events (reliable fallback) | 15–30 seconds |
|
|
118
|
+
| Explorer API | `txlist` + `txlistinternal` | Direct calls + internal calls | 30–60 seconds |
|
|
119
|
+
|
|
120
|
+
**Creating a watcher:**
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
import { ContractWatcher } from 'xdc-interaction-detector';
|
|
124
|
+
|
|
125
|
+
const watcher = new ContractWatcher({
|
|
126
|
+
// ─── Required ───────────────────────────────────────────
|
|
127
|
+
rpcUrl: 'https://rpc.xinfin.network',
|
|
128
|
+
contracts: [
|
|
129
|
+
{
|
|
130
|
+
address: '0xContractAddress', // 0x or xdc prefix both work
|
|
131
|
+
abi: [...], // optional — enables event decoding
|
|
132
|
+
name: 'MyDeFiPool', // optional — human label
|
|
133
|
+
},
|
|
134
|
+
// Watch multiple contracts simultaneously
|
|
135
|
+
{ address: '0xAnotherContract', name: 'Governance' },
|
|
136
|
+
],
|
|
137
|
+
|
|
138
|
+
// ─── Optional: WebSocket (real-time push) ───────────────
|
|
139
|
+
wsUrl: 'wss://ws.xinfin.network',
|
|
140
|
+
chainId: 50, // default: 50 (XDC Mainnet)
|
|
141
|
+
|
|
142
|
+
// ─── Optional: Explorer API (direct + internal calls) ───
|
|
143
|
+
explorer: {
|
|
144
|
+
apiUrl: 'https://xdc.blocksscan.io/api',
|
|
145
|
+
apiKey: 'YOUR_XDCSCAN_API_KEY', // optional — get higher rate limits
|
|
146
|
+
chainId: 50, // required for Etherscan v2
|
|
147
|
+
rateLimitPerSec: 5, // default: 5 req/s
|
|
148
|
+
pollIntervalMs: 60_000, // how often to check explorer (default: 60s)
|
|
149
|
+
},
|
|
150
|
+
|
|
151
|
+
// ─── Optional: Polling tuning ───────────────────────────
|
|
152
|
+
polling: {
|
|
153
|
+
intervalMs: 15_000, // default: 30s
|
|
154
|
+
maxBlockRange: 100, // XDC limit — don't change unless using another chain
|
|
155
|
+
concurrency: 3, // parallel chunk fetches
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
// ─── Optional: WebSocket tuning ─────────────────────────
|
|
159
|
+
ws: {
|
|
160
|
+
enabled: true, // default: true
|
|
161
|
+
reconnectDelayBaseMs: 5_000,
|
|
162
|
+
reconnectDelayMaxMs: 30_000,
|
|
163
|
+
maxReconnectAttempts: 20,
|
|
164
|
+
heartbeatIntervalMs: 60_000,
|
|
165
|
+
heartbeatTimeoutMs: 10_000,
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
// ─── Optional: Checkpoint persistence ───────────────────
|
|
169
|
+
checkpoint: {
|
|
170
|
+
backend: 'file', // 'memory' | 'file' | 'custom'
|
|
171
|
+
path: './checkpoints',
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
// ─── Optional: Fallback RPCs ────────────────────────────
|
|
175
|
+
fallbackRpcUrls: ['https://rpc1.xinfin.network'],
|
|
176
|
+
|
|
177
|
+
// ─── Optional: Log level ────────────────────────────────
|
|
178
|
+
logLevel: 'info', // 'debug' | 'info' | 'warn' | 'error' | 'silent'
|
|
179
|
+
});
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
**Listening for events:**
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
// ── Decoded contract events ──────────────────────────────────
|
|
186
|
+
watcher.on('event', event => {
|
|
187
|
+
console.log(event.contract); // '0xcontractaddress'
|
|
188
|
+
console.log(event.contractName); // 'MyDeFiPool'
|
|
189
|
+
console.log(event.name); // 'Swap'
|
|
190
|
+
console.log(event.args); // { sender: '0x...', amount0In: 1000n, ... }
|
|
191
|
+
console.log(event.blockNumber); // 75123456
|
|
192
|
+
console.log(event.txHash); // '0xabc...'
|
|
193
|
+
console.log(event.logIndex); // 3
|
|
194
|
+
console.log(event.timestamp); // 1711792800 (Unix seconds)
|
|
195
|
+
console.log(event.signature); // 'Swap(address,uint256,uint256,address)'
|
|
196
|
+
console.log(event.raw); // { topics: [...], data: '0x...' }
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// ── ALL interactions (unified type) ──────────────────────────
|
|
200
|
+
watcher.on('interaction', interaction => {
|
|
201
|
+
console.log(interaction.type); // 'event' | 'direct_call' | 'internal_call' | 'delegate_call' | 'static_call'
|
|
202
|
+
console.log(interaction.source); // 'rpc_logs' | 'ws_logs' | 'explorer_txlist' | 'explorer_internal'
|
|
203
|
+
console.log(interaction.txHash);
|
|
204
|
+
console.log(interaction.from); // sender (when available)
|
|
205
|
+
console.log(interaction.to); // contract address
|
|
206
|
+
console.log(interaction.methodId); // '0xa9059cbb' (for direct/internal calls)
|
|
207
|
+
console.log(interaction.methodName); // 'transfer(address,uint256)' (if ABI found)
|
|
208
|
+
console.log(interaction.value); // native value in wei
|
|
209
|
+
console.log(interaction.isError); // true if call reverted
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// ── Raw logs (for contracts without ABI) ─────────────────────
|
|
213
|
+
watcher.on('log', log => {
|
|
214
|
+
console.log(log.topics[0]); // event signature hash
|
|
215
|
+
console.log(log.data); // raw encoded data
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// ── Lifecycle events ─────────────────────────────────────────
|
|
219
|
+
watcher.on('connected', info => {
|
|
220
|
+
console.log(`Connected via ${info.type}: ${info.url}`);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
watcher.on('disconnected', info => {
|
|
224
|
+
console.log(`Disconnected: ${info.reason}`);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
watcher.on('checkpoint', blockNumber => {
|
|
228
|
+
console.log(`Checkpoint saved at block ${blockNumber}`);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
watcher.on('error', err => {
|
|
232
|
+
console.error(`Error: ${err.message}`);
|
|
233
|
+
});
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
**Starting and stopping:**
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
// Start monitoring
|
|
240
|
+
await watcher.start();
|
|
241
|
+
|
|
242
|
+
// ... later, graceful shutdown
|
|
243
|
+
await watcher.stop();
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
**Deduplication:** Events detected by both WebSocket and polling are automatically deduplicated using `txHash + logIndex` composite keys. This prevents double-counting when both detection paths catch the same event.
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
### 2. BlockScanner — Historical Queries
|
|
251
|
+
|
|
252
|
+
Scans a block range for all events and interactions involving a contract. Automatically handles XDC's 100-block `eth_getLogs` limit with chunked parallel fetching.
|
|
253
|
+
|
|
254
|
+
**Scanning for events:**
|
|
255
|
+
|
|
256
|
+
```typescript
|
|
257
|
+
import { BlockScanner } from 'xdc-interaction-detector';
|
|
258
|
+
|
|
259
|
+
const scanner = new BlockScanner({
|
|
260
|
+
rpcUrl: 'https://rpc.xinfin.network',
|
|
261
|
+
maxBlockRange: 100, // default: 100 (XDC limit)
|
|
262
|
+
concurrency: 3, // parallel chunk fetches
|
|
263
|
+
logLevel: 'info',
|
|
264
|
+
|
|
265
|
+
// Optional: explorer for direct + internal tx enrichment
|
|
266
|
+
explorer: {
|
|
267
|
+
apiUrl: 'https://xdc.blocksscan.io/api',
|
|
268
|
+
apiKey: 'YOUR_API_KEY',
|
|
269
|
+
},
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// ── Scan for decoded events ──────────────────────────────────
|
|
273
|
+
const events = await scanner.getEvents({
|
|
274
|
+
address: '0x0000000000000000000000000000000000000088',
|
|
275
|
+
abi: [
|
|
276
|
+
'event Vote(address indexed _voter, address indexed _candidate, uint256 _cap)',
|
|
277
|
+
'event Unvote(address indexed _voter, address indexed _candidate, uint256 _cap)',
|
|
278
|
+
],
|
|
279
|
+
fromBlock: 75_000_000,
|
|
280
|
+
toBlock: 75_100_000,
|
|
281
|
+
|
|
282
|
+
// Optional: filter by specific event name
|
|
283
|
+
eventFilter: 'Vote',
|
|
284
|
+
|
|
285
|
+
// Optional: progress callback for large scans
|
|
286
|
+
onProgress: (processed, total) => {
|
|
287
|
+
console.log(`${Math.round((processed / total) * 100)}% complete`);
|
|
288
|
+
},
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
// events is DecodedEvent[]
|
|
292
|
+
for (const event of events) {
|
|
293
|
+
console.log(`[block ${event.blockNumber}] ${event.name}`, event.args);
|
|
294
|
+
}
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
**Scanning for all interactions (events + explorer txlist + txlistinternal):**
|
|
298
|
+
|
|
299
|
+
```typescript
|
|
300
|
+
const interactions = await scanner.getInteractions({
|
|
301
|
+
address: '0x0000000000000000000000000000000000000088',
|
|
302
|
+
abi: [...],
|
|
303
|
+
fromBlock: 75_000_000,
|
|
304
|
+
toBlock: 75_100_000,
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// interactions is ContractInteraction[]
|
|
308
|
+
for (const i of interactions) {
|
|
309
|
+
console.log(`[${i.type}] block ${i.blockNumber} tx ${i.txHash}`);
|
|
310
|
+
if (i.type === 'event') console.log(' Event:', i.event?.name, i.event?.args);
|
|
311
|
+
if (i.type === 'direct_call') console.log(' Method:', i.methodName);
|
|
312
|
+
if (i.type === 'internal_call') console.log(' From:', i.from);
|
|
313
|
+
}
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
---
|
|
317
|
+
|
|
318
|
+
### 3. TransactionTracer — Deep Transaction Analysis
|
|
319
|
+
|
|
320
|
+
Traces a single transaction to extract the full execution story: call tree, state diffs, balance changes, and all events.
|
|
321
|
+
|
|
322
|
+
> **Note:** Requires an RPC endpoint with the `debug` namespace enabled. For historical transactions, an archive node is required. For recent/current blocks, a regular full node works.
|
|
323
|
+
|
|
324
|
+
**Full trace:**
|
|
325
|
+
|
|
326
|
+
```typescript
|
|
327
|
+
import { TransactionTracer } from 'xdc-interaction-detector';
|
|
328
|
+
|
|
329
|
+
const tracer = new TransactionTracer({
|
|
330
|
+
rpcUrl: 'https://archive-rpc.xinfin.network', // must support debug_traceTransaction
|
|
331
|
+
timeoutMs: 120_000, // tracing can be slow on complex txs
|
|
332
|
+
logLevel: 'info',
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
// Register ABIs for method name decoding in call trees
|
|
336
|
+
tracer.registerABI(
|
|
337
|
+
'0xContractAddress',
|
|
338
|
+
['function swap(uint256,uint256,address,bytes)', 'function transfer(address,uint256)'],
|
|
339
|
+
'MyDEX',
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
const result = await tracer.trace('0xTransactionHash');
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
**Using the trace result:**
|
|
346
|
+
|
|
347
|
+
```typescript
|
|
348
|
+
// ── Call Tree ────────────────────────────────────────────────
|
|
349
|
+
// Nested structure showing every CALL, STATICCALL, DELEGATECALL
|
|
350
|
+
console.log(result.callTree.type); // 'CALL'
|
|
351
|
+
console.log(result.callTree.from); // '0xSender'
|
|
352
|
+
console.log(result.callTree.to); // '0xContract'
|
|
353
|
+
console.log(result.callTree.method); // 'swap(uint256,uint256,address,bytes)' (if ABI registered)
|
|
354
|
+
console.log(result.callTree.value); // '0x0' (native value)
|
|
355
|
+
console.log(result.callTree.calls); // CallTreeNode[] — nested sub-calls
|
|
356
|
+
// Example nested call:
|
|
357
|
+
// CALL 0xRouter → swap(...)
|
|
358
|
+
// CALL 0xPool → mint(...)
|
|
359
|
+
// STATICCALL 0xOracle → getPrice()
|
|
360
|
+
// CALL 0xTokenA → transfer(...)
|
|
361
|
+
|
|
362
|
+
// ── State Diffs ──────────────────────────────────────────────
|
|
363
|
+
// Storage slot changes (before/after for each modified slot)
|
|
364
|
+
for (const diff of result.stateDiffs) {
|
|
365
|
+
console.log(`${diff.contract} slot ${diff.slot}`);
|
|
366
|
+
console.log(` before: ${diff.before}`);
|
|
367
|
+
console.log(` after: ${diff.after}`);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// ── Balance Changes ──────────────────────────────────────────
|
|
371
|
+
// Native token balance changes per address
|
|
372
|
+
for (const change of result.balanceChanges) {
|
|
373
|
+
console.log(`${change.address}: ${change.delta} wei (${change.token})`);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// ── Events ───────────────────────────────────────────────────
|
|
377
|
+
// All events emitted during execution (decoded if ABI registered)
|
|
378
|
+
for (const event of result.events) {
|
|
379
|
+
console.log(`${event.name} from ${event.contract}`, event.args);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// ── Metadata ─────────────────────────────────────────────────
|
|
383
|
+
console.log(result.gasUsed); // 234567
|
|
384
|
+
console.log(result.involvedContracts); // ['0x...', '0x...', '0x...']
|
|
385
|
+
console.log(result.blockNumber); // 75123456
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
**Partial traces (lighter weight):**
|
|
389
|
+
|
|
390
|
+
```typescript
|
|
391
|
+
// Just the call tree (no state diffs)
|
|
392
|
+
const callTree = await tracer.traceCallTree('0xTxHash');
|
|
393
|
+
|
|
394
|
+
// Just the state diffs + balance changes (no call tree)
|
|
395
|
+
const { stateDiffs, balanceChanges } = await tracer.traceStateDiffs('0xTxHash');
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
**Call tree utilities:**
|
|
399
|
+
|
|
400
|
+
```typescript
|
|
401
|
+
import { flattenCallTree, findCallsTo, extractInvolvedContracts } from 'xdc-interaction-detector';
|
|
402
|
+
|
|
403
|
+
// Flatten the nested tree into a linear array
|
|
404
|
+
const allCalls = flattenCallTree(result.callTree);
|
|
405
|
+
console.log(`Total calls in execution: ${allCalls.length}`);
|
|
406
|
+
|
|
407
|
+
// Find all calls targeting a specific contract
|
|
408
|
+
const tokenCalls = findCallsTo(result.callTree, '0xTokenAddress');
|
|
409
|
+
|
|
410
|
+
// Get all unique addresses involved
|
|
411
|
+
const addresses = extractInvolvedContracts(result.callTree);
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
---
|
|
415
|
+
|
|
416
|
+
## Explorer API Client
|
|
417
|
+
|
|
418
|
+
Standalone Etherscan-compatible API client. Works with XDCScan, Etherscan v2, BSCScan, PolygonScan, and any Etherscan-compatible explorer.
|
|
419
|
+
|
|
420
|
+
```typescript
|
|
421
|
+
import { ExplorerClient } from 'xdc-interaction-detector';
|
|
422
|
+
|
|
423
|
+
const explorer = new ExplorerClient({
|
|
424
|
+
apiUrl: 'https://xdc.blocksscan.io/api', // XDCScan
|
|
425
|
+
// apiUrl: 'https://api.etherscan.io/v2/api', // Etherscan v2 (80+ chains)
|
|
426
|
+
// apiUrl: 'https://api.bscscan.com/api', // BSCScan
|
|
427
|
+
apiKey: 'YOUR_API_KEY', // optional — higher rate limits
|
|
428
|
+
chainId: 50, // required for Etherscan v2
|
|
429
|
+
rateLimitPerSec: 5, // built-in token-bucket rate limiter
|
|
430
|
+
});
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
**Available methods:**
|
|
434
|
+
|
|
435
|
+
```typescript
|
|
436
|
+
// ── Transaction lists ────────────────────────────────────────
|
|
437
|
+
// Get external transactions to/from a contract
|
|
438
|
+
const txs = await explorer.getTransactions('0xAddress', {
|
|
439
|
+
startBlock: 75_000_000,
|
|
440
|
+
endBlock: 75_100_000,
|
|
441
|
+
sort: 'asc', // or 'desc'
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
// Get internal transactions (CALL, DELEGATECALL from other contracts)
|
|
445
|
+
const internalTxs = await explorer.getInternalTransactions('0xAddress', {
|
|
446
|
+
startBlock: 75_000_000,
|
|
447
|
+
endBlock: 75_100_000,
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
// Get internal transactions within a specific tx
|
|
451
|
+
const txInternals = await explorer.getInternalTransactionsByTx('0xTxHash');
|
|
452
|
+
|
|
453
|
+
// ── Events ───────────────────────────────────────────────────
|
|
454
|
+
// Get event logs with optional topic filter
|
|
455
|
+
const logs = await explorer.getLogs('0xAddress', {
|
|
456
|
+
fromBlock: 75_000_000,
|
|
457
|
+
toBlock: 75_100_000,
|
|
458
|
+
topic0: '0xddf252ad...', // optional — filter by event signature
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
// ── Contract ABI ─────────────────────────────────────────────
|
|
462
|
+
// Auto-fetch verified contract ABI
|
|
463
|
+
const abi = await explorer.getContractABI('0xAddress');
|
|
464
|
+
if (abi) {
|
|
465
|
+
console.log(`Fetched ABI with ${abi.length} entries`);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// ── Token transfers ──────────────────────────────────────────
|
|
469
|
+
const transfers = await explorer.getTokenTransfers('0xAddress', {
|
|
470
|
+
startBlock: 75_000_000,
|
|
471
|
+
endBlock: 75_100_000,
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
// ── Balance ──────────────────────────────────────────────────
|
|
475
|
+
const balance = await explorer.getBalance('0xAddress');
|
|
476
|
+
|
|
477
|
+
// ── Cleanup ──────────────────────────────────────────────────
|
|
478
|
+
explorer.destroy(); // Cleans up rate limiter timers
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
**Explorer compatibility:**
|
|
482
|
+
|
|
483
|
+
| Explorer | Base URL | Chain | Free Rate Limit |
|
|
484
|
+
| ---------------- | --------------------------------- | ------------------------ | --------------- |
|
|
485
|
+
| **XDCScan** | `https://xdc.blocksscan.io/api` | XDC Mainnet (50) | 5 req/s |
|
|
486
|
+
| **Etherscan v2** | `https://api.etherscan.io/v2/api` | 80+ chains via `chainId` | 5 req/s |
|
|
487
|
+
| **BSCScan** | `https://api.bscscan.com/api` | BSC (56) | 5 req/s |
|
|
488
|
+
| **PolygonScan** | `https://api.polygonscan.com/api` | Polygon (137) | 5 req/s |
|
|
489
|
+
| **Custom** | Any Etherscan-compatible URL | Any EVM chain | Configurable |
|
|
490
|
+
|
|
491
|
+
---
|
|
492
|
+
|
|
493
|
+
## Event Decoder & ABI Registry
|
|
494
|
+
|
|
495
|
+
The decoder handles both standard Solidity ABI encoding and XDC's non-standard encoding (where all params are packed into the `data` field).
|
|
496
|
+
|
|
497
|
+
```typescript
|
|
498
|
+
import { AbiRegistry, EventDecoder } from 'xdc-interaction-detector';
|
|
499
|
+
|
|
500
|
+
// ── Register ABIs ────────────────────────────────────────────
|
|
501
|
+
const registry = new AbiRegistry();
|
|
502
|
+
|
|
503
|
+
// Register manually
|
|
504
|
+
registry.register(
|
|
505
|
+
'0xContractAddress',
|
|
506
|
+
[
|
|
507
|
+
'event Transfer(address indexed from, address indexed to, uint256 value)',
|
|
508
|
+
'event Approval(address indexed owner, address indexed spender, uint256 value)',
|
|
509
|
+
],
|
|
510
|
+
'MyToken',
|
|
511
|
+
);
|
|
512
|
+
|
|
513
|
+
// Register from a JSON ABI array
|
|
514
|
+
registry.register('0xAnotherContract', require('./MyContract.json').abi, 'MyContract');
|
|
515
|
+
|
|
516
|
+
// Auto-fetch from block explorer (verified contracts only)
|
|
517
|
+
const explorer = new ExplorerClient({ apiUrl: 'https://xdc.blocksscan.io/api' });
|
|
518
|
+
const success = await registry.registerFromExplorer('0xVerifiedContract', explorer, 'VerifiedToken');
|
|
519
|
+
console.log(success ? 'ABI fetched!' : 'Contract not verified');
|
|
520
|
+
|
|
521
|
+
// ── Query the registry ───────────────────────────────────────
|
|
522
|
+
registry.has('0xContractAddress'); // true
|
|
523
|
+
registry.getName('0xContractAddress'); // 'MyToken'
|
|
524
|
+
registry.getAddresses(); // ['0x...', '0x...']
|
|
525
|
+
registry.getEventName('0xddf252ad...'); // 'Transfer' (from any registered contract)
|
|
526
|
+
|
|
527
|
+
// ── Decode raw logs ──────────────────────────────────────────
|
|
528
|
+
const decoder = new EventDecoder(registry);
|
|
529
|
+
|
|
530
|
+
const decoded = decoder.decode(rawLog);
|
|
531
|
+
if (decoded) {
|
|
532
|
+
console.log(decoded.name); // 'Transfer'
|
|
533
|
+
console.log(decoded.args.from); // '0x...'
|
|
534
|
+
console.log(decoded.args.to); // '0x...'
|
|
535
|
+
console.log(decoded.args.value); // 1000000000000000000n
|
|
536
|
+
}
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
---
|
|
540
|
+
|
|
541
|
+
## Checkpoint Persistence
|
|
542
|
+
|
|
543
|
+
Checkpoints let the watcher resume from where it left off after a restart.
|
|
544
|
+
|
|
545
|
+
```typescript
|
|
546
|
+
import { MemoryCheckpoint, FileCheckpoint, createCheckpointBackend } from 'xdc-interaction-detector';
|
|
547
|
+
|
|
548
|
+
// ── Memory (development / testing) ───────────────────────────
|
|
549
|
+
const memCp = new MemoryCheckpoint();
|
|
550
|
+
await memCp.save('watcher-key', 75_000_000);
|
|
551
|
+
await memCp.load('watcher-key'); // 75000000
|
|
552
|
+
|
|
553
|
+
// ── File (simple production) ─────────────────────────────────
|
|
554
|
+
const fileCp = new FileCheckpoint('./checkpoints');
|
|
555
|
+
await fileCp.save('watcher-key', 75_000_000);
|
|
556
|
+
// Persists to ./checkpoints/checkpoints.json
|
|
557
|
+
// Survives process restarts
|
|
558
|
+
|
|
559
|
+
// ── Custom backend (bring your own) ──────────────────────────
|
|
560
|
+
const customCp = {
|
|
561
|
+
async save(key: string, blockNumber: number) {
|
|
562
|
+
await redis.set(`checkpoint:${key}`, blockNumber);
|
|
563
|
+
},
|
|
564
|
+
async load(key: string) {
|
|
565
|
+
const val = await redis.get(`checkpoint:${key}`);
|
|
566
|
+
return val ? parseInt(val) : null;
|
|
567
|
+
},
|
|
568
|
+
};
|
|
569
|
+
|
|
570
|
+
// ── Factory function ─────────────────────────────────────────
|
|
571
|
+
const cp = createCheckpointBackend({ backend: 'file', path: './data' });
|
|
572
|
+
const cp2 = createCheckpointBackend({ backend: 'custom', custom: customCp });
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
---
|
|
576
|
+
|
|
577
|
+
## Utility Functions
|
|
578
|
+
|
|
579
|
+
```typescript
|
|
580
|
+
import {
|
|
581
|
+
// Address utilities
|
|
582
|
+
normalizeAddress, // '0xABC...' or 'xdcABC...' → '0xabc...'
|
|
583
|
+
toXdcAddress, // '0xabc...' → 'xdcabc...'
|
|
584
|
+
toEthAddress, // 'xdcabc...' → '0xabc...'
|
|
585
|
+
isAddress, // validate hex address (20 bytes)
|
|
586
|
+
addressEqual, // compare addresses (case-insensitive, prefix-aware)
|
|
587
|
+
|
|
588
|
+
// Formatting
|
|
589
|
+
formatXDC, // bigint wei → '1.23M XDC' / '456.78K XDC'
|
|
590
|
+
formatWei, // bigint wei → '1.23M' (generic, no unit)
|
|
591
|
+
parseHexOrDecimal, // '0x100' → 256, '256' → 256
|
|
592
|
+
toHex, // 256 → '0x100'
|
|
593
|
+
shortAddress, // '0x1234...abcd'
|
|
594
|
+
sleep, // await sleep(1000)
|
|
595
|
+
|
|
596
|
+
// Logger
|
|
597
|
+
Logger, // new Logger('MyModule', 'info')
|
|
598
|
+
} from 'xdc-interaction-detector';
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
---
|
|
602
|
+
|
|
603
|
+
## Configuration Reference
|
|
604
|
+
|
|
605
|
+
### InteractionDetectorConfig (ContractWatcher)
|
|
606
|
+
|
|
607
|
+
| Option | Type | Default | Description |
|
|
608
|
+
| ----------------- | ------------------ | ---------- | -------------------------------------------- |
|
|
609
|
+
| `rpcUrl` | `string` | _required_ | Primary HTTP RPC URL |
|
|
610
|
+
| `wsUrl` | `string` | — | WebSocket RPC URL (enables real-time events) |
|
|
611
|
+
| `contracts` | `ContractConfig[]` | _required_ | Contracts to monitor |
|
|
612
|
+
| `explorer` | `ExplorerConfig` | — | Block explorer API settings |
|
|
613
|
+
| `polling` | `PollingConfig` | — | HTTP polling tuning |
|
|
614
|
+
| `ws` | `WsConfig` | — | WebSocket tuning |
|
|
615
|
+
| `checkpoint` | `CheckpointConfig` | `memory` | Checkpoint persistence |
|
|
616
|
+
| `chainId` | `number` | `50` | Chain ID |
|
|
617
|
+
| `fallbackRpcUrls` | `string[]` | `[]` | Fallback RPC endpoints |
|
|
618
|
+
| `logLevel` | `LogLevel` | `'info'` | Log verbosity |
|
|
619
|
+
|
|
620
|
+
### ContractConfig
|
|
621
|
+
|
|
622
|
+
| Option | Type | Default | Description |
|
|
623
|
+
| --------- | -------- | ---------- | ----------------------------------------------------- |
|
|
624
|
+
| `address` | `string` | _required_ | Contract address (`0x` or `xdc` prefix) |
|
|
625
|
+
| `abi` | `any[]` | — | ABI for event decoding. Human-readable or JSON format |
|
|
626
|
+
| `name` | `string` | — | Human-readable label (shown in decoded events) |
|
|
627
|
+
|
|
628
|
+
### ExplorerConfig
|
|
629
|
+
|
|
630
|
+
| Option | Type | Default | Description |
|
|
631
|
+
| ----------------- | -------- | ---------- | --------------------------------------- |
|
|
632
|
+
| `apiUrl` | `string` | _required_ | Explorer API base URL |
|
|
633
|
+
| `apiKey` | `string` | — | API key for higher rate limits |
|
|
634
|
+
| `chainId` | `number` | — | Chain ID (required for Etherscan v2) |
|
|
635
|
+
| `rateLimitPerSec` | `number` | `5` | Max requests per second |
|
|
636
|
+
| `pollIntervalMs` | `number` | `60000` | How often to poll txlist/txlistinternal |
|
|
637
|
+
|
|
638
|
+
### PollingConfig
|
|
639
|
+
|
|
640
|
+
| Option | Type | Default | Description |
|
|
641
|
+
| --------------- | -------- | ------- | ---------------------------------------- |
|
|
642
|
+
| `intervalMs` | `number` | `30000` | How often to poll eth_getLogs |
|
|
643
|
+
| `maxBlockRange` | `number` | `100` | Max blocks per request (XDC limit = 100) |
|
|
644
|
+
| `concurrency` | `number` | `3` | Parallel chunk fetches |
|
|
645
|
+
|
|
646
|
+
### WsConfig
|
|
647
|
+
|
|
648
|
+
| Option | Type | Default | Description |
|
|
649
|
+
| ---------------------- | --------- | ------- | --------------------------------------------- |
|
|
650
|
+
| `enabled` | `boolean` | `true` | Enable WebSocket subscription |
|
|
651
|
+
| `reconnectDelayBaseMs` | `number` | `5000` | Base reconnect delay |
|
|
652
|
+
| `reconnectDelayMaxMs` | `number` | `30000` | Max reconnect delay (exponential backoff cap) |
|
|
653
|
+
| `maxReconnectAttempts` | `number` | `20` | Max attempts before 5-min cooldown |
|
|
654
|
+
| `heartbeatIntervalMs` | `number` | `60000` | Heartbeat ping interval |
|
|
655
|
+
| `heartbeatTimeoutMs` | `number` | `10000` | Heartbeat response timeout |
|
|
656
|
+
|
|
657
|
+
### CheckpointConfig
|
|
658
|
+
|
|
659
|
+
| Option | Type | Default | Description |
|
|
660
|
+
| --------- | ------------------- | ----------------- | -------------------------------------------- |
|
|
661
|
+
| `backend` | `string` | `'memory'` | `'memory'`, `'file'`, or `'custom'` |
|
|
662
|
+
| `path` | `string` | `'./checkpoints'` | Directory for `'file'` backend |
|
|
663
|
+
| `custom` | `CheckpointBackend` | — | Custom implementation for `'custom'` backend |
|
|
664
|
+
|
|
665
|
+
---
|
|
666
|
+
|
|
667
|
+
## XDC-Specific Notes
|
|
668
|
+
|
|
669
|
+
- **Non-standard ABI encoding:** Some XDC system contracts (including XDCValidator at `0x...0088`) encode all event parameters in the `data` field with only `topic[0]` (the event signature). Standard decoders like `ethers.js` fail on these. The library includes an automatic fallback decoder that handles this transparently.
|
|
670
|
+
|
|
671
|
+
- **Block range limit:** XDC RPC limits `eth_getLogs` to 100 blocks per request. The `LogPoller` and `BlockScanner` automatically chunk requests. Don't set `maxBlockRange` above 100 for XDC.
|
|
672
|
+
|
|
673
|
+
- **Address prefix:** XDC uses the `xdc` prefix instead of `0x`. All library functions accept both formats. Internally, everything normalizes to lowercase `0x`.
|
|
674
|
+
|
|
675
|
+
- **Tracing:** `debug_traceTransaction` requires an archive node for historical transactions. For monitoring current blocks in real-time, a standard full node works fine.
|
|
676
|
+
|
|
677
|
+
---
|
|
678
|
+
|
|
679
|
+
## Architecture
|
|
680
|
+
|
|
681
|
+
```
|
|
682
|
+
┌──────────────────────────────────────────────────────────┐
|
|
683
|
+
│ xdc-interaction-detector │
|
|
684
|
+
│ │
|
|
685
|
+
│ ┌───────────────────────────────────────────────────┐ │
|
|
686
|
+
│ │ ContractWatcher (real-time monitoring) │ │
|
|
687
|
+
│ │ ├── WsManager (WebSocket subscription) │ │
|
|
688
|
+
│ │ ├── LogPoller (eth_getLogs fallback) │ │
|
|
689
|
+
│ │ ├── ExplorerClient (txlist + txlistinternal)│ │
|
|
690
|
+
│ │ ├── EventDecoder (ABI + XDC fallback) │ │
|
|
691
|
+
│ │ └── Checkpoint (pluggable persistence) │ │
|
|
692
|
+
│ └───────────────────────────────────────────────────┘ │
|
|
693
|
+
│ │
|
|
694
|
+
│ ┌────────────────────────────────────────────────────┐ │
|
|
695
|
+
│ │ BlockScanner (historical queries) │ │
|
|
696
|
+
│ │ └── ExplorerClient (API adapter) │ │
|
|
697
|
+
│ └────────────────────────────────────────────────────┘ │
|
|
698
|
+
│ │
|
|
699
|
+
│ ┌────────────────────────────────────────────────────┐ │
|
|
700
|
+
│ │ TransactionTracer (debug_traceTransaction) │ │
|
|
701
|
+
│ │ ├── CallTreeParser (callTracer output) │ │
|
|
702
|
+
│ │ └── StateDiffParser (prestateTracer output) │ │
|
|
703
|
+
│ └────────────────────────────────────────────────────┘ │
|
|
704
|
+
│ │
|
|
705
|
+
│ ┌────────────────────┐ ┌───────────────────────────┐ │
|
|
706
|
+
│ │ RPC Client │ │ Utilities │ │
|
|
707
|
+
│ │ (retry, timeout, │ │ (address normalize, │ │
|
|
708
|
+
│ │ fallback, WS) │ │ format, logger, cache) │ │
|
|
709
|
+
│ └────────────────────┘ └───────────────────────────┘ │
|
|
710
|
+
└──────────────────────────────────────────────────────────┘
|
|
711
|
+
```
|
|
712
|
+
|
|
713
|
+
**Detection pipeline:**
|
|
714
|
+
|
|
715
|
+
```
|
|
716
|
+
Events (eth_subscribe + eth_getLogs)
|
|
717
|
+
↓ collect tx hashes
|
|
718
|
+
XDCScan (txlist + txlistinternal)
|
|
719
|
+
↓ merge + deduplicate
|
|
720
|
+
ContractInteraction records
|
|
721
|
+
↓ optionally
|
|
722
|
+
debug_traceTransaction (per tx hash)
|
|
723
|
+
↓ parse call tree + state diffs
|
|
724
|
+
Full execution story
|
|
725
|
+
```
|
|
726
|
+
|
|
727
|
+
---
|
|
728
|
+
|
|
729
|
+
## Examples
|
|
730
|
+
|
|
731
|
+
See the `examples/` directory for runnable scripts:
|
|
732
|
+
|
|
733
|
+
| Example | What it demonstrates |
|
|
734
|
+
| ------------------------------ | ------------------------------------------ |
|
|
735
|
+
| `basic-watcher.ts` | Real-time event monitoring with checkpoint |
|
|
736
|
+
| `historical-scan.ts` | Scanning a block range for events |
|
|
737
|
+
| `trace-transaction.ts` | Deep-diving a single transaction |
|
|
738
|
+
| `full-interaction-detector.ts` | All 3 methods combined |
|
|
739
|
+
|
|
740
|
+
Run any example with:
|
|
741
|
+
|
|
742
|
+
```bash
|
|
743
|
+
npx tsx examples/basic-watcher.ts
|
|
744
|
+
npx tsx examples/trace-transaction.ts 0xYourTxHash
|
|
745
|
+
```
|
|
746
|
+
|
|
747
|
+
---
|
|
748
|
+
|
|
749
|
+
## Development
|
|
750
|
+
|
|
751
|
+
```bash
|
|
752
|
+
# Install dependencies
|
|
753
|
+
npm install
|
|
754
|
+
|
|
755
|
+
# Compile TypeScript
|
|
756
|
+
npm run build
|
|
757
|
+
|
|
758
|
+
# Run unit tests (23 tests)
|
|
759
|
+
npm test
|
|
760
|
+
|
|
761
|
+
# Watch mode (auto-recompile on save)
|
|
762
|
+
npm run dev
|
|
763
|
+
```
|
|
764
|
+
|
|
765
|
+
## License
|
|
766
|
+
|
|
767
|
+
MIT
|