@openreceive/http 0.4.10 → 0.4.11
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 +4 -2
- package/dist/{adapter-surface-DHngkTKQ.d.ts → adapter-surface-Cs4g29Ez.d.ts} +95 -54
- package/dist/adapter-surface.d.ts +1 -1
- package/dist/adapter-surface.js +1 -1
- package/dist/{chunk-TJY2HBVU.js → chunk-PGZAFYN6.js} +440 -88
- package/dist/index.d.ts +11 -8
- package/dist/index.js +1 -1
- package/package.json +3 -3
- package/skills/integrate-openreceive/references/btcpay.md +6 -4
- package/skills/integrate-openreceive/references/django.md +5 -3
- package/skills/integrate-openreceive/references/fastapi.md +6 -4
- package/skills/integrate-openreceive/references/fastify.md +6 -4
- package/skills/integrate-openreceive/references/laravel.md +5 -3
- package/skills/integrate-openreceive/references/next.md +6 -4
- package/skills/integrate-openreceive/references/node.md +6 -4
- package/skills/integrate-openreceive/references/php.md +5 -3
- package/skills/integrate-openreceive/references/rails.md +5 -3
- package/skills/integrate-openreceive/references/woocommerce.md +3 -1
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// src/errors.ts
|
|
2
2
|
import {
|
|
3
3
|
isErrorCode,
|
|
4
|
-
isRetryableErrorCode
|
|
4
|
+
isRetryableErrorCode,
|
|
5
|
+
publicErrorBody
|
|
5
6
|
} from "@openreceive/core";
|
|
6
7
|
var HttpError = class extends Error {
|
|
7
8
|
status;
|
|
@@ -56,13 +57,13 @@ function hostError(message, status = 400, code = "INVALID_REQUEST") {
|
|
|
56
57
|
}
|
|
57
58
|
function mapHostRouteError(error) {
|
|
58
59
|
if (error instanceof HostError || isServiceErrorShape(error)) {
|
|
59
|
-
return { status: error.status, body: error.body };
|
|
60
|
+
return { status: error.status, body: publicErrorBody(error.body) };
|
|
60
61
|
}
|
|
61
62
|
if (isWalletErrorShape(error)) {
|
|
62
63
|
const retryable = error.retryable ?? isRetryableErrorCode(error.code);
|
|
63
64
|
return {
|
|
64
65
|
status: walletErrorStatus(error.code, retryable),
|
|
65
|
-
body: { code: error.code, message: error.message, retryable }
|
|
66
|
+
body: publicErrorBody({ code: error.code, message: error.message, retryable })
|
|
66
67
|
};
|
|
67
68
|
}
|
|
68
69
|
return null;
|
|
@@ -98,13 +99,13 @@ function errorResponse(error, requestId) {
|
|
|
98
99
|
if (error instanceof HttpError) {
|
|
99
100
|
return jsonResponse(
|
|
100
101
|
error.status,
|
|
101
|
-
{
|
|
102
|
+
publicErrorBody({
|
|
102
103
|
code: error.code,
|
|
103
104
|
message: error.message,
|
|
104
105
|
...error.retryable === void 0 ? {} : { retryable: error.retryable },
|
|
105
106
|
request_id: requestId,
|
|
106
107
|
...error.details === void 0 ? {} : { details: error.details }
|
|
107
|
-
},
|
|
108
|
+
}),
|
|
108
109
|
requestId,
|
|
109
110
|
error.retryAfterSeconds === void 0 ? void 0 : [["retry-after", String(Math.max(1, Math.ceil(error.retryAfterSeconds)))]]
|
|
110
111
|
);
|
|
@@ -112,7 +113,7 @@ function errorResponse(error, requestId) {
|
|
|
112
113
|
if (isServiceErrorShape(error)) {
|
|
113
114
|
return jsonResponse(
|
|
114
115
|
error.status,
|
|
115
|
-
{ ...error.body, request_id: error.body.request_id ?? requestId },
|
|
116
|
+
publicErrorBody({ ...error.body, request_id: error.body.request_id ?? requestId }),
|
|
116
117
|
requestId
|
|
117
118
|
);
|
|
118
119
|
}
|
|
@@ -120,7 +121,12 @@ function errorResponse(error, requestId) {
|
|
|
120
121
|
const retryable = error.retryable ?? isRetryableErrorCode(error.code);
|
|
121
122
|
return jsonResponse(
|
|
122
123
|
walletErrorStatus(error.code, retryable),
|
|
123
|
-
{
|
|
124
|
+
publicErrorBody({
|
|
125
|
+
code: error.code,
|
|
126
|
+
message: error.message,
|
|
127
|
+
retryable,
|
|
128
|
+
request_id: requestId
|
|
129
|
+
}),
|
|
124
130
|
requestId
|
|
125
131
|
);
|
|
126
132
|
}
|
|
@@ -220,10 +226,10 @@ function isUnderPrefix(pathname, prefix) {
|
|
|
220
226
|
}
|
|
221
227
|
|
|
222
228
|
// src/handler.ts
|
|
223
|
-
import { compact
|
|
229
|
+
import { compact, unixSeconds as unixSeconds7 } from "@openreceive/core";
|
|
224
230
|
|
|
225
231
|
// src/reconcile-gate.ts
|
|
226
|
-
import {
|
|
232
|
+
import { createPaymentScanWindow, unixSeconds as unixSeconds5 } from "@openreceive/core";
|
|
227
233
|
|
|
228
234
|
// src/host-payments.ts
|
|
229
235
|
import { unixSeconds as unixSeconds3 } from "@openreceive/core";
|
|
@@ -283,7 +289,9 @@ import { randomUUID } from "crypto";
|
|
|
283
289
|
import {
|
|
284
290
|
OPENRECEIVE_PAYMENTS_SCHEMA_VERSION,
|
|
285
291
|
paymentsDdlStatements,
|
|
286
|
-
unixSeconds as unixSeconds2
|
|
292
|
+
unixSeconds as unixSeconds2,
|
|
293
|
+
OPENRECEIVE_ATTEMPT_EXPIRY_GRACE_SECONDS as OPENRECEIVE_ATTEMPT_EXPIRY_GRACE_SECONDS2,
|
|
294
|
+
redactSecrets
|
|
287
295
|
} from "@openreceive/core";
|
|
288
296
|
|
|
289
297
|
// src/sql-adapters.ts
|
|
@@ -472,13 +480,18 @@ function isMissingTableError(error, dialect) {
|
|
|
472
480
|
}
|
|
473
481
|
return /no such table/i.test(message);
|
|
474
482
|
}
|
|
475
|
-
function
|
|
483
|
+
function parseGate(value) {
|
|
484
|
+
let gate;
|
|
476
485
|
try {
|
|
477
|
-
|
|
478
|
-
return typeof parsed.claimed_at === "number" && Number.isFinite(parsed.claimed_at) ? parsed.claimed_at : void 0;
|
|
486
|
+
gate = JSON.parse(String(value));
|
|
479
487
|
} catch {
|
|
480
488
|
return void 0;
|
|
481
489
|
}
|
|
490
|
+
if (gate.version > 1)
|
|
491
|
+
throw new TypeError(
|
|
492
|
+
"A newer reconcile scheduler is installed; upgrade every application and worker together."
|
|
493
|
+
);
|
|
494
|
+
return gate.version === 1 ? gate : void 0;
|
|
482
495
|
}
|
|
483
496
|
function paymentsSchemaSql(dialect, tableName = "openreceive_payments", metaTableName = "openreceive_meta") {
|
|
484
497
|
return paymentsDdlStatements({ dialect, tableName, metaTableName }).map((statement) => `${statement};`).join("\n");
|
|
@@ -539,19 +552,30 @@ function createSqlPayments(db, options = {}) {
|
|
|
539
552
|
await assertSupportedSchema();
|
|
540
553
|
return rowsForReference(adapter, reference);
|
|
541
554
|
},
|
|
542
|
-
async
|
|
555
|
+
async findByPaymentHash(paymentHash) {
|
|
556
|
+
await assertSupportedSchema();
|
|
557
|
+
const rows = await adapter.query(statement(`SELECT * FROM ${table} WHERE payment_hash = ?`), [
|
|
558
|
+
paymentHash.toLowerCase()
|
|
559
|
+
]);
|
|
560
|
+
return rows[0] === void 0 ? void 0 : recordFromRow(rows[0]);
|
|
561
|
+
},
|
|
562
|
+
async listReconcilableAttempts(after) {
|
|
543
563
|
await assertSupportedSchema();
|
|
544
564
|
const rows = await adapter.query(
|
|
545
565
|
statement(
|
|
546
|
-
`SELECT payment_hash, created_at,
|
|
566
|
+
`SELECT payment_hash, created_at, checkout_data FROM ${table} WHERE status = 'pending' ${after == null ? "" : "AND (created_at > ? OR (created_at = ? AND payment_hash > ?))"} ORDER BY created_at ASC, payment_hash ASC LIMIT ?`
|
|
547
567
|
),
|
|
548
|
-
[
|
|
568
|
+
[
|
|
569
|
+
...after == null ? [] : [after.created_at, after.created_at, after.payment_hash],
|
|
570
|
+
OPENRECEIVE_RECONCILE_BATCH_SIZE
|
|
571
|
+
]
|
|
549
572
|
);
|
|
550
573
|
return rows.map(
|
|
551
574
|
(row) => ({
|
|
552
575
|
paymentHash: asString(row.payment_hash, "payment_hash"),
|
|
553
576
|
createdAt: asInteger(row.created_at, "created_at"),
|
|
554
|
-
|
|
577
|
+
createdAtSource: savedCheckout(row).createdAtSource ?? "host",
|
|
578
|
+
expiresAt: settlementExpiresAt(row)
|
|
555
579
|
})
|
|
556
580
|
);
|
|
557
581
|
},
|
|
@@ -559,7 +583,7 @@ function createSqlPayments(db, options = {}) {
|
|
|
559
583
|
await assertSupportedSchema();
|
|
560
584
|
const rows = await adapter.query(
|
|
561
585
|
statement(
|
|
562
|
-
`SELECT payment_hash, created_at,
|
|
586
|
+
`SELECT payment_hash, created_at, checkout_data FROM ${table} WHERE payment_hash = ? AND status = 'pending'`
|
|
563
587
|
),
|
|
564
588
|
[paymentHash.toLowerCase()]
|
|
565
589
|
);
|
|
@@ -568,13 +592,13 @@ function createSqlPayments(db, options = {}) {
|
|
|
568
592
|
return {
|
|
569
593
|
paymentHash: asString(row.payment_hash, "payment_hash"),
|
|
570
594
|
createdAt: asInteger(row.created_at, "created_at"),
|
|
571
|
-
|
|
595
|
+
createdAtSource: savedCheckout(row).createdAtSource ?? "host",
|
|
596
|
+
expiresAt: settlementExpiresAt(row)
|
|
572
597
|
};
|
|
573
598
|
},
|
|
574
|
-
async claimReconcileGate({ now, intervalSeconds }) {
|
|
599
|
+
async claimReconcileGate({ now, intervalSeconds, leaseSeconds = 10 }) {
|
|
575
600
|
await assertSupportedSchema();
|
|
576
601
|
const token = randomUUID();
|
|
577
|
-
const claimValue = JSON.stringify({ claimed_at: now, token });
|
|
578
602
|
const insertIfAbsent = adapter.dialect === "postgres" ? `INSERT INTO ${metaTable} (key, value, rev) VALUES (?, ?, 0) ON CONFLICT (key) DO NOTHING` : `INSERT OR IGNORE INTO ${metaTable} (key, value, rev) VALUES (?, ?, 0)`;
|
|
579
603
|
for (let attempt = 0; attempt < RECONCILE_GATE_CAS_RETRIES; attempt += 1) {
|
|
580
604
|
const rows = await adapter.query(
|
|
@@ -582,13 +606,27 @@ function createSqlPayments(db, options = {}) {
|
|
|
582
606
|
[RECONCILE_GATE_META_KEY]
|
|
583
607
|
);
|
|
584
608
|
const current = rows[0];
|
|
609
|
+
const gate = current === void 0 ? void 0 : parseGate(current.value);
|
|
610
|
+
if (gate !== void 0 && isFreshTimestamp(
|
|
611
|
+
now,
|
|
612
|
+
gate.claimed_at,
|
|
613
|
+
Math.max(intervalSeconds, gate.interval_seconds ?? 2)
|
|
614
|
+
))
|
|
615
|
+
return null;
|
|
616
|
+
if (gate !== void 0 && gate.claimed_at <= now + META_CLOCK_SKEW_SECONDS && gate.lease_until > now)
|
|
617
|
+
return null;
|
|
618
|
+
const scheduler = gate?.scheduler ?? { cursor: null, windows: [] };
|
|
619
|
+
const claimValue = JSON.stringify({
|
|
620
|
+
version: 1,
|
|
621
|
+
claimed_at: now,
|
|
622
|
+
token,
|
|
623
|
+
lease_until: now + leaseSeconds,
|
|
624
|
+
interval_seconds: intervalSeconds,
|
|
625
|
+
scheduler
|
|
626
|
+
});
|
|
585
627
|
if (current === void 0) {
|
|
586
628
|
await adapter.query(statement(insertIfAbsent), [RECONCILE_GATE_META_KEY, claimValue]);
|
|
587
629
|
} else {
|
|
588
|
-
const claimedAt = parseClaimedAt(current.value);
|
|
589
|
-
if (claimedAt !== void 0 && isFreshTimestamp(now, claimedAt, intervalSeconds)) {
|
|
590
|
-
return false;
|
|
591
|
-
}
|
|
592
630
|
await adapter.query(
|
|
593
631
|
statement(`UPDATE ${metaTable} SET value = ?, rev = rev + 1 WHERE key = ? AND rev = ?`),
|
|
594
632
|
[claimValue, RECONCILE_GATE_META_KEY, asInteger(current.rev, "rev")]
|
|
@@ -598,9 +636,99 @@ function createSqlPayments(db, options = {}) {
|
|
|
598
636
|
statement(`SELECT value FROM ${metaTable} WHERE key = ? LIMIT 1`),
|
|
599
637
|
[RECONCILE_GATE_META_KEY]
|
|
600
638
|
);
|
|
601
|
-
if (readback[0] !== void 0 && String(readback[0].value) === claimValue)
|
|
639
|
+
if (readback[0] !== void 0 && String(readback[0].value) === claimValue)
|
|
640
|
+
return { token, scheduler };
|
|
602
641
|
}
|
|
603
|
-
return
|
|
642
|
+
return null;
|
|
643
|
+
},
|
|
644
|
+
async checkpointReconcileGate({ claim, scheduler, now, release = false, intervalSeconds }) {
|
|
645
|
+
await assertSupportedSchema();
|
|
646
|
+
if (scheduler.windows.length > 2 || scheduler.windows.some(
|
|
647
|
+
(window) => window.attempts.length > OPENRECEIVE_RECONCILE_BATCH_SIZE
|
|
648
|
+
))
|
|
649
|
+
throw new RangeError("Reconcile progress exceeds its cohort bound.");
|
|
650
|
+
const rows = await adapter.query(
|
|
651
|
+
statement(`SELECT value, rev FROM ${metaTable} WHERE key = ? LIMIT 1`),
|
|
652
|
+
[RECONCILE_GATE_META_KEY]
|
|
653
|
+
);
|
|
654
|
+
const current = rows[0];
|
|
655
|
+
if (current === void 0) return false;
|
|
656
|
+
const gate = parseGate(current.value);
|
|
657
|
+
if (gate?.token !== claim.token || gate.lease_until <= now) return false;
|
|
658
|
+
const value = JSON.stringify({
|
|
659
|
+
...gate,
|
|
660
|
+
scheduler,
|
|
661
|
+
lease_until: release ? 0 : gate.lease_until,
|
|
662
|
+
interval_seconds: intervalSeconds ?? gate.interval_seconds
|
|
663
|
+
});
|
|
664
|
+
if (new TextEncoder().encode(value).length > 131072)
|
|
665
|
+
throw new RangeError("Reconcile progress exceeds 128 KiB.");
|
|
666
|
+
await adapter.query(
|
|
667
|
+
statement(`UPDATE ${metaTable} SET value = ?, rev = rev + 1 WHERE key = ? AND rev = ?`),
|
|
668
|
+
[value, RECONCILE_GATE_META_KEY, asInteger(current.rev, "rev")]
|
|
669
|
+
);
|
|
670
|
+
const readback = await adapter.query(
|
|
671
|
+
statement(`SELECT value FROM ${metaTable} WHERE key = ? LIMIT 1`),
|
|
672
|
+
[RECONCILE_GATE_META_KEY]
|
|
673
|
+
);
|
|
674
|
+
return readback[0]?.value === value;
|
|
675
|
+
},
|
|
676
|
+
async listRepairCandidates({ after = "", limit = 100 } = {}) {
|
|
677
|
+
await assertSupportedSchema();
|
|
678
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 200)
|
|
679
|
+
throw new RangeError("Repair report limit must be between 1 and 200.");
|
|
680
|
+
const rows = await adapter.query(
|
|
681
|
+
statement(
|
|
682
|
+
`SELECT payment_hash, reference, status, status_reason, updated_at, expires_at, checkout_data, swap_data IS NOT NULL AS has_swap FROM ${table} WHERE status IN ('expired', 'attention') AND paid_at IS NULL AND payment_hash > ? ORDER BY payment_hash LIMIT ?`
|
|
683
|
+
),
|
|
684
|
+
[after, limit]
|
|
685
|
+
);
|
|
686
|
+
return {
|
|
687
|
+
candidates: rows.map(repairCandidate).filter((row) => row !== null),
|
|
688
|
+
nextCursor: rows.length === limit ? asString(rows.at(-1).payment_hash, "payment_hash") : null
|
|
689
|
+
};
|
|
690
|
+
},
|
|
691
|
+
async requeueAttempt(input) {
|
|
692
|
+
await assertSupportedSchema();
|
|
693
|
+
if (!input.reason.trim() || input.reason.length > 500)
|
|
694
|
+
throw new TypeError("A reviewed repair reason of 1\u2013500 characters is required.");
|
|
695
|
+
return adapter.transaction(async (tx) => {
|
|
696
|
+
const hash = input.paymentHash.toLowerCase();
|
|
697
|
+
const preliminary = await tx.query(
|
|
698
|
+
statement(`SELECT reference FROM ${table} WHERE payment_hash = ?`),
|
|
699
|
+
[hash]
|
|
700
|
+
);
|
|
701
|
+
if (preliminary[0] === void 0) return false;
|
|
702
|
+
await lockReference(tx, asString(preliminary[0].reference, "reference"));
|
|
703
|
+
const rows = await tx.query(
|
|
704
|
+
statement(
|
|
705
|
+
`SELECT payment_hash, reference, status, status_reason, updated_at, expires_at, checkout_data, swap_data IS NOT NULL AS has_swap FROM ${table} WHERE payment_hash = ? AND paid_at IS NULL`
|
|
706
|
+
),
|
|
707
|
+
[hash]
|
|
708
|
+
);
|
|
709
|
+
const row = rows[0];
|
|
710
|
+
if (row === void 0 || row.status !== input.expectedStatus || asInteger(row.updated_at, "updated_at") !== input.expectedUpdatedAt)
|
|
711
|
+
return false;
|
|
712
|
+
const candidate = repairCandidate(row);
|
|
713
|
+
if (candidate === null) return false;
|
|
714
|
+
const auditKey = `payment_repair:${hash}:${input.expectedStatus}:${input.expectedUpdatedAt}`;
|
|
715
|
+
const prior = await tx.query(statement(`SELECT key FROM ${metaTable} WHERE key = ?`), [
|
|
716
|
+
auditKey
|
|
717
|
+
]);
|
|
718
|
+
if (prior.length > 0) return false;
|
|
719
|
+
const now = clock();
|
|
720
|
+
await tx.query(
|
|
721
|
+
statement(
|
|
722
|
+
`UPDATE ${table} SET status = 'pending', status_reason = 'operator_requeued', updated_at = ? WHERE payment_hash = ? AND status = ? AND updated_at = ?`
|
|
723
|
+
),
|
|
724
|
+
[now, hash, input.expectedStatus, input.expectedUpdatedAt]
|
|
725
|
+
);
|
|
726
|
+
await tx.query(statement(`INSERT INTO ${metaTable} (key, value, rev) VALUES (?, ?, 0)`), [
|
|
727
|
+
auditKey,
|
|
728
|
+
JSON.stringify({ ...candidate, reviewedAt: now, reason: redactSecrets(input.reason) })
|
|
729
|
+
]);
|
|
730
|
+
return true;
|
|
731
|
+
});
|
|
604
732
|
},
|
|
605
733
|
async commitAttempt(input) {
|
|
606
734
|
await assertSupportedSchema();
|
|
@@ -654,6 +782,7 @@ function createSqlPayments(db, options = {}) {
|
|
|
654
782
|
});
|
|
655
783
|
},
|
|
656
784
|
async countAttemptsFromIp(clientIp, sinceUnixSeconds) {
|
|
785
|
+
await assertSupportedSchema();
|
|
657
786
|
const rows = await adapter.query(
|
|
658
787
|
statement(`SELECT COUNT(*) AS n FROM ${table} WHERE client_ip = ? AND inserted_at >= ?`),
|
|
659
788
|
[clientIp, sinceUnixSeconds]
|
|
@@ -663,6 +792,12 @@ function createSqlPayments(db, options = {}) {
|
|
|
663
792
|
async recordReconciliation(transition) {
|
|
664
793
|
await assertSupportedSchema();
|
|
665
794
|
await adapter.transaction(async (tx) => {
|
|
795
|
+
const rows = await tx.query(
|
|
796
|
+
statement(`SELECT reference FROM ${table} WHERE payment_hash = ?`),
|
|
797
|
+
[transition.paymentHash.toLowerCase()]
|
|
798
|
+
);
|
|
799
|
+
if (rows[0] === void 0) return;
|
|
800
|
+
await lockReference(tx, asString(rows[0].reference, "reference"));
|
|
666
801
|
await tx.query(
|
|
667
802
|
statement(
|
|
668
803
|
`UPDATE ${table} SET status = ?, status_reason = ?, updated_at = ? WHERE payment_hash = ? AND status = 'pending'`
|
|
@@ -676,10 +811,10 @@ function createSqlPayments(db, options = {}) {
|
|
|
676
811
|
);
|
|
677
812
|
});
|
|
678
813
|
},
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
814
|
+
recordSettlementWithFulfillment: (settlement, fulfill) => markPaidOnce(
|
|
815
|
+
settlement,
|
|
816
|
+
({ query, ...context }) => fulfill({ ...context, transaction: { query } })
|
|
817
|
+
),
|
|
683
818
|
markPaidOnce
|
|
684
819
|
};
|
|
685
820
|
async function markPaidOnce(input, fulfill) {
|
|
@@ -695,7 +830,7 @@ function createSqlPayments(db, options = {}) {
|
|
|
695
830
|
await lockReference(tx, asString(reference, "reference"));
|
|
696
831
|
const rows = await rowsForReference(tx, asString(reference, "reference"));
|
|
697
832
|
const row = rows.find((candidate) => candidate.paymentHash === paymentHash);
|
|
698
|
-
if (row === void 0 || row.status
|
|
833
|
+
if (row === void 0 || row.status !== "pending") return false;
|
|
699
834
|
const firstForReference = !rows.some((candidate) => candidate.status === "settled");
|
|
700
835
|
const now = clock();
|
|
701
836
|
await tx.query(
|
|
@@ -741,6 +876,50 @@ function recordFromRow(row) {
|
|
|
741
876
|
swapData: swapData === null || swapData === void 0 ? null : parseRowJson(asString(swapData, "swap_data"), "swap_data", paymentHash)
|
|
742
877
|
};
|
|
743
878
|
}
|
|
879
|
+
function savedCheckout(row) {
|
|
880
|
+
const hash = asString(row.payment_hash, "payment_hash");
|
|
881
|
+
const checkout = parseRowJson(
|
|
882
|
+
asString(row.checkout_data, "checkout_data"),
|
|
883
|
+
"checkout_data",
|
|
884
|
+
hash
|
|
885
|
+
);
|
|
886
|
+
if (typeof checkout === "object" && checkout !== null) {
|
|
887
|
+
const saved = checkout;
|
|
888
|
+
const value = saved.expiresAt ?? saved.expires_at;
|
|
889
|
+
if (typeof value === "number" && Number.isSafeInteger(value) && value > 0)
|
|
890
|
+
return {
|
|
891
|
+
expiresAt: value,
|
|
892
|
+
createdAtSource: (saved.createdAtSource ?? saved.created_at_source) === "wallet" ? "wallet" : "host"
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
throw new TypeError(
|
|
896
|
+
`Invalid checkout_data invoice expiry on openreceive payment attempt ${hash}.`
|
|
897
|
+
);
|
|
898
|
+
}
|
|
899
|
+
function settlementExpiresAt(row) {
|
|
900
|
+
return savedCheckout(row).expiresAt;
|
|
901
|
+
}
|
|
902
|
+
function repairCandidate(row) {
|
|
903
|
+
if (row.status !== "expired" && row.status !== "attention") return null;
|
|
904
|
+
const expires = settlementExpiresAt(row);
|
|
905
|
+
const instruction = asInteger(row.expires_at, "expires_at");
|
|
906
|
+
const updated = asInteger(row.updated_at, "updated_at");
|
|
907
|
+
const reason = row.status_reason == null ? null : asString(row.status_reason, "status_reason");
|
|
908
|
+
const early = Boolean(row.has_swap) && expires > instruction && updated >= instruction + OPENRECEIVE_ATTEMPT_EXPIRY_GRACE_SECONDS2 && updated < expires + OPENRECEIVE_ATTEMPT_EXPIRY_GRACE_SECONDS2 && ["not_found_after_expiry", "no_finality_after_expiry", "unsettled_after_expiry"].includes(
|
|
909
|
+
reason ?? ""
|
|
910
|
+
);
|
|
911
|
+
if (!early && row.status !== "attention") return null;
|
|
912
|
+
return {
|
|
913
|
+
paymentHash: asString(row.payment_hash, "payment_hash"),
|
|
914
|
+
reference: asString(row.reference, "reference"),
|
|
915
|
+
status: row.status,
|
|
916
|
+
statusReason: reason,
|
|
917
|
+
updatedAt: updated,
|
|
918
|
+
instructionExpiresAt: instruction,
|
|
919
|
+
settlementExpiresAt: expires,
|
|
920
|
+
category: early ? "early_swap_closure" : "attention"
|
|
921
|
+
};
|
|
922
|
+
}
|
|
744
923
|
function parseRowJson(value, column, paymentHash) {
|
|
745
924
|
try {
|
|
746
925
|
return JSON.parse(value);
|
|
@@ -802,7 +981,7 @@ function createHost(options) {
|
|
|
802
981
|
}
|
|
803
982
|
if (options.onPaid === void 0) {
|
|
804
983
|
throw new TypeError(
|
|
805
|
-
"OpenReceive host requires onPaid (per-reference settlement context in db mode; the raw
|
|
984
|
+
"OpenReceive host requires onPaid (per-reference settlement context in db mode; the raw transaction context in custom repository mode). Pass onPaid to write fulfillment atomically for the first settled attempt per reference. https://openreceive.org/guides/api-reference.md#onpaid"
|
|
806
985
|
);
|
|
807
986
|
}
|
|
808
987
|
let payments;
|
|
@@ -816,6 +995,7 @@ function createHost(options) {
|
|
|
816
995
|
const fulfill = options.onPaid;
|
|
817
996
|
onPaid = async (input) => {
|
|
818
997
|
await repository.markPaidOnce(input, fulfill);
|
|
998
|
+
await assertSettlementCommitted(repository, input.paymentHash);
|
|
819
999
|
};
|
|
820
1000
|
} else {
|
|
821
1001
|
if (options.payments?.listForReference === void 0) {
|
|
@@ -838,21 +1018,20 @@ function createHost(options) {
|
|
|
838
1018
|
"OpenReceive host requires payments.recordReconciliation \u2014 how a scan outcome (settled/expired/attention) is written back. Implement it, or pass db to use the built-in SQL repository. https://openreceive.org/guides/storage.md"
|
|
839
1019
|
);
|
|
840
1020
|
}
|
|
841
|
-
if (typeof options.payments.
|
|
1021
|
+
if (typeof options.payments.recordSettlementWithFulfillment !== "function") {
|
|
842
1022
|
throw new TypeError(
|
|
843
|
-
"OpenReceive host requires payments.
|
|
1023
|
+
"OpenReceive host requires payments.recordSettlementWithFulfillment(input, fulfill). Await fulfillment inside the reference settlement transaction; boolean-only repositories are unsupported. Implement it, or pass db to use the built-in SQL repository. https://openreceive.org/guides/storage.md"
|
|
844
1024
|
);
|
|
845
1025
|
}
|
|
1026
|
+
if (typeof options.payments.findByPaymentHash !== "function")
|
|
1027
|
+
throw new TypeError(
|
|
1028
|
+
"OpenReceive host requires payments.findByPaymentHash to verify durable settlement acknowledgment."
|
|
1029
|
+
);
|
|
846
1030
|
payments = options.payments;
|
|
847
1031
|
const custom = options.payments;
|
|
848
|
-
const notify = options.onPaid;
|
|
849
1032
|
onPaid = async (settlement) => {
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
paidAt: settlement.paidAt,
|
|
853
|
-
...settlement.details === void 0 ? {} : { details: settlement.details }
|
|
854
|
-
});
|
|
855
|
-
if (claimed) await notify(settlement);
|
|
1033
|
+
await custom.recordSettlementWithFulfillment(settlement, options.onPaid);
|
|
1034
|
+
await assertSettlementCommitted(custom, settlement.paymentHash);
|
|
856
1035
|
};
|
|
857
1036
|
}
|
|
858
1037
|
const clock = options.clock ?? unixSeconds3;
|
|
@@ -913,7 +1092,19 @@ function createHost(options) {
|
|
|
913
1092
|
};
|
|
914
1093
|
return {
|
|
915
1094
|
resolveCheckout,
|
|
916
|
-
onCheckoutCreated: (input) =>
|
|
1095
|
+
onCheckoutCreated: async (input) => {
|
|
1096
|
+
try {
|
|
1097
|
+
await payments.commitAttempt(input);
|
|
1098
|
+
} catch (error) {
|
|
1099
|
+
if (error instanceof HttpError || isServiceErrorShape(error)) throw error;
|
|
1100
|
+
throw new HttpError(
|
|
1101
|
+
503,
|
|
1102
|
+
"INTERNAL",
|
|
1103
|
+
"Payment storage is unavailable; payer instructions were withheld. Please retry.",
|
|
1104
|
+
{ retryable: true }
|
|
1105
|
+
);
|
|
1106
|
+
}
|
|
1107
|
+
},
|
|
917
1108
|
onPaid,
|
|
918
1109
|
payments
|
|
919
1110
|
};
|
|
@@ -985,6 +1176,16 @@ function storedPaymentHash(value) {
|
|
|
985
1176
|
}
|
|
986
1177
|
return normalized;
|
|
987
1178
|
}
|
|
1179
|
+
async function assertSettlementCommitted(repository, paymentHash) {
|
|
1180
|
+
const committed = await repository.findByPaymentHash(paymentHash);
|
|
1181
|
+
if (committed?.status !== "settled")
|
|
1182
|
+
throw new HttpError(
|
|
1183
|
+
503,
|
|
1184
|
+
"INTERNAL",
|
|
1185
|
+
"The payment settlement is not committed; retry reconciliation.",
|
|
1186
|
+
{ retryable: true }
|
|
1187
|
+
);
|
|
1188
|
+
}
|
|
988
1189
|
|
|
989
1190
|
// src/reconcile-loop.ts
|
|
990
1191
|
import { unixSeconds as unixSeconds4 } from "@openreceive/core";
|
|
@@ -996,7 +1197,7 @@ async function reconcileHostPayments(input) {
|
|
|
996
1197
|
attempts.map((attempt) => [attempt.paymentHash.toLowerCase(), attempt])
|
|
997
1198
|
);
|
|
998
1199
|
const scannedAt = clock();
|
|
999
|
-
const checks = await input.service.reconcilePayments({
|
|
1200
|
+
const checks = input.checks ?? await input.service.reconcilePayments({
|
|
1000
1201
|
attempts,
|
|
1001
1202
|
overlapSeconds: input.overlapSeconds,
|
|
1002
1203
|
...input.maxPages === void 0 ? {} : { maxPages: input.maxPages }
|
|
@@ -1026,7 +1227,7 @@ async function reconcileHostPayments(input) {
|
|
|
1026
1227
|
const transition = reconciliationTransition(
|
|
1027
1228
|
attempt,
|
|
1028
1229
|
checked.status,
|
|
1029
|
-
scannedAt,
|
|
1230
|
+
checked.coverageStartedAt ?? scannedAt,
|
|
1030
1231
|
walletTransaction?.state ?? walletTransaction?.transaction_state
|
|
1031
1232
|
);
|
|
1032
1233
|
if (transition !== null) {
|
|
@@ -1050,9 +1251,9 @@ async function startReconciler(input) {
|
|
|
1050
1251
|
if (!Number.isSafeInteger(pollIntervalMs) || pollIntervalMs < 250) {
|
|
1051
1252
|
throw new RangeError("pollIntervalMs must be a safe integer of at least 250");
|
|
1052
1253
|
}
|
|
1053
|
-
if (typeof input.host.payments.claimReconcileGate !== "function") {
|
|
1254
|
+
if (typeof input.host.payments.claimReconcileGate !== "function" || typeof input.host.payments.checkpointReconcileGate !== "function") {
|
|
1054
1255
|
throw new TypeError(
|
|
1055
|
-
"The reconciler requires payments.claimReconcileGate (a durable CAS gate shared by every worker); implement it on the repository so all scan entry points stay within one budget."
|
|
1256
|
+
"The reconciler requires payments.claimReconcileGate and checkpointReconcileGate (a durable CAS gate shared by every worker); implement it on the repository so all scan entry points stay within one budget."
|
|
1056
1257
|
);
|
|
1057
1258
|
}
|
|
1058
1259
|
const overlapSeconds = input.overlapSeconds ?? 60;
|
|
@@ -1129,9 +1330,9 @@ function intervalForInvoiceAge(elapsedSeconds) {
|
|
|
1129
1330
|
}
|
|
1130
1331
|
async function maybeReconcilePayments(input) {
|
|
1131
1332
|
const claimReconcileGate = input.host.payments.claimReconcileGate;
|
|
1132
|
-
if (typeof claimReconcileGate !== "function") {
|
|
1333
|
+
if (typeof claimReconcileGate !== "function" || typeof input.host.payments.checkpointReconcileGate !== "function") {
|
|
1133
1334
|
throw new TypeError(
|
|
1134
|
-
"Opportunistic reconcile requires payments.claimReconcileGate (
|
|
1335
|
+
"Opportunistic reconcile requires payments.claimReconcileGate and checkpointReconcileGate (durable lease and progress CAS); implement it on the repository or disable with opportunisticReconcile: false."
|
|
1135
1336
|
);
|
|
1136
1337
|
}
|
|
1137
1338
|
const report = input.onError ?? ((error) => {
|
|
@@ -1142,36 +1343,153 @@ async function maybeReconcilePayments(input) {
|
|
|
1142
1343
|
);
|
|
1143
1344
|
});
|
|
1144
1345
|
try {
|
|
1145
|
-
const attempts = await input.host.payments.listReconcilableAttempts();
|
|
1146
|
-
if (attempts.length === 0) return { reason: "no_pending" };
|
|
1147
1346
|
const clock = input.clock ?? unixSeconds5;
|
|
1148
1347
|
const now = clock();
|
|
1149
|
-
const
|
|
1150
|
-
const
|
|
1151
|
-
|
|
1152
|
-
const checks = await withScanTimeout(
|
|
1153
|
-
reconcileHostPayments({
|
|
1154
|
-
service: input.service,
|
|
1155
|
-
host: input.host,
|
|
1156
|
-
// Already read above to size the gate interval; the pass scans that
|
|
1157
|
-
// exact batch rather than repeating the query on the request path.
|
|
1158
|
-
attempts,
|
|
1159
|
-
overlapSeconds: input.overlapSeconds,
|
|
1160
|
-
maxPages: input.maxPages ?? OPENRECEIVE_RECONCILE_SCAN_MAX_PAGES,
|
|
1161
|
-
...compact({ clock: input.clock })
|
|
1162
|
-
}),
|
|
1348
|
+
const minInterval = Math.max(2, input.minIntervalSeconds ?? 2);
|
|
1349
|
+
const timeout = Math.min(
|
|
1350
|
+
OPENRECEIVE_RECONCILE_SCAN_TIMEOUT_MS,
|
|
1163
1351
|
input.scanTimeoutMs ?? OPENRECEIVE_RECONCILE_SCAN_TIMEOUT_MS
|
|
1164
1352
|
);
|
|
1165
|
-
|
|
1353
|
+
const claim = await claimReconcileGate.call(input.host.payments, {
|
|
1354
|
+
now,
|
|
1355
|
+
intervalSeconds: minInterval,
|
|
1356
|
+
leaseSeconds: Math.ceil(timeout / 1e3) + 1
|
|
1357
|
+
});
|
|
1358
|
+
if (claim === null) return { reason: "gate_busy" };
|
|
1359
|
+
if (typeof claim !== "object" || typeof claim.token !== "string")
|
|
1360
|
+
throw new TypeError(
|
|
1361
|
+
"claimReconcileGate must return a lease claim or null; boolean gate repositories must upgrade."
|
|
1362
|
+
);
|
|
1363
|
+
const scheduler = structuredClone(claim.scheduler);
|
|
1364
|
+
const queued = new Set(
|
|
1365
|
+
scheduler.windows.flatMap((window2) => window2.attempts.map((attempt) => attempt.payment_hash))
|
|
1366
|
+
);
|
|
1367
|
+
if (scheduler.windows.length < 2) {
|
|
1368
|
+
let candidates = await input.host.payments.listReconcilableAttempts(scheduler.cursor);
|
|
1369
|
+
if (candidates.length === 0 && scheduler.cursor !== null)
|
|
1370
|
+
candidates = await input.host.payments.listReconcilableAttempts(null);
|
|
1371
|
+
const last = candidates.at(-1);
|
|
1372
|
+
scheduler.cursor = last === void 0 || candidates.length < OPENRECEIVE_RECONCILE_BATCH_SIZE ? null : { created_at: last.createdAt, payment_hash: last.paymentHash };
|
|
1373
|
+
const fresh = candidates.filter((attempt) => !queued.has(attempt.paymentHash));
|
|
1374
|
+
if (fresh.length > 0)
|
|
1375
|
+
scheduler.windows.push(
|
|
1376
|
+
createPaymentScanWindow(
|
|
1377
|
+
fresh.map((attempt) => ({
|
|
1378
|
+
payment_hash: attempt.paymentHash,
|
|
1379
|
+
created_at: attempt.createdAt,
|
|
1380
|
+
expires_at: attempt.expiresAt,
|
|
1381
|
+
created_at_source: attempt.createdAtSource ?? "host"
|
|
1382
|
+
})),
|
|
1383
|
+
now,
|
|
1384
|
+
input.overlapSeconds
|
|
1385
|
+
)
|
|
1386
|
+
);
|
|
1387
|
+
}
|
|
1388
|
+
const window = scheduler.windows.shift();
|
|
1389
|
+
if (window === void 0) {
|
|
1390
|
+
await input.host.payments.checkpointReconcileGate({
|
|
1391
|
+
claim,
|
|
1392
|
+
scheduler,
|
|
1393
|
+
now: clock(),
|
|
1394
|
+
release: true
|
|
1395
|
+
});
|
|
1396
|
+
return { reason: "no_pending" };
|
|
1397
|
+
}
|
|
1398
|
+
const attempts = window.attempts.map((attempt) => ({
|
|
1399
|
+
paymentHash: attempt.payment_hash,
|
|
1400
|
+
createdAt: attempt.created_at,
|
|
1401
|
+
expiresAt: attempt.expires_at,
|
|
1402
|
+
createdAtSource: attempt.created_at_source
|
|
1403
|
+
}));
|
|
1404
|
+
const intervalSeconds = reconcileIntervalSeconds(attempts, now, minInterval);
|
|
1405
|
+
if (!await input.host.payments.checkpointReconcileGate({
|
|
1406
|
+
claim,
|
|
1407
|
+
scheduler,
|
|
1408
|
+
now: clock(),
|
|
1409
|
+
intervalSeconds
|
|
1410
|
+
}))
|
|
1411
|
+
return { reason: "gate_busy" };
|
|
1412
|
+
const streamed = /* @__PURE__ */ new Set();
|
|
1413
|
+
const deliveryFailures = [];
|
|
1414
|
+
const deadline = Date.now() + timeout;
|
|
1415
|
+
const controller = new AbortController();
|
|
1416
|
+
const slice = await withScanTimeout(
|
|
1417
|
+
input.service.scanPaymentSlice({
|
|
1418
|
+
window,
|
|
1419
|
+
maxPages: Math.min(
|
|
1420
|
+
OPENRECEIVE_RECONCILE_SCAN_MAX_PAGES,
|
|
1421
|
+
input.maxPages ?? OPENRECEIVE_RECONCILE_SCAN_MAX_PAGES
|
|
1422
|
+
),
|
|
1423
|
+
deadline,
|
|
1424
|
+
signal: controller.signal,
|
|
1425
|
+
onFinality: async (check) => {
|
|
1426
|
+
if (streamed.has(check.paymentHash) || Date.now() >= deadline) return;
|
|
1427
|
+
if (!await input.host.payments.checkpointReconcileGate({
|
|
1428
|
+
claim,
|
|
1429
|
+
scheduler,
|
|
1430
|
+
now: clock(),
|
|
1431
|
+
intervalSeconds
|
|
1432
|
+
}))
|
|
1433
|
+
throw new Error("Reconciliation lease expired before finality delivery.");
|
|
1434
|
+
streamed.add(check.paymentHash);
|
|
1435
|
+
try {
|
|
1436
|
+
await reconcileHostPayments({
|
|
1437
|
+
service: input.service,
|
|
1438
|
+
host: input.host,
|
|
1439
|
+
attempts,
|
|
1440
|
+
checks: [check],
|
|
1441
|
+
clock
|
|
1442
|
+
});
|
|
1443
|
+
} catch (error) {
|
|
1444
|
+
deliveryFailures.push(error);
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
}),
|
|
1448
|
+
timeout,
|
|
1449
|
+
controller
|
|
1450
|
+
);
|
|
1451
|
+
if (slice.outcome === "continued") {
|
|
1452
|
+
const halves = scheduler.windows.length === 0 ? splitWindow(slice.window, clock(), input.overlapSeconds) : void 0;
|
|
1453
|
+
scheduler.windows.push(...halves ?? [slice.window]);
|
|
1454
|
+
}
|
|
1455
|
+
if (slice.outcome === "stalled")
|
|
1456
|
+
report(
|
|
1457
|
+
new Error(
|
|
1458
|
+
"Wallet history made no pagination progress; attempts remain pending and other cohorts will be served."
|
|
1459
|
+
)
|
|
1460
|
+
);
|
|
1461
|
+
if (!await input.host.payments.checkpointReconcileGate({
|
|
1462
|
+
claim,
|
|
1463
|
+
scheduler,
|
|
1464
|
+
now: clock(),
|
|
1465
|
+
release: true,
|
|
1466
|
+
intervalSeconds
|
|
1467
|
+
}))
|
|
1468
|
+
return { reason: "gate_busy" };
|
|
1469
|
+
await reconcileHostPayments({
|
|
1470
|
+
service: input.service,
|
|
1471
|
+
host: input.host,
|
|
1472
|
+
attempts,
|
|
1473
|
+
checks: slice.checks.filter((check) => !streamed.has(check.paymentHash)),
|
|
1474
|
+
clock
|
|
1475
|
+
});
|
|
1476
|
+
if (deliveryFailures.length > 0)
|
|
1477
|
+
throw new AggregateError(
|
|
1478
|
+
deliveryFailures,
|
|
1479
|
+
"One or more settlement transactions failed; pending attempts will be retried."
|
|
1480
|
+
);
|
|
1481
|
+
return { reason: "ran", checks: slice.checks };
|
|
1166
1482
|
} catch (error) {
|
|
1167
1483
|
report(error);
|
|
1168
1484
|
return { reason: "scan_failed" };
|
|
1169
1485
|
}
|
|
1170
1486
|
}
|
|
1171
|
-
function withScanTimeout(work, timeoutMs) {
|
|
1487
|
+
function withScanTimeout(work, timeoutMs, controller) {
|
|
1172
1488
|
return new Promise((resolve, reject) => {
|
|
1173
1489
|
const timer = setTimeout(() => {
|
|
1174
|
-
|
|
1490
|
+
const error = new Error(`reconcile scan exceeded ${timeoutMs}ms`);
|
|
1491
|
+
controller.abort(error);
|
|
1492
|
+
reject(error);
|
|
1175
1493
|
}, timeoutMs);
|
|
1176
1494
|
work.then(
|
|
1177
1495
|
(value) => {
|
|
@@ -1185,6 +1503,18 @@ function withScanTimeout(work, timeoutMs) {
|
|
|
1185
1503
|
);
|
|
1186
1504
|
});
|
|
1187
1505
|
}
|
|
1506
|
+
function splitWindow(window, now, overlap) {
|
|
1507
|
+
if (!window.attempts.every((attempt) => attempt.created_at_source === "wallet")) return void 0;
|
|
1508
|
+
const timestamps = [...new Set(window.attempts.map((attempt) => attempt.created_at))].sort(
|
|
1509
|
+
(a, b) => a - b
|
|
1510
|
+
);
|
|
1511
|
+
if (timestamps.length < 2) return void 0;
|
|
1512
|
+
const boundary = timestamps[Math.floor(timestamps.length / 2)];
|
|
1513
|
+
return [
|
|
1514
|
+
window.attempts.filter((attempt) => attempt.created_at < boundary),
|
|
1515
|
+
window.attempts.filter((attempt) => attempt.created_at >= boundary)
|
|
1516
|
+
].map((attempts) => createPaymentScanWindow(attempts, now, overlap));
|
|
1517
|
+
}
|
|
1188
1518
|
|
|
1189
1519
|
// src/http-request.ts
|
|
1190
1520
|
import { isRecord, nonEmptyString } from "@openreceive/core";
|
|
@@ -1290,15 +1620,16 @@ function ratesCurrencies(raw) {
|
|
|
1290
1620
|
}
|
|
1291
1621
|
return currencies;
|
|
1292
1622
|
}
|
|
1293
|
-
function optionalCheckoutFields(body) {
|
|
1294
|
-
const
|
|
1295
|
-
if (
|
|
1623
|
+
function optionalCheckoutFields(body, hostDescription) {
|
|
1624
|
+
const bodyMemo = trimmedField(body.memo);
|
|
1625
|
+
if (bodyMemo !== void 0 && bodyMemo.length > MAX_MEMO_LENGTH) {
|
|
1296
1626
|
throw new HttpError(
|
|
1297
1627
|
400,
|
|
1298
1628
|
"INVALID_REQUEST",
|
|
1299
1629
|
`memo must be ${MAX_MEMO_LENGTH} characters or fewer.`
|
|
1300
1630
|
);
|
|
1301
1631
|
}
|
|
1632
|
+
const memo = bodyMemo ?? hostDescription;
|
|
1302
1633
|
const metadata = readRecord(body.metadata);
|
|
1303
1634
|
return {
|
|
1304
1635
|
...memo === void 0 ? {} : { memo },
|
|
@@ -1564,9 +1895,9 @@ function createHttpHandler(options) {
|
|
|
1564
1895
|
}
|
|
1565
1896
|
const rateLimit = options.rateLimitHook ?? resolveRateLimiting(options);
|
|
1566
1897
|
const reconcile = options.opportunisticReconcile === false ? void 0 : typeof options.opportunisticReconcile === "object" ? options.opportunisticReconcile : {};
|
|
1567
|
-
if (reconcile !== void 0 && typeof options.host.payments.claimReconcileGate !== "function") {
|
|
1898
|
+
if (reconcile !== void 0 && (typeof options.host.payments.claimReconcileGate !== "function" || typeof options.host.payments.checkpointReconcileGate !== "function")) {
|
|
1568
1899
|
throw new TypeError(
|
|
1569
|
-
"Opportunistic reconcile (on by default) requires payments.claimReconcileGate \u2014 a durable compare-and-set gate shared by every worker (the built-in SQL repository implements it over openreceive_meta). Implement it on the custom repository, or pass opportunisticReconcile: false and run your own settlement worker. https://openreceive.org/guides/storage.md"
|
|
1900
|
+
"Opportunistic reconcile (on by default) requires payments.claimReconcileGate and checkpointReconcileGate \u2014 a durable compare-and-set gate shared by every worker (the built-in SQL repository implements it over openreceive_meta). Implement it on the custom repository, or pass opportunisticReconcile: false and run your own settlement worker. https://openreceive.org/guides/storage.md"
|
|
1570
1901
|
);
|
|
1571
1902
|
}
|
|
1572
1903
|
const rawExtractClientIp = (typeof options.rateLimiting === "object" ? options.rateLimiting.ip : void 0) ?? resolveClientIp;
|
|
@@ -1619,7 +1950,7 @@ async function dispatch(runtime, request, requestId, native) {
|
|
|
1619
1950
|
host: runtime.host,
|
|
1620
1951
|
// `service`/`host` stay outside compact: it is recursive, and they
|
|
1621
1952
|
// are handles whose identity the reconcile pass relies on.
|
|
1622
|
-
...
|
|
1953
|
+
...compact({
|
|
1623
1954
|
minIntervalSeconds: runtime.reconcile.minIntervalSeconds,
|
|
1624
1955
|
clock: runtime.clock
|
|
1625
1956
|
})
|
|
@@ -1671,7 +2002,7 @@ async function dispatch(runtime, request, requestId, native) {
|
|
|
1671
2002
|
mint: () => runtime.service.createCheckout({
|
|
1672
2003
|
reference,
|
|
1673
2004
|
amount: requireResolvedAmount(resolved2),
|
|
1674
|
-
...optionalCheckoutFields(body)
|
|
2005
|
+
...optionalCheckoutFields(body, resolved2.description)
|
|
1675
2006
|
}),
|
|
1676
2007
|
recover: () => committedCheckout(reference, resolved2),
|
|
1677
2008
|
attempt: (minted) => ({ paymentHash: minted.paymentHash, checkout: minted })
|
|
@@ -1764,7 +2095,7 @@ async function dispatch(runtime, request, requestId, native) {
|
|
|
1764
2095
|
reference,
|
|
1765
2096
|
amount: requireResolvedAmount(resolved2),
|
|
1766
2097
|
payInAsset,
|
|
1767
|
-
...optionalCheckoutFields(body)
|
|
2098
|
+
...optionalCheckoutFields(body, resolved2.description)
|
|
1768
2099
|
}),
|
|
1769
2100
|
recover: () => recoverCommittedSwap(runtime, reference, resolved2),
|
|
1770
2101
|
attempt: (minted) => ({
|
|
@@ -1876,10 +2207,9 @@ async function persistCheckoutAttempt(runtime, input) {
|
|
|
1876
2207
|
} catch (error) {
|
|
1877
2208
|
if (error instanceof HttpError || isServiceErrorShape(error)) throw error;
|
|
1878
2209
|
throw new HttpError(
|
|
1879
|
-
|
|
1880
|
-
"
|
|
1881
|
-
"The host
|
|
1882
|
-
{ retryable: true }
|
|
2210
|
+
409,
|
|
2211
|
+
"CONFLICT",
|
|
2212
|
+
"The host did not accept this payment attempt; payer instructions were withheld."
|
|
1883
2213
|
);
|
|
1884
2214
|
}
|
|
1885
2215
|
}
|
|
@@ -1974,9 +2304,9 @@ async function startNotificationListener(input) {
|
|
|
1974
2304
|
retryable: false
|
|
1975
2305
|
});
|
|
1976
2306
|
}
|
|
1977
|
-
if (typeof input.host.payments.claimReconcileGate !== "function") {
|
|
2307
|
+
if (typeof input.host.payments.claimReconcileGate !== "function" || typeof input.host.payments.checkpointReconcileGate !== "function") {
|
|
1978
2308
|
throw new TypeError(
|
|
1979
|
-
"The notification listener requires payments.claimReconcileGate (a durable CAS gate shared by every worker); implement it on the repository so all scan entry points stay within one budget."
|
|
2309
|
+
"The notification listener requires payments.claimReconcileGate and checkpointReconcileGate (a durable CAS gate shared by every worker); implement it on the repository so all scan entry points stay within one budget."
|
|
1980
2310
|
);
|
|
1981
2311
|
}
|
|
1982
2312
|
const reportError = (error) => {
|
|
@@ -2125,9 +2455,29 @@ async function startNotificationWorker(input) {
|
|
|
2125
2455
|
|
|
2126
2456
|
// src/orm-adapters.ts
|
|
2127
2457
|
function knexDb(knex, dialect) {
|
|
2458
|
+
const connectionOwner = (executor) => {
|
|
2459
|
+
const owner = executor.client;
|
|
2460
|
+
if (typeof owner?.acquireConnection !== "function" || typeof owner.releaseConnection !== "function") {
|
|
2461
|
+
throw new TypeError(
|
|
2462
|
+
"PostgreSQL knexDb requires Knex client.acquireConnection/releaseConnection on both the instance and transaction."
|
|
2463
|
+
);
|
|
2464
|
+
}
|
|
2465
|
+
return owner;
|
|
2466
|
+
};
|
|
2467
|
+
if (dialect === "postgres") connectionOwner(knex);
|
|
2128
2468
|
const queryOn = (executor) => async (sql, params = []) => {
|
|
2129
|
-
|
|
2130
|
-
|
|
2469
|
+
if (dialect === "sqlite")
|
|
2470
|
+
return await executor.raw(sql, [...params]);
|
|
2471
|
+
const owner = connectionOwner(executor);
|
|
2472
|
+
const connection = await owner.acquireConnection();
|
|
2473
|
+
try {
|
|
2474
|
+
const driver = connection;
|
|
2475
|
+
if (typeof driver?.query !== "function")
|
|
2476
|
+
throw new TypeError("PostgreSQL knexDb requires a native pg connection.");
|
|
2477
|
+
return (await driver.query(sql, [...params])).rows ?? [];
|
|
2478
|
+
} finally {
|
|
2479
|
+
await owner.releaseConnection(connection);
|
|
2480
|
+
}
|
|
2131
2481
|
};
|
|
2132
2482
|
return {
|
|
2133
2483
|
dialect,
|
|
@@ -2177,14 +2527,14 @@ function sequelizeDb(sequelize, dialect) {
|
|
|
2177
2527
|
}
|
|
2178
2528
|
|
|
2179
2529
|
// src/stack.ts
|
|
2180
|
-
import { compact as
|
|
2530
|
+
import { compact as compact2, redactSecrets as redactSecrets2 } from "@openreceive/core";
|
|
2181
2531
|
import { createOpenReceive } from "@openreceive/node";
|
|
2182
2532
|
function createStack(options) {
|
|
2183
2533
|
const { wallet, storage, amountFor, clock, onBootFailure, ...handlerOptions } = options;
|
|
2184
2534
|
const host = createHost({
|
|
2185
2535
|
...storage,
|
|
2186
2536
|
amountFor,
|
|
2187
|
-
...
|
|
2537
|
+
...compact2({ clock })
|
|
2188
2538
|
});
|
|
2189
2539
|
const prefix = normalizePrefix(options.prefix ?? "/openreceive");
|
|
2190
2540
|
let ownedService;
|
|
@@ -2193,13 +2543,15 @@ function createStack(options) {
|
|
|
2193
2543
|
if (wallet.service === void 0) ownedService = resolved;
|
|
2194
2544
|
return createHttpHandler({
|
|
2195
2545
|
...handlerOptions,
|
|
2196
|
-
...
|
|
2546
|
+
...compact2({ clock }),
|
|
2197
2547
|
service: resolved,
|
|
2198
2548
|
host
|
|
2199
2549
|
});
|
|
2200
2550
|
})();
|
|
2201
2551
|
boot.catch((error) => {
|
|
2202
|
-
const message = `OpenReceive stack failed to start: ${
|
|
2552
|
+
const message = `OpenReceive stack failed to start: ${redactSecrets2(
|
|
2553
|
+
error instanceof Error ? error.message : String(error)
|
|
2554
|
+
)}`;
|
|
2203
2555
|
if (onBootFailure === void 0) console.error(message);
|
|
2204
2556
|
else onBootFailure(message);
|
|
2205
2557
|
});
|