@wopr-network/platform-core 1.48.0 → 1.49.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/dist/billing/crypto/__tests__/unified-checkout.test.d.ts +1 -0
- package/dist/billing/crypto/__tests__/unified-checkout.test.js +63 -0
- package/dist/billing/crypto/__tests__/watcher-service.test.d.ts +1 -0
- package/dist/billing/crypto/__tests__/watcher-service.test.js +174 -0
- package/dist/billing/crypto/__tests__/webhook-confirmations.test.d.ts +1 -0
- package/dist/billing/crypto/__tests__/webhook-confirmations.test.js +304 -0
- package/dist/billing/crypto/btc/__tests__/settler.test.js +1 -0
- package/dist/billing/crypto/btc/__tests__/watcher.test.d.ts +1 -0
- package/dist/billing/crypto/btc/__tests__/watcher.test.js +170 -0
- package/dist/billing/crypto/btc/types.d.ts +3 -1
- package/dist/billing/crypto/btc/watcher.d.ts +6 -1
- package/dist/billing/crypto/btc/watcher.js +20 -6
- package/dist/billing/crypto/charge-store.d.ts +27 -2
- package/dist/billing/crypto/charge-store.js +67 -1
- package/dist/billing/crypto/charge-store.test.js +180 -1
- package/dist/billing/crypto/client.d.ts +2 -0
- package/dist/billing/crypto/cursor-store.d.ts +10 -3
- package/dist/billing/crypto/cursor-store.js +21 -1
- package/dist/billing/crypto/evm/__tests__/eth-settler.test.js +2 -0
- package/dist/billing/crypto/evm/__tests__/eth-watcher.test.js +31 -4
- package/dist/billing/crypto/evm/__tests__/settler.test.js +2 -0
- package/dist/billing/crypto/evm/__tests__/watcher-confirmations.test.d.ts +1 -0
- package/dist/billing/crypto/evm/__tests__/watcher-confirmations.test.js +144 -0
- package/dist/billing/crypto/evm/__tests__/watcher.test.js +6 -2
- package/dist/billing/crypto/evm/eth-watcher.d.ts +11 -8
- package/dist/billing/crypto/evm/eth-watcher.js +27 -13
- package/dist/billing/crypto/evm/types.d.ts +5 -1
- package/dist/billing/crypto/evm/watcher.d.ts +9 -1
- package/dist/billing/crypto/evm/watcher.js +36 -13
- package/dist/billing/crypto/index.d.ts +3 -3
- package/dist/billing/crypto/index.js +1 -1
- package/dist/billing/crypto/key-server-webhook.d.ts +17 -4
- package/dist/billing/crypto/key-server-webhook.js +76 -15
- package/dist/billing/crypto/types.d.ts +16 -0
- package/dist/billing/crypto/unified-checkout.d.ts +8 -17
- package/dist/billing/crypto/unified-checkout.js +17 -131
- package/dist/billing/crypto/watcher-service.d.ts +22 -2
- package/dist/billing/crypto/watcher-service.js +71 -30
- package/dist/db/schema/crypto.d.ts +68 -0
- package/dist/db/schema/crypto.js +8 -0
- package/dist/monetization/crypto/__tests__/webhook.test.js +2 -1
- package/drizzle/migrations/0016_charge_progress_columns.sql +4 -0
- package/drizzle/migrations/meta/_journal.json +7 -0
- package/package.json +1 -1
- package/src/billing/crypto/__tests__/unified-checkout.test.ts +83 -0
- package/src/billing/crypto/__tests__/watcher-service.test.ts +242 -0
- package/src/billing/crypto/__tests__/webhook-confirmations.test.ts +367 -0
- package/src/billing/crypto/btc/__tests__/settler.test.ts +1 -0
- package/src/billing/crypto/btc/__tests__/watcher.test.ts +201 -0
- package/src/billing/crypto/btc/types.ts +3 -1
- package/src/billing/crypto/btc/watcher.ts +22 -6
- package/src/billing/crypto/charge-store.test.ts +204 -1
- package/src/billing/crypto/charge-store.ts +86 -2
- package/src/billing/crypto/client.ts +2 -0
- package/src/billing/crypto/cursor-store.ts +31 -3
- package/src/billing/crypto/evm/__tests__/eth-settler.test.ts +2 -0
- package/src/billing/crypto/evm/__tests__/eth-watcher.test.ts +31 -4
- package/src/billing/crypto/evm/__tests__/settler.test.ts +2 -0
- package/src/billing/crypto/evm/__tests__/watcher-confirmations.test.ts +176 -0
- package/src/billing/crypto/evm/__tests__/watcher.test.ts +6 -2
- package/src/billing/crypto/evm/eth-watcher.ts +34 -14
- package/src/billing/crypto/evm/types.ts +5 -1
- package/src/billing/crypto/evm/watcher.ts +39 -13
- package/src/billing/crypto/index.ts +12 -3
- package/src/billing/crypto/key-server-webhook.ts +92 -21
- package/src/billing/crypto/types.ts +18 -0
- package/src/billing/crypto/unified-checkout.ts +20 -179
- package/src/billing/crypto/watcher-service.ts +85 -32
- package/src/db/schema/crypto.ts +8 -0
- package/src/monetization/crypto/__tests__/webhook.test.ts +2 -1
|
@@ -113,6 +113,13 @@
|
|
|
113
113
|
"when": 1742918400000,
|
|
114
114
|
"tag": "0015_callback_url",
|
|
115
115
|
"breakpoints": true
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"idx": 16,
|
|
119
|
+
"version": "7",
|
|
120
|
+
"when": 1743004800000,
|
|
121
|
+
"tag": "0016_charge_progress_columns",
|
|
122
|
+
"breakpoints": true
|
|
116
123
|
}
|
|
117
124
|
]
|
|
118
125
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import type { CryptoServiceClient } from "../client.js";
|
|
3
|
+
import { createUnifiedCheckout, MIN_CHECKOUT_USD } from "../unified-checkout.js";
|
|
4
|
+
|
|
5
|
+
function mockCryptoService(): CryptoServiceClient {
|
|
6
|
+
return {
|
|
7
|
+
createCharge: vi.fn().mockResolvedValue({
|
|
8
|
+
chargeId: "btc:bc1qtest",
|
|
9
|
+
address: "bc1qtest",
|
|
10
|
+
chain: "bitcoin",
|
|
11
|
+
token: "BTC",
|
|
12
|
+
amountUsd: 50,
|
|
13
|
+
displayAmount: "0.00076923 BTC",
|
|
14
|
+
derivationIndex: 7,
|
|
15
|
+
expiresAt: "2026-03-21T23:00:00Z",
|
|
16
|
+
}),
|
|
17
|
+
listChains: vi.fn(),
|
|
18
|
+
deriveAddress: vi.fn(),
|
|
19
|
+
getCharge: vi.fn(),
|
|
20
|
+
} as unknown as CryptoServiceClient;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe("createUnifiedCheckout", () => {
|
|
24
|
+
it("delegates to CryptoServiceClient.createCharge", async () => {
|
|
25
|
+
const service = mockCryptoService();
|
|
26
|
+
const result = await createUnifiedCheckout({ cryptoService: service }, "btc", { tenant: "t-1", amountUsd: 50 });
|
|
27
|
+
|
|
28
|
+
expect(result.depositAddress).toBe("bc1qtest");
|
|
29
|
+
expect(result.displayAmount).toBe("0.00076923 BTC");
|
|
30
|
+
expect(result.amountUsd).toBe(50);
|
|
31
|
+
expect(result.token).toBe("BTC");
|
|
32
|
+
expect(result.chain).toBe("bitcoin");
|
|
33
|
+
expect(result.referenceId).toBe("btc:bc1qtest");
|
|
34
|
+
|
|
35
|
+
expect(service.createCharge).toHaveBeenCalledWith({
|
|
36
|
+
chain: "btc",
|
|
37
|
+
amountUsd: 50,
|
|
38
|
+
callbackUrl: undefined,
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("passes callbackUrl to createCharge", async () => {
|
|
43
|
+
const service = mockCryptoService();
|
|
44
|
+
await createUnifiedCheckout({ cryptoService: service }, "base-usdc", {
|
|
45
|
+
tenant: "t-1",
|
|
46
|
+
amountUsd: 25,
|
|
47
|
+
callbackUrl: "https://example.com/hook",
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
expect(service.createCharge).toHaveBeenCalledWith({
|
|
51
|
+
chain: "base-usdc",
|
|
52
|
+
amountUsd: 25,
|
|
53
|
+
callbackUrl: "https://example.com/hook",
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("rejects amount below minimum", async () => {
|
|
58
|
+
const service = mockCryptoService();
|
|
59
|
+
await expect(
|
|
60
|
+
createUnifiedCheckout({ cryptoService: service }, "btc", { tenant: "t-1", amountUsd: 5 }),
|
|
61
|
+
).rejects.toThrow(`Minimum payment amount is $${MIN_CHECKOUT_USD}`);
|
|
62
|
+
|
|
63
|
+
expect(service.createCharge).not.toHaveBeenCalled();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("rejects non-finite amount", async () => {
|
|
67
|
+
const service = mockCryptoService();
|
|
68
|
+
await expect(
|
|
69
|
+
createUnifiedCheckout({ cryptoService: service }, "btc", { tenant: "t-1", amountUsd: NaN }),
|
|
70
|
+
).rejects.toThrow(`Minimum payment amount is $${MIN_CHECKOUT_USD}`);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("propagates createCharge errors", async () => {
|
|
74
|
+
const service = mockCryptoService();
|
|
75
|
+
(service.createCharge as ReturnType<typeof vi.fn>).mockRejectedValue(
|
|
76
|
+
new Error("CryptoService createCharge failed (500): Internal Server Error"),
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
await expect(
|
|
80
|
+
createUnifiedCheckout({ cryptoService: service }, "btc", { tenant: "t-1", amountUsd: 50 }),
|
|
81
|
+
).rejects.toThrow("CryptoService createCharge failed (500)");
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { handlePayment } from "../watcher-service.js";
|
|
3
|
+
|
|
4
|
+
function mockChargeStore(overrides: Record<string, unknown> = {}) {
|
|
5
|
+
return {
|
|
6
|
+
getByDepositAddress: vi.fn().mockResolvedValue({
|
|
7
|
+
referenceId: "btc:test",
|
|
8
|
+
tenantId: "t1",
|
|
9
|
+
amountUsdCents: 5000,
|
|
10
|
+
creditedAt: null,
|
|
11
|
+
chain: "bitcoin",
|
|
12
|
+
depositAddress: "bc1qtest",
|
|
13
|
+
token: "BTC",
|
|
14
|
+
callbackUrl: "https://example.com/hook",
|
|
15
|
+
expectedAmount: "50000",
|
|
16
|
+
receivedAmount: "0",
|
|
17
|
+
confirmations: 0,
|
|
18
|
+
confirmationsRequired: 6,
|
|
19
|
+
...overrides,
|
|
20
|
+
}),
|
|
21
|
+
updateProgress: vi.fn().mockResolvedValue(undefined),
|
|
22
|
+
updateStatus: vi.fn().mockResolvedValue(undefined),
|
|
23
|
+
markCredited: vi.fn().mockResolvedValue(undefined),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function mockDb() {
|
|
28
|
+
return {
|
|
29
|
+
insert: vi.fn().mockReturnValue({ values: vi.fn().mockResolvedValue(undefined) }),
|
|
30
|
+
update: vi.fn().mockReturnValue({
|
|
31
|
+
set: vi.fn().mockReturnValue({ where: vi.fn().mockResolvedValue(undefined) }),
|
|
32
|
+
}),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const noop = () => {};
|
|
37
|
+
|
|
38
|
+
describe("handlePayment", () => {
|
|
39
|
+
it("fires webhook with confirmations: 0 on first tx detection", async () => {
|
|
40
|
+
const chargeStore = mockChargeStore();
|
|
41
|
+
const db = mockDb();
|
|
42
|
+
const enqueuedPayloads: Record<string, unknown>[] = [];
|
|
43
|
+
db.insert = vi.fn().mockReturnValue({
|
|
44
|
+
values: vi.fn().mockImplementation((val: Record<string, unknown>) => {
|
|
45
|
+
if (val.payload) enqueuedPayloads.push(JSON.parse(val.payload as string));
|
|
46
|
+
return Promise.resolve(undefined);
|
|
47
|
+
}),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
await handlePayment(
|
|
51
|
+
db as never,
|
|
52
|
+
chargeStore as never,
|
|
53
|
+
"bc1qtest",
|
|
54
|
+
"50000",
|
|
55
|
+
{
|
|
56
|
+
txHash: "abc123",
|
|
57
|
+
confirmations: 0,
|
|
58
|
+
confirmationsRequired: 6,
|
|
59
|
+
amountReceivedCents: 5000,
|
|
60
|
+
},
|
|
61
|
+
noop,
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
expect(enqueuedPayloads).toHaveLength(1);
|
|
65
|
+
expect(enqueuedPayloads[0]).toMatchObject({
|
|
66
|
+
chargeId: "btc:test",
|
|
67
|
+
status: "partial",
|
|
68
|
+
confirmations: 0,
|
|
69
|
+
confirmationsRequired: 6,
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("fires webhook on each confirmation increment", async () => {
|
|
74
|
+
const chargeStore = mockChargeStore({ confirmations: 2 });
|
|
75
|
+
const db = mockDb();
|
|
76
|
+
const enqueuedPayloads: Record<string, unknown>[] = [];
|
|
77
|
+
db.insert = vi.fn().mockReturnValue({
|
|
78
|
+
values: vi.fn().mockImplementation((val: Record<string, unknown>) => {
|
|
79
|
+
if (val.payload) enqueuedPayloads.push(JSON.parse(val.payload as string));
|
|
80
|
+
return Promise.resolve(undefined);
|
|
81
|
+
}),
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
await handlePayment(
|
|
85
|
+
db as never,
|
|
86
|
+
chargeStore as never,
|
|
87
|
+
"bc1qtest",
|
|
88
|
+
"0", // no additional payment, just confirmation update
|
|
89
|
+
{
|
|
90
|
+
txHash: "abc123",
|
|
91
|
+
confirmations: 3,
|
|
92
|
+
confirmationsRequired: 6,
|
|
93
|
+
amountReceivedCents: 5000,
|
|
94
|
+
},
|
|
95
|
+
noop,
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
expect(enqueuedPayloads).toHaveLength(1);
|
|
99
|
+
expect(enqueuedPayloads[0]).toMatchObject({
|
|
100
|
+
status: "partial",
|
|
101
|
+
confirmations: 3,
|
|
102
|
+
confirmationsRequired: 6,
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("fires final webhook with status confirmed at threshold", async () => {
|
|
107
|
+
const chargeStore = mockChargeStore({
|
|
108
|
+
receivedAmount: "50000",
|
|
109
|
+
confirmations: 5,
|
|
110
|
+
});
|
|
111
|
+
const db = mockDb();
|
|
112
|
+
const enqueuedPayloads: Record<string, unknown>[] = [];
|
|
113
|
+
db.insert = vi.fn().mockReturnValue({
|
|
114
|
+
values: vi.fn().mockImplementation((val: Record<string, unknown>) => {
|
|
115
|
+
if (val.payload) enqueuedPayloads.push(JSON.parse(val.payload as string));
|
|
116
|
+
return Promise.resolve(undefined);
|
|
117
|
+
}),
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
await handlePayment(
|
|
121
|
+
db as never,
|
|
122
|
+
chargeStore as never,
|
|
123
|
+
"bc1qtest",
|
|
124
|
+
"0",
|
|
125
|
+
{
|
|
126
|
+
txHash: "abc123",
|
|
127
|
+
confirmations: 6,
|
|
128
|
+
confirmationsRequired: 6,
|
|
129
|
+
amountReceivedCents: 5000,
|
|
130
|
+
},
|
|
131
|
+
noop,
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
expect(enqueuedPayloads).toHaveLength(1);
|
|
135
|
+
expect(enqueuedPayloads[0]).toMatchObject({
|
|
136
|
+
status: "confirmed",
|
|
137
|
+
confirmations: 6,
|
|
138
|
+
confirmationsRequired: 6,
|
|
139
|
+
});
|
|
140
|
+
expect(chargeStore.markCredited).toHaveBeenCalledOnce();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("all webhooks use canonical status values only", async () => {
|
|
144
|
+
const chargeStore = mockChargeStore();
|
|
145
|
+
const db = mockDb();
|
|
146
|
+
const enqueuedPayloads: Record<string, unknown>[] = [];
|
|
147
|
+
db.insert = vi.fn().mockReturnValue({
|
|
148
|
+
values: vi.fn().mockImplementation((val: Record<string, unknown>) => {
|
|
149
|
+
if (val.payload) enqueuedPayloads.push(JSON.parse(val.payload as string));
|
|
150
|
+
return Promise.resolve(undefined);
|
|
151
|
+
}),
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
await handlePayment(
|
|
155
|
+
db as never,
|
|
156
|
+
chargeStore as never,
|
|
157
|
+
"bc1qtest",
|
|
158
|
+
"50000",
|
|
159
|
+
{
|
|
160
|
+
txHash: "abc123",
|
|
161
|
+
confirmations: 0,
|
|
162
|
+
confirmationsRequired: 6,
|
|
163
|
+
amountReceivedCents: 5000,
|
|
164
|
+
},
|
|
165
|
+
noop,
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
const validStatuses = ["pending", "partial", "confirmed", "expired", "failed"];
|
|
169
|
+
for (const payload of enqueuedPayloads) {
|
|
170
|
+
expect(validStatuses).toContain(payload.status);
|
|
171
|
+
}
|
|
172
|
+
// Must NEVER contain legacy statuses
|
|
173
|
+
for (const payload of enqueuedPayloads) {
|
|
174
|
+
expect(payload.status).not.toBe("Settled");
|
|
175
|
+
expect(payload.status).not.toBe("Processing");
|
|
176
|
+
expect(payload.status).not.toBe("New");
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("updates charge progress via updateProgress()", async () => {
|
|
181
|
+
const chargeStore = mockChargeStore();
|
|
182
|
+
const db = mockDb();
|
|
183
|
+
db.insert = vi.fn().mockReturnValue({
|
|
184
|
+
values: vi.fn().mockResolvedValue(undefined),
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
await handlePayment(
|
|
188
|
+
db as never,
|
|
189
|
+
chargeStore as never,
|
|
190
|
+
"bc1qtest",
|
|
191
|
+
"25000",
|
|
192
|
+
{
|
|
193
|
+
txHash: "abc123",
|
|
194
|
+
confirmations: 2,
|
|
195
|
+
confirmationsRequired: 6,
|
|
196
|
+
amountReceivedCents: 2500,
|
|
197
|
+
},
|
|
198
|
+
noop,
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
expect(chargeStore.updateProgress).toHaveBeenCalledWith("btc:test", {
|
|
202
|
+
status: "partial",
|
|
203
|
+
amountReceivedCents: 2500,
|
|
204
|
+
confirmations: 2,
|
|
205
|
+
confirmationsRequired: 6,
|
|
206
|
+
txHash: "abc123",
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("skips already-credited charges", async () => {
|
|
211
|
+
const chargeStore = mockChargeStore({ creditedAt: "2026-01-01" });
|
|
212
|
+
const db = mockDb();
|
|
213
|
+
|
|
214
|
+
await handlePayment(
|
|
215
|
+
db as never,
|
|
216
|
+
chargeStore as never,
|
|
217
|
+
"bc1qtest",
|
|
218
|
+
"50000",
|
|
219
|
+
{ txHash: "abc123", confirmations: 6, confirmationsRequired: 6, amountReceivedCents: 5000 },
|
|
220
|
+
noop,
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
expect(chargeStore.updateProgress).not.toHaveBeenCalled();
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("skips unknown addresses", async () => {
|
|
227
|
+
const chargeStore = mockChargeStore();
|
|
228
|
+
chargeStore.getByDepositAddress.mockResolvedValue(null);
|
|
229
|
+
const db = mockDb();
|
|
230
|
+
|
|
231
|
+
await handlePayment(
|
|
232
|
+
db as never,
|
|
233
|
+
chargeStore as never,
|
|
234
|
+
"bc1qunknown",
|
|
235
|
+
"50000",
|
|
236
|
+
{ txHash: "abc123", confirmations: 0, confirmationsRequired: 6, amountReceivedCents: 5000 },
|
|
237
|
+
noop,
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
expect(chargeStore.updateProgress).not.toHaveBeenCalled();
|
|
241
|
+
});
|
|
242
|
+
});
|