@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
|
@@ -1,20 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { deriveAddress, deriveP2pkhAddress } from "./btc/address-gen.js";
|
|
3
|
-
import type { ICryptoChargeRepository } from "./charge-store.js";
|
|
4
|
-
import { deriveDepositAddress } from "./evm/address-gen.js";
|
|
5
|
-
import { centsToNative } from "./oracle/convert.js";
|
|
6
|
-
import type { IPriceOracle } from "./oracle/types.js";
|
|
7
|
-
import type { PaymentMethodRecord } from "./payment-method-store.js";
|
|
1
|
+
import type { CryptoServiceClient } from "./client.js";
|
|
8
2
|
|
|
9
3
|
export const MIN_CHECKOUT_USD = 10;
|
|
10
4
|
|
|
11
5
|
export interface UnifiedCheckoutDeps {
|
|
12
|
-
|
|
13
|
-
oracle: IPriceOracle;
|
|
14
|
-
evmXpub: string;
|
|
15
|
-
btcXpub?: string;
|
|
16
|
-
/** UTXO network override (auto-detected from node in production). Default: "mainnet". */
|
|
17
|
-
utxoNetwork?: "mainnet" | "testnet" | "regtest";
|
|
6
|
+
cryptoService: CryptoServiceClient;
|
|
18
7
|
}
|
|
19
8
|
|
|
20
9
|
export interface UnifiedCheckoutResult {
|
|
@@ -30,182 +19,34 @@ export interface UnifiedCheckoutResult {
|
|
|
30
19
|
}
|
|
31
20
|
|
|
32
21
|
/**
|
|
33
|
-
* Unified checkout —
|
|
22
|
+
* Unified checkout — delegates to CryptoServiceClient.createCharge().
|
|
34
23
|
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* - native (BTC): derives BTC address, oracle-priced
|
|
39
|
-
*
|
|
40
|
-
* CRITICAL: amountUsd → integer cents via Credit.fromDollars().toCentsRounded().
|
|
24
|
+
* The pay server handles xpub management, address derivation, and charge
|
|
25
|
+
* creation. This function is a thin wrapper that validates the amount
|
|
26
|
+
* and maps the response to `UnifiedCheckoutResult`.
|
|
41
27
|
*/
|
|
42
28
|
export async function createUnifiedCheckout(
|
|
43
29
|
deps: UnifiedCheckoutDeps,
|
|
44
|
-
|
|
45
|
-
opts: { tenant: string; amountUsd: number },
|
|
30
|
+
chain: string,
|
|
31
|
+
opts: { tenant: string; amountUsd: number; callbackUrl?: string },
|
|
46
32
|
): Promise<UnifiedCheckoutResult> {
|
|
47
33
|
if (!Number.isFinite(opts.amountUsd) || opts.amountUsd < MIN_CHECKOUT_USD) {
|
|
48
34
|
throw new Error(`Minimum payment amount is $${MIN_CHECKOUT_USD}`);
|
|
49
35
|
}
|
|
50
36
|
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
if (method.type === "native" && method.chain === "base") {
|
|
57
|
-
return handleNativeEvm(deps, method, opts.tenant, amountUsdCents, opts.amountUsd);
|
|
58
|
-
}
|
|
59
|
-
if (method.type === "native") {
|
|
60
|
-
return handleNativeUtxo(deps, method, opts.tenant, amountUsdCents, opts.amountUsd);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
throw new Error(`Unsupported payment method type: ${method.type}/${method.token}`);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
async function handleErc20(
|
|
67
|
-
deps: UnifiedCheckoutDeps,
|
|
68
|
-
method: PaymentMethodRecord,
|
|
69
|
-
tenant: string,
|
|
70
|
-
amountUsdCents: number,
|
|
71
|
-
amountUsd: number,
|
|
72
|
-
): Promise<UnifiedCheckoutResult> {
|
|
73
|
-
const depositAddress = await deriveAndStore(deps, method, tenant, amountUsdCents);
|
|
37
|
+
const result = await deps.cryptoService.createCharge({
|
|
38
|
+
chain,
|
|
39
|
+
amountUsd: opts.amountUsd,
|
|
40
|
+
callbackUrl: opts.callbackUrl,
|
|
41
|
+
});
|
|
74
42
|
|
|
75
43
|
return {
|
|
76
|
-
depositAddress,
|
|
77
|
-
displayAmount: `${amountUsd} ${
|
|
78
|
-
amountUsd,
|
|
79
|
-
token:
|
|
80
|
-
chain:
|
|
81
|
-
referenceId:
|
|
44
|
+
depositAddress: result.address,
|
|
45
|
+
displayAmount: result.displayAmount ?? `${opts.amountUsd} ${result.token}`,
|
|
46
|
+
amountUsd: opts.amountUsd,
|
|
47
|
+
token: result.token,
|
|
48
|
+
chain: result.chain,
|
|
49
|
+
referenceId: result.chargeId,
|
|
50
|
+
priceCents: result.priceCents,
|
|
82
51
|
};
|
|
83
52
|
}
|
|
84
|
-
|
|
85
|
-
async function handleNativeEvm(
|
|
86
|
-
deps: UnifiedCheckoutDeps,
|
|
87
|
-
method: PaymentMethodRecord,
|
|
88
|
-
tenant: string,
|
|
89
|
-
amountUsdCents: number,
|
|
90
|
-
amountUsd: number,
|
|
91
|
-
): Promise<UnifiedCheckoutResult> {
|
|
92
|
-
const { priceCents } = await deps.oracle.getPrice("ETH");
|
|
93
|
-
const expectedWei = centsToNative(amountUsdCents, priceCents, 18);
|
|
94
|
-
const depositAddress = await deriveAndStore(deps, method, tenant, amountUsdCents);
|
|
95
|
-
|
|
96
|
-
const divisor = BigInt("1000000000000000000");
|
|
97
|
-
const whole = expectedWei / divisor;
|
|
98
|
-
const frac = (expectedWei % divisor).toString().padStart(18, "0").slice(0, 6);
|
|
99
|
-
|
|
100
|
-
return {
|
|
101
|
-
depositAddress,
|
|
102
|
-
displayAmount: `${whole}.${frac} ETH`,
|
|
103
|
-
amountUsd,
|
|
104
|
-
token: "ETH",
|
|
105
|
-
chain: method.chain,
|
|
106
|
-
referenceId: `${method.type}:${method.chain}:${depositAddress}`,
|
|
107
|
-
priceCents,
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Handle native UTXO coins (BTC, LTC, DOGE, BCH, etc.).
|
|
113
|
-
* Uses the xpub from the payment method record (DB-driven).
|
|
114
|
-
* Derives bech32 addresses for BTC/LTC, Base58 P2PKH for DOGE.
|
|
115
|
-
*/
|
|
116
|
-
async function handleNativeUtxo(
|
|
117
|
-
deps: UnifiedCheckoutDeps,
|
|
118
|
-
method: PaymentMethodRecord,
|
|
119
|
-
tenant: string,
|
|
120
|
-
amountUsdCents: number,
|
|
121
|
-
amountUsd: number,
|
|
122
|
-
): Promise<UnifiedCheckoutResult> {
|
|
123
|
-
const xpub = method.xpub ?? deps.btcXpub;
|
|
124
|
-
if (!xpub) throw new Error(`${method.token} payments not configured (no xpub)`);
|
|
125
|
-
|
|
126
|
-
const { priceCents } = await deps.oracle.getPrice(method.token);
|
|
127
|
-
const rawAmount = centsToNative(amountUsdCents, priceCents, method.decimals);
|
|
128
|
-
|
|
129
|
-
const maxRetries = 3;
|
|
130
|
-
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
131
|
-
const derivationIndex = await deps.chargeStore.getNextDerivationIndex();
|
|
132
|
-
|
|
133
|
-
// Derive address by chain type
|
|
134
|
-
let depositAddress: string;
|
|
135
|
-
if (method.chain === "dogecoin") {
|
|
136
|
-
depositAddress = deriveP2pkhAddress(xpub, derivationIndex, "dogecoin");
|
|
137
|
-
} else {
|
|
138
|
-
depositAddress = deriveAddress(
|
|
139
|
-
xpub,
|
|
140
|
-
derivationIndex,
|
|
141
|
-
deps.utxoNetwork ?? "mainnet",
|
|
142
|
-
method.chain as "bitcoin" | "litecoin",
|
|
143
|
-
);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
const referenceId = `${method.token.toLowerCase()}:${depositAddress}`;
|
|
147
|
-
|
|
148
|
-
try {
|
|
149
|
-
await deps.chargeStore.createStablecoinCharge({
|
|
150
|
-
referenceId,
|
|
151
|
-
tenantId: tenant,
|
|
152
|
-
amountUsdCents,
|
|
153
|
-
chain: method.chain,
|
|
154
|
-
token: method.token,
|
|
155
|
-
depositAddress,
|
|
156
|
-
derivationIndex,
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
const divisor = 10 ** method.decimals;
|
|
160
|
-
const displayAmt = (Number(rawAmount) / divisor).toFixed(method.decimals);
|
|
161
|
-
return {
|
|
162
|
-
depositAddress,
|
|
163
|
-
displayAmount: `${displayAmt} ${method.token}`,
|
|
164
|
-
amountUsd,
|
|
165
|
-
token: method.token,
|
|
166
|
-
chain: method.chain,
|
|
167
|
-
referenceId,
|
|
168
|
-
priceCents,
|
|
169
|
-
};
|
|
170
|
-
} catch (err: unknown) {
|
|
171
|
-
const code = (err as { code?: string }).code;
|
|
172
|
-
const isConflict = code === "23505" || (err instanceof Error && err.message.includes("unique_violation"));
|
|
173
|
-
if (!isConflict || attempt === maxRetries) throw err;
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
throw new Error("Failed to claim derivation index after retries");
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/** Derive an EVM deposit address and store the charge. Retries on unique conflict. */
|
|
181
|
-
async function deriveAndStore(
|
|
182
|
-
deps: UnifiedCheckoutDeps,
|
|
183
|
-
method: PaymentMethodRecord,
|
|
184
|
-
tenant: string,
|
|
185
|
-
amountUsdCents: number,
|
|
186
|
-
): Promise<string> {
|
|
187
|
-
const maxRetries = 3;
|
|
188
|
-
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
189
|
-
const derivationIndex = await deps.chargeStore.getNextDerivationIndex();
|
|
190
|
-
const depositAddress = deriveDepositAddress(deps.evmXpub, derivationIndex);
|
|
191
|
-
const referenceId = `${method.type}:${method.chain}:${depositAddress}`;
|
|
192
|
-
|
|
193
|
-
try {
|
|
194
|
-
await deps.chargeStore.createStablecoinCharge({
|
|
195
|
-
referenceId,
|
|
196
|
-
tenantId: tenant,
|
|
197
|
-
amountUsdCents,
|
|
198
|
-
chain: method.chain,
|
|
199
|
-
token: method.token,
|
|
200
|
-
depositAddress,
|
|
201
|
-
derivationIndex,
|
|
202
|
-
});
|
|
203
|
-
return depositAddress;
|
|
204
|
-
} catch (err: unknown) {
|
|
205
|
-
const code = (err as { code?: string }).code;
|
|
206
|
-
const isConflict = code === "23505" || (err instanceof Error && err.message.includes("unique_violation"));
|
|
207
|
-
if (!isConflict || attempt === maxRetries) throw err;
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
throw new Error("Failed to claim derivation index after retries");
|
|
211
|
-
}
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* Payment flow:
|
|
5
5
|
* 1. Watcher detects payment → handlePayment()
|
|
6
6
|
* 2. Accumulate native amount (supports partial payments)
|
|
7
|
-
* 3. When totalReceived >= expectedAmount →
|
|
8
|
-
* 4. Every payment
|
|
7
|
+
* 3. When totalReceived >= expectedAmount AND confirmations >= required → confirmed + credit
|
|
8
|
+
* 4. Every payment/confirmation change enqueues a webhook delivery
|
|
9
9
|
* 5. Outbox processor retries failed deliveries with exponential backoff
|
|
10
10
|
*
|
|
11
11
|
* Amount comparison is ALWAYS in native crypto units (sats, wei, token base units).
|
|
@@ -23,7 +23,7 @@ import type { EvmChain, EvmPaymentEvent, StablecoinToken } from "./evm/types.js"
|
|
|
23
23
|
import { createRpcCaller, EvmWatcher } from "./evm/watcher.js";
|
|
24
24
|
import type { IPriceOracle } from "./oracle/types.js";
|
|
25
25
|
import type { IPaymentMethodStore } from "./payment-method-store.js";
|
|
26
|
-
import type {
|
|
26
|
+
import type { CryptoChargeStatus } from "./types.js";
|
|
27
27
|
|
|
28
28
|
const MAX_DELIVERY_ATTEMPTS = 10;
|
|
29
29
|
const BACKOFF_BASE_MS = 5_000;
|
|
@@ -131,20 +131,34 @@ async function processDeliveries(
|
|
|
131
131
|
return delivered;
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
-
// --- Payment handling (partial + full) ---
|
|
134
|
+
// --- Payment handling (partial + full + confirmation tracking) ---
|
|
135
|
+
|
|
136
|
+
export interface PaymentPayload {
|
|
137
|
+
txHash: string;
|
|
138
|
+
confirmations: number;
|
|
139
|
+
confirmationsRequired: number;
|
|
140
|
+
amountReceivedCents: number;
|
|
141
|
+
[key: string]: unknown;
|
|
142
|
+
}
|
|
135
143
|
|
|
136
144
|
/**
|
|
137
145
|
* Handle a payment event. Accumulates partial payments in native units.
|
|
138
|
-
*
|
|
146
|
+
* Fires webhook on every payment/confirmation change with canonical statuses.
|
|
139
147
|
*
|
|
140
|
-
*
|
|
148
|
+
* 3-phase webhook lifecycle:
|
|
149
|
+
* 1. Tx first seen -> status: "partial", confirmations: 0
|
|
150
|
+
* 2. Each new block -> status: "partial", confirmations: current
|
|
151
|
+
* 3. Threshold reached + full payment -> status: "confirmed"
|
|
152
|
+
*
|
|
153
|
+
* @param nativeAmount — received amount in native base units (sats for BTC/DOGE, raw token units for ERC20).
|
|
154
|
+
* Pass "0" for confirmation-only updates (no new payment, just more confirmations).
|
|
141
155
|
*/
|
|
142
|
-
async function handlePayment(
|
|
156
|
+
export async function handlePayment(
|
|
143
157
|
db: DrizzleDb,
|
|
144
158
|
chargeStore: ICryptoChargeRepository,
|
|
145
159
|
address: string,
|
|
146
160
|
nativeAmount: string,
|
|
147
|
-
payload:
|
|
161
|
+
payload: PaymentPayload,
|
|
148
162
|
log: (msg: string, meta?: Record<string, unknown>) => void,
|
|
149
163
|
): Promise<void> {
|
|
150
164
|
const charge = await chargeStore.getByDepositAddress(address);
|
|
@@ -156,41 +170,64 @@ async function handlePayment(
|
|
|
156
170
|
return; // Already fully paid and credited
|
|
157
171
|
}
|
|
158
172
|
|
|
159
|
-
|
|
173
|
+
const { confirmations, confirmationsRequired, amountReceivedCents, txHash } = payload;
|
|
174
|
+
|
|
175
|
+
// Accumulate: add this payment to the running total (if nativeAmount > 0)
|
|
160
176
|
const prevReceived = BigInt(charge.receivedAmount ?? "0");
|
|
161
177
|
const thisPayment = BigInt(nativeAmount);
|
|
162
178
|
const totalReceived = (prevReceived + thisPayment).toString();
|
|
163
179
|
const expected = BigInt(charge.expectedAmount ?? "0");
|
|
164
180
|
const isFull = expected > 0n && BigInt(totalReceived) >= expected;
|
|
181
|
+
const isConfirmed = isFull && confirmations >= confirmationsRequired;
|
|
182
|
+
|
|
183
|
+
// Update received_amount in DB (only when there's a new payment)
|
|
184
|
+
if (thisPayment > 0n) {
|
|
185
|
+
await db
|
|
186
|
+
.update(cryptoCharges)
|
|
187
|
+
.set({ receivedAmount: totalReceived, filledAmount: totalReceived })
|
|
188
|
+
.where(eq(cryptoCharges.referenceId, charge.referenceId));
|
|
189
|
+
}
|
|
165
190
|
|
|
166
|
-
//
|
|
167
|
-
|
|
168
|
-
.update(cryptoCharges)
|
|
169
|
-
.set({ receivedAmount: totalReceived, filledAmount: totalReceived })
|
|
170
|
-
.where(eq(cryptoCharges.referenceId, charge.referenceId));
|
|
191
|
+
// Determine canonical status
|
|
192
|
+
const status: CryptoChargeStatus = isConfirmed ? "confirmed" : "partial";
|
|
171
193
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
194
|
+
// Update progress via new API
|
|
195
|
+
await chargeStore.updateProgress(charge.referenceId, {
|
|
196
|
+
status,
|
|
197
|
+
amountReceivedCents,
|
|
198
|
+
confirmations,
|
|
199
|
+
confirmationsRequired,
|
|
200
|
+
txHash,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
if (isConfirmed) {
|
|
175
204
|
await chargeStore.markCredited(charge.referenceId);
|
|
176
|
-
log("Charge
|
|
205
|
+
log("Charge confirmed", {
|
|
206
|
+
chargeId: charge.referenceId,
|
|
207
|
+
confirmations,
|
|
208
|
+
confirmationsRequired,
|
|
209
|
+
});
|
|
177
210
|
} else {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
211
|
+
log("Payment progress", {
|
|
212
|
+
chargeId: charge.referenceId,
|
|
213
|
+
confirmations,
|
|
214
|
+
confirmationsRequired,
|
|
215
|
+
received: totalReceived,
|
|
216
|
+
});
|
|
181
217
|
}
|
|
182
218
|
|
|
183
|
-
// Webhook on every
|
|
219
|
+
// Webhook on every event — product shows confirmation progress to user
|
|
184
220
|
if (charge.callbackUrl) {
|
|
185
221
|
await enqueueWebhook(db, charge.referenceId, charge.callbackUrl, {
|
|
186
222
|
chargeId: charge.referenceId,
|
|
187
223
|
chain: charge.chain,
|
|
188
224
|
address: charge.depositAddress,
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
225
|
+
amountExpectedCents: charge.amountUsdCents,
|
|
226
|
+
amountReceivedCents,
|
|
227
|
+
confirmations,
|
|
228
|
+
confirmationsRequired,
|
|
229
|
+
txHash,
|
|
230
|
+
status,
|
|
194
231
|
});
|
|
195
232
|
}
|
|
196
233
|
}
|
|
@@ -245,8 +282,14 @@ export async function startWatchers(opts: WatcherServiceOpts): Promise<() => voi
|
|
|
245
282
|
oracle,
|
|
246
283
|
cursorStore,
|
|
247
284
|
onPayment: async (event: BtcPaymentEvent) => {
|
|
248
|
-
log("UTXO payment", {
|
|
249
|
-
|
|
285
|
+
log("UTXO payment", {
|
|
286
|
+
chain: method.chain,
|
|
287
|
+
address: event.address,
|
|
288
|
+
txid: event.txid,
|
|
289
|
+
sats: event.amountSats,
|
|
290
|
+
confirmations: event.confirmations,
|
|
291
|
+
confirmationsRequired: event.confirmationsRequired,
|
|
292
|
+
});
|
|
250
293
|
await handlePayment(
|
|
251
294
|
db,
|
|
252
295
|
chargeStore,
|
|
@@ -255,6 +298,8 @@ export async function startWatchers(opts: WatcherServiceOpts): Promise<() => voi
|
|
|
255
298
|
{
|
|
256
299
|
txHash: event.txid,
|
|
257
300
|
confirmations: event.confirmations,
|
|
301
|
+
confirmationsRequired: event.confirmationsRequired,
|
|
302
|
+
amountReceivedCents: event.amountUsdCents,
|
|
258
303
|
},
|
|
259
304
|
log,
|
|
260
305
|
);
|
|
@@ -323,8 +368,14 @@ export async function startWatchers(opts: WatcherServiceOpts): Promise<() => voi
|
|
|
323
368
|
watchedAddresses: chainAddresses,
|
|
324
369
|
cursorStore,
|
|
325
370
|
onPayment: async (event: EvmPaymentEvent) => {
|
|
326
|
-
log("EVM payment", {
|
|
327
|
-
|
|
371
|
+
log("EVM payment", {
|
|
372
|
+
chain: event.chain,
|
|
373
|
+
token: event.token,
|
|
374
|
+
to: event.to,
|
|
375
|
+
txHash: event.txHash,
|
|
376
|
+
confirmations: event.confirmations,
|
|
377
|
+
confirmationsRequired: event.confirmationsRequired,
|
|
378
|
+
});
|
|
328
379
|
await handlePayment(
|
|
329
380
|
db,
|
|
330
381
|
chargeStore,
|
|
@@ -332,7 +383,9 @@ export async function startWatchers(opts: WatcherServiceOpts): Promise<() => voi
|
|
|
332
383
|
event.rawAmount,
|
|
333
384
|
{
|
|
334
385
|
txHash: event.txHash,
|
|
335
|
-
confirmations:
|
|
386
|
+
confirmations: event.confirmations,
|
|
387
|
+
confirmationsRequired: event.confirmationsRequired,
|
|
388
|
+
amountReceivedCents: event.amountUsdCents,
|
|
336
389
|
},
|
|
337
390
|
log,
|
|
338
391
|
);
|
package/src/db/schema/crypto.ts
CHANGED
|
@@ -30,6 +30,14 @@ export const cryptoCharges = pgTable(
|
|
|
30
30
|
expectedAmount: text("expected_amount"),
|
|
31
31
|
/** Running total of received crypto in native units. Accumulates across partial payments. */
|
|
32
32
|
receivedAmount: text("received_amount"),
|
|
33
|
+
/** Number of blockchain confirmations observed so far. */
|
|
34
|
+
confirmations: integer("confirmations").notNull().default(0),
|
|
35
|
+
/** Required confirmations for settlement (copied from payment method at creation). */
|
|
36
|
+
confirmationsRequired: integer("confirmations_required").notNull().default(1),
|
|
37
|
+
/** Blockchain transaction hash for the payment. */
|
|
38
|
+
txHash: text("tx_hash"),
|
|
39
|
+
/** Amount received so far in USD cents (integer). Converted from crypto at time of receipt. */
|
|
40
|
+
amountReceivedCents: integer("amount_received_cents").notNull().default(0),
|
|
33
41
|
},
|
|
34
42
|
(table) => [
|
|
35
43
|
index("idx_crypto_charges_tenant").on(table.tenantId),
|
|
@@ -164,7 +164,8 @@ describe("handleCryptoWebhook (monetization layer)", () => {
|
|
|
164
164
|
await handleCryptoWebhook(deps, makePayload({ status: "partial" }));
|
|
165
165
|
|
|
166
166
|
const charge = await chargeStore.getByReferenceId("chg-test-001");
|
|
167
|
-
|
|
167
|
+
// DB stores legacy status values; "partial" maps to "Processing" internally
|
|
168
|
+
expect(charge?.status).toBe("Processing");
|
|
168
169
|
});
|
|
169
170
|
|
|
170
171
|
it("settles charge when status is confirmed", async () => {
|