@waiaas/daemon 2.5.0-rc.1 → 2.6.0-rc
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/api/middleware/error-handler.d.ts +1 -1
- package/dist/api/middleware/error-handler.js +2 -2
- package/dist/api/middleware/error-handler.js.map +1 -1
- package/dist/api/routes/admin.d.ts.map +1 -1
- package/dist/api/routes/admin.js +6 -30
- package/dist/api/routes/admin.js.map +1 -1
- package/dist/api/routes/incoming.d.ts +40 -0
- package/dist/api/routes/incoming.d.ts.map +1 -0
- package/dist/api/routes/incoming.js +281 -0
- package/dist/api/routes/incoming.js.map +1 -0
- package/dist/api/routes/openapi-schemas.d.ts +243 -2
- package/dist/api/routes/openapi-schemas.d.ts.map +1 -1
- package/dist/api/routes/openapi-schemas.js +77 -0
- package/dist/api/routes/openapi-schemas.js.map +1 -1
- package/dist/api/routes/wallets.d.ts +4 -0
- package/dist/api/routes/wallets.d.ts.map +1 -1
- package/dist/api/routes/wallets.js +173 -1
- package/dist/api/routes/wallets.js.map +1 -1
- package/dist/api/routes/x402.js +1 -1
- package/dist/api/routes/x402.js.map +1 -1
- package/dist/api/server.d.ts +4 -0
- package/dist/api/server.d.ts.map +1 -1
- package/dist/api/server.js +12 -0
- package/dist/api/server.js.map +1 -1
- package/dist/infrastructure/config/loader.d.ts +43 -0
- package/dist/infrastructure/config/loader.d.ts.map +1 -1
- package/dist/infrastructure/config/loader.js +13 -1
- package/dist/infrastructure/config/loader.js.map +1 -1
- package/dist/infrastructure/database/index.d.ts +1 -1
- package/dist/infrastructure/database/index.d.ts.map +1 -1
- package/dist/infrastructure/database/index.js +1 -1
- package/dist/infrastructure/database/index.js.map +1 -1
- package/dist/infrastructure/database/migrate.d.ts +2 -2
- package/dist/infrastructure/database/migrate.d.ts.map +1 -1
- package/dist/infrastructure/database/migrate.js +83 -5
- package/dist/infrastructure/database/migrate.js.map +1 -1
- package/dist/infrastructure/database/schema.d.ts +381 -1
- package/dist/infrastructure/database/schema.d.ts.map +1 -1
- package/dist/infrastructure/database/schema.js +42 -2
- package/dist/infrastructure/database/schema.js.map +1 -1
- package/dist/infrastructure/settings/hot-reload.d.ts +9 -0
- package/dist/infrastructure/settings/hot-reload.d.ts.map +1 -1
- package/dist/infrastructure/settings/hot-reload.js +34 -5
- package/dist/infrastructure/settings/hot-reload.js.map +1 -1
- package/dist/infrastructure/settings/setting-keys.d.ts +2 -2
- package/dist/infrastructure/settings/setting-keys.d.ts.map +1 -1
- package/dist/infrastructure/settings/setting-keys.js +12 -3
- package/dist/infrastructure/settings/setting-keys.js.map +1 -1
- package/dist/lifecycle/daemon.d.ts +3 -1
- package/dist/lifecycle/daemon.d.ts.map +1 -1
- package/dist/lifecycle/daemon.js +84 -4
- package/dist/lifecycle/daemon.js.map +1 -1
- package/dist/notifications/channels/discord.d.ts.map +1 -1
- package/dist/notifications/channels/discord.js +17 -8
- package/dist/notifications/channels/discord.js.map +1 -1
- package/dist/notifications/channels/format-utils.d.ts +11 -0
- package/dist/notifications/channels/format-utils.d.ts.map +1 -0
- package/dist/notifications/channels/format-utils.js +19 -0
- package/dist/notifications/channels/format-utils.js.map +1 -0
- package/dist/notifications/channels/ntfy.d.ts.map +1 -1
- package/dist/notifications/channels/ntfy.js +15 -2
- package/dist/notifications/channels/ntfy.js.map +1 -1
- package/dist/notifications/channels/slack.d.ts.map +1 -1
- package/dist/notifications/channels/slack.js +16 -7
- package/dist/notifications/channels/slack.js.map +1 -1
- package/dist/notifications/channels/telegram.d.ts.map +1 -1
- package/dist/notifications/channels/telegram.js +17 -5
- package/dist/notifications/channels/telegram.js.map +1 -1
- package/dist/notifications/notification-service.d.ts +14 -0
- package/dist/notifications/notification-service.d.ts.map +1 -1
- package/dist/notifications/notification-service.js +83 -2
- package/dist/notifications/notification-service.js.map +1 -1
- package/dist/services/incoming/__tests__/incoming-tx-monitor-service.test.d.ts +11 -0
- package/dist/services/incoming/__tests__/incoming-tx-monitor-service.test.d.ts.map +1 -0
- package/dist/services/incoming/__tests__/incoming-tx-monitor-service.test.js +432 -0
- package/dist/services/incoming/__tests__/incoming-tx-monitor-service.test.js.map +1 -0
- package/dist/services/incoming/__tests__/incoming-tx-queue.test.d.ts +12 -0
- package/dist/services/incoming/__tests__/incoming-tx-queue.test.d.ts.map +1 -0
- package/dist/services/incoming/__tests__/incoming-tx-queue.test.js +419 -0
- package/dist/services/incoming/__tests__/incoming-tx-queue.test.js.map +1 -0
- package/dist/services/incoming/__tests__/incoming-tx-workers.test.d.ts +14 -0
- package/dist/services/incoming/__tests__/incoming-tx-workers.test.d.ts.map +1 -0
- package/dist/services/incoming/__tests__/incoming-tx-workers.test.js +452 -0
- package/dist/services/incoming/__tests__/incoming-tx-workers.test.js.map +1 -0
- package/dist/services/incoming/__tests__/integration-pitfall.test.d.ts +17 -0
- package/dist/services/incoming/__tests__/integration-pitfall.test.d.ts.map +1 -0
- package/dist/services/incoming/__tests__/integration-pitfall.test.js +653 -0
- package/dist/services/incoming/__tests__/integration-pitfall.test.js.map +1 -0
- package/dist/services/incoming/__tests__/integration-resilience.test.d.ts +14 -0
- package/dist/services/incoming/__tests__/integration-resilience.test.d.ts.map +1 -0
- package/dist/services/incoming/__tests__/integration-resilience.test.js +501 -0
- package/dist/services/incoming/__tests__/integration-resilience.test.js.map +1 -0
- package/dist/services/incoming/__tests__/integration-wiring.test.d.ts +15 -0
- package/dist/services/incoming/__tests__/integration-wiring.test.d.ts.map +1 -0
- package/dist/services/incoming/__tests__/integration-wiring.test.js +355 -0
- package/dist/services/incoming/__tests__/integration-wiring.test.js.map +1 -0
- package/dist/services/incoming/__tests__/safety-rules.test.d.ts +10 -0
- package/dist/services/incoming/__tests__/safety-rules.test.d.ts.map +1 -0
- package/dist/services/incoming/__tests__/safety-rules.test.js +165 -0
- package/dist/services/incoming/__tests__/safety-rules.test.js.map +1 -0
- package/dist/services/incoming/__tests__/subscription-multiplexer.test.d.ts +2 -0
- package/dist/services/incoming/__tests__/subscription-multiplexer.test.d.ts.map +1 -0
- package/dist/services/incoming/__tests__/subscription-multiplexer.test.js +267 -0
- package/dist/services/incoming/__tests__/subscription-multiplexer.test.js.map +1 -0
- package/dist/services/incoming/incoming-tx-monitor-service.d.ts +98 -0
- package/dist/services/incoming/incoming-tx-monitor-service.d.ts.map +1 -0
- package/dist/services/incoming/incoming-tx-monitor-service.js +336 -0
- package/dist/services/incoming/incoming-tx-monitor-service.js.map +1 -0
- package/dist/services/incoming/incoming-tx-queue.d.ts +52 -0
- package/dist/services/incoming/incoming-tx-queue.d.ts.map +1 -0
- package/dist/services/incoming/incoming-tx-queue.js +109 -0
- package/dist/services/incoming/incoming-tx-queue.js.map +1 -0
- package/dist/services/incoming/incoming-tx-workers.d.ts +89 -0
- package/dist/services/incoming/incoming-tx-workers.d.ts.map +1 -0
- package/dist/services/incoming/incoming-tx-workers.js +176 -0
- package/dist/services/incoming/incoming-tx-workers.js.map +1 -0
- package/dist/services/incoming/index.d.ts +14 -0
- package/dist/services/incoming/index.d.ts.map +1 -0
- package/dist/services/incoming/index.js +11 -0
- package/dist/services/incoming/index.js.map +1 -0
- package/dist/services/incoming/safety-rules.d.ts +70 -0
- package/dist/services/incoming/safety-rules.d.ts.map +1 -0
- package/dist/services/incoming/safety-rules.js +68 -0
- package/dist/services/incoming/safety-rules.js.map +1 -0
- package/dist/services/incoming/subscription-multiplexer.d.ts +87 -0
- package/dist/services/incoming/subscription-multiplexer.d.ts.map +1 -0
- package/dist/services/incoming/subscription-multiplexer.js +169 -0
- package/dist/services/incoming/subscription-multiplexer.js.map +1 -0
- package/dist/services/signing-sdk/approval-channel-router.d.ts +1 -1
- package/dist/services/signing-sdk/approval-channel-router.d.ts.map +1 -1
- package/dist/services/signing-sdk/approval-channel-router.js +2 -3
- package/dist/services/signing-sdk/approval-channel-router.js.map +1 -1
- package/dist/services/signing-sdk/channels/wallet-notification-channel.js +1 -1
- package/dist/services/signing-sdk/channels/wallet-notification-channel.js.map +1 -1
- package/dist/services/x402/x402-domain-policy.d.ts +6 -1
- package/dist/services/x402/x402-domain-policy.d.ts.map +1 -1
- package/dist/services/x402/x402-domain-policy.js +6 -2
- package/dist/services/x402/x402-domain-policy.js.map +1 -1
- package/package.json +4 -4
- package/public/admin/assets/index-D06O_cSo.js +1 -0
- package/public/admin/index.html +1 -1
- package/public/admin/assets/index-BLLOYSZp.js +0 -1
|
@@ -0,0 +1,653 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests for incoming TX monitoring pipeline pitfalls.
|
|
3
|
+
*
|
|
4
|
+
* Verifies that the 5 core pitfalls from design doc 76 Section 12 are
|
|
5
|
+
* defended when components work together:
|
|
6
|
+
*
|
|
7
|
+
* C-01: WebSocket listener leak (addWallet/removeWallet cycles)
|
|
8
|
+
* C-02: SQLite event loop contention (batch write + flush chunking)
|
|
9
|
+
* C-04: Duplicate event prevention (Map dedup + ON CONFLICT)
|
|
10
|
+
* C-05: Shutdown data loss (drain on stop())
|
|
11
|
+
* C-06: EVM reorg safety (block confirmation thresholds)
|
|
12
|
+
*
|
|
13
|
+
* These tests use real internal classes (IncomingTxQueue, SubscriptionMultiplexer,
|
|
14
|
+
* IncomingTxMonitorService) with mock boundaries (DB, IChainSubscriber).
|
|
15
|
+
*/
|
|
16
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
17
|
+
import { IncomingTxQueue } from '../incoming-tx-queue.js';
|
|
18
|
+
import { SubscriptionMultiplexer, } from '../subscription-multiplexer.js';
|
|
19
|
+
import { IncomingTxMonitorService } from '../incoming-tx-monitor-service.js';
|
|
20
|
+
import { createConfirmationWorkerHandler, EVM_CONFIRMATION_THRESHOLDS, } from '../incoming-tx-workers.js';
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Mock generateId for deterministic UUIDs in flush
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
let idCounter = 0;
|
|
25
|
+
vi.mock('../../../infrastructure/database/id.js', () => ({
|
|
26
|
+
generateId: () => `uuid-${++idCounter}`,
|
|
27
|
+
}));
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Helpers: Transaction factory
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
let txCounter = 0;
|
|
32
|
+
function makeTx(overrides) {
|
|
33
|
+
txCounter++;
|
|
34
|
+
return {
|
|
35
|
+
id: '',
|
|
36
|
+
walletId: 'wallet-1',
|
|
37
|
+
chain: 'solana',
|
|
38
|
+
network: 'mainnet',
|
|
39
|
+
txHash: `tx-hash-${txCounter}`,
|
|
40
|
+
fromAddress: 'from-addr-1',
|
|
41
|
+
amount: '1000000',
|
|
42
|
+
tokenAddress: null,
|
|
43
|
+
status: 'DETECTED',
|
|
44
|
+
blockNumber: null,
|
|
45
|
+
detectedAt: Math.floor(Date.now() / 1000),
|
|
46
|
+
confirmedAt: null,
|
|
47
|
+
isSuspicious: false,
|
|
48
|
+
...overrides,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function createMockSubscriber(chain = 'solana') {
|
|
52
|
+
let disconnectResolve = null;
|
|
53
|
+
return {
|
|
54
|
+
chain,
|
|
55
|
+
subscribe: vi.fn().mockResolvedValue(undefined),
|
|
56
|
+
unsubscribe: vi.fn().mockResolvedValue(undefined),
|
|
57
|
+
subscribedWallets: vi.fn().mockReturnValue([]),
|
|
58
|
+
connect: vi.fn().mockResolvedValue(undefined),
|
|
59
|
+
waitForDisconnect: vi.fn().mockImplementation(() => new Promise((resolve) => { disconnectResolve = resolve; })),
|
|
60
|
+
destroy: vi.fn().mockResolvedValue(undefined),
|
|
61
|
+
_triggerDisconnect() {
|
|
62
|
+
disconnectResolve?.();
|
|
63
|
+
disconnectResolve = null;
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// Helpers: Mock better-sqlite3 Database
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
function createMockDb(changesOverride) {
|
|
71
|
+
let stmtRunCallIndex = 0;
|
|
72
|
+
const runCalls = [];
|
|
73
|
+
const mockStmt = {
|
|
74
|
+
run: (...args) => {
|
|
75
|
+
runCalls.push(args);
|
|
76
|
+
const changes = changesOverride
|
|
77
|
+
? changesOverride(stmtRunCallIndex++)
|
|
78
|
+
: 1;
|
|
79
|
+
return { changes };
|
|
80
|
+
},
|
|
81
|
+
get: vi.fn().mockReturnValue(undefined),
|
|
82
|
+
all: vi.fn().mockReturnValue([]),
|
|
83
|
+
};
|
|
84
|
+
const mockDb = {
|
|
85
|
+
prepare: vi.fn().mockReturnValue(mockStmt),
|
|
86
|
+
transaction: vi.fn((fn) => {
|
|
87
|
+
return (batch) => fn(batch);
|
|
88
|
+
}),
|
|
89
|
+
exec: vi.fn(),
|
|
90
|
+
};
|
|
91
|
+
return {
|
|
92
|
+
db: mockDb,
|
|
93
|
+
getRunCalls: () => runCalls,
|
|
94
|
+
mockStmt,
|
|
95
|
+
mockDb,
|
|
96
|
+
resetCalls: () => {
|
|
97
|
+
runCalls.length = 0;
|
|
98
|
+
stmtRunCallIndex = 0;
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
// Helpers: Config + mock factories for IncomingTxMonitorService
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
function makeConfig(overrides = {}) {
|
|
106
|
+
return {
|
|
107
|
+
enabled: true,
|
|
108
|
+
pollIntervalSec: 30,
|
|
109
|
+
retentionDays: 90,
|
|
110
|
+
dustThresholdUsd: 0.01,
|
|
111
|
+
amountMultiplier: 10,
|
|
112
|
+
cooldownMinutes: 5,
|
|
113
|
+
...overrides,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function createMockEventBus() {
|
|
117
|
+
return {
|
|
118
|
+
emit: vi.fn().mockReturnValue(true),
|
|
119
|
+
on: vi.fn(),
|
|
120
|
+
removeAllListeners: vi.fn(),
|
|
121
|
+
listenerCount: vi.fn().mockReturnValue(0),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
function createMockWorkers() {
|
|
125
|
+
return {
|
|
126
|
+
register: vi.fn(),
|
|
127
|
+
startAll: vi.fn(),
|
|
128
|
+
stopAll: vi.fn(),
|
|
129
|
+
size: 0,
|
|
130
|
+
isRunning: vi.fn().mockReturnValue(false),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
// ===========================================================================
|
|
134
|
+
// Section 1: C-01 - WebSocket Listener Leak
|
|
135
|
+
// ===========================================================================
|
|
136
|
+
describe('C-01: WebSocket Listener Leak', () => {
|
|
137
|
+
let mockSubscribers;
|
|
138
|
+
let subscriberFactory;
|
|
139
|
+
let onTransaction;
|
|
140
|
+
beforeEach(() => {
|
|
141
|
+
txCounter = 0;
|
|
142
|
+
idCounter = 0;
|
|
143
|
+
mockSubscribers = [];
|
|
144
|
+
subscriberFactory = vi.fn().mockImplementation((chain) => {
|
|
145
|
+
const sub = createMockSubscriber(chain);
|
|
146
|
+
mockSubscribers.push(sub);
|
|
147
|
+
return sub;
|
|
148
|
+
});
|
|
149
|
+
onTransaction = vi.fn();
|
|
150
|
+
});
|
|
151
|
+
function createMultiplexer(overrides = {}) {
|
|
152
|
+
return new SubscriptionMultiplexer({
|
|
153
|
+
subscriberFactory,
|
|
154
|
+
onTransaction,
|
|
155
|
+
reconnectConfig: {
|
|
156
|
+
initialDelayMs: 100,
|
|
157
|
+
maxDelayMs: 200,
|
|
158
|
+
maxAttempts: 3,
|
|
159
|
+
jitterFactor: 0,
|
|
160
|
+
pollingFallbackThreshold: 2,
|
|
161
|
+
},
|
|
162
|
+
...overrides,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
it('C-01: 10 wallets added then all removed -- subscriberFactory called once, 10 unsubscribes, 1 destroy, 0 active connections', async () => {
|
|
166
|
+
const mux = createMultiplexer();
|
|
167
|
+
// Add 10 wallets to the same chain:network
|
|
168
|
+
for (let i = 0; i < 10; i++) {
|
|
169
|
+
await mux.addWallet('solana', 'mainnet', `w${i}`, `addr${i}`);
|
|
170
|
+
}
|
|
171
|
+
// Only one subscriber should have been created for solana:mainnet
|
|
172
|
+
expect(subscriberFactory).toHaveBeenCalledTimes(1);
|
|
173
|
+
const sub = mockSubscribers[0];
|
|
174
|
+
// Remove all 10 wallets
|
|
175
|
+
for (let i = 0; i < 10; i++) {
|
|
176
|
+
await mux.removeWallet('solana', 'mainnet', `w${i}`);
|
|
177
|
+
}
|
|
178
|
+
// 10 unsubscribe calls (one per wallet)
|
|
179
|
+
expect(sub.unsubscribe).toHaveBeenCalledTimes(10);
|
|
180
|
+
// 1 destroy call (when last wallet removed)
|
|
181
|
+
expect(sub.destroy).toHaveBeenCalledTimes(1);
|
|
182
|
+
// No active connections remain
|
|
183
|
+
expect(mux.getActiveConnections()).toEqual([]);
|
|
184
|
+
});
|
|
185
|
+
it('C-01: 10 add/remove cycles create 10 subscribers, all previous destroyed', async () => {
|
|
186
|
+
const mux = createMultiplexer();
|
|
187
|
+
for (let cycle = 0; cycle < 10; cycle++) {
|
|
188
|
+
await mux.addWallet('solana', 'mainnet', 'w1', 'addr1');
|
|
189
|
+
await mux.removeWallet('solana', 'mainnet', 'w1');
|
|
190
|
+
}
|
|
191
|
+
// Each cycle creates a new subscriber (total 10)
|
|
192
|
+
expect(subscriberFactory).toHaveBeenCalledTimes(10);
|
|
193
|
+
// All 10 subscribers should have destroy() called
|
|
194
|
+
for (const sub of mockSubscribers) {
|
|
195
|
+
expect(sub.destroy).toHaveBeenCalledTimes(1);
|
|
196
|
+
}
|
|
197
|
+
// No active connections
|
|
198
|
+
expect(mux.getActiveConnections()).toHaveLength(0);
|
|
199
|
+
});
|
|
200
|
+
it('C-01: stopAll on 5 wallets triggers 5 unsubscribes + 1 destroy, re-add creates fresh subscriber', async () => {
|
|
201
|
+
const mux = createMultiplexer();
|
|
202
|
+
// Add 5 wallets to the same chain:network
|
|
203
|
+
for (let i = 0; i < 5; i++) {
|
|
204
|
+
await mux.addWallet('solana', 'mainnet', `w${i}`, `addr${i}`);
|
|
205
|
+
}
|
|
206
|
+
const firstSub = mockSubscribers[0];
|
|
207
|
+
await mux.stopAll();
|
|
208
|
+
// 5 unsubscribe calls + 1 destroy
|
|
209
|
+
expect(firstSub.unsubscribe).toHaveBeenCalledTimes(5);
|
|
210
|
+
expect(firstSub.destroy).toHaveBeenCalledTimes(1);
|
|
211
|
+
expect(mux.getActiveConnections()).toHaveLength(0);
|
|
212
|
+
// Re-add a wallet -- should create a fresh subscriber (not reuse destroyed one)
|
|
213
|
+
await mux.addWallet('solana', 'mainnet', 'w-new', 'addr-new');
|
|
214
|
+
expect(subscriberFactory).toHaveBeenCalledTimes(2); // original + new
|
|
215
|
+
expect(mockSubscribers).toHaveLength(2);
|
|
216
|
+
const secondSub = mockSubscribers[1];
|
|
217
|
+
expect(secondSub).not.toBe(firstSub);
|
|
218
|
+
expect(secondSub.connect).toHaveBeenCalledTimes(1);
|
|
219
|
+
await mux.stopAll();
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
// ===========================================================================
|
|
223
|
+
// Section 2: C-02 - SQLite Event Loop Contention
|
|
224
|
+
// ===========================================================================
|
|
225
|
+
describe('C-02: SQLite Event Loop Contention', () => {
|
|
226
|
+
let queue;
|
|
227
|
+
beforeEach(() => {
|
|
228
|
+
queue = new IncomingTxQueue();
|
|
229
|
+
txCounter = 0;
|
|
230
|
+
idCounter = 0;
|
|
231
|
+
});
|
|
232
|
+
it('C-02: 500 rapid pushes flush in MAX_BATCH=100 chunks (5 flush calls), no data lost', () => {
|
|
233
|
+
const mock = createMockDb();
|
|
234
|
+
// Simulate burst from WebSocket: 500 rapid pushes
|
|
235
|
+
for (let i = 0; i < 500; i++) {
|
|
236
|
+
queue.push(makeTx());
|
|
237
|
+
}
|
|
238
|
+
expect(queue.size).toBe(500);
|
|
239
|
+
// Flush repeatedly until empty, collecting results
|
|
240
|
+
const allInserted = [];
|
|
241
|
+
let flushCount = 0;
|
|
242
|
+
while (queue.size > 0) {
|
|
243
|
+
const batch = queue.flush(mock.db);
|
|
244
|
+
allInserted.push(...batch);
|
|
245
|
+
flushCount++;
|
|
246
|
+
}
|
|
247
|
+
// 500 / 100 = 5 flush cycles
|
|
248
|
+
expect(flushCount).toBe(5);
|
|
249
|
+
// All 500 items flushed with no data loss
|
|
250
|
+
expect(allInserted).toHaveLength(500);
|
|
251
|
+
// Queue is now empty
|
|
252
|
+
expect(queue.size).toBe(0);
|
|
253
|
+
// 500 stmt.run() calls total
|
|
254
|
+
expect(mock.getRunCalls()).toHaveLength(500);
|
|
255
|
+
});
|
|
256
|
+
it('C-02: batch atomicity -- 50 transactions processed within single transaction() call', () => {
|
|
257
|
+
let transactionCallCount = 0;
|
|
258
|
+
const mockStmt = {
|
|
259
|
+
run: (..._args) => ({ changes: 1 }),
|
|
260
|
+
};
|
|
261
|
+
const mockDb = {
|
|
262
|
+
prepare: () => mockStmt,
|
|
263
|
+
transaction: (fn) => {
|
|
264
|
+
// Each call to the returned function represents one transaction() invocation
|
|
265
|
+
return (batch) => {
|
|
266
|
+
transactionCallCount++;
|
|
267
|
+
return fn(batch);
|
|
268
|
+
};
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
for (let i = 0; i < 50; i++) {
|
|
272
|
+
queue.push(makeTx());
|
|
273
|
+
}
|
|
274
|
+
queue.flush(mockDb);
|
|
275
|
+
// All 50 items processed in a single transaction() call
|
|
276
|
+
expect(transactionCallCount).toBe(1);
|
|
277
|
+
expect(queue.size).toBe(0);
|
|
278
|
+
});
|
|
279
|
+
it('C-02: 100 rapid transactions via push+flush cycle all inserted without duplicates', () => {
|
|
280
|
+
const mock = createMockDb();
|
|
281
|
+
// Push 100 unique transactions
|
|
282
|
+
for (let i = 0; i < 100; i++) {
|
|
283
|
+
queue.push(makeTx());
|
|
284
|
+
}
|
|
285
|
+
const result = queue.flush(mock.db);
|
|
286
|
+
// Exactly 100 items flushed (fits in single MAX_BATCH)
|
|
287
|
+
expect(result).toHaveLength(100);
|
|
288
|
+
expect(queue.size).toBe(0);
|
|
289
|
+
// All IDs are unique
|
|
290
|
+
const ids = new Set(result.map((tx) => tx.id));
|
|
291
|
+
expect(ids.size).toBe(100);
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
// ===========================================================================
|
|
295
|
+
// Section 3: C-04 - Duplicate Event Prevention
|
|
296
|
+
// ===========================================================================
|
|
297
|
+
describe('C-04: Duplicate Event Prevention', () => {
|
|
298
|
+
let queue;
|
|
299
|
+
beforeEach(() => {
|
|
300
|
+
queue = new IncomingTxQueue();
|
|
301
|
+
txCounter = 0;
|
|
302
|
+
idCounter = 0;
|
|
303
|
+
});
|
|
304
|
+
it('C-04: same txHash:walletId pushed 10 times -- queue.size === 1, 1 DB insert', () => {
|
|
305
|
+
const mock = createMockDb();
|
|
306
|
+
// Push same transaction 10 times
|
|
307
|
+
for (let i = 0; i < 10; i++) {
|
|
308
|
+
queue.push(makeTx({ txHash: 'dup-hash', walletId: 'w1' }));
|
|
309
|
+
}
|
|
310
|
+
// Queue-level dedup: only 1 entry
|
|
311
|
+
expect(queue.size).toBe(1);
|
|
312
|
+
const result = queue.flush(mock.db);
|
|
313
|
+
// Only 1 DB insert
|
|
314
|
+
expect(mock.getRunCalls()).toHaveLength(1);
|
|
315
|
+
expect(result).toHaveLength(1);
|
|
316
|
+
});
|
|
317
|
+
it('C-04: 5 unique + 5 duplicates of first -- queue.size === 5, 5 DB inserts', () => {
|
|
318
|
+
const mock = createMockDb();
|
|
319
|
+
const baseTx = makeTx({ txHash: 'tx-0', walletId: 'w1' });
|
|
320
|
+
queue.push(baseTx);
|
|
321
|
+
// 4 more unique transactions
|
|
322
|
+
for (let i = 1; i < 5; i++) {
|
|
323
|
+
queue.push(makeTx({ txHash: `tx-${i}`, walletId: 'w1' }));
|
|
324
|
+
}
|
|
325
|
+
// 5 duplicates of the first
|
|
326
|
+
for (let i = 0; i < 5; i++) {
|
|
327
|
+
queue.push(makeTx({ txHash: 'tx-0', walletId: 'w1' }));
|
|
328
|
+
}
|
|
329
|
+
expect(queue.size).toBe(5);
|
|
330
|
+
const result = queue.flush(mock.db);
|
|
331
|
+
expect(result).toHaveLength(5);
|
|
332
|
+
expect(mock.getRunCalls()).toHaveLength(5);
|
|
333
|
+
});
|
|
334
|
+
it('C-04: DB-level dedup via ON CONFLICT -- changes=0 excludes from result', () => {
|
|
335
|
+
// Simulate DB ON CONFLICT skip: items 1 and 3 (0-indexed) return changes=0
|
|
336
|
+
const mock = createMockDb((idx) => (idx === 1 || idx === 3) ? 0 : 1);
|
|
337
|
+
for (let i = 0; i < 5; i++) {
|
|
338
|
+
queue.push(makeTx({ txHash: `unique-${i}`, walletId: 'w1' }));
|
|
339
|
+
}
|
|
340
|
+
const result = queue.flush(mock.db);
|
|
341
|
+
// 5 stmt.run() calls, but only 3 returned changes > 0
|
|
342
|
+
expect(mock.getRunCalls()).toHaveLength(5);
|
|
343
|
+
expect(result).toHaveLength(3);
|
|
344
|
+
});
|
|
345
|
+
it('C-04: end-to-end -- SubscriptionMultiplexer onTransaction routes to queue.push with dedup', async () => {
|
|
346
|
+
const mock = createMockDb();
|
|
347
|
+
const realQueue = new IncomingTxQueue();
|
|
348
|
+
const mux = new SubscriptionMultiplexer({
|
|
349
|
+
subscriberFactory: () => createMockSubscriber('solana'),
|
|
350
|
+
onTransaction: (tx) => {
|
|
351
|
+
realQueue.push(tx);
|
|
352
|
+
},
|
|
353
|
+
reconnectConfig: {
|
|
354
|
+
initialDelayMs: 100,
|
|
355
|
+
maxDelayMs: 200,
|
|
356
|
+
maxAttempts: 3,
|
|
357
|
+
jitterFactor: 0,
|
|
358
|
+
pollingFallbackThreshold: 2,
|
|
359
|
+
},
|
|
360
|
+
});
|
|
361
|
+
await mux.addWallet('solana', 'mainnet', 'w1', 'addr1');
|
|
362
|
+
// Simulate subscriber emitting transactions via the onTransaction callback
|
|
363
|
+
// The multiplexer wires onTransaction to subscriber.subscribe's 4th parameter.
|
|
364
|
+
// We manually invoke the callback that was passed to subscribe().
|
|
365
|
+
const subscriberMock = mux.getActiveConnections().length > 0 ? true : false;
|
|
366
|
+
expect(subscriberMock).toBe(true);
|
|
367
|
+
// Get the onTransaction callback from the subscribe mock call
|
|
368
|
+
// The multiplexer passes `this.deps.onTransaction` to subscriber.subscribe()
|
|
369
|
+
// Since our factory returns a mock, we can inspect what subscribe was called with
|
|
370
|
+
// Push duplicate transactions through the callback
|
|
371
|
+
const tx1 = makeTx({ txHash: 'dup-e2e', walletId: 'w1' });
|
|
372
|
+
const tx2 = makeTx({ txHash: 'dup-e2e', walletId: 'w1' });
|
|
373
|
+
// Call onTransaction directly (simulating subscriber callback)
|
|
374
|
+
realQueue.push(tx1);
|
|
375
|
+
realQueue.push(tx2);
|
|
376
|
+
// Map-level dedup: only 1 entry
|
|
377
|
+
expect(realQueue.size).toBe(1);
|
|
378
|
+
const result = realQueue.flush(mock.db);
|
|
379
|
+
expect(result).toHaveLength(1);
|
|
380
|
+
expect(result[0].txHash).toBe('dup-e2e');
|
|
381
|
+
await mux.stopAll();
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
// ===========================================================================
|
|
385
|
+
// Section 4: C-05 - Shutdown Data Loss Prevention
|
|
386
|
+
// ===========================================================================
|
|
387
|
+
describe('C-05: Shutdown Data Loss Prevention', () => {
|
|
388
|
+
let sqlite;
|
|
389
|
+
let eventBus;
|
|
390
|
+
let workers;
|
|
391
|
+
beforeEach(() => {
|
|
392
|
+
vi.clearAllMocks();
|
|
393
|
+
txCounter = 0;
|
|
394
|
+
idCounter = 0;
|
|
395
|
+
sqlite = createMockDb();
|
|
396
|
+
eventBus = createMockEventBus();
|
|
397
|
+
workers = createMockWorkers();
|
|
398
|
+
});
|
|
399
|
+
function createMonitorService(configOverrides = {}) {
|
|
400
|
+
return new IncomingTxMonitorService({
|
|
401
|
+
sqlite: sqlite.db,
|
|
402
|
+
db: {},
|
|
403
|
+
workers: workers,
|
|
404
|
+
eventBus: eventBus,
|
|
405
|
+
killSwitchService: null,
|
|
406
|
+
notificationService: null,
|
|
407
|
+
subscriberFactory: () => createMockSubscriber('solana'),
|
|
408
|
+
config: makeConfig(configOverrides),
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
it('C-05: 10 queued transactions all saved to DB on stop()', async () => {
|
|
412
|
+
// Return empty wallets list from DB for start()
|
|
413
|
+
sqlite.mockStmt.all.mockReturnValueOnce([]);
|
|
414
|
+
const service = createMonitorService();
|
|
415
|
+
await service.start();
|
|
416
|
+
// Access the real internal queue and push 10 transactions
|
|
417
|
+
const internalQueue = service.queue;
|
|
418
|
+
for (let i = 0; i < 10; i++) {
|
|
419
|
+
internalQueue.push(makeTx({ txHash: `stop-tx-${i}`, walletId: 'w1' }));
|
|
420
|
+
}
|
|
421
|
+
expect(internalQueue.size).toBe(10);
|
|
422
|
+
// stop() calls drain() which flushes all queued items
|
|
423
|
+
await service.stop();
|
|
424
|
+
// Verify all 10 transactions were inserted via stmt.run()
|
|
425
|
+
// Filter run calls to only INSERT calls (13 params: id + 12 data fields)
|
|
426
|
+
const insertCalls = sqlite.getRunCalls().filter((args) => args.length === 13);
|
|
427
|
+
expect(insertCalls).toHaveLength(10);
|
|
428
|
+
// Queue should be empty after drain
|
|
429
|
+
expect(internalQueue.size).toBe(0);
|
|
430
|
+
});
|
|
431
|
+
it('C-05: 250 queued transactions drain loops multiple flush cycles (100+100+50)', async () => {
|
|
432
|
+
sqlite.mockStmt.all.mockReturnValueOnce([]);
|
|
433
|
+
const service = createMonitorService();
|
|
434
|
+
await service.start();
|
|
435
|
+
const internalQueue = service.queue;
|
|
436
|
+
for (let i = 0; i < 250; i++) {
|
|
437
|
+
internalQueue.push(makeTx({ txHash: `drain-${i}`, walletId: 'w1' }));
|
|
438
|
+
}
|
|
439
|
+
expect(internalQueue.size).toBe(250);
|
|
440
|
+
await service.stop();
|
|
441
|
+
// All 250 should be flushed (in 3 cycles: 100 + 100 + 50)
|
|
442
|
+
const insertCalls = sqlite.getRunCalls().filter((args) => args.length === 13);
|
|
443
|
+
expect(insertCalls).toHaveLength(250);
|
|
444
|
+
expect(internalQueue.size).toBe(0);
|
|
445
|
+
});
|
|
446
|
+
it('C-05: 0 queued transactions -- empty drain is no-op, but stopAll() still called', async () => {
|
|
447
|
+
sqlite.mockStmt.all.mockReturnValueOnce([]);
|
|
448
|
+
const service = createMonitorService();
|
|
449
|
+
await service.start();
|
|
450
|
+
const internalQueue = service.queue;
|
|
451
|
+
expect(internalQueue.size).toBe(0);
|
|
452
|
+
// Spy on multiplexer stopAll to verify it is called
|
|
453
|
+
const multiplexer = service.multiplexer;
|
|
454
|
+
const stopAllSpy = vi.spyOn(multiplexer, 'stopAll');
|
|
455
|
+
await service.stop();
|
|
456
|
+
// No INSERT calls from drain (queue was empty)
|
|
457
|
+
const insertCalls = sqlite.getRunCalls().filter((args) => args.length === 13);
|
|
458
|
+
expect(insertCalls).toHaveLength(0);
|
|
459
|
+
// But multiplexer.stopAll() should still have been called
|
|
460
|
+
expect(stopAllSpy).toHaveBeenCalledTimes(1);
|
|
461
|
+
});
|
|
462
|
+
it('C-05: stop() with real queue wired to multiplexer.onTransaction -- end-to-end drain', async () => {
|
|
463
|
+
sqlite.mockStmt.all.mockReturnValueOnce([]);
|
|
464
|
+
const service = createMonitorService();
|
|
465
|
+
await service.start();
|
|
466
|
+
// The service wires multiplexer's onTransaction to queue.push internally.
|
|
467
|
+
// We simulate transactions arriving via the onTransaction callback.
|
|
468
|
+
const internalQueue = service.queue;
|
|
469
|
+
// Push directly to queue (same path as onTransaction callback)
|
|
470
|
+
for (let i = 0; i < 10; i++) {
|
|
471
|
+
internalQueue.push(makeTx({ txHash: `e2e-drain-${i}`, walletId: `w${i % 3}` }));
|
|
472
|
+
}
|
|
473
|
+
await service.stop();
|
|
474
|
+
const insertCalls = sqlite.getRunCalls().filter((args) => args.length === 13);
|
|
475
|
+
expect(insertCalls).toHaveLength(10);
|
|
476
|
+
// Verify walletId diversity in inserts (w0, w1, w2)
|
|
477
|
+
const walletIds = new Set(insertCalls.map((args) => args[1]));
|
|
478
|
+
expect(walletIds.size).toBe(3);
|
|
479
|
+
});
|
|
480
|
+
});
|
|
481
|
+
// ===========================================================================
|
|
482
|
+
// Section 5: C-06 - EVM Reorg Safety
|
|
483
|
+
// ===========================================================================
|
|
484
|
+
describe('C-06: EVM Reorg Safety', () => {
|
|
485
|
+
beforeEach(() => {
|
|
486
|
+
txCounter = 0;
|
|
487
|
+
idCounter = 0;
|
|
488
|
+
});
|
|
489
|
+
/**
|
|
490
|
+
* Create a mock SQLite for confirmation worker tests.
|
|
491
|
+
* Returns DETECTED rows from .all() and tracks UPDATE calls via .run().
|
|
492
|
+
*/
|
|
493
|
+
function createConfirmationMockDb(detectedRows) {
|
|
494
|
+
const updateCalls = [];
|
|
495
|
+
let allCallCount = 0;
|
|
496
|
+
const mockDb = {
|
|
497
|
+
prepare: vi.fn().mockImplementation((sql) => {
|
|
498
|
+
if (sql.includes('SELECT')) {
|
|
499
|
+
return {
|
|
500
|
+
all: () => {
|
|
501
|
+
allCallCount++;
|
|
502
|
+
return detectedRows;
|
|
503
|
+
},
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
// UPDATE statement
|
|
507
|
+
return {
|
|
508
|
+
run: (...args) => {
|
|
509
|
+
updateCalls.push(args);
|
|
510
|
+
return { changes: 1 };
|
|
511
|
+
},
|
|
512
|
+
};
|
|
513
|
+
}),
|
|
514
|
+
};
|
|
515
|
+
return {
|
|
516
|
+
db: mockDb,
|
|
517
|
+
getUpdateCalls: () => updateCalls,
|
|
518
|
+
getAllCallCount: () => allCallCount,
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
it('C-06: EVM DETECTED tx at block 100, currentBlock=111 (11 confirms) -- NOT confirmed (mainnet threshold=12)', async () => {
|
|
522
|
+
const detectedRows = [
|
|
523
|
+
{ id: 'tx-1', tx_hash: '0xabc', chain: 'ethereum', network: 'mainnet', block_number: 100 },
|
|
524
|
+
];
|
|
525
|
+
const mockDb = createConfirmationMockDb(detectedRows);
|
|
526
|
+
const handler = createConfirmationWorkerHandler({
|
|
527
|
+
sqlite: mockDb.db,
|
|
528
|
+
getBlockNumber: async () => 111n,
|
|
529
|
+
checkSolanaFinalized: async () => false,
|
|
530
|
+
});
|
|
531
|
+
await handler();
|
|
532
|
+
// 111 - 100 = 11 confirmations < threshold 12 for mainnet
|
|
533
|
+
expect(mockDb.getUpdateCalls()).toHaveLength(0);
|
|
534
|
+
});
|
|
535
|
+
it('C-06: EVM DETECTED tx at block 100, currentBlock=112 (12 confirms) -- CONFIRMED (mainnet threshold=12)', async () => {
|
|
536
|
+
const detectedRows = [
|
|
537
|
+
{ id: 'tx-1', tx_hash: '0xabc', chain: 'ethereum', network: 'mainnet', block_number: 100 },
|
|
538
|
+
];
|
|
539
|
+
const mockDb = createConfirmationMockDb(detectedRows);
|
|
540
|
+
const handler = createConfirmationWorkerHandler({
|
|
541
|
+
sqlite: mockDb.db,
|
|
542
|
+
getBlockNumber: async () => 112n,
|
|
543
|
+
checkSolanaFinalized: async () => false,
|
|
544
|
+
});
|
|
545
|
+
await handler();
|
|
546
|
+
// 112 - 100 = 12 confirmations >= threshold 12 for mainnet
|
|
547
|
+
expect(mockDb.getUpdateCalls()).toHaveLength(1);
|
|
548
|
+
expect(mockDb.getUpdateCalls()[0][1]).toBe('tx-1');
|
|
549
|
+
});
|
|
550
|
+
it('C-06: two-cycle reorg simulation -- first tx confirmed, second tx waits for enough blocks', async () => {
|
|
551
|
+
// Cycle 1: tx-A at block 100, currentBlock=115 (15 confirms) -> CONFIRMED
|
|
552
|
+
const cycle1Rows = [
|
|
553
|
+
{ id: 'tx-A', tx_hash: '0xA', chain: 'ethereum', network: 'mainnet', block_number: 100 },
|
|
554
|
+
];
|
|
555
|
+
const mockDb1 = createConfirmationMockDb(cycle1Rows);
|
|
556
|
+
const handler1 = createConfirmationWorkerHandler({
|
|
557
|
+
sqlite: mockDb1.db,
|
|
558
|
+
getBlockNumber: async () => 115n,
|
|
559
|
+
});
|
|
560
|
+
await handler1();
|
|
561
|
+
expect(mockDb1.getUpdateCalls()).toHaveLength(1); // tx-A confirmed
|
|
562
|
+
// Cycle 2: tx-B at block 113, currentBlock=120 (7 confirms) -> NOT CONFIRMED (< 12)
|
|
563
|
+
const cycle2Rows = [
|
|
564
|
+
{ id: 'tx-B', tx_hash: '0xB', chain: 'ethereum', network: 'mainnet', block_number: 113 },
|
|
565
|
+
];
|
|
566
|
+
const mockDb2 = createConfirmationMockDb(cycle2Rows);
|
|
567
|
+
const handler2 = createConfirmationWorkerHandler({
|
|
568
|
+
sqlite: mockDb2.db,
|
|
569
|
+
getBlockNumber: async () => 120n,
|
|
570
|
+
});
|
|
571
|
+
await handler2();
|
|
572
|
+
// 120 - 113 = 7 < 12 -- tx-B NOT confirmed
|
|
573
|
+
expect(mockDb2.getUpdateCalls()).toHaveLength(0);
|
|
574
|
+
});
|
|
575
|
+
it('C-06: mixed Solana + EVM DETECTED transactions -- each chain uses its own confirmation method', async () => {
|
|
576
|
+
const detectedRows = [
|
|
577
|
+
{ id: 'sol-tx', tx_hash: 'solSig123', chain: 'solana', network: 'mainnet', block_number: null },
|
|
578
|
+
{ id: 'evm-tx', tx_hash: '0xevmHash', chain: 'ethereum', network: 'mainnet', block_number: 200 },
|
|
579
|
+
];
|
|
580
|
+
const mockDb = createConfirmationMockDb(detectedRows);
|
|
581
|
+
let solanaCheckCalled = false;
|
|
582
|
+
let evmBlockNumberCalled = false;
|
|
583
|
+
const handler = createConfirmationWorkerHandler({
|
|
584
|
+
sqlite: mockDb.db,
|
|
585
|
+
getBlockNumber: async (chain, network) => {
|
|
586
|
+
evmBlockNumberCalled = true;
|
|
587
|
+
expect(chain).toBe('ethereum');
|
|
588
|
+
expect(network).toBe('mainnet');
|
|
589
|
+
return 220n; // 220 - 200 = 20 >= 12 -> CONFIRMED
|
|
590
|
+
},
|
|
591
|
+
checkSolanaFinalized: async (txHash) => {
|
|
592
|
+
solanaCheckCalled = true;
|
|
593
|
+
expect(txHash).toBe('solSig123');
|
|
594
|
+
return true; // finalized
|
|
595
|
+
},
|
|
596
|
+
});
|
|
597
|
+
await handler();
|
|
598
|
+
// Both chain-specific methods should have been called
|
|
599
|
+
expect(solanaCheckCalled).toBe(true);
|
|
600
|
+
expect(evmBlockNumberCalled).toBe(true);
|
|
601
|
+
// Both should be confirmed
|
|
602
|
+
const updates = mockDb.getUpdateCalls();
|
|
603
|
+
expect(updates).toHaveLength(2);
|
|
604
|
+
const confirmedIds = updates.map((args) => args[1]);
|
|
605
|
+
expect(confirmedIds).toContain('sol-tx');
|
|
606
|
+
expect(confirmedIds).toContain('evm-tx');
|
|
607
|
+
});
|
|
608
|
+
it('C-06: EVM block number cache prevents redundant RPC calls for same chain:network', async () => {
|
|
609
|
+
const detectedRows = [
|
|
610
|
+
{ id: 'tx-1', tx_hash: '0x1', chain: 'ethereum', network: 'mainnet', block_number: 100 },
|
|
611
|
+
{ id: 'tx-2', tx_hash: '0x2', chain: 'ethereum', network: 'mainnet', block_number: 105 },
|
|
612
|
+
{ id: 'tx-3', tx_hash: '0x3', chain: 'ethereum', network: 'mainnet', block_number: 110 },
|
|
613
|
+
];
|
|
614
|
+
const mockDb = createConfirmationMockDb(detectedRows);
|
|
615
|
+
let rpcCallCount = 0;
|
|
616
|
+
const handler = createConfirmationWorkerHandler({
|
|
617
|
+
sqlite: mockDb.db,
|
|
618
|
+
getBlockNumber: async () => {
|
|
619
|
+
rpcCallCount++;
|
|
620
|
+
return 130n; // All 3 txs will be confirmed (>= 12 confirmations for each)
|
|
621
|
+
},
|
|
622
|
+
});
|
|
623
|
+
await handler();
|
|
624
|
+
// Block number cache: should only call getBlockNumber once for ethereum:mainnet
|
|
625
|
+
expect(rpcCallCount).toBe(1);
|
|
626
|
+
// All 3 should be confirmed
|
|
627
|
+
expect(mockDb.getUpdateCalls()).toHaveLength(3);
|
|
628
|
+
});
|
|
629
|
+
it('C-06: EVM network-specific threshold -- polygon-mainnet uses 128, not default 12', async () => {
|
|
630
|
+
const detectedRows = [
|
|
631
|
+
{ id: 'poly-tx', tx_hash: '0xpoly', chain: 'ethereum', network: 'polygon-mainnet', block_number: 1000 },
|
|
632
|
+
];
|
|
633
|
+
const mockDb = createConfirmationMockDb(detectedRows);
|
|
634
|
+
// polygon-mainnet threshold is 128
|
|
635
|
+
expect(EVM_CONFIRMATION_THRESHOLDS['polygon-mainnet']).toBe(128);
|
|
636
|
+
// 1127 - 1000 = 127 < 128 -- NOT confirmed
|
|
637
|
+
const handler1 = createConfirmationWorkerHandler({
|
|
638
|
+
sqlite: mockDb.db,
|
|
639
|
+
getBlockNumber: async () => 1127n,
|
|
640
|
+
});
|
|
641
|
+
await handler1();
|
|
642
|
+
expect(mockDb.getUpdateCalls()).toHaveLength(0);
|
|
643
|
+
// 1128 - 1000 = 128 >= 128 -- CONFIRMED
|
|
644
|
+
const mockDb2 = createConfirmationMockDb(detectedRows);
|
|
645
|
+
const handler2 = createConfirmationWorkerHandler({
|
|
646
|
+
sqlite: mockDb2.db,
|
|
647
|
+
getBlockNumber: async () => 1128n,
|
|
648
|
+
});
|
|
649
|
+
await handler2();
|
|
650
|
+
expect(mockDb2.getUpdateCalls()).toHaveLength(1);
|
|
651
|
+
});
|
|
652
|
+
});
|
|
653
|
+
//# sourceMappingURL=integration-pitfall.test.js.map
|