@dvmkit/sdk 0.0.0 → 0.1.0-rc.2
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/NOTICE +2 -0
- package/README.md +38 -2
- package/dist/chunk-27V2ILSR.js +291 -0
- package/dist/chunk-365P52XQ.js +4121 -0
- package/dist/chunk-5GFED3GJ.js +955 -0
- package/dist/chunk-6JZIX5WW.js +1155 -0
- package/dist/chunk-7IH5SG2A.js +1038 -0
- package/dist/chunk-AT6V3SY7.js +102 -0
- package/dist/chunk-DCNT4PJS.js +733 -0
- package/dist/chunk-DMNLFNTW.js +135 -0
- package/dist/chunk-FROTD5XQ.js +70 -0
- package/dist/chunk-H25M54MI.js +149 -0
- package/dist/chunk-KQAJVVZT.js +712 -0
- package/dist/chunk-KXWROQGK.js +74 -0
- package/dist/chunk-L4OYF4DQ.js +67 -0
- package/dist/chunk-OJ5WFIB2.js +1266 -0
- package/dist/chunk-S3XAHZQY.js +63 -0
- package/dist/chunk-YG7G4DPZ.js +25 -0
- package/dist/credit-ledger-RO4FGSHG.js +28 -0
- package/dist/index.d.ts +144 -0
- package/dist/index.js +303 -0
- package/dist/job-store-6gR4pZRP.d.ts +5350 -0
- package/dist/memory-credit-ledger-I2G64DDK.js +9 -0
- package/dist/mpp-secret-state-WNAQQ6K4.js +127 -0
- package/dist/mpp-setup-MOBWGTWJ.js +30 -0
- package/dist/payout-reporter-4TNWRS5F.js +753 -0
- package/dist/postgres-consumed-credential-store-VHBT4KEA.js +72 -0
- package/dist/postgres-job-store-J5F4GUWU.js +7 -0
- package/dist/postgres-kv-store-JFBDP5IP.js +7 -0
- package/dist/postgres-replay-store-UJXRT6VO.js +7 -0
- package/dist/pricing-4CEB34RM.js +48 -0
- package/dist/processed-payment-store-HAA4SFNK.js +11 -0
- package/dist/revenue-reporter-GB4WKLDC.js +510 -0
- package/dist/server/index.d.ts +4168 -0
- package/dist/server/index.js +22716 -0
- package/dist/ssrf-DZi-xJyn.d.ts +325 -0
- package/dist/tempo-charge-store-6GJEMNUU.js +130 -0
- package/dist/tempo-session-store-FTEEGZXA.js +467 -0
- package/dist/testing/index.d.ts +135 -0
- package/dist/testing/index.js +151 -0
- package/dist/x402-35VLYFKZ.js +1272 -0
- package/package.json +89 -6
|
@@ -0,0 +1,4168 @@
|
|
|
1
|
+
import { X as CashuMode, R as ResolvedCreditConfig, Y as FundingMethod, _ as CreditLedgerLike, $ as CreditSnapshot, g as CreditView, a0 as CreditInvoiceRecord, a1 as CreditDepositEnqueue, a2 as InvoiceSettlement, Z as ZodLike, a as DVMDescriptor, K as KVStore, o as JobStore, a3 as MppxServer, a4 as X402Config, p as PaymentMethod, a5 as ClientCompatibilityGate, a6 as Message, a7 as FundingReceipt, a8 as TopUpCapUnenforcedReason, a9 as JobReceipt, aa as AppendOutgoingOptions, S as SDKJobContext, ab as StepCache, v as ResponseContent, P as PaymentContent, ac as X402Receipt, ad as X402ExactVersionSupport, ae as X402SettlementIntent, af as X402FacilitatorAuth, ag as X402BatchSettlementConfig, ah as PostgresX402ChannelStorage, ai as PaymentRequirementsV2, aj as CreditLedgerQuerier, ak as X402SettlementCursor, al as X402SettlementStatus, am as X402SettlementWriteOff, an as X402RefundSettlementGate, ao as MppxCredential, J as JobRecord, ap as ReceiptCredit, aq as DrainReceiptEvent, ar as DrainReceipt, z as SignedRequestAudience, as as CreditDrawReleaseEnqueue, at as CreditDepositPayload, au as RevenueSkippedNoRailPayload, av as MessageType, aw as ClientCompatibility, ax as CreditDrainEnqueue, ay as DVMAuthScheme, B as SignedRequestDomain, az as DrawResult, aA as X402UnresolvedRefund, aB as GrownDrawResult, aC as DrawResolution, aD as FundingRecord, aE as BlockedInvoiceCursor, aF as InvoiceReconciliation, aG as InvoiceWriteOff, aH as DrawRecord, aI as StalePendingDrawCursor, aJ as TempoCreditLossEvidence, aK as TempoCreditLoss, aL as X402CreditLossEvidence, aM as X402CreditLoss, aN as DrainMethod, aO as DrainRequestResult, aP as BitcoinDepositLiability, aQ as FundingLot, aR as CreditDrainRecord, aS as ChannelDrainCursor, aT as DrainWriteOff, aU as DrainReleaseResult, aV as DrainFulfilment, aW as DrainTransitionResult, aX as StreamableJobStore, aY as ReceiptIssuingStore, aZ as RequestIdClaim, a_ as RequestIdClaimResult, a$ as OutgoingMessage, b0 as PaymentCreditDelta, b1 as VerifyAndCreditResult, b2 as JobCounters, G as SignedRequestReplayStore } from '../job-store-6gR4pZRP.js';
|
|
2
|
+
export { b3 as CLIENT_COMPATIBILITY_HEADERS, d as CanonicalEnvelope, b4 as ClientCompatibilityEnv, b5 as ClientCompatibilityRequirement, b6 as ClientSemVer, e as CreateSignedRequestVerifierOpts, b7 as CreditFundingBasis, b8 as CreditInvoiceStatus, b9 as CreditLedger, ba as CreditLedgerError, bb as CreditLedgerErrorCode, bc as CreditLedgerErrorDetails, bd as CreditLedgerPool, be as CreditStatus, bf as DRAIN_DELIVERY_RESERVE_SATS, bg as DVM_PROTOCOL_VERSION, bh as DrainConflictReason, bi as DrawRailValue, bj as DrawStatus, n as JobStatus, bk as ReplayStoreBackend, bl as RevenueSkippedNoRailReason, x as SIGNED_REQUEST_AUTH_ID, y as SIGNED_REQUEST_STATEMENT_VERSION, bm as Secp256k1AuthOpts, E as SignedRequestError, F as SignedRequestFailure, H as SignedRequestSignOpts, M as SignedRequestStatementHeader, N as SignedRequestVerifier, bn as X402ChannelStorageOpts, bo as X402RelayLockHolder, bp as X402RelaySubmissionLock, bq as X402RelaySubmissionLockError, br as allocateDrawValue, bs as clientCompatibilityAttributes, bt as clientCompatibilityMiddleware, bu as clientUpgradeRequired, O as createSignedRequestVerifier, bv as isStreamableJobStore, bw as parseClientCapabilities, bx as parseClientCompatibility, by as parseDvmClient, bz as parseProtocolVersion, bA as requireClientCompatibility, bB as secp256k1Auth, bC as signedRequestInput, V as signedRequestStatementHeader } from '../job-store-6gR4pZRP.js';
|
|
3
|
+
import { Pool } from 'pg';
|
|
4
|
+
import { F as FxFetcher } from '../ssrf-DZi-xJyn.js';
|
|
5
|
+
export { P as PinnedFetch, S as SSRFError, e as SSRFGuardOpts, f as SSRFReason, g as SSRFResolver, h as assertSafeUrl, j as createPinnedFetch } from '../ssrf-DZi-xJyn.js';
|
|
6
|
+
import { Hono, Context } from 'hono';
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
import { ProofLike } from '@cashu/cashu-ts';
|
|
9
|
+
import { SupportedResponse, SettleResponse } from '@x402/core/types';
|
|
10
|
+
import { Channel, AutoSettlementConfig } from '@x402/evm/batch-settlement/server';
|
|
11
|
+
import 'mppx';
|
|
12
|
+
import '@x402/core/server';
|
|
13
|
+
|
|
14
|
+
/** The channel view the tracker sums claims over. */
|
|
15
|
+
interface X402TrackedChannel {
|
|
16
|
+
channelId: string;
|
|
17
|
+
chargedCumulativeAmount: string;
|
|
18
|
+
totalClaimed: string;
|
|
19
|
+
}
|
|
20
|
+
/** A refund settlement wedged between chain and ledger (internal-review), for the wedge list. */
|
|
21
|
+
interface X402WedgedRefund {
|
|
22
|
+
settlementId: string;
|
|
23
|
+
/** Epoch ms the settlement was first prepared. */
|
|
24
|
+
createdAt: number;
|
|
25
|
+
native?: number;
|
|
26
|
+
}
|
|
27
|
+
/** What the batch-settlement server hands the tracker once it knows its scope. */
|
|
28
|
+
interface X402PayoutContext {
|
|
29
|
+
/** `${network}|${payTo}|${token}` — the scope the settle-pending marker is keyed on. */
|
|
30
|
+
scope: string;
|
|
31
|
+
payTo: string;
|
|
32
|
+
network: string;
|
|
33
|
+
storage: {
|
|
34
|
+
list(): Promise<X402TrackedChannel[]>;
|
|
35
|
+
};
|
|
36
|
+
/** The scheduler's settle cadence — what a wedge's `nextRetry` is derived from. */
|
|
37
|
+
settleIntervalMs: number;
|
|
38
|
+
listWedgedRefunds?: () => Promise<X402WedgedRefund[]>;
|
|
39
|
+
}
|
|
40
|
+
/** The two manager verbs the tracker wraps. */
|
|
41
|
+
interface X402TrackedManager {
|
|
42
|
+
claim(...args: never[]): Promise<{
|
|
43
|
+
vouchers: number;
|
|
44
|
+
transaction: string;
|
|
45
|
+
}[]>;
|
|
46
|
+
settle(): Promise<{
|
|
47
|
+
transaction: string;
|
|
48
|
+
}>;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The batch-settlement server's view of the reporter: attach once with the
|
|
52
|
+
* scope, then route every claim and settle through the tracked manager so
|
|
53
|
+
* the open batch is kept and the settle emits the payout.
|
|
54
|
+
*/
|
|
55
|
+
interface X402PayoutObserver {
|
|
56
|
+
attach(ctx: X402PayoutContext): void;
|
|
57
|
+
trackManager<M extends X402TrackedManager>(manager: M): M;
|
|
58
|
+
/** A settle that landed outside the tracked manager — the manual claim-and-settle's own retry loop. */
|
|
59
|
+
recordSettle(transaction: string): Promise<void>;
|
|
60
|
+
/** A settle attempt that failed outside the tracked manager. One call per attempt a builder would count as one. */
|
|
61
|
+
recordSettleFailure(error: unknown): Promise<void>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
declare const lockPubkeyBrand: unique symbol;
|
|
65
|
+
/**
|
|
66
|
+
* NUT-11 P2PK lock pubkey, lowercase-by-construction. Hex-encoded compressed
|
|
67
|
+
* secp256k1 pubkey (66 chars starting with `02` / `03`), normalised via
|
|
68
|
+
* `toLockPubkey`. The brand exists so any in-memory site that compares
|
|
69
|
+
* lock-pubkeys against each other — receive matching, monitor orphan/retired
|
|
70
|
+
* lookups, melt-pending keypair maps — gets a compile-time guarantee that
|
|
71
|
+
* both sides have been normalised. Raw strings off the wire or out of the DB
|
|
72
|
+
* must pass through `toLockPubkey` before they're treated as one.
|
|
73
|
+
*/
|
|
74
|
+
type LockPubkey = string & {
|
|
75
|
+
readonly [lockPubkeyBrand]: never;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/** Pool-shaped handle (`query` + `connect`). Required for the transactional insert path. */
|
|
79
|
+
type AccumulatorPool = Pick<Pool, "query" | "connect">;
|
|
80
|
+
|
|
81
|
+
/** Cashu accumulator opts resolved from environment variables. */
|
|
82
|
+
interface ResolvedCashuOpts {
|
|
83
|
+
cashuMode: CashuMode | undefined;
|
|
84
|
+
lockPubkey: string | undefined;
|
|
85
|
+
lockPubkeyGraceSeconds: number;
|
|
86
|
+
db: Pool | undefined;
|
|
87
|
+
dvmId: string | undefined;
|
|
88
|
+
canonicalDvmId: string | undefined;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Resolves cashu accumulator opts from environment variables, initializing DB
|
|
92
|
+
* tables when all prerequisites are met. Extracted from serve() so DVMs that
|
|
93
|
+
* call createDVMServer directly (cast, scrape) can wire cashu without using
|
|
94
|
+
* the serve() wrapper (internal-review).
|
|
95
|
+
*
|
|
96
|
+
* @param pool - existing Postgres pool to reuse; pass undefined when no DB is available
|
|
97
|
+
* @param cashuModeHint - programmatic override for cashu mode (serve()'s opts.cashuMode)
|
|
98
|
+
*/
|
|
99
|
+
declare function resolveCashuOptsFromEnv(env: Record<string, string>, pool?: Pool, cashuModeHint?: CashuMode): Promise<ResolvedCashuOpts>;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Tracks MPP challenge ids that have already been consumed by an upfront-flow
|
|
103
|
+
* request, scoped per-DVM realm. Entries auto-expire after a TTL and the store
|
|
104
|
+
* is bounded so a long-running DVM doesn't accumulate state without bound.
|
|
105
|
+
*
|
|
106
|
+
* Used by `verifyUpfrontPayment` to reject replay of an MPP credential whose
|
|
107
|
+
* `(realm, challenge.id)` was already accepted within the current TTL window
|
|
108
|
+
* (internal-review). Mid-job replay is handled separately via per-job
|
|
109
|
+
* `pendingMppChallengeIds` (internal-review).
|
|
110
|
+
*/
|
|
111
|
+
interface ConsumedCredentialStore {
|
|
112
|
+
/** True iff `(realm, challengeId)` was marked and the entry has not expired. */
|
|
113
|
+
has(realm: string, challengeId: string): Promise<boolean>;
|
|
114
|
+
/** Mark `(realm, challengeId)` as consumed for `ttlSeconds`. */
|
|
115
|
+
mark(realm: string, challengeId: string, ttlSeconds: number): Promise<void>;
|
|
116
|
+
}
|
|
117
|
+
/** Options for `MemoryConsumedCredentialStore`. */
|
|
118
|
+
interface MemoryConsumedCredentialStoreOpts {
|
|
119
|
+
/** FIFO eviction kicks in when entry count would exceed this. Default: 10_000. */
|
|
120
|
+
maxEntries?: number;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* In-memory `ConsumedCredentialStore`. Insertion-ordered Map provides FIFO
|
|
124
|
+
* eviction at the configured cap; TTL is enforced lazily on `has()` (same
|
|
125
|
+
* pattern as `MemoryKVStore`).
|
|
126
|
+
*
|
|
127
|
+
* Per-process and not multi-instance-safe — each replica tracks its own
|
|
128
|
+
* consumed set, so a credential redirected to a different replica still works
|
|
129
|
+
* once. Acceptable for first-party single-instance Fly DVMs (internal-review open
|
|
130
|
+
* question); swap to a shared backend before deploying multiple replicas.
|
|
131
|
+
*/
|
|
132
|
+
declare class MemoryConsumedCredentialStore implements ConsumedCredentialStore {
|
|
133
|
+
private readonly maxEntries;
|
|
134
|
+
private readonly data;
|
|
135
|
+
constructor(opts?: MemoryConsumedCredentialStoreOpts);
|
|
136
|
+
has(realm: string, challengeId: string): Promise<boolean>;
|
|
137
|
+
mark(realm: string, challengeId: string, ttlSeconds: number): Promise<void>;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Credit fields riding the signed request body (internal-review, spec §2).
|
|
142
|
+
*
|
|
143
|
+
* An explicit draw references the credit INSIDE the secp256k1/Schnorr-signed
|
|
144
|
+
* `body.data` — no new caller crypto. Wire field names (snake_case, matching
|
|
145
|
+
* the rest of the wire surface):
|
|
146
|
+
*
|
|
147
|
+
* - `credit_id` — the credit to draw against.
|
|
148
|
+
* - `draw_id` — client-generated draw id; the ledger's idempotency key.
|
|
149
|
+
* - `fund` — optional `{ amount_micro, commitment }` funding commitment:
|
|
150
|
+
* present iff a funding artifact (X-Cashu / X-PAYMENT / mpp credential)
|
|
151
|
+
* rides the same request. `commitment` is the SHA-256 hex over the raw
|
|
152
|
+
* artifact bytes, binding the header-borne proof — which sits OUTSIDE the
|
|
153
|
+
* signed body — to the credit its sender intended (spec §2 condition 3).
|
|
154
|
+
*
|
|
155
|
+
* These are envelope-level fields, not capability input: **the SDK** removes
|
|
156
|
+
* them before any builder-owned schema parses ({@link stripCreditEnvelope} in
|
|
157
|
+
* `JobManager.parseInput` and on the quote path; {@link
|
|
158
|
+
* creditEnvelopeIgnoreFields} feeding the auth verifier's own schema check), so
|
|
159
|
+
* handlers never see them and a top-level `.strict()` capability schema is
|
|
160
|
+
* fine (internal-review). Relying on Zod's default strip mode instead was the trap:
|
|
161
|
+
* `.strict()` rejects unknown keys, so an explicit draw 400'd at parse before
|
|
162
|
+
* the envelope was ever extracted.
|
|
163
|
+
*
|
|
164
|
+
* The strip never touches the canonical signing bytes. Every auth call site
|
|
165
|
+
* verifies the raw wire body (`signedRequestInput`, internal-review), so the credit
|
|
166
|
+
* fields are in the verified bytes because they were never taken out of them —
|
|
167
|
+
* the strip is only ever applied to what a schema is about to parse.
|
|
168
|
+
*/
|
|
169
|
+
interface CreditEnvelope {
|
|
170
|
+
creditId: string;
|
|
171
|
+
drawId: string;
|
|
172
|
+
fund?: CreditFundCommitment;
|
|
173
|
+
}
|
|
174
|
+
/** The signed funding commitment for a fund-and-draw request. */
|
|
175
|
+
interface CreditFundCommitment {
|
|
176
|
+
/** Funding amount in 1e-6 units of the credit's currency. */
|
|
177
|
+
amountMicro: number;
|
|
178
|
+
/** SHA-256 hex over the raw funding artifact bytes. */
|
|
179
|
+
commitment: string;
|
|
180
|
+
}
|
|
181
|
+
/** Wire keys reserved for the credit envelope inside `body.data`. */
|
|
182
|
+
declare const CREDIT_ENVELOPE_KEYS: readonly ["credit_id", "draw_id", "fund"];
|
|
183
|
+
/**
|
|
184
|
+
* Thrown by {@link extractCreditEnvelope} on a structurally invalid credit
|
|
185
|
+
* envelope — half a pair, a `fund` block without credit fields, wrong types.
|
|
186
|
+
* Carries a caller-facing message; the route maps it to a 400.
|
|
187
|
+
*/
|
|
188
|
+
declare class CreditEnvelopeError extends Error {
|
|
189
|
+
constructor(message: string);
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Extract the credit envelope from the raw wire `body.data`. Returns
|
|
193
|
+
* `undefined` when no credit field is present (the implicit / legacy shape).
|
|
194
|
+
* Throws {@link CreditEnvelopeError} on partial or malformed fields — a
|
|
195
|
+
* request that names half a draw is a caller bug, not an implicit payment.
|
|
196
|
+
*/
|
|
197
|
+
declare function extractCreditEnvelope(data: unknown): CreditEnvelope | undefined;
|
|
198
|
+
/**
|
|
199
|
+
* The reserved keys `schema` does not itself declare — the ones the SDK is
|
|
200
|
+
* free to remove before handing it an input (internal-review).
|
|
201
|
+
*
|
|
202
|
+
* A capability schema declares none of them, so all three are removable. The
|
|
203
|
+
* one exception this exists for is `/v1/credit`'s own `CREDIT_REQUEST_SCHEMA`,
|
|
204
|
+
* where `credit_id` and `fund` are real fields on a different wire shape and
|
|
205
|
+
* removing them would empty the request. It is not an escape hatch for a
|
|
206
|
+
* capability: the three names stay reserved on `body.data`, and declaring one
|
|
207
|
+
* doesn't win a builder their own value — `app.ts` runs
|
|
208
|
+
* {@link extractCreditEnvelope} over the raw body on every submit, so a
|
|
209
|
+
* capability that declares `fund` answers `invalid_credit_fields` instead.
|
|
210
|
+
*/
|
|
211
|
+
declare function creditEnvelopeIgnoreFields(schema: unknown): readonly string[];
|
|
212
|
+
/**
|
|
213
|
+
* Remove the reserved credit keys from `data` before `schema` parses it, so a
|
|
214
|
+
* strict schema doesn't reject the protocol's own envelope (internal-review). Keys
|
|
215
|
+
* `schema` declares are left alone — see {@link creditEnvelopeIgnoreFields}.
|
|
216
|
+
*
|
|
217
|
+
* Returns `data` unchanged when it isn't a plain object or carries none of the
|
|
218
|
+
* removable keys, so the ordinary no-credit request allocates nothing. Never
|
|
219
|
+
* use the result to derive canonical signing bytes: those cover the wire form,
|
|
220
|
+
* credit fields included (`signedRequestInput`).
|
|
221
|
+
*/
|
|
222
|
+
declare function stripCreditEnvelope(schema: unknown, data: unknown): unknown;
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* The public prepaid-credit terms a DVM advertises before a caller identifies
|
|
226
|
+
* itself. Snake_case: this is the wire shape.
|
|
227
|
+
*
|
|
228
|
+
* This type deliberately excludes the per-caller echo. `/v1/info` is public
|
|
229
|
+
* and cacheable, so its type must make a caller credit ID or balance
|
|
230
|
+
* unrepresentable rather than relying on the current handler not to ask for
|
|
231
|
+
* one.
|
|
232
|
+
*/
|
|
233
|
+
interface CreditTerms {
|
|
234
|
+
/** Smallest funding accepted, in 1e-6 units of `currency`. */
|
|
235
|
+
min_micro: number;
|
|
236
|
+
/** Largest residual balance a caller may hold, in 1e-6 units of `currency`. */
|
|
237
|
+
max_micro: number;
|
|
238
|
+
/** Credit lifetime from the most recent funding, in ms. */
|
|
239
|
+
ttl_ms: number;
|
|
240
|
+
/** Currency the whole block and resulting credit are denominated in. */
|
|
241
|
+
currency: string;
|
|
242
|
+
/** Rails a top-up may be funded over. */
|
|
243
|
+
funding: FundingMethod[];
|
|
244
|
+
/** MPP instruments offered or temporarily withheld for credit funding. */
|
|
245
|
+
tempo?: {
|
|
246
|
+
methods: {
|
|
247
|
+
method: string;
|
|
248
|
+
intent: "charge" | "session";
|
|
249
|
+
}[];
|
|
250
|
+
withheld?: {
|
|
251
|
+
method: string;
|
|
252
|
+
intent: "charge" | "session";
|
|
253
|
+
reason: string;
|
|
254
|
+
}[];
|
|
255
|
+
};
|
|
256
|
+
/** x402 flavours accepted for credit funding. */
|
|
257
|
+
x402?: {
|
|
258
|
+
schemes: {
|
|
259
|
+
scheme: string;
|
|
260
|
+
network: string;
|
|
261
|
+
}[];
|
|
262
|
+
};
|
|
263
|
+
/** Smallest Lightning funding accepted, in 1e-6 units of `currency`. */
|
|
264
|
+
lightning_min_micro?: number;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* The **funding menu** a DVM advertises on `/v1/quote` and in every 402
|
|
268
|
+
* (internal-review, credits spec §4/§6). Snake_case: this is the wire shape.
|
|
269
|
+
*
|
|
270
|
+
* `min_micro` / `max_micro` / `ttl_ms` are the DVM's sizing terms — `max_micro`
|
|
271
|
+
* binds the **residual balance**, not a funding amount, so a job priced above
|
|
272
|
+
* it still clears as fund-and-immediately-draw. `funding` is the rail list a
|
|
273
|
+
* top-up may arrive on — a {@link FundingMethod}, not a `PaymentMethod`, since
|
|
274
|
+
* `lightning` funds a credit without ever being an attached-proof wire method
|
|
275
|
+
* (internal-review). It degrades per spec §4: a rail whose backing wallet is
|
|
276
|
+
* unreachable drops out and the sale survives on the others.
|
|
277
|
+
*
|
|
278
|
+
* The `credit_id` / `balance_micro` / `remaining_micro` / `expiry_ms` echo
|
|
279
|
+
* appears only for an authenticated caller who already holds a live credit —
|
|
280
|
+
* so one round trip answers "what does this cost, and what do I have left?"
|
|
281
|
+
* (spec §7).
|
|
282
|
+
*/
|
|
283
|
+
interface CreditMenu extends CreditTerms {
|
|
284
|
+
/**
|
|
285
|
+
* MPP instruments accepted for this credit, reusable sessions first when
|
|
286
|
+
* available.
|
|
287
|
+
*
|
|
288
|
+
* `withheld` names an instrument this DVM is configured for and is not
|
|
289
|
+
* offering at this instant, with the reason (internal-review). Two audiences read
|
|
290
|
+
* it. An agent gets to relay *why* `tempo/session` vanished instead of
|
|
291
|
+
* inferring the DVM never had it. And the platform's deploy probe reads
|
|
292
|
+
* capability from `methods ∪ withheld`, so a deploy landing during an
|
|
293
|
+
* observer blip no longer stamps `tempo_session_advertised_at` NULL and
|
|
294
|
+
* drops the DVM out of the close observer's payee fallback permanently.
|
|
295
|
+
* `methods` stays the list a caller acts on — never widen it to include a
|
|
296
|
+
* withheld instrument.
|
|
297
|
+
*/
|
|
298
|
+
/**
|
|
299
|
+
* Which x402 flavours a top-up may arrive on, and the chain each rides
|
|
300
|
+
* (internal-review). Present exactly when `funding` lists `x402`. Reusable
|
|
301
|
+
* `batch-settlement` is admitted by default; one-payment `exact` appears
|
|
302
|
+
* only when the builder explicitly accepts its manual refund obligation.
|
|
303
|
+
*/
|
|
304
|
+
/**
|
|
305
|
+
* Smallest funding the `lightning` rail accepts, in 1e-6 units of
|
|
306
|
+
* `currency`. Present only when `funding` lists `lightning`. This is the
|
|
307
|
+
* deployment's sats-physical receive floor (internal-review) rendered into fiat
|
|
308
|
+
* with a ×{@link LIGHTNING_FLOOR_FX_MARGIN} margin absorbing rate drift
|
|
309
|
+
* between menu fetch and funding — issuance enforces the *raw* floor at
|
|
310
|
+
* request-time rates, so funding exactly this figure always clears. Rails
|
|
311
|
+
* without an entry (cashu) have no floor beyond `min_micro`.
|
|
312
|
+
*/
|
|
313
|
+
/** The caller's credit this echo describes. Present only with the echo. */
|
|
314
|
+
credit_id?: string;
|
|
315
|
+
/** Funded balance, pending holds NOT subtracted. */
|
|
316
|
+
balance_micro?: number;
|
|
317
|
+
/** Spendable balance — what a draw is actually checked against. */
|
|
318
|
+
remaining_micro?: number;
|
|
319
|
+
/** Absolute expiry of the echoed credit, ms since epoch. */
|
|
320
|
+
expiry_ms?: number;
|
|
321
|
+
}
|
|
322
|
+
/** Everything {@link buildCreditMenu} needs, assembled per request. */
|
|
323
|
+
interface BuildCreditMenuArgs {
|
|
324
|
+
/**
|
|
325
|
+
* Which runtime is answering. `"isolate"` is refused unconditionally — see
|
|
326
|
+
* the gate in {@link buildCreditMenu}.
|
|
327
|
+
*/
|
|
328
|
+
runtime: "container" | "isolate";
|
|
329
|
+
/** The DVM's resolved `credit` block; `undefined` means it didn't opt in. */
|
|
330
|
+
config: ResolvedCreditConfig | undefined;
|
|
331
|
+
/**
|
|
332
|
+
* Currency the menu — and the credit it funds — denominates in: the DVM's
|
|
333
|
+
* declared `DVMConfig.currency`, which every call site reads off the single
|
|
334
|
+
* `DVMServer.creditCurrency()` (internal-review). Deliberately *not* the currency of
|
|
335
|
+
* the response this menu rides on: a quote priced elsewhere is refused before
|
|
336
|
+
* it reaches here, so the two agree, and sourcing it from the response is
|
|
337
|
+
* what left `POST /v1/credit` — which has no response to read — funding in
|
|
338
|
+
* hardwired USD at a DVM whose jobs draw in something else.
|
|
339
|
+
*/
|
|
340
|
+
currency: string;
|
|
341
|
+
/** Rails this DVM settles over — the menu's funding options. */
|
|
342
|
+
funding: FundingMethod[];
|
|
343
|
+
/** Tempo methods wired for credit funding; builder policy filters the actionable menu. */
|
|
344
|
+
tempoMethods?: {
|
|
345
|
+
method: string;
|
|
346
|
+
intent: "charge" | "session";
|
|
347
|
+
}[];
|
|
348
|
+
/** Tempo methods this DVM is configured for but is not offering right now. */
|
|
349
|
+
tempoWithheld?: {
|
|
350
|
+
method: string;
|
|
351
|
+
intent: "charge" | "session";
|
|
352
|
+
reason: string;
|
|
353
|
+
}[];
|
|
354
|
+
/**
|
|
355
|
+
* x402 flavours this DVM can actually accept a top-up on (internal-review) — one
|
|
356
|
+
* server read, so what the sub-block advertises is what `/v1/credit` will
|
|
357
|
+
* answer a 402 with.
|
|
358
|
+
*/
|
|
359
|
+
x402Schemes?: {
|
|
360
|
+
scheme: string;
|
|
361
|
+
network: string;
|
|
362
|
+
}[];
|
|
363
|
+
/**
|
|
364
|
+
* The deployment's Lightning receive floor in sats, when `lightning` is on
|
|
365
|
+
* the menu — rendered into `lightning_min_micro`. An fx failure during that
|
|
366
|
+
* rendering drops `lightning` from `funding` rather than suppressing the
|
|
367
|
+
* menu: a rail whose minimum we can't state honestly is a rail we must not
|
|
368
|
+
* advertise, but the other rails' sale survives.
|
|
369
|
+
*/
|
|
370
|
+
lightningFundingMinSats?: number;
|
|
371
|
+
/**
|
|
372
|
+
* Whether this DVM has at least one accepted Cashu mint configured
|
|
373
|
+
* (internal-review). Without one, no Bitcoin funding rail may be advertised — see
|
|
374
|
+
* the filter in {@link buildCreditMenu}.
|
|
375
|
+
*
|
|
376
|
+
* Read off the **configured** mint list, never the health-filtered
|
|
377
|
+
* advertised one: a mint blip must not make a funding rail appear and
|
|
378
|
+
* disappear between quotes.
|
|
379
|
+
*/
|
|
380
|
+
hasAcceptedMint?: boolean;
|
|
381
|
+
fxFetcher: FxFetcher;
|
|
382
|
+
/** True when the descriptor declares an auth scheme (see the gate). */
|
|
383
|
+
hasAuth: boolean;
|
|
384
|
+
ledger?: CreditLedgerLike;
|
|
385
|
+
/** devMode tolerates a non-durable ledger; production does not. */
|
|
386
|
+
devMode?: boolean;
|
|
387
|
+
/** Verified caller pubkey — required for the balance echo, never a fallback id. */
|
|
388
|
+
callerPubkey?: string;
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Assemble the funding menu, or return `undefined` when this DVM must not
|
|
392
|
+
* advertise credit. **The refusal ladder is the feature**; each rung exists
|
|
393
|
+
* because advertising anyway would misadvertise:
|
|
394
|
+
*
|
|
395
|
+
* 1. **Isolate runtime** (credits spec §3) — checked first and unconditionally,
|
|
396
|
+
* before config is even read. Isolate DVMs have no Postgres of their own;
|
|
397
|
+
* their only DVM-side persistence is the last-write-wins callback KV, which
|
|
398
|
+
* spec §2 condition 2 prohibits for credit state. Until [internal-review] resolves
|
|
399
|
+
* the gate they run implicit N=1 only, and their quote simply never offers
|
|
400
|
+
* credit so nothing is misadvertised.
|
|
401
|
+
* 2. **Not opted in** — no `credit` block on `configureDVM`.
|
|
402
|
+
* 3. **No descriptor auth** — a credit belongs to a verified secp256k1 pubkey.
|
|
403
|
+
* Without auth the ledger's funder identity degrades to the requester id or
|
|
404
|
+
* `"anonymous"` (see `resolveLedgerContext`), which would pool every
|
|
405
|
+
* anonymous caller's money into one balance. Advertising a per-caller
|
|
406
|
+
* balance on such a DVM would be a lie at best and a disclosure at worst.
|
|
407
|
+
* 4. **Non-durable ledger outside devMode** — a `MemoryCreditLedger` is
|
|
408
|
+
* per-process, so a caller funding on machine A and drawing on machine B
|
|
409
|
+
* gets `credit_not_found`. Advertise only what survives the fleet.
|
|
410
|
+
* 5. **Bounds not expressible in `currency`** — an fx outage, or a currency the
|
|
411
|
+
* snapshot doesn't carry. Omit rather than quote a number we can't state.
|
|
412
|
+
*
|
|
413
|
+
* The balance echo is added only when a verified `callerPubkey` holds a live,
|
|
414
|
+
* same-currency credit.
|
|
415
|
+
*
|
|
416
|
+
* Rungs 1–4 live in {@link creditMenuConfigured}, which the capability refresh
|
|
417
|
+
* reads too.
|
|
418
|
+
*/
|
|
419
|
+
declare function buildCreditMenu(args: BuildCreditMenuArgs): Promise<CreditMenu | undefined>;
|
|
420
|
+
/**
|
|
421
|
+
* The one credit a caller's quote echoes, from the several they may hold —
|
|
422
|
+
* every implicit N=1 payment mints its own `imp:<rail>:<paymentId>` credit, so
|
|
423
|
+
* this list grows with each paid job.
|
|
424
|
+
*
|
|
425
|
+
* Live, same-currency credits with something left in them; largest spendable
|
|
426
|
+
* balance wins, newest breaks a tie. That is the credit a caller would in fact
|
|
427
|
+
* draw against next. A failed job's released hold leaves a positive implicit
|
|
428
|
+
* balance, which is real caller money (spec §1, no debit on failure) and is
|
|
429
|
+
* echoed like any other.
|
|
430
|
+
*
|
|
431
|
+
* **Requires a verified pubkey.** Never call this with a requester id or
|
|
432
|
+
* `"anonymous"`: on an auth-less DVM the ledger pools every anonymous caller
|
|
433
|
+
* under one `caller_pubkey`, so echoing it would show one caller another's
|
|
434
|
+
* balance.
|
|
435
|
+
*/
|
|
436
|
+
declare function selectPrimaryCredit(args: {
|
|
437
|
+
ledger: CreditLedgerLike;
|
|
438
|
+
callerPubkey?: string;
|
|
439
|
+
currency: string;
|
|
440
|
+
nowMs?: number;
|
|
441
|
+
}): Promise<CreditSnapshot | undefined>;
|
|
442
|
+
/** Narrow a ledger snapshot to the read-only shape a quote handler receives. */
|
|
443
|
+
declare function toCreditView(snapshot: CreditSnapshot): CreditView;
|
|
444
|
+
/**
|
|
445
|
+
* Attach a menu to a response body when there is one. Keeps the "omit the key
|
|
446
|
+
* entirely rather than emit `credit: null`" rule in one place — every consumer
|
|
447
|
+
* checks presence, and the isolate tier depends on the key never appearing.
|
|
448
|
+
*/
|
|
449
|
+
declare function attachCreditMenu(body: Record<string, unknown>, menu: CreditMenu | undefined): Record<string, unknown>;
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Canonical deploy-time attestation payload (internal-review). One signature serves
|
|
453
|
+
* both auth-on-deploy and the `/v1/info#builder` attestation — the bytes the
|
|
454
|
+
* builder signs are the canonical JSON of this object. Making a field required
|
|
455
|
+
* also requires the coordinated fleet-before-caller release note in
|
|
456
|
+
* `public compatibility guide`.
|
|
457
|
+
*/
|
|
458
|
+
interface AttestationPayload {
|
|
459
|
+
/** Immutable platform or self-hosted DVM identifier this attestation applies to. */
|
|
460
|
+
dvm_id: string;
|
|
461
|
+
/** DVM slug the attestation applies to (must match the deploy request's slug). */
|
|
462
|
+
slug: string;
|
|
463
|
+
/** sha256(`canonicaliseForSigning(capabilities)`) hex. Binds the attestation
|
|
464
|
+
* to the capability shape declared at deploy time. */
|
|
465
|
+
capabilities_hash: string;
|
|
466
|
+
/** Builder identity x-only secp256k1 pubkey, hex (32 bytes / 64 chars). */
|
|
467
|
+
builder_pubkey: string;
|
|
468
|
+
/**
|
|
469
|
+
* Per-DVM receipt-signing x-only pubkey, hex (internal-review). Binds the key the
|
|
470
|
+
* deployed DVM signs job receipts with to the cold builder identity: the
|
|
471
|
+
* builder signs this payload, so a receipt verifying under
|
|
472
|
+
* `receipt_pubkey` inherits the builder's authority.
|
|
473
|
+
*
|
|
474
|
+
* Optional: attestations signed before receipts shipped omit it and still
|
|
475
|
+
* verify byte-for-byte (canonical JSON drops absent keys), and deploy paths
|
|
476
|
+
* that can't provision the matching secret leave it unset rather than
|
|
477
|
+
* attesting a key the running process doesn't hold.
|
|
478
|
+
*/
|
|
479
|
+
receipt_pubkey?: string;
|
|
480
|
+
/** Unix seconds at which the attestation was signed. */
|
|
481
|
+
deployed_at: number;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/** A Lightning invoice created by the backend. */
|
|
485
|
+
interface CreatedInvoice {
|
|
486
|
+
/** BOLT11 payment request string. */
|
|
487
|
+
bolt11: string;
|
|
488
|
+
/** Payment hash (hex) — primary key for looking up settlement. */
|
|
489
|
+
paymentHash: string;
|
|
490
|
+
/** Amount in millisatoshis. */
|
|
491
|
+
amountMsats: number;
|
|
492
|
+
/** Unix milliseconds when the invoice expires. */
|
|
493
|
+
expiresAt: number;
|
|
494
|
+
}
|
|
495
|
+
/** Result of looking up an invoice's settlement status. */
|
|
496
|
+
interface InvoiceStatus {
|
|
497
|
+
/** True once the invoice has been paid and preimage is known. */
|
|
498
|
+
settled: boolean;
|
|
499
|
+
/** Preimage (hex) — present only after settlement. */
|
|
500
|
+
preimage?: string;
|
|
501
|
+
/** Unix milliseconds when the invoice settled (if settled). */
|
|
502
|
+
settledAt?: number;
|
|
503
|
+
/** Unix milliseconds when the invoice expires. */
|
|
504
|
+
expiresAt?: number;
|
|
505
|
+
}
|
|
506
|
+
/** How a payment reached a settled state — see `payInvoiceReconciling`. */
|
|
507
|
+
type LightningPayOutcome = "paid" | "reconciled" | "republished";
|
|
508
|
+
/** Settled payment plus the audit trail of how it got there. */
|
|
509
|
+
interface LightningPayment {
|
|
510
|
+
/** Payment preimage (hex) — proof the invoice was paid. */
|
|
511
|
+
preimage: string;
|
|
512
|
+
/** Routing fee reported by the wallet, in millisatoshis. Absent means unknown. */
|
|
513
|
+
feesPaidMsats?: number;
|
|
514
|
+
/** Whether the wallet answered directly, or the outcome had to be reconciled. */
|
|
515
|
+
outcome: LightningPayOutcome;
|
|
516
|
+
}
|
|
517
|
+
/** What a backend can tell us about the wallet behind it. */
|
|
518
|
+
interface LightningWalletInfo {
|
|
519
|
+
/** Human-readable wallet name (e.g. "Alby Hub"). */
|
|
520
|
+
alias?: string;
|
|
521
|
+
/** Operations the wallet supports (e.g. `pay_invoice`, `lookup_invoice`). */
|
|
522
|
+
methods: string[];
|
|
523
|
+
/**
|
|
524
|
+
* Budget fields the wallet volunteered, if any.
|
|
525
|
+
*
|
|
526
|
+
* No rail behind this interface is required to expose a connection's
|
|
527
|
+
* spending cap, and NWC — today's only implementation — has no method that
|
|
528
|
+
* returns one. Present it when a wallet offers it, never infer it.
|
|
529
|
+
*/
|
|
530
|
+
walletReportedBudget?: Record<string, unknown>;
|
|
531
|
+
}
|
|
532
|
+
/** Abstraction over Lightning wallet backends (NWC, future). */
|
|
533
|
+
interface LightningBackend {
|
|
534
|
+
/**
|
|
535
|
+
* Pay a bolt11 invoice exactly once. Returns the preimage on success.
|
|
536
|
+
*
|
|
537
|
+
* Implementations must never re-send a payment on an unknown outcome without
|
|
538
|
+
* first reconciling against the wallet — the rails behind this interface have
|
|
539
|
+
* no idempotency key, so a blind retry is a second payment.
|
|
540
|
+
*
|
|
541
|
+
* `amountMsats` is passed when known (callers always have it from the
|
|
542
|
+
* provider's payment-request). Backends that don't need it may ignore the
|
|
543
|
+
* param.
|
|
544
|
+
*/
|
|
545
|
+
payInvoice(bolt11: string, amountMsats?: number): Promise<LightningPayment>;
|
|
546
|
+
/** Get wallet balance in millisatoshis. */
|
|
547
|
+
getBalance(): Promise<number>;
|
|
548
|
+
/** Get wallet info (name, supported methods, any volunteered budget fields). */
|
|
549
|
+
getInfo(): Promise<LightningWalletInfo>;
|
|
550
|
+
/** Create a new incoming invoice for the given amount and description. */
|
|
551
|
+
createInvoice(params: {
|
|
552
|
+
amountMsats: number;
|
|
553
|
+
description?: string;
|
|
554
|
+
expirySeconds?: number;
|
|
555
|
+
}): Promise<CreatedInvoice>;
|
|
556
|
+
/** Look up an invoice's settlement status by payment hash. */
|
|
557
|
+
lookupInvoice(paymentHash: string): Promise<InvoiceStatus>;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/** Cached liveness signal for a platform-hosted DVM's Lightning receive rail. */
|
|
561
|
+
interface LightningRailHealth {
|
|
562
|
+
/** Whether the receive rail may be advertised at this instant. */
|
|
563
|
+
available(): boolean;
|
|
564
|
+
/** Prime the cached signal at boot when the source supports it. */
|
|
565
|
+
refresh?(): Promise<void>;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/** Builder-facing wiring for the Lightning receive leg (internal-review). */
|
|
569
|
+
interface LightningReceiveConfig {
|
|
570
|
+
/**
|
|
571
|
+
* Receive-only NWC connection URI (`DVMKIT_NWC_RECEIVE_URI`). Validated at
|
|
572
|
+
* boot: a connection that can *spend* is refused outright.
|
|
573
|
+
*/
|
|
574
|
+
uri?: string;
|
|
575
|
+
/**
|
|
576
|
+
* Pre-built backend, bypassing URI parsing and the connect-time probe. Test
|
|
577
|
+
* seam only — production always goes through `uri` so the receive-only
|
|
578
|
+
* property is actually checked.
|
|
579
|
+
*/
|
|
580
|
+
backend?: LightningBackend;
|
|
581
|
+
/** Invoice lifetime in seconds. Defaults to {@link DEFAULT_INVOICE_TTL_SECONDS}. */
|
|
582
|
+
invoiceTtlSeconds?: number;
|
|
583
|
+
/**
|
|
584
|
+
* Smallest funding this deployment can receive over Lightning, in sats
|
|
585
|
+
* (`DVMKIT_LIGHTNING_FUNDING_MIN_SATS`). The floor is a property of the
|
|
586
|
+
* receive wallet's channel policy (`htlc_minimum_msat` upstream), so it is
|
|
587
|
+
* per-deployment and sats-physical — the menu renders it into fiat with a
|
|
588
|
+
* margin, but everything that *enforces* it compares raw sats. Defaults to
|
|
589
|
+
* {@link DEFAULT_LIGHTNING_FUNDING_MIN_SATS}.
|
|
590
|
+
*/
|
|
591
|
+
fundingMinSats?: number;
|
|
592
|
+
/**
|
|
593
|
+
* Optional platform-derived channel-liveness gate. Platform-hosted DVMs use
|
|
594
|
+
* it alongside their NWC reachability probe; self-hosted DVMs omit it.
|
|
595
|
+
*/
|
|
596
|
+
railHealth?: LightningRailHealth;
|
|
597
|
+
}
|
|
598
|
+
/** Default bolt11 lifetime — long enough to pay by hand, short enough to retire. */
|
|
599
|
+
declare const DEFAULT_INVOICE_TTL_SECONDS = 900;
|
|
600
|
+
/**
|
|
601
|
+
* Floor on the invoice lifetime: twice the signed-request drift window, so the
|
|
602
|
+
* bolt11 always outlives the funding negotiation that produced it (the issue's
|
|
603
|
+
* "invoice expiry ≥ the funding-negotiation window"). A shorter one would
|
|
604
|
+
* expire inside the caller's own retry budget and strand them mid-top-up.
|
|
605
|
+
*/
|
|
606
|
+
declare const MIN_INVOICE_TTL_SECONDS = 600;
|
|
607
|
+
/**
|
|
608
|
+
* The builder-side Lightning receive leg (internal-review, credits spec §4).
|
|
609
|
+
*
|
|
610
|
+
* Issues a bolt11 over a **receive-only** NWC connection while the DVM is
|
|
611
|
+
* awake serving the 402, and credits the ledger when a later request observes
|
|
612
|
+
* settlement. Two properties are load-bearing:
|
|
613
|
+
*
|
|
614
|
+
* - **It never holds a send credential.** `pay_invoice` on this connection is
|
|
615
|
+
* refused at construction, so a compromised DVM can mint invoices and
|
|
616
|
+
* nothing else. The drain/refund sender (internal-review) is a separate, budgeted
|
|
617
|
+
* connection by rule.
|
|
618
|
+
* - **Crediting is pull-based — there is no settlement watcher.** The caller's
|
|
619
|
+
* next request drives `lookup_invoice`, which is what makes this work on a
|
|
620
|
+
* suspend-to-zero fleet: a machine that is asleep has nothing to miss.
|
|
621
|
+
* Wallet downtime at that moment delays crediting; it never loses money,
|
|
622
|
+
* because the invoice→credit binding is a durable row.
|
|
623
|
+
*
|
|
624
|
+
* Reachability is **cached**, never probed per request: the funding menu is
|
|
625
|
+
* assembled on every quote and every 402, so a live NIP-47 round trip there
|
|
626
|
+
* would put a nostr relay in the latency path of every priced call.
|
|
627
|
+
*/
|
|
628
|
+
declare class LightningReceive {
|
|
629
|
+
private readonly source;
|
|
630
|
+
readonly invoiceTtlSeconds: number;
|
|
631
|
+
readonly fundingMinSats: number;
|
|
632
|
+
private health;
|
|
633
|
+
private checkedAtMs;
|
|
634
|
+
private refreshing;
|
|
635
|
+
private permissionRefusal;
|
|
636
|
+
private constructor();
|
|
637
|
+
private readonly railHealth;
|
|
638
|
+
/**
|
|
639
|
+
* Validate the configured connection and build the receive leg, or return
|
|
640
|
+
* `undefined` when this DVM didn't configure one.
|
|
641
|
+
*
|
|
642
|
+
* **Throws on a credential that is wrong, degrades on one that is merely
|
|
643
|
+
* unreachable.** A wallet outage at boot must not stop a DVM whose other
|
|
644
|
+
* rails are fine — the menu simply omits `lightning` until a later probe
|
|
645
|
+
* succeeds. A connection that can spend, or one that can't state what it can
|
|
646
|
+
* do, is a different thing entirely: it is a standing money risk that no
|
|
647
|
+
* amount of retrying fixes, so it fails the boot loudly with the fix in the
|
|
648
|
+
* message.
|
|
649
|
+
*/
|
|
650
|
+
static create(config: LightningReceiveConfig): Promise<LightningReceive | undefined>;
|
|
651
|
+
/**
|
|
652
|
+
* Whether `lightning` belongs on the funding menu right now.
|
|
653
|
+
*
|
|
654
|
+
* Reads the cached observation and never blocks; a stale one kicks off a
|
|
655
|
+
* background refresh and answers with what we last knew. Advertising a rail
|
|
656
|
+
* whose wallet is down would hand the caller an option that 402s on use —
|
|
657
|
+
* the §4 posture is to drop it and let the sale survive on the others.
|
|
658
|
+
*/
|
|
659
|
+
available(): boolean;
|
|
660
|
+
/**
|
|
661
|
+
* Issue (or re-issue) the bolt11 funding `(creditId, fundId)`.
|
|
662
|
+
*
|
|
663
|
+
* The invoice is minted **for** the credit and amount named in the caller's
|
|
664
|
+
* signed body — that binding is the internal-review condition-3 commitment on this
|
|
665
|
+
* rail, and it is why no artifact hash rides the request: there is no
|
|
666
|
+
* caller-supplied artifact to hash. A re-poll returns the stored row rather
|
|
667
|
+
* than minting again; a second bolt11 for one `fund_id` would leave two
|
|
668
|
+
* payable invoices against a funding that can only be credited once.
|
|
669
|
+
*/
|
|
670
|
+
issue(ledger: CreditLedgerLike, args: {
|
|
671
|
+
creditId: string;
|
|
672
|
+
fundId: string;
|
|
673
|
+
callerPubkey: string;
|
|
674
|
+
currency: string;
|
|
675
|
+
amountMicro: number;
|
|
676
|
+
amountMsats: number;
|
|
677
|
+
description?: string;
|
|
678
|
+
}): Promise<CreditInvoiceRecord>;
|
|
679
|
+
/**
|
|
680
|
+
* Ask the wallet whether one payment hash is paid, and when (internal-review).
|
|
681
|
+
*
|
|
682
|
+
* The operator's reconcile verb runs this before it credits anything. A
|
|
683
|
+
* `blocked` row implies payment — {@link applyOne} returns above the block
|
|
684
|
+
* classification when the lookup says unpaid — but that is *this fleet's
|
|
685
|
+
* belief, recorded possibly weeks ago*, and the verb it gates mints balance
|
|
686
|
+
* against it. One round trip turns the belief into a fact and recovers the
|
|
687
|
+
* true settlement instant for `settled_at`, which a blocked row never got to
|
|
688
|
+
* write.
|
|
689
|
+
*
|
|
690
|
+
* Unlike {@link settlePending} this **throws**: there is no request whose
|
|
691
|
+
* latency it protects, and crediting on an unverifiable wallet is exactly
|
|
692
|
+
* what it exists to prevent. `NOT_FOUND` is the one exception, and it is not
|
|
693
|
+
* an outage — it is the wallet saying it has never seen this hash, the same
|
|
694
|
+
* reading {@link applyOne} and the caller-side reconcile in `src/lib/nwc.ts`
|
|
695
|
+
* take. It comes back as `known: false` because the operator's repair for it
|
|
696
|
+
* ("you are pointed at a different wallet than the one that minted this")
|
|
697
|
+
* is not the repair for an unpaid invoice, and certainly not for a retry.
|
|
698
|
+
*/
|
|
699
|
+
lookupSettlement(paymentHash: string): Promise<{
|
|
700
|
+
settled: boolean;
|
|
701
|
+
settledAt?: number;
|
|
702
|
+
known: boolean;
|
|
703
|
+
}>;
|
|
704
|
+
/**
|
|
705
|
+
* Consult the wallet about this caller's outstanding invoices and credit the
|
|
706
|
+
* ones that settled — the pull half of pull-based crediting.
|
|
707
|
+
*
|
|
708
|
+
* The pending rows are read locally first, so the common case (nothing
|
|
709
|
+
* outstanding) costs one indexed SELECT and no network at all. Bounded to
|
|
710
|
+
* {@link MAX_SETTLE_CHECKS} invoices on a short deadline, and **never throws
|
|
711
|
+
* into the request**: a wallet failure here means the caller's balance is
|
|
712
|
+
* merely not updated yet, which the ordinary `insufficient_credit` path
|
|
713
|
+
* already states honestly.
|
|
714
|
+
*
|
|
715
|
+
* **Each invoice gets its own failure boundary.** The sweep window is a few
|
|
716
|
+
* rows wide, so a row that fails on its own terms — a `fund_id` the caller
|
|
717
|
+
* reused on another rail, a credit someone else opened first — must not take
|
|
718
|
+
* the rest of the pass down with it: the next invoice in the window may be
|
|
719
|
+
* the paid one, and it would never be looked up. Only a transport failure
|
|
720
|
+
* ends the pass early, because there the wallet itself is gone and the
|
|
721
|
+
* remaining lookups would just spend the caller's latency confirming it.
|
|
722
|
+
*/
|
|
723
|
+
settlePending(ledger: CreditLedgerLike, args: {
|
|
724
|
+
callerPubkey: string;
|
|
725
|
+
creditId?: string;
|
|
726
|
+
creditTtlMs: number;
|
|
727
|
+
dvmId?: string;
|
|
728
|
+
enqueueCreditDeposit?: CreditDepositEnqueue;
|
|
729
|
+
nowMs?: number;
|
|
730
|
+
}): Promise<InvoiceSettlement[]>;
|
|
731
|
+
/**
|
|
732
|
+
* One invoice's settlement check. Returns the applied settlement, or
|
|
733
|
+
* `undefined` when nothing changed (still unpaid, or retired unpaid).
|
|
734
|
+
*
|
|
735
|
+
* Throws whatever the wallet or the ledger threw — classifying that is
|
|
736
|
+
* {@link retireOrLog}'s job, so this stays one invoice's happy path.
|
|
737
|
+
*/
|
|
738
|
+
private applyOne;
|
|
739
|
+
/**
|
|
740
|
+
* Decide what one invoice's failure means, and retire the invoice when the
|
|
741
|
+
* answer is "this can never succeed".
|
|
742
|
+
*
|
|
743
|
+
* The split that matters is permanent-versus-transient, because it decides
|
|
744
|
+
* whether the row keeps a slot in the sweep window. A transient failure — the
|
|
745
|
+
* database blinked, the wallet answered oddly — leaves it `pending` and the
|
|
746
|
+
* caller's next request retries it. A **permanent** one means the ledger will
|
|
747
|
+
* refuse this invoice identically forever, so leaving it pending costs a
|
|
748
|
+
* `lookup_invoice` on every subsequent request and, at
|
|
749
|
+
* {@link MAX_SETTLE_CHECKS} of them, fills the window so a genuinely payable
|
|
750
|
+
* invoice behind them is never even looked up. Those get `blocked`, which
|
|
751
|
+
* drops them out of the sweep and hands the operator the payment hash.
|
|
752
|
+
*/
|
|
753
|
+
private retireOrLog;
|
|
754
|
+
/**
|
|
755
|
+
* The reason this invoice can never be credited, or `undefined` if the
|
|
756
|
+
* failure was transient.
|
|
757
|
+
*
|
|
758
|
+
* Every code here is decided by state a retry cannot move — a row the ledger
|
|
759
|
+
* has already committed (a `credit_fundings` entry at this
|
|
760
|
+
* `(credit_id, fund_id)`, or a `credits` row whose owner, denomination or
|
|
761
|
+
* rail disagrees with the invoice), or the basis this module rebuilds
|
|
762
|
+
* identically from the invoice row on every pass. None of them is a race.
|
|
763
|
+
*
|
|
764
|
+
* `funding_replayed` is the one that needs a second read to classify.
|
|
765
|
+
* Usually it *is* benign — a concurrent check won the race and the money is
|
|
766
|
+
* credited either way — but the key is `(credit_id, fund_id)` and `fund_id`
|
|
767
|
+
* is **caller-chosen**: reuse it on cashu or x402 after this bolt11 was
|
|
768
|
+
* minted and the row that collided is a different payment entirely, so the
|
|
769
|
+
* sats this invoice received have nowhere to land.
|
|
770
|
+
*/
|
|
771
|
+
private blockingReason;
|
|
772
|
+
private withBackend;
|
|
773
|
+
/** Record activity without letting it promote an unvalidated connection. */
|
|
774
|
+
private noteOperationOk;
|
|
775
|
+
private notePermissionsOk;
|
|
776
|
+
private notePermissionRefused;
|
|
777
|
+
/**
|
|
778
|
+
* Only a *transport* failure from an ordinary invoice operation is a health
|
|
779
|
+
* signal. Permission-probe refusals are classified by {@link refresh}.
|
|
780
|
+
*/
|
|
781
|
+
private noteFailure;
|
|
782
|
+
private refresh;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
/** Tunables for `checkMintHealth`. */
|
|
786
|
+
interface CheckMintHealthOptions {
|
|
787
|
+
/** Per-attempt timeout in ms. Default: 5000. */
|
|
788
|
+
timeoutMs?: number;
|
|
789
|
+
/** Total attempts including the first. Default: 3. */
|
|
790
|
+
retries?: number;
|
|
791
|
+
/** Inter-attempt backoff schedule in ms. Default: [200, 500, 1000]. */
|
|
792
|
+
backoffMs?: number[];
|
|
793
|
+
/** Injected `fetch` for tests. Default: global fetch. */
|
|
794
|
+
fetchImpl?: typeof fetch;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
/**
|
|
798
|
+
* Per-DVM runtime mint-health tracker (internal-review). Alongside the platform's
|
|
799
|
+
* durable mint-health cron, probes every configured
|
|
800
|
+
* mint on a timer, tracks consecutive failures, and flips a mint to `sick`
|
|
801
|
+
* once the threshold is crossed. The SDK uses the resulting view to:
|
|
802
|
+
*
|
|
803
|
+
* 1. Filter `/v1/info`'s advertised `mints` to currently-healthy ones.
|
|
804
|
+
* 2. Reject new accumulator receives at sick mints before they touch the
|
|
805
|
+
* DB (`cashu_mint_sick` 503 from `verifyAccumulatorReceipt`).
|
|
806
|
+
*
|
|
807
|
+
* Since internal-review this tracker is the sole owner of mint health: boot no longer
|
|
808
|
+
* runs a fail-fast check before binding the port. To preserve what boot used
|
|
809
|
+
* to guarantee it (a) treats a mint as healthy only when it's reachable AND
|
|
810
|
+
* NUT-compliant (reproducing `assertNutSupport` via `nutCompliant`), and (b)
|
|
811
|
+
* exposes `initialProbeComplete()` so the paid path holds calls at
|
|
812
|
+
* `mint_health_pending` until the first probe validates ≥1 mint.
|
|
813
|
+
*
|
|
814
|
+
* Existing accumulator entries are not touched — they're P2PK-locked to the
|
|
815
|
+
* original mint and stay put until `dvmctl melt-pending` drains them. The
|
|
816
|
+
* accumulator monitor's `wallet_accumulator_batch_expiring_soon` already
|
|
817
|
+
* pages the builder when a stuck batch nears `t_expire`.
|
|
818
|
+
*
|
|
819
|
+
* In-memory only — no DB persistence. The platform cron keeps the durable
|
|
820
|
+
* record + email path (internal-review); the SDK only needs fresh in-process state
|
|
821
|
+
* to make routing decisions.
|
|
822
|
+
*/
|
|
823
|
+
|
|
824
|
+
/** Per-mint runtime view exposed by `snapshot()`. */
|
|
825
|
+
interface MintHealthSnapshot {
|
|
826
|
+
/** Last-observed wall-clock latency of the probe in ms. */
|
|
827
|
+
latencyMs?: number;
|
|
828
|
+
/** Consecutive failures up to the most recent tick. Reset on any success. */
|
|
829
|
+
consecutiveFailures: number;
|
|
830
|
+
/**
|
|
831
|
+
* True once the mint is considered unhealthy for routing: either
|
|
832
|
+
* `consecutiveFailures` crossed the threshold, or the last probe reached the
|
|
833
|
+
* mint but it fails the NUT gate (a deterministic, permanent config error
|
|
834
|
+
* that flips `sick` immediately — internal-review).
|
|
835
|
+
*/
|
|
836
|
+
sick: boolean;
|
|
837
|
+
/**
|
|
838
|
+
* True when the last successful probe reached the mint but it fails the NUT
|
|
839
|
+
* capability gate ({@link assertNutSupport}'s required NUTs + bolt11/sat).
|
|
840
|
+
* Distinct from a plain outage: NUT-incompatibility is deterministic and
|
|
841
|
+
* never resolves without a config/mint change.
|
|
842
|
+
*/
|
|
843
|
+
nutIncompatible?: boolean;
|
|
844
|
+
/** Mint version reported by the last successful probe. */
|
|
845
|
+
version?: string;
|
|
846
|
+
/** Error category from the most recent failed probe. */
|
|
847
|
+
errorCode?: string;
|
|
848
|
+
}
|
|
849
|
+
/** Tunables for {@link MintHealthTracker}. */
|
|
850
|
+
interface MintHealthTrackerOptions {
|
|
851
|
+
/** Cashu mint URLs the DVM accepts. The tracker probes every entry. */
|
|
852
|
+
mints: string[];
|
|
853
|
+
/**
|
|
854
|
+
* Tick cadence in ms. Defaults to {@link DEFAULT_TICK_INTERVAL_MS} or the
|
|
855
|
+
* `DVMKIT_MINT_HEALTH_TICK_MS` env var when set.
|
|
856
|
+
*/
|
|
857
|
+
tickIntervalMs?: number;
|
|
858
|
+
/**
|
|
859
|
+
* Consecutive failed probes before flipping a mint to `sick`. Defaults to
|
|
860
|
+
* {@link DEFAULT_FAILURES_TO_SICK} or `DVMKIT_MINT_FAILURES_TO_SICK` env.
|
|
861
|
+
*/
|
|
862
|
+
consecutiveFailuresToSick?: number;
|
|
863
|
+
/** Injected `fetch` for tests. */
|
|
864
|
+
fetchImpl?: typeof fetch;
|
|
865
|
+
/** Injected `Date.now` for tests. */
|
|
866
|
+
now?: () => number;
|
|
867
|
+
/**
|
|
868
|
+
* Per-probe timeout / retry / backoff, forwarded to {@link checkMintHealth}.
|
|
869
|
+
* Defaults to that function's own defaults — 3 attempts with `[200, 500]`
|
|
870
|
+
* backoff, which is what a real tick should do.
|
|
871
|
+
*
|
|
872
|
+
* Tests that drive several failing ticks set `backoffMs: [0]`: a failing
|
|
873
|
+
* probe otherwise costs 700ms of real `setTimeout`, and four of them put a
|
|
874
|
+
* unit test within a loaded CI runner's reach of the 5s default timeout.
|
|
875
|
+
* `fetchImpl` is set on the tracker, not here.
|
|
876
|
+
*/
|
|
877
|
+
probe?: Omit<CheckMintHealthOptions, "fetchImpl">;
|
|
878
|
+
}
|
|
879
|
+
/**
|
|
880
|
+
* Runtime mint-health tracker. Owns its own timer; `start()` runs an initial
|
|
881
|
+
* probe immediately so the first received call doesn't see a stale snapshot.
|
|
882
|
+
*/
|
|
883
|
+
declare class MintHealthTracker {
|
|
884
|
+
private readonly mints;
|
|
885
|
+
private readonly tickIntervalMs;
|
|
886
|
+
private readonly failuresToSick;
|
|
887
|
+
private readonly fetchImpl?;
|
|
888
|
+
private readonly probeOpts;
|
|
889
|
+
private readonly nowFn;
|
|
890
|
+
private readonly state;
|
|
891
|
+
private timer?;
|
|
892
|
+
private stopped;
|
|
893
|
+
private inFlight?;
|
|
894
|
+
private firstProbeComplete;
|
|
895
|
+
constructor(opts: MintHealthTrackerOptions);
|
|
896
|
+
/** Kick off the periodic probe. Runs one tick immediately. */
|
|
897
|
+
start(): void;
|
|
898
|
+
/** Stop the timer. Safe to call multiple times. */
|
|
899
|
+
stop(): void;
|
|
900
|
+
/**
|
|
901
|
+
* Probe every configured mint once. Sequential per mint to bound concurrent
|
|
902
|
+
* outbound load; the tick cadence makes parallelism unnecessary.
|
|
903
|
+
*/
|
|
904
|
+
runOnce(): Promise<void>;
|
|
905
|
+
/**
|
|
906
|
+
* True once the first full probe tick has landed. Before this, paid calls
|
|
907
|
+
* are held at `503 mint_health_pending` rather than accepted against
|
|
908
|
+
* unvalidated mints (internal-review).
|
|
909
|
+
*/
|
|
910
|
+
initialProbeComplete(): boolean;
|
|
911
|
+
/**
|
|
912
|
+
* True when the mint is either healthy or unknown to the tracker. Unknown
|
|
913
|
+
* mints (e.g. a value coming in via an externally-supplied tracker) get the
|
|
914
|
+
* benefit of the doubt — the receive path's mint-allowlist check is the
|
|
915
|
+
* authoritative gate on which mints the DVM accepts.
|
|
916
|
+
*/
|
|
917
|
+
isHealthy(mintUrl: string): boolean;
|
|
918
|
+
/** Currently-healthy subset of the configured mints, preserving config order. */
|
|
919
|
+
healthyMints(): string[];
|
|
920
|
+
/**
|
|
921
|
+
* True when the last probe reached the mint but it failed the NUT capability
|
|
922
|
+
* gate — a deterministic, permanent config error, unlike a transient outage.
|
|
923
|
+
* Unknown mints return false. `advertisedMints()` uses this to keep a
|
|
924
|
+
* permanently-unsettleable mint off `/v1/info` even in the all-sick fallback,
|
|
925
|
+
* where a merely-flapping mint is still published (internal-review).
|
|
926
|
+
*/
|
|
927
|
+
isNutIncompatible(mintUrl: string): boolean;
|
|
928
|
+
/** Plain-object snapshot keyed by mint URL — for tests + observability. */
|
|
929
|
+
snapshot(): Record<string, MintHealthSnapshot>;
|
|
930
|
+
private applyResult;
|
|
931
|
+
/**
|
|
932
|
+
* One-shot structured summary emitted after the first probe tick. Replaces
|
|
933
|
+
* the boot check's per-mint `cashu_mint_startup_health` lines and surfaces an
|
|
934
|
+
* all-mints-unhealthy warning — the operator alert that the Fly restart-loop
|
|
935
|
+
* used to be before boot stopped refusing to start (internal-review).
|
|
936
|
+
*/
|
|
937
|
+
private logBootHealth;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
/**
|
|
941
|
+
* Owner display identity surfaced on `/v1/info#owner` (internal-review). Personal orgs
|
|
942
|
+
* resolve to the owner builder's profile; shared orgs resolve to the org's own
|
|
943
|
+
* profile. Container DVMs read from env vars; isolate DVMs resolve from Postgres.
|
|
944
|
+
*/
|
|
945
|
+
interface OwnerDisplay {
|
|
946
|
+
handle: string;
|
|
947
|
+
displayName?: string | null;
|
|
948
|
+
avatarUrl?: string | null;
|
|
949
|
+
type: "builder" | "org";
|
|
950
|
+
}
|
|
951
|
+
/** Optional builder identity surfaced on `/v1/info` (forward-compatible stub). */
|
|
952
|
+
interface BuilderIdentity {
|
|
953
|
+
id?: string;
|
|
954
|
+
name?: string;
|
|
955
|
+
url?: string;
|
|
956
|
+
/**
|
|
957
|
+
* Builder identity x-only secp256k1 pubkey (internal-review). When populated, the
|
|
958
|
+
* `/v1/info#builder` block also carries `attestation` + `signature` so
|
|
959
|
+
* consumers can verify the deploy was signed by the holder of this key.
|
|
960
|
+
* Populated at deploy time from `DVMKIT_BUILDER_PUBKEY` (Fly secret); the
|
|
961
|
+
* SDK never re-signs at runtime.
|
|
962
|
+
*/
|
|
963
|
+
pubkey?: string;
|
|
964
|
+
/** Canonical deploy-time attestation payload (internal-review). Served verbatim. */
|
|
965
|
+
attestation?: AttestationPayload;
|
|
966
|
+
/** Schnorr signature over `canonicaliseForSigning(attestation)` (internal-review). */
|
|
967
|
+
signature?: string;
|
|
968
|
+
}
|
|
969
|
+
/** Platform reporter overrides — the host falls back to env when omitted. */
|
|
970
|
+
interface PlatformReporterOpts {
|
|
971
|
+
/** Bearer token. Defaults to `DVMKIT_PLATFORM_TOKEN` env. */
|
|
972
|
+
token?: string;
|
|
973
|
+
/** Platform internal URL. Defaults to `DVMKIT_PLATFORM_URL` env. */
|
|
974
|
+
url?: string;
|
|
975
|
+
}
|
|
976
|
+
/** Options for {@link createDVMHost}. */
|
|
977
|
+
interface DVMHostOpts {
|
|
978
|
+
/**
|
|
979
|
+
* Postgres connection string. Defaults to `DATABASE_URL` env.
|
|
980
|
+
* Required at runtime (internal-review). Boot fails when missing unless `jobStore`
|
|
981
|
+
* is explicitly supplied or `devMode` is true (test/dev escape hatches).
|
|
982
|
+
*/
|
|
983
|
+
database?: string;
|
|
984
|
+
/** Listen port. Defaults to `PORT` env or 8080. */
|
|
985
|
+
port?: number;
|
|
986
|
+
/** Environment variables. Defaults to `process.env`. */
|
|
987
|
+
env?: Record<string, string>;
|
|
988
|
+
/** Builder identity for `/v1/info` (forward-compatible). */
|
|
989
|
+
builder?: BuilderIdentity;
|
|
990
|
+
/** Owner display identity for `/v1/info#owner` (internal-review). Falls back to env vars. */
|
|
991
|
+
owner?: OwnerDisplay;
|
|
992
|
+
/** Platform reporter overrides. Defaults to env-derived values. */
|
|
993
|
+
platformReporter?: PlatformReporterOpts;
|
|
994
|
+
/** Override the SDK's fx fetcher. Defaults to env-configured CoinGecko. */
|
|
995
|
+
fx?: FxFetcher;
|
|
996
|
+
/**
|
|
997
|
+
* Custom `/health` handler. When set, the SDK installs this instead of the
|
|
998
|
+
* default `{ status: "ok" }` responder — useful for runtime-specific health
|
|
999
|
+
* (pool depth, draining state, 503 while draining).
|
|
1000
|
+
*/
|
|
1001
|
+
healthHandler?: (c: Context) => Response | Promise<Response>;
|
|
1002
|
+
/** KV store override. Defaults to PostgresKVStore (or MemoryKVStore in dev). */
|
|
1003
|
+
store?: KVStore;
|
|
1004
|
+
/** JobStore override. Takes precedence over `database`. */
|
|
1005
|
+
jobStore?: JobStore;
|
|
1006
|
+
/**
|
|
1007
|
+
* Existing Postgres pool to reuse instead of opening one from `database`
|
|
1008
|
+
* (internal-review test seam). When set, the host builds its JobStore / KVStore /
|
|
1009
|
+
* cashu accumulator on this pool and leaves it open at shutdown — the caller
|
|
1010
|
+
* owns it. Lets the e2e harness run DVMs in `p2pk-accumulator` mode against
|
|
1011
|
+
* its single shared pool rather than spawning a pool per DVM.
|
|
1012
|
+
*/
|
|
1013
|
+
pool?: Pool;
|
|
1014
|
+
/** Cashu mints accepted. Defaults to env `DVMKIT_CASHU_MINTS`. */
|
|
1015
|
+
mints?: string[];
|
|
1016
|
+
/** Cashu receive mode override. */
|
|
1017
|
+
cashuMode?: CashuMode;
|
|
1018
|
+
/** MPP handle override. Defaults to env-resolved via `createMppFromOpts`. */
|
|
1019
|
+
mpp?: MppxServer;
|
|
1020
|
+
/** x402 stablecoin payment configuration. Defaults to env-resolved. */
|
|
1021
|
+
x402?: X402Config;
|
|
1022
|
+
/** Payment methods override. Defaults to derived from configured rails. */
|
|
1023
|
+
paymentMethods?: PaymentMethod[];
|
|
1024
|
+
/**
|
|
1025
|
+
* Lightning receive leg for credit funding (internal-review). Defaults to the
|
|
1026
|
+
* env-resolved `DVMKIT_NWC_RECEIVE_URI` /
|
|
1027
|
+
* `DVMKIT_NWC_RECEIVE_INVOICE_TTL_SECONDS`. Pass `backend` to inject a
|
|
1028
|
+
* wallet directly — a test seam that skips the connect-time probe, so
|
|
1029
|
+
* production must always come through the URI.
|
|
1030
|
+
*/
|
|
1031
|
+
lightningReceive?: LightningReceiveConfig;
|
|
1032
|
+
/** When true, payment is skipped if no mints are configured (dev/test). */
|
|
1033
|
+
devMode?: boolean;
|
|
1034
|
+
/** Consumed-credential store override (test seam). */
|
|
1035
|
+
consumedCredentialStore?: ConsumedCredentialStore;
|
|
1036
|
+
/** Mint-health tracker override (test seam). */
|
|
1037
|
+
mintHealthTracker?: MintHealthTracker;
|
|
1038
|
+
/**
|
|
1039
|
+
* Wrap the SDK compatibility gate before it is exposed to descriptor custom
|
|
1040
|
+
* routes. Platform hosts use this to enforce their independently released
|
|
1041
|
+
* caller rollout policy; self-hosted SDK consumers receive the default gate.
|
|
1042
|
+
*/
|
|
1043
|
+
wrapRouteClientCompatibility?: (sdkGate: ClientCompatibilityGate) => ClientCompatibilityGate;
|
|
1044
|
+
}
|
|
1045
|
+
/** Per-mount options. Currently only `prefix` is supported. */
|
|
1046
|
+
interface MountOpts {
|
|
1047
|
+
/** Path prefix for this DVM's protocol routes (e.g. `/delete-feed`). */
|
|
1048
|
+
prefix?: string;
|
|
1049
|
+
}
|
|
1050
|
+
/** Live DVM host. Single wiring locus for the SDK (internal-review). */
|
|
1051
|
+
interface DVMHost {
|
|
1052
|
+
/**
|
|
1053
|
+
* Underlying Hono app for custom non-protocol routes that belong to the
|
|
1054
|
+
* host (cross-DVM `/admin`, cron callbacks, etc.). For DVM-scoped routes,
|
|
1055
|
+
* declare `routes(app)` on the descriptor.
|
|
1056
|
+
*/
|
|
1057
|
+
readonly app: Hono;
|
|
1058
|
+
/**
|
|
1059
|
+
* Resolved Postgres pool — undefined before `host.serve()` completes
|
|
1060
|
+
* database resolution, set thereafter. Exposed so DVM-local Postgres
|
|
1061
|
+
* consumers (e.g. scrape's `ScrapeDb` per-fetch event store, internal-review)
|
|
1062
|
+
* can reuse the host's pool rather than constructing their own. Wire
|
|
1063
|
+
* inside descriptor `onBoot` (fires after pool init, before listener opens).
|
|
1064
|
+
*/
|
|
1065
|
+
readonly pool: Pool | undefined;
|
|
1066
|
+
/**
|
|
1067
|
+
* Money-safe credit ledger (internal-review) — undefined before `host.serve()`
|
|
1068
|
+
* resolves stores, set thereafter. Postgres-backed when a pool exists;
|
|
1069
|
+
* pool-less hosts get a shared in-memory ledger so the internal-review fund+draw
|
|
1070
|
+
* semantics hold everywhere. Same wiring window as `pool` (descriptor
|
|
1071
|
+
* `onBoot` fires after init, before the listener opens).
|
|
1072
|
+
*/
|
|
1073
|
+
readonly creditLedger: CreditLedgerLike | undefined;
|
|
1074
|
+
/**
|
|
1075
|
+
* Add a DVM to this host. Multiple mounts compose at distinct prefixes;
|
|
1076
|
+
* a single mount with no prefix attaches at root.
|
|
1077
|
+
*/
|
|
1078
|
+
mount<S, I extends ZodLike | undefined>(descriptor: DVMDescriptor<S, I>, opts?: MountOpts): void;
|
|
1079
|
+
/** Start listening. Returns the live server handle. */
|
|
1080
|
+
serve(opts?: {
|
|
1081
|
+
port?: number;
|
|
1082
|
+
}): Promise<{
|
|
1083
|
+
url: string;
|
|
1084
|
+
close: () => Promise<void>;
|
|
1085
|
+
}>;
|
|
1086
|
+
/** Graceful shutdown. Runs descriptor `onShutdown` hooks in reverse mount order. */
|
|
1087
|
+
shutdown(): Promise<void>;
|
|
1088
|
+
}
|
|
1089
|
+
/**
|
|
1090
|
+
* Single wiring locus for SDK-side construction (internal-review). Replaces both the
|
|
1091
|
+
* `serve()` wrapper and the `createDVMServer` direct path. Reads env once,
|
|
1092
|
+
* resolves payment rails, owns the Postgres pool, and mounts each DVM
|
|
1093
|
+
* descriptor as a Hono sub-app on `host.app`.
|
|
1094
|
+
*/
|
|
1095
|
+
declare function createDVMHost(opts?: DVMHostOpts): DVMHost;
|
|
1096
|
+
|
|
1097
|
+
/** Promise resolver for a pending client prompt response. */
|
|
1098
|
+
interface PromptResolver {
|
|
1099
|
+
resolve: (value: ResponseContent) => void;
|
|
1100
|
+
reject: (reason: Error) => void;
|
|
1101
|
+
}
|
|
1102
|
+
/** Promise resolver for a pending client payment. */
|
|
1103
|
+
interface PaymentResolver {
|
|
1104
|
+
resolve: (value: PaymentContent) => void;
|
|
1105
|
+
reject: (reason: Error) => void;
|
|
1106
|
+
}
|
|
1107
|
+
/** In-memory representation of a running job. */
|
|
1108
|
+
interface ServerJob {
|
|
1109
|
+
id: string;
|
|
1110
|
+
tags: string[];
|
|
1111
|
+
/**
|
|
1112
|
+
* Capability name this job belongs to (internal-review). Set at submission from
|
|
1113
|
+
* `POST /v1/job` body's `capability` field; routes the SDK runtime to the
|
|
1114
|
+
* right `onJob` / `onResponse` / `onPayment` handler and the right input
|
|
1115
|
+
* schema. Required — the wire layer rejects requests that omit it.
|
|
1116
|
+
*/
|
|
1117
|
+
capability: string;
|
|
1118
|
+
input: string;
|
|
1119
|
+
params: Record<string, string>;
|
|
1120
|
+
requesterId: string;
|
|
1121
|
+
/**
|
|
1122
|
+
* SHA-256 hex of the per-job opaque `job_token` minted at creation for
|
|
1123
|
+
* anonymous callers (internal-review). Mirror of `JobRecord.requesterTokenHash`;
|
|
1124
|
+
* see that field for the wire-level gating semantics.
|
|
1125
|
+
*/
|
|
1126
|
+
requesterTokenHash?: string;
|
|
1127
|
+
/**
|
|
1128
|
+
* Submission provenance (internal-review) — mirrors `JobRecord.requesterToken` /
|
|
1129
|
+
* `requestFingerprint` / `requesterPubkey`. Carried on the job purely so
|
|
1130
|
+
* `toJobRecord` persists it; nothing in the runtime reads it. It's the
|
|
1131
|
+
* replay gate: a retried paid submit that reproduces the fingerprint (and,
|
|
1132
|
+
* on a signed DVM, the pubkey) is handed this token back.
|
|
1133
|
+
*/
|
|
1134
|
+
requesterToken?: string;
|
|
1135
|
+
requestFingerprint?: string;
|
|
1136
|
+
requesterPubkey?: string;
|
|
1137
|
+
requestId?: string;
|
|
1138
|
+
/** HTTP path independently recorded when the caller proof was accepted. */
|
|
1139
|
+
authRequestPath?: string;
|
|
1140
|
+
status: "processing" | "completed" | "failed" | "awaiting-input" | "cancelled" | "working";
|
|
1141
|
+
summary?: string;
|
|
1142
|
+
messages: Message[];
|
|
1143
|
+
seq: number;
|
|
1144
|
+
paidMsats: number;
|
|
1145
|
+
paymentMint?: string;
|
|
1146
|
+
/** Rail of the most recent successful credit. Set on each credit branch in payment processing. */
|
|
1147
|
+
paymentRail?: FundingMethod;
|
|
1148
|
+
/**
|
|
1149
|
+
* Settlement reference for the most-recent successful credit. Threaded
|
|
1150
|
+
* into `onJobCompleted` so the platform revenue ledger can populate
|
|
1151
|
+
* `revenue_events.tx_hash`. mpp/x402 carry the credential's `challenge.id`
|
|
1152
|
+
* / EVM tx hash from the facilitator (internal-review); cashu accumulator carries
|
|
1153
|
+
* the `X-Cashu-Request-Id` UUID (internal-review). Updated on every credit
|
|
1154
|
+
* (upfront or mid-job via `processIncomingPayment`) — internal-review.
|
|
1155
|
+
* Single-row-per-job accounting keeps the most recent credit's hash,
|
|
1156
|
+
* mirroring `paymentRail`.
|
|
1157
|
+
*/
|
|
1158
|
+
paymentTxHash?: string;
|
|
1159
|
+
/** Chain transaction returned to callers recovering x402 or Tempo acceptance. */
|
|
1160
|
+
paymentTransactionHash?: string;
|
|
1161
|
+
/**
|
|
1162
|
+
* Rail-native amount accumulated across all successful credits on this
|
|
1163
|
+
* job (sats for mpp, USDC microunits for x402). The single
|
|
1164
|
+
* `revenue_events` row written at completion uses this as `gross_native`,
|
|
1165
|
+
* so multi-credit jobs sum the per-credit native amounts (internal-review).
|
|
1166
|
+
* internal-review wired the upfront credit; mid-job credits accumulate same-rail
|
|
1167
|
+
* and reset on a cross-rail switch (see `processIncomingPayment`).
|
|
1168
|
+
*/
|
|
1169
|
+
nativeAmount?: number;
|
|
1170
|
+
/** Native asset tag — paired with `nativeAmount`. internal-review. */
|
|
1171
|
+
nativeAsset?: "sats" | "usdc" | "usdc.e" | "usd-cents";
|
|
1172
|
+
/** Cashu flow discriminator written into `revenue_events.metadata.cashu_flow` (internal-review). */
|
|
1173
|
+
cashuFlow?: "p2pk_accumulator";
|
|
1174
|
+
/** Credit the upfront payment funded/drew (internal-review). See `JobRecord.creditId`. */
|
|
1175
|
+
creditId?: string;
|
|
1176
|
+
/** The draw placed for this job on `creditId` (internal-review). */
|
|
1177
|
+
drawId?: string;
|
|
1178
|
+
/** Original funding artifact returned by an explicit fund-and-draw. */
|
|
1179
|
+
fundingReceipt?: FundingReceipt;
|
|
1180
|
+
fundingCredit?: CreditSnapshot;
|
|
1181
|
+
receivedProofs: ProofLike[];
|
|
1182
|
+
pendingPaymentMsats?: number;
|
|
1183
|
+
/**
|
|
1184
|
+
* Per-job binding for MPP credentials. Contains the `challenge.id` of every
|
|
1185
|
+
* mppx challenge issued in the most-recent `requestPayment` yield. mppx HMAC
|
|
1186
|
+
* binds challenges to realm/method/amount/expiry, not to the dvmkit job-id;
|
|
1187
|
+
* verifying the incoming credential's `challenge.id` is in this set blocks
|
|
1188
|
+
* a credential lifted from a sibling job that requested the same fields.
|
|
1189
|
+
* Cleared on successful credit (internal-review).
|
|
1190
|
+
*/
|
|
1191
|
+
pendingMppChallengeIds?: string[];
|
|
1192
|
+
/**
|
|
1193
|
+
* Per-job binding for x402 mid-job credentials (internal-review). 32-byte 0x-hex
|
|
1194
|
+
* nonce issued by `requestPayment` and stamped onto the outbound x402
|
|
1195
|
+
* envelope; the caller signs `TransferWithAuthorization` against this exact
|
|
1196
|
+
* `bytes32`. Verification rejects an incoming `x402_payment` whose decoded
|
|
1197
|
+
* authorization nonce differs, blocking a cross-job replay before the
|
|
1198
|
+
* facilitator settles. Cleared on successful credit.
|
|
1199
|
+
*/
|
|
1200
|
+
pendingX402Nonce?: string;
|
|
1201
|
+
/**
|
|
1202
|
+
* Exact USDC microunit amount paired with `pendingX402Nonce`. Kept as a
|
|
1203
|
+
* decimal string so the EIP-3009 uint256 value remains lossless.
|
|
1204
|
+
*/
|
|
1205
|
+
pendingX402AmountUsdcMicro?: string;
|
|
1206
|
+
/**
|
|
1207
|
+
* Fiat micro-units the outstanding `requestPayment` ask is worth, pinned when
|
|
1208
|
+
* the payment-request was emitted (internal-review). See `JobRecord`.
|
|
1209
|
+
*/
|
|
1210
|
+
pendingPaymentFiatMicro?: number;
|
|
1211
|
+
/** Currency of {@link pendingPaymentFiatMicro} (lowercase ISO-4217). */
|
|
1212
|
+
pendingPaymentFiatCurrency?: string;
|
|
1213
|
+
/**
|
|
1214
|
+
* Cumulative fiat micro this job has asked for mid-job, the ceiling on its
|
|
1215
|
+
* draw growth (internal-review). See `JobRecord.askedTopUpMicro` for the sentinel.
|
|
1216
|
+
*/
|
|
1217
|
+
askedTopUpMicro?: number;
|
|
1218
|
+
/**
|
|
1219
|
+
* Currency of {@link askedTopUpMicro} — and this job's sticky ask
|
|
1220
|
+
* denomination (internal-review). See `JobRecord.askedTopUpCurrency`.
|
|
1221
|
+
*/
|
|
1222
|
+
askedTopUpCurrency?: string;
|
|
1223
|
+
/**
|
|
1224
|
+
* Why this job's draw growth is not capped, when it isn't (internal-review). Mirror
|
|
1225
|
+
* of `JobRecord.topUpCapUnenforcedReason`.
|
|
1226
|
+
*/
|
|
1227
|
+
topUpCapUnenforcedReason?: TopUpCapUnenforcedReason;
|
|
1228
|
+
/** Price the job was charged at (msats). Used to detect overpayment for change/melt gating. */
|
|
1229
|
+
requiredMsats?: number;
|
|
1230
|
+
/**
|
|
1231
|
+
* The signed receipt for this job's terminal outcome (internal-review). Mirror of
|
|
1232
|
+
* `JobRecord.receipt`, set once the terminal funnel has signed and persisted
|
|
1233
|
+
* it so the local read paths (which serve from `activeJobs` during the
|
|
1234
|
+
* cleanup window) hand back the same bytes as a cross-machine re-read.
|
|
1235
|
+
*/
|
|
1236
|
+
receipt?: JobReceipt;
|
|
1237
|
+
/**
|
|
1238
|
+
* The in-flight terminal chain — persist, then sign + store the receipt
|
|
1239
|
+
* (internal-review). Assigned synchronously by `ctx.complete()` / `ctx.fail()` at
|
|
1240
|
+
* the instant the job goes terminal, so the `POST /v1/job` fast path can
|
|
1241
|
+
* await it (bounded) and put the receipt on the synchronous 200 rather than
|
|
1242
|
+
* making the caller poll for it. Never rejects — the chain swallows its own
|
|
1243
|
+
* failures.
|
|
1244
|
+
*/
|
|
1245
|
+
terminalWork?: Promise<void>;
|
|
1246
|
+
listeners: Set<(msg: Message) => void>;
|
|
1247
|
+
/**
|
|
1248
|
+
* Cancellation signal for the running handler (internal-review). Aborted whenever
|
|
1249
|
+
* the job is cancelled on this process — caller cancel, idle timeout,
|
|
1250
|
+
* stale-sweep teardown, supersession. Surfaced to builders as `ctx.signal`
|
|
1251
|
+
* and auto-threaded into `ctx.fetch`, so an in-flight provider call
|
|
1252
|
+
* (ElevenLabs, Scrapfly, …) is torn down instead of billing the caller for
|
|
1253
|
+
* work they just cancelled. Per-process runtime state like `listeners` —
|
|
1254
|
+
* never persisted, and re-created fresh on reactivation.
|
|
1255
|
+
*/
|
|
1256
|
+
abort: AbortController;
|
|
1257
|
+
/**
|
|
1258
|
+
* Hook for cross-machine message persistence (Tx A, internal-review). Set by
|
|
1259
|
+
* `JobManager` when the store is streamable. The appender runs the
|
|
1260
|
+
* outgoing-message tx atomically with any payment-request counter bump so
|
|
1261
|
+
* `(message, pending_payment_msats)` land together — never separately.
|
|
1262
|
+
*
|
|
1263
|
+
* Per-job serialised (internal-review): each call chains onto `messageAppenderTail`
|
|
1264
|
+
* so two synchronous `providerMessage` calls (e.g. `artifact` followed by
|
|
1265
|
+
* `complete`) can't race for the DB-side seq allocation.
|
|
1266
|
+
*/
|
|
1267
|
+
messageAppender?: (msg: Message, opts?: AppendOutgoingOptions) => void;
|
|
1268
|
+
/**
|
|
1269
|
+
* Tail of the per-job appender chain (internal-review). `wireMessageAppender` updates
|
|
1270
|
+
* this on every call so `persistJob` can await it before the terminal snapshot
|
|
1271
|
+
* save runs `DELETE FROM job_messages`.
|
|
1272
|
+
*/
|
|
1273
|
+
messageAppenderTail?: Promise<unknown>;
|
|
1274
|
+
sdkCtx?: SDKJobContext<unknown, unknown>;
|
|
1275
|
+
stepCache: StepCache;
|
|
1276
|
+
state: unknown;
|
|
1277
|
+
pendingPrompts: Map<string, PromptResolver>;
|
|
1278
|
+
pendingPayment: PaymentResolver | null;
|
|
1279
|
+
/** When not null, indicates replay mode — prompt/payment scan message history for cached responses. */
|
|
1280
|
+
replayHighSeq: number | null;
|
|
1281
|
+
/** Number of provider messages to suppress during replay. Cleared on handler error. */
|
|
1282
|
+
replayProviderSkip: number;
|
|
1283
|
+
/** Index of the next payment to match during replay (for multi-payment handlers). */
|
|
1284
|
+
replayPaymentIndex: number;
|
|
1285
|
+
/**
|
|
1286
|
+
* Set once Tx C applies the current payment ask during this reactivation.
|
|
1287
|
+
* Runtime-only: replayed prompts may mutate `status`, but never clear this
|
|
1288
|
+
* marker before the matching payment is consumed.
|
|
1289
|
+
*/
|
|
1290
|
+
reactivationPaymentApplied?: true;
|
|
1291
|
+
/** Unix ms timestamp of job creation. */
|
|
1292
|
+
createdAt: number;
|
|
1293
|
+
/** Unix ms timestamp of last activity (message, yield, terminal). */
|
|
1294
|
+
lastActivityAt: number;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
/** Lifetime of a per-call implicit credit before the ledger refuses new draws. */
|
|
1298
|
+
declare const IMPLICIT_CREDIT_TTL_MS: number;
|
|
1299
|
+
|
|
1300
|
+
/**
|
|
1301
|
+
* Fiat denomination of the job price, pinned per request (internal-review).
|
|
1302
|
+
*
|
|
1303
|
+
* The credit ledger is fiat-micro denominated (internal-review baked decision:
|
|
1304
|
+
* 1e-6 of the DVM's pricing currency, never msats). The conversion pins at
|
|
1305
|
+
* quote time: the verify path already fixes both the fiat price and its msat
|
|
1306
|
+
* equivalent, so the implicit N=1 fund credits the QUOTED fiat amount when
|
|
1307
|
+
* the attached token satisfies the quoted msats — no fresh BTC/USD lookup in
|
|
1308
|
+
* the rail verify hot path, and fund = draw = quoted price means zero
|
|
1309
|
+
* rounding drift at N=1.
|
|
1310
|
+
*/
|
|
1311
|
+
interface PriceFiat {
|
|
1312
|
+
/** Lowercase currency code, e.g. `"usd"`. */
|
|
1313
|
+
currency: string;
|
|
1314
|
+
/** Job price in 1e-6 currency units. */
|
|
1315
|
+
amountMicro: number;
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
/**
|
|
1319
|
+
* SHA-256 hex over the raw funding artifact — the value an explicit
|
|
1320
|
+
* fund-and-draw request must sign as `fund.commitment` (spec §2 condition 3).
|
|
1321
|
+
* The artifact is the exact header-borne string: the `X-Cashu` header value,
|
|
1322
|
+
* the `X-PAYMENT` header value, or the extracted `Payment <…>` mpp scheme
|
|
1323
|
+
* string. Hashing the verbatim wire string keeps the commitment computable by
|
|
1324
|
+
* the client without any decode step.
|
|
1325
|
+
*/
|
|
1326
|
+
declare function fundingCommitment(artifact: string): string;
|
|
1327
|
+
/**
|
|
1328
|
+
* Settlement reference stamped on the revenue event of an **explicit** draw
|
|
1329
|
+
* (internal-review). One funding backs N draws, so the funding's own rail reference
|
|
1330
|
+
* cannot key them: `revenue_events` carries a global `UNIQUE (rail, tx_hash)`
|
|
1331
|
+
* that deliberately drops cross-job reuse of a settlement reference, and
|
|
1332
|
+
* reusing it would silently discard every draw but the first.
|
|
1333
|
+
*
|
|
1334
|
+
* Hashed rather than concatenated because `draw_id` is caller-generated: the
|
|
1335
|
+
* DVM id is mixed in so two DVMs whose callers both pick `draw_id: "1"` can't
|
|
1336
|
+
* collide, and the hash keeps caller-controlled bytes out of a column ops
|
|
1337
|
+
* reads. Deterministic, so a replayed report lands on the same row.
|
|
1338
|
+
*/
|
|
1339
|
+
declare function drawSettlementRef(dvmId: string, creditId: string, drawId: string): string;
|
|
1340
|
+
|
|
1341
|
+
/**
|
|
1342
|
+
* Durable marker for a rail payment that already funded the credit ledger
|
|
1343
|
+
* (internal-review, spec §2 condition 3). The x402 and mpp rails have no dvmkit-side
|
|
1344
|
+
* commit point of their own — the money moves at the facilitator / the payer's
|
|
1345
|
+
* mpp server — so the marker row, inserted in the SAME transaction as
|
|
1346
|
+
* `CreditLedger.fund` + `draw`, is what makes a crash-replayed credential
|
|
1347
|
+
* unable to fund twice: the second attempt collides on the primary key and
|
|
1348
|
+
* surfaces as {@link ProcessedPaymentReplayError}, which the verifier routes
|
|
1349
|
+
* into the internal-review recovery gates instead of a second fund.
|
|
1350
|
+
*
|
|
1351
|
+
* The Cashu rail does not use this store — its accumulator insert is already
|
|
1352
|
+
* the durable commit point, and its replay key (the `X-Cashu-Request-Id`
|
|
1353
|
+
* regime) must stay exactly as-is (dual-idempotency rule, spec §2).
|
|
1354
|
+
*/
|
|
1355
|
+
interface ProcessedPaymentStore {
|
|
1356
|
+
/**
|
|
1357
|
+
* Record that `paymentId` on `rail` funded a credit. Throws
|
|
1358
|
+
* {@link ProcessedPaymentReplayError} when the `(dvmId, rail, paymentId)`
|
|
1359
|
+
* marker already exists. `tx` (Postgres implementation only) joins the
|
|
1360
|
+
* caller-owned transaction so marker + fund + draw commit atomically.
|
|
1361
|
+
*/
|
|
1362
|
+
record(args: {
|
|
1363
|
+
dvmId: string;
|
|
1364
|
+
rail: ProcessedPaymentRail;
|
|
1365
|
+
paymentId: string;
|
|
1366
|
+
creditId: string;
|
|
1367
|
+
drawId: string;
|
|
1368
|
+
jobId?: string;
|
|
1369
|
+
tx?: ProcessedPaymentQuerier;
|
|
1370
|
+
nowMs?: number;
|
|
1371
|
+
}): Promise<void>;
|
|
1372
|
+
/** Read one marker, `undefined` when absent. */
|
|
1373
|
+
get(args: {
|
|
1374
|
+
dvmId: string;
|
|
1375
|
+
rail: ProcessedPaymentRail;
|
|
1376
|
+
paymentId: string;
|
|
1377
|
+
}): Promise<ProcessedPaymentRecord | undefined>;
|
|
1378
|
+
/**
|
|
1379
|
+
* Run `fn` inside one transaction (Postgres: BEGIN → fn → COMMIT, rollback
|
|
1380
|
+
* on throw). The verifier funds and draws the ledger inside `fn`, passing
|
|
1381
|
+
* the handle through to `record` / `CreditLedger.fund` / `draw` so all
|
|
1382
|
+
* three commit atomically. The memory implementation invokes `fn` with no
|
|
1383
|
+
* handle — its operations are synchronous-atomic in-process.
|
|
1384
|
+
*/
|
|
1385
|
+
withTx<T>(fn: (tx?: ProcessedPaymentQuerier) => Promise<T>): Promise<T>;
|
|
1386
|
+
}
|
|
1387
|
+
/** Rails that commit through the marker (Cashu commits via the accumulator). */
|
|
1388
|
+
type ProcessedPaymentRail = "x402" | "tempo";
|
|
1389
|
+
/** One recorded funding marker. */
|
|
1390
|
+
interface ProcessedPaymentRecord {
|
|
1391
|
+
dvmId: string;
|
|
1392
|
+
rail: ProcessedPaymentRail;
|
|
1393
|
+
paymentId: string;
|
|
1394
|
+
creditId: string;
|
|
1395
|
+
drawId: string;
|
|
1396
|
+
jobId: string | null;
|
|
1397
|
+
createdAt: number;
|
|
1398
|
+
}
|
|
1399
|
+
/** Subset of `pg.Pool` the store queries through; `PoolClient` satisfies it too. */
|
|
1400
|
+
interface ProcessedPaymentQuerier {
|
|
1401
|
+
query: Pool["query"];
|
|
1402
|
+
}
|
|
1403
|
+
/** Thrown by `record` when the payment id was already processed on this rail. */
|
|
1404
|
+
declare class ProcessedPaymentReplayError extends Error {
|
|
1405
|
+
readonly rail: ProcessedPaymentRail;
|
|
1406
|
+
readonly paymentId: string;
|
|
1407
|
+
constructor(rail: ProcessedPaymentRail, paymentId: string);
|
|
1408
|
+
}
|
|
1409
|
+
/** Postgres-backed {@link ProcessedPaymentStore} — the production implementation. */
|
|
1410
|
+
declare class PostgresProcessedPaymentStore implements ProcessedPaymentStore {
|
|
1411
|
+
private readonly pool;
|
|
1412
|
+
constructor(pool: Pick<Pool, "query" | "connect">);
|
|
1413
|
+
withTx<T>(fn: (tx?: ProcessedPaymentQuerier) => Promise<T>): Promise<T>;
|
|
1414
|
+
/** Create the `processed_payments` table if absent. Call once at SDK boot. */
|
|
1415
|
+
init(): Promise<void>;
|
|
1416
|
+
/** The boot DDL itself — always runs under {@link withSdkInitLock} (internal-review). */
|
|
1417
|
+
private createTables;
|
|
1418
|
+
record(args: {
|
|
1419
|
+
dvmId: string;
|
|
1420
|
+
rail: ProcessedPaymentRail;
|
|
1421
|
+
paymentId: string;
|
|
1422
|
+
creditId: string;
|
|
1423
|
+
drawId: string;
|
|
1424
|
+
jobId?: string;
|
|
1425
|
+
tx?: ProcessedPaymentQuerier;
|
|
1426
|
+
nowMs?: number;
|
|
1427
|
+
}): Promise<void>;
|
|
1428
|
+
get(args: {
|
|
1429
|
+
dvmId: string;
|
|
1430
|
+
rail: ProcessedPaymentRail;
|
|
1431
|
+
paymentId: string;
|
|
1432
|
+
}): Promise<ProcessedPaymentRecord | undefined>;
|
|
1433
|
+
}
|
|
1434
|
+
/** In-memory {@link ProcessedPaymentStore} for pool-less setups. Per-process. */
|
|
1435
|
+
declare class MemoryProcessedPaymentStore implements ProcessedPaymentStore {
|
|
1436
|
+
private readonly markers;
|
|
1437
|
+
withTx<T>(fn: (tx?: ProcessedPaymentQuerier) => Promise<T>): Promise<T>;
|
|
1438
|
+
record(args: {
|
|
1439
|
+
dvmId: string;
|
|
1440
|
+
rail: ProcessedPaymentRail;
|
|
1441
|
+
paymentId: string;
|
|
1442
|
+
creditId: string;
|
|
1443
|
+
drawId: string;
|
|
1444
|
+
jobId?: string;
|
|
1445
|
+
tx?: ProcessedPaymentQuerier;
|
|
1446
|
+
nowMs?: number;
|
|
1447
|
+
}): Promise<void>;
|
|
1448
|
+
get(args: {
|
|
1449
|
+
dvmId: string;
|
|
1450
|
+
rail: ProcessedPaymentRail;
|
|
1451
|
+
paymentId: string;
|
|
1452
|
+
}): Promise<ProcessedPaymentRecord | undefined>;
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
/** Durable state of one exact x402 authorization. */
|
|
1456
|
+
type X402ExactSettlementStatus = "prepared" | "submitting" | "settled" | "local_committed" | "ambiguous" | "rejected" | "complete";
|
|
1457
|
+
/** Serializable local economic effect that follows an exact settlement. */
|
|
1458
|
+
interface X402ExactSettlementEffect {
|
|
1459
|
+
priceFiat: {
|
|
1460
|
+
currency: string;
|
|
1461
|
+
amountMicro: number;
|
|
1462
|
+
};
|
|
1463
|
+
requiredMsats: number;
|
|
1464
|
+
callerId: string;
|
|
1465
|
+
dvmId: string;
|
|
1466
|
+
jobId?: string;
|
|
1467
|
+
/** Upfront job request bound to this payment; omitted for credit-only and mid-job effects. */
|
|
1468
|
+
jobAdmission?: {
|
|
1469
|
+
requestFingerprint: string;
|
|
1470
|
+
authFingerprint?: string;
|
|
1471
|
+
};
|
|
1472
|
+
creditEnvelope?: CreditEnvelope;
|
|
1473
|
+
fundOnly?: {
|
|
1474
|
+
creditId: string;
|
|
1475
|
+
fundId: string;
|
|
1476
|
+
commitment: string;
|
|
1477
|
+
allowOneShotStablecoin?: boolean;
|
|
1478
|
+
};
|
|
1479
|
+
topUp?: {
|
|
1480
|
+
creditId: string;
|
|
1481
|
+
drawId: string;
|
|
1482
|
+
};
|
|
1483
|
+
topUpCapMicro?: number;
|
|
1484
|
+
askSatisfied?: true;
|
|
1485
|
+
midJob?: true;
|
|
1486
|
+
drawBasis: boolean;
|
|
1487
|
+
ttlMs: number;
|
|
1488
|
+
paidMsats: number;
|
|
1489
|
+
fundedMicro: number;
|
|
1490
|
+
nativeAmount: number;
|
|
1491
|
+
}
|
|
1492
|
+
/** Canonical authorization facts retained without the bearer signature. */
|
|
1493
|
+
interface X402ExactAuthorizationFacts {
|
|
1494
|
+
network: string;
|
|
1495
|
+
asset: string;
|
|
1496
|
+
from: string;
|
|
1497
|
+
to: string;
|
|
1498
|
+
value: string;
|
|
1499
|
+
validAfter: string;
|
|
1500
|
+
validBefore: string;
|
|
1501
|
+
nonce: string;
|
|
1502
|
+
fingerprint: string;
|
|
1503
|
+
}
|
|
1504
|
+
/** One durable exact-x402 settlement intent. */
|
|
1505
|
+
interface X402ExactSettlementIntent extends X402ExactAuthorizationFacts {
|
|
1506
|
+
id: string;
|
|
1507
|
+
dvmId: string;
|
|
1508
|
+
resource: string;
|
|
1509
|
+
status: X402ExactSettlementStatus;
|
|
1510
|
+
effect?: X402ExactSettlementEffect;
|
|
1511
|
+
submissionBlock?: string;
|
|
1512
|
+
transaction?: string;
|
|
1513
|
+
settleResponse?: Record<string, unknown>;
|
|
1514
|
+
lastOutcome?: string;
|
|
1515
|
+
createdAt: number;
|
|
1516
|
+
updatedAt: number;
|
|
1517
|
+
resolvedAt?: number;
|
|
1518
|
+
}
|
|
1519
|
+
/** Evidence recorded around one facilitator settlement call. */
|
|
1520
|
+
interface X402ExactSettlementAttempt {
|
|
1521
|
+
id: string;
|
|
1522
|
+
intentId: string;
|
|
1523
|
+
outcome: "submitting" | "success" | "rejected" | "timeout" | "missing_reference";
|
|
1524
|
+
detail?: string;
|
|
1525
|
+
transaction?: string;
|
|
1526
|
+
createdAt: number;
|
|
1527
|
+
}
|
|
1528
|
+
/** Persistence contract for exact x402 settlement recovery. */
|
|
1529
|
+
interface X402ExactSettlementStore {
|
|
1530
|
+
prepare(intent: Omit<X402ExactSettlementIntent, "status" | "createdAt" | "updatedAt" | "resolvedAt">): Promise<X402ExactSettlementIntent>;
|
|
1531
|
+
get(dvmId: string, intentId: string): Promise<X402ExactSettlementIntent | undefined>;
|
|
1532
|
+
markSubmitting(args: {
|
|
1533
|
+
dvmId: string;
|
|
1534
|
+
intentId: string;
|
|
1535
|
+
submissionBlock?: string;
|
|
1536
|
+
}): Promise<boolean>;
|
|
1537
|
+
recordOutcome(args: {
|
|
1538
|
+
dvmId: string;
|
|
1539
|
+
intentId: string;
|
|
1540
|
+
status: Extract<X402ExactSettlementStatus, "settled" | "ambiguous" | "rejected">;
|
|
1541
|
+
outcome: X402ExactSettlementAttempt["outcome"];
|
|
1542
|
+
detail?: string;
|
|
1543
|
+
transaction?: string;
|
|
1544
|
+
settleResponse?: Record<string, unknown>;
|
|
1545
|
+
}): Promise<X402ExactSettlementIntent>;
|
|
1546
|
+
finalize<T>(dvmId: string, intentId: string, fn: (intent: X402ExactSettlementIntent, tx?: ProcessedPaymentQuerier) => Promise<T>): Promise<{
|
|
1547
|
+
intent: X402ExactSettlementIntent;
|
|
1548
|
+
value?: T;
|
|
1549
|
+
replayed: boolean;
|
|
1550
|
+
}>;
|
|
1551
|
+
markAdmissionComplete(dvmId: string, intentId: string): Promise<X402ExactSettlementIntent>;
|
|
1552
|
+
list(args: {
|
|
1553
|
+
dvmId: string;
|
|
1554
|
+
limit: number;
|
|
1555
|
+
includeResolved?: boolean;
|
|
1556
|
+
}): Promise<X402ExactSettlementIntent[]>;
|
|
1557
|
+
attempts(dvmId: string, intentId: string): Promise<X402ExactSettlementAttempt[]>;
|
|
1558
|
+
}
|
|
1559
|
+
/** Postgres-backed exact settlement intent store. */
|
|
1560
|
+
declare class PostgresX402ExactSettlementStore implements X402ExactSettlementStore {
|
|
1561
|
+
private readonly pool;
|
|
1562
|
+
constructor(pool: Pick<Pool, "query" | "connect">);
|
|
1563
|
+
/** Create the exact settlement tables. */
|
|
1564
|
+
init(): Promise<void>;
|
|
1565
|
+
prepare(intent: Omit<X402ExactSettlementIntent, "status" | "createdAt" | "updatedAt" | "resolvedAt">): Promise<X402ExactSettlementIntent>;
|
|
1566
|
+
get(dvmId: string, intentId: string): Promise<X402ExactSettlementIntent | undefined>;
|
|
1567
|
+
markSubmitting(args: {
|
|
1568
|
+
dvmId: string;
|
|
1569
|
+
intentId: string;
|
|
1570
|
+
submissionBlock?: string;
|
|
1571
|
+
}): Promise<boolean>;
|
|
1572
|
+
recordOutcome(args: {
|
|
1573
|
+
dvmId: string;
|
|
1574
|
+
intentId: string;
|
|
1575
|
+
status: Extract<X402ExactSettlementStatus, "settled" | "ambiguous" | "rejected">;
|
|
1576
|
+
outcome: X402ExactSettlementAttempt["outcome"];
|
|
1577
|
+
detail?: string;
|
|
1578
|
+
transaction?: string;
|
|
1579
|
+
settleResponse?: Record<string, unknown>;
|
|
1580
|
+
}): Promise<X402ExactSettlementIntent>;
|
|
1581
|
+
finalize<T>(dvmId: string, intentId: string, fn: (intent: X402ExactSettlementIntent, tx?: ProcessedPaymentQuerier) => Promise<T>): Promise<{
|
|
1582
|
+
intent: X402ExactSettlementIntent;
|
|
1583
|
+
value?: T;
|
|
1584
|
+
replayed: boolean;
|
|
1585
|
+
}>;
|
|
1586
|
+
markAdmissionComplete(dvmId: string, intentId: string): Promise<X402ExactSettlementIntent>;
|
|
1587
|
+
list(args: {
|
|
1588
|
+
dvmId: string;
|
|
1589
|
+
limit: number;
|
|
1590
|
+
includeResolved?: boolean;
|
|
1591
|
+
}): Promise<X402ExactSettlementIntent[]>;
|
|
1592
|
+
attempts(dvmId: string, intentId: string): Promise<X402ExactSettlementAttempt[]>;
|
|
1593
|
+
private insertAttempt;
|
|
1594
|
+
}
|
|
1595
|
+
/** In-memory exact settlement store used by dev and unit tests. */
|
|
1596
|
+
declare class MemoryX402ExactSettlementStore implements X402ExactSettlementStore {
|
|
1597
|
+
private readonly intents;
|
|
1598
|
+
private readonly evidence;
|
|
1599
|
+
private readonly authBindings;
|
|
1600
|
+
private readonly locks;
|
|
1601
|
+
prepare(intent: Omit<X402ExactSettlementIntent, "status" | "createdAt" | "updatedAt" | "resolvedAt">): Promise<X402ExactSettlementIntent>;
|
|
1602
|
+
get(dvmId: string, intentId: string): Promise<X402ExactSettlementIntent | undefined>;
|
|
1603
|
+
markSubmitting(args: {
|
|
1604
|
+
dvmId: string;
|
|
1605
|
+
intentId: string;
|
|
1606
|
+
submissionBlock?: string;
|
|
1607
|
+
}): Promise<boolean>;
|
|
1608
|
+
recordOutcome(args: {
|
|
1609
|
+
dvmId: string;
|
|
1610
|
+
intentId: string;
|
|
1611
|
+
status: Extract<X402ExactSettlementStatus, "settled" | "ambiguous" | "rejected">;
|
|
1612
|
+
outcome: X402ExactSettlementAttempt["outcome"];
|
|
1613
|
+
detail?: string;
|
|
1614
|
+
transaction?: string;
|
|
1615
|
+
settleResponse?: Record<string, unknown>;
|
|
1616
|
+
}): Promise<X402ExactSettlementIntent>;
|
|
1617
|
+
finalize<T>(dvmId: string, intentId: string, fn: (intent: X402ExactSettlementIntent, tx?: ProcessedPaymentQuerier) => Promise<T>): Promise<{
|
|
1618
|
+
intent: X402ExactSettlementIntent;
|
|
1619
|
+
value?: T;
|
|
1620
|
+
replayed: boolean;
|
|
1621
|
+
}>;
|
|
1622
|
+
markAdmissionComplete(dvmId: string, intentId: string): Promise<X402ExactSettlementIntent>;
|
|
1623
|
+
list(args: {
|
|
1624
|
+
dvmId: string;
|
|
1625
|
+
limit: number;
|
|
1626
|
+
includeResolved?: boolean;
|
|
1627
|
+
}): Promise<X402ExactSettlementIntent[]>;
|
|
1628
|
+
attempts(dvmId: string, intentId: string): Promise<X402ExactSettlementAttempt[]>;
|
|
1629
|
+
private addAttempt;
|
|
1630
|
+
private withLock;
|
|
1631
|
+
}
|
|
1632
|
+
/** Raised when the same on-chain authorization is presented for a different local effect. */
|
|
1633
|
+
declare class X402ExactIntentConflictError extends Error {
|
|
1634
|
+
readonly intentId: string;
|
|
1635
|
+
constructor(intentId: string);
|
|
1636
|
+
}
|
|
1637
|
+
/** Raised when local finalization is attempted before settlement is evidenced. */
|
|
1638
|
+
declare class X402ExactSettlementNotReadyError extends Error {
|
|
1639
|
+
readonly intentId: string;
|
|
1640
|
+
readonly status: X402ExactSettlementStatus;
|
|
1641
|
+
constructor(intentId: string, status: X402ExactSettlementStatus);
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
/** Chain evidence recovered for one exact x402 authorization. */
|
|
1645
|
+
interface X402ExactSettlementChainEvidence {
|
|
1646
|
+
transaction: string;
|
|
1647
|
+
amount: string;
|
|
1648
|
+
}
|
|
1649
|
+
/** Chain boundary used by exact-settlement recovery. */
|
|
1650
|
+
interface X402ExactSettlementEvidenceReader {
|
|
1651
|
+
getBlockNumber(): Promise<bigint>;
|
|
1652
|
+
findExactPayment?(intent: X402ExactSettlementIntent): Promise<X402ExactSettlementChainEvidence | undefined>;
|
|
1653
|
+
}
|
|
1654
|
+
/** Result of accepting or recovering one exact x402 authorization. */
|
|
1655
|
+
type X402ExactAcceptance<T> = {
|
|
1656
|
+
outcome: "accepted";
|
|
1657
|
+
intentId: string;
|
|
1658
|
+
receipt: X402Receipt;
|
|
1659
|
+
committed?: T;
|
|
1660
|
+
replayed: boolean;
|
|
1661
|
+
admissionPending: boolean;
|
|
1662
|
+
effect?: X402ExactSettlementEffect;
|
|
1663
|
+
} | {
|
|
1664
|
+
outcome: "rejected";
|
|
1665
|
+
intentId?: string;
|
|
1666
|
+
reason: string;
|
|
1667
|
+
} | {
|
|
1668
|
+
outcome: "ambiguous";
|
|
1669
|
+
intentId: string;
|
|
1670
|
+
reason: string;
|
|
1671
|
+
};
|
|
1672
|
+
/** Dependencies for the durable exact x402 settlement coordinator. */
|
|
1673
|
+
interface X402ExactSettlementServerOpts {
|
|
1674
|
+
store: X402ExactSettlementStore;
|
|
1675
|
+
evidence?: X402ExactSettlementEvidenceReader;
|
|
1676
|
+
repair?: (effect: X402ExactSettlementEffect | undefined, receipt: X402Receipt, tx?: ProcessedPaymentQuerier) => Promise<unknown>;
|
|
1677
|
+
}
|
|
1678
|
+
/** Durable coordinator for irreversible exact x402 settlement. */
|
|
1679
|
+
declare class X402ExactSettlementServer {
|
|
1680
|
+
private readonly opts;
|
|
1681
|
+
private readonly activeSubmissions;
|
|
1682
|
+
constructor(opts: X402ExactSettlementServerOpts);
|
|
1683
|
+
/** Verify, settle, and atomically finalize one exact authorization. */
|
|
1684
|
+
accept<T>(args: {
|
|
1685
|
+
paymentHeader: string;
|
|
1686
|
+
config: X402Config;
|
|
1687
|
+
requiredUsdcMicro: bigint;
|
|
1688
|
+
resource: string;
|
|
1689
|
+
btcUsdRate: number;
|
|
1690
|
+
exactVersions?: X402ExactVersionSupport;
|
|
1691
|
+
dvmId: string;
|
|
1692
|
+
effect?: X402ExactSettlementEffect;
|
|
1693
|
+
commit: (receipt: X402Receipt, tx?: ProcessedPaymentQuerier, effect?: X402ExactSettlementEffect) => Promise<T>;
|
|
1694
|
+
}): Promise<X402ExactAcceptance<T>>;
|
|
1695
|
+
/** Read the operator queue. */
|
|
1696
|
+
list(args: {
|
|
1697
|
+
dvmId: string;
|
|
1698
|
+
limit: number;
|
|
1699
|
+
includeResolved?: boolean;
|
|
1700
|
+
}): Promise<X402ExactSettlementIntent[]>;
|
|
1701
|
+
/** Read durable attempt evidence for one intent. */
|
|
1702
|
+
attempts(dvmId: string, intentId: string): Promise<X402ExactSettlementAttempt[]>;
|
|
1703
|
+
/** Reconcile and finish one intent from chain evidence. */
|
|
1704
|
+
reconcile(dvmId: string, intentId: string): Promise<{
|
|
1705
|
+
intent: X402ExactSettlementIntent;
|
|
1706
|
+
replayed: boolean;
|
|
1707
|
+
}>;
|
|
1708
|
+
/** Mark a locally committed upfront settlement complete after its job row lands. */
|
|
1709
|
+
completeAdmission(dvmId: string, intentId: string): Promise<X402ExactSettlementIntent>;
|
|
1710
|
+
private finalize;
|
|
1711
|
+
private recoverEvidence;
|
|
1712
|
+
private requiredIntent;
|
|
1713
|
+
}
|
|
1714
|
+
/** Raised when operator repair cannot prove the payment on chain. */
|
|
1715
|
+
declare class X402ExactSettlementEvidenceMissingError extends Error {
|
|
1716
|
+
readonly intentId: string;
|
|
1717
|
+
constructor(intentId: string);
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
/** Exact successful chain action recovered for one ambiguous settlement attempt. */
|
|
1721
|
+
interface X402SettlementChainEvidence {
|
|
1722
|
+
transaction: string;
|
|
1723
|
+
amount: string;
|
|
1724
|
+
}
|
|
1725
|
+
/** Chain boundary used to bookmark and reconcile irreversible x402 submissions. */
|
|
1726
|
+
interface X402SettlementEvidenceReader extends X402ExactSettlementEvidenceReader {
|
|
1727
|
+
getBlockNumber(): Promise<bigint>;
|
|
1728
|
+
findAppliedSettlement(intent: X402SettlementIntent): Promise<X402SettlementChainEvidence | undefined>;
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
/** Hosted-facilitator channel network retained for the existing testnet path. */
|
|
1732
|
+
declare const X402_BATCH_SETTLEMENT_NETWORK = "eip155:84532";
|
|
1733
|
+
/** Policy floor advertised to callers and enforced at boot. */
|
|
1734
|
+
declare const X402_BATCH_MIN_WITHDRAW_DELAY_SECONDS = 86400;
|
|
1735
|
+
/** Default claim and transfer cadence; comfortably inside the 24-hour exit window. */
|
|
1736
|
+
declare const X402_BATCH_AUTO_SETTLEMENT: AutoSettlementConfig;
|
|
1737
|
+
/** Natural identity and incremental value carried by an accepted channel voucher. */
|
|
1738
|
+
interface X402BatchFunding {
|
|
1739
|
+
channelId: string;
|
|
1740
|
+
maxClaimableAmount: string;
|
|
1741
|
+
paymentId: string;
|
|
1742
|
+
amount: string;
|
|
1743
|
+
}
|
|
1744
|
+
/** Successful batch-settlement result plus the caller-owned ledger result. */
|
|
1745
|
+
interface X402BatchAcceptance<T> {
|
|
1746
|
+
funding: X402BatchFunding;
|
|
1747
|
+
settlement: SettleResponse;
|
|
1748
|
+
committed: T;
|
|
1749
|
+
}
|
|
1750
|
+
/** Authoritative contract views needed to refresh one stored batch channel. */
|
|
1751
|
+
interface X402BatchChannelObservation {
|
|
1752
|
+
balance: string;
|
|
1753
|
+
totalClaimed: string;
|
|
1754
|
+
withdrawRequestedAt: number;
|
|
1755
|
+
refundNonce: number;
|
|
1756
|
+
}
|
|
1757
|
+
/**
|
|
1758
|
+
* The chain submission behind an accepted voucher did not return success (internal-review).
|
|
1759
|
+
*
|
|
1760
|
+
* Typed and `retryable` because it is a service-availability fault, not a fault
|
|
1761
|
+
* in the credential: the voucher verified, the ledger leg rolled back, and the
|
|
1762
|
+
* same request first reconciles the durable submission bookmark before it can
|
|
1763
|
+
* submit again. Without this, the branch threw a bare `Error`, which
|
|
1764
|
+
* `mapLedgerCommitError` could not recognise
|
|
1765
|
+
* and `verifyUpfrontPayment` answered as a 402 `payment_invalid` — copy that
|
|
1766
|
+
* tells an agent its perfectly good payment can never be reused.
|
|
1767
|
+
*
|
|
1768
|
+
* It is deliberately *not* the relay submission lock's own error class. Upstream's
|
|
1769
|
+
* `BatchSettlementEvmScheme` catches everything the signer throws and returns
|
|
1770
|
+
* `{success: false, errorReason}`, so {@link X402RelaySubmissionLockError} never
|
|
1771
|
+
* reaches this layer — it arrives as `settle_transaction_failed` with its message
|
|
1772
|
+
* in `errorMessage`. Every sibling reason (RPC read, simulation, a reverted
|
|
1773
|
+
* submission) is the same class of fault and had the same wrong copy, so the
|
|
1774
|
+
* classification is made where upstream draws it rather than by re-deriving the
|
|
1775
|
+
* one cause from a string.
|
|
1776
|
+
*
|
|
1777
|
+
* `errorReason` is upstream's enum and is safe to hand an unauthenticated caller.
|
|
1778
|
+
* `errorMessage` — and therefore `.message` — is not: upstream builds it from
|
|
1779
|
+
* whatever the signer threw, so it can carry a relay lock holder's backend pid
|
|
1780
|
+
* and `client_addr`, or a viem error quoting the RPC URL. The self-relay path
|
|
1781
|
+
* drops it before constructing this error; hosted facilitators retain their
|
|
1782
|
+
* provider message for server logs. The wire gets the reason in either case.
|
|
1783
|
+
*/
|
|
1784
|
+
declare class X402SettlementSubmissionError extends Error {
|
|
1785
|
+
readonly errorReason: string;
|
|
1786
|
+
readonly errorMessage?: string | undefined;
|
|
1787
|
+
readonly code = "x402_settlement_submission_failed";
|
|
1788
|
+
readonly retryable = true;
|
|
1789
|
+
constructor(errorReason: string, errorMessage?: string | undefined);
|
|
1790
|
+
}
|
|
1791
|
+
/** Structured refusal that can be turned back into a corrective 402. */
|
|
1792
|
+
interface X402BatchRefusal {
|
|
1793
|
+
ok: false;
|
|
1794
|
+
reason: string;
|
|
1795
|
+
requirements: PaymentRequirementsV2[];
|
|
1796
|
+
/**
|
|
1797
|
+
* `false` when re-signing cannot fix this: the caller must be answered with a
|
|
1798
|
+
* terminal error rather than a 402 carrying fresh requirements, because an
|
|
1799
|
+
* agent reads a corrective 402 as "sign this and resubmit" and lands straight
|
|
1800
|
+
* back on the same refusal (internal-review). Omitted means corrective.
|
|
1801
|
+
*/
|
|
1802
|
+
corrective?: boolean;
|
|
1803
|
+
}
|
|
1804
|
+
/**
|
|
1805
|
+
* What the chain says about one wedged settlement (internal-review).
|
|
1806
|
+
*
|
|
1807
|
+
* The four answers are kept apart because their repairs differ and because an
|
|
1808
|
+
* operator re-pointing an RPC needs a definitive answer to read as definitive
|
|
1809
|
+
* — the same split `LightningReceive.lookupSettlement` draws between
|
|
1810
|
+
* `invoice_unknown_to_wallet` and `wallet_unreachable`. `findAppliedSettlement`
|
|
1811
|
+
* collapses all of them (`undefined` for absent *and* unbookmarked, a throw for
|
|
1812
|
+
* transport), so this is where they are separated.
|
|
1813
|
+
*/
|
|
1814
|
+
type X402SettlementEvidenceOutcome =
|
|
1815
|
+
/** The chain carries this exact settlement, at this exact amount. */
|
|
1816
|
+
{
|
|
1817
|
+
chain: "found";
|
|
1818
|
+
transaction: string;
|
|
1819
|
+
amount: string;
|
|
1820
|
+
}
|
|
1821
|
+
/** Scanned from the submission bookmark to head and found nothing. */
|
|
1822
|
+
| {
|
|
1823
|
+
chain: "absent";
|
|
1824
|
+
}
|
|
1825
|
+
/** The RPC could not be read — says nothing about whether the money moved. */
|
|
1826
|
+
| {
|
|
1827
|
+
chain: "unreachable";
|
|
1828
|
+
error: string;
|
|
1829
|
+
}
|
|
1830
|
+
/** No submission bookmark, so there is no range to scan and no answer to give. */
|
|
1831
|
+
| {
|
|
1832
|
+
chain: "unbookmarked";
|
|
1833
|
+
};
|
|
1834
|
+
/** One row of the operator's repair queue: the durable intent plus what the chain says. */
|
|
1835
|
+
interface X402WedgedSettlement {
|
|
1836
|
+
intent: X402SettlementIntent;
|
|
1837
|
+
evidence?: X402SettlementEvidenceOutcome;
|
|
1838
|
+
}
|
|
1839
|
+
/** Why {@link X402SettlementRepair.reconcile} could not complete a settlement. */
|
|
1840
|
+
type X402SettlementRepairRefusal = {
|
|
1841
|
+
refusal: "settlement_not_found";
|
|
1842
|
+
}
|
|
1843
|
+
/** `prepared` — nothing was ever submitted, so there is nothing to repair. */
|
|
1844
|
+
| {
|
|
1845
|
+
refusal: "settlement_not_wedged";
|
|
1846
|
+
status: X402SettlementStatus;
|
|
1847
|
+
}
|
|
1848
|
+
/** A wedged deposit; the caller's own retry recovers it. See {@link X402SettlementRepair}. */
|
|
1849
|
+
| {
|
|
1850
|
+
refusal: "settlement_not_repairable";
|
|
1851
|
+
operation: "fund";
|
|
1852
|
+
} | {
|
|
1853
|
+
refusal: "settlement_unbookmarked";
|
|
1854
|
+
} | {
|
|
1855
|
+
refusal: "settlement_not_on_chain";
|
|
1856
|
+
} | {
|
|
1857
|
+
refusal: "chain_unreachable";
|
|
1858
|
+
error: string;
|
|
1859
|
+
}
|
|
1860
|
+
/** A concurrent repair or a caller retry moved the row out from under this one. */
|
|
1861
|
+
| {
|
|
1862
|
+
refusal: "settlement_raced";
|
|
1863
|
+
error: string;
|
|
1864
|
+
};
|
|
1865
|
+
/** One page of the repair queue, each row carrying what the chain says about it. */
|
|
1866
|
+
interface X402WedgedSettlementPage {
|
|
1867
|
+
settlements: X402WedgedSettlement[];
|
|
1868
|
+
/** Absent when this page was not full — there is nothing behind it. */
|
|
1869
|
+
nextAfter?: X402SettlementCursor;
|
|
1870
|
+
}
|
|
1871
|
+
/** A settlement the repair completed — or found already complete. */
|
|
1872
|
+
interface X402SettlementRepaired<T> {
|
|
1873
|
+
intent: X402SettlementIntent;
|
|
1874
|
+
/** The chain figure the ledger leg was completed against; absent on a replay. */
|
|
1875
|
+
evidence?: X402SettlementEvidenceOutcome & {
|
|
1876
|
+
chain: "found";
|
|
1877
|
+
};
|
|
1878
|
+
committed?: T;
|
|
1879
|
+
/** True when the settlement was already `complete` and nothing was booked again. */
|
|
1880
|
+
replayed: boolean;
|
|
1881
|
+
}
|
|
1882
|
+
/**
|
|
1883
|
+
* The operator's exit from a settlement stuck between "chain paid" and "ledger
|
|
1884
|
+
* booked" (internal-review).
|
|
1885
|
+
*
|
|
1886
|
+
* `reconcile` is the ordinary recovery path with the chain's figure substituted
|
|
1887
|
+
* for the facilitator's: the amount is re-derived from
|
|
1888
|
+
* `x402-settlement-evidence.ts` and never supplied by the operator, which is
|
|
1889
|
+
* what keeps this door from extinguishing a different figure than the chain
|
|
1890
|
+
* actually paid. The ledger legs stay outside this module — they arrive as the
|
|
1891
|
+
* same `commit`/`complete` callbacks {@link X402BatchSettlementServer.accept}
|
|
1892
|
+
* takes, so the live path and the repair cannot drift about what completing a
|
|
1893
|
+
* drain means.
|
|
1894
|
+
*
|
|
1895
|
+
* Both write verbs are `refund`-only. A wedged `fund` is recovered by the
|
|
1896
|
+
* caller's own retry (that leg has no amount guard to wedge on), and its ledger
|
|
1897
|
+
* effect is `payment.ts`'s fund-and-draw, which an admin route has no way to
|
|
1898
|
+
* reconstruct.
|
|
1899
|
+
*/
|
|
1900
|
+
interface X402SettlementRepair {
|
|
1901
|
+
/** One settlement by its own id, with no chain lookup — the repair's own read. */
|
|
1902
|
+
get(settlementId: string): Promise<X402SettlementIntent | undefined>;
|
|
1903
|
+
list(args: {
|
|
1904
|
+
limit: number;
|
|
1905
|
+
after?: X402SettlementCursor;
|
|
1906
|
+
/** Staleness floor — a settlement younger than this is in flight, not wedged. */
|
|
1907
|
+
updatedBeforeMs: number;
|
|
1908
|
+
includeResolved?: boolean;
|
|
1909
|
+
/** Skip the per-row chain lookups; the queue itself needs no RPC. */
|
|
1910
|
+
evidence?: boolean;
|
|
1911
|
+
}): Promise<X402WedgedSettlementPage>;
|
|
1912
|
+
reconcile<T>(args: {
|
|
1913
|
+
settlementId: string;
|
|
1914
|
+
commit: (intent: X402SettlementIntent, tx: CreditLedgerQuerier) => Promise<T>;
|
|
1915
|
+
complete: (evidence: {
|
|
1916
|
+
transaction: string;
|
|
1917
|
+
amount: string;
|
|
1918
|
+
}, committed: T, tx: CreditLedgerQuerier) => Promise<T>;
|
|
1919
|
+
nowMs?: number;
|
|
1920
|
+
}): Promise<X402SettlementRepaired<T> | X402SettlementRepairRefusal>;
|
|
1921
|
+
writeOff(args: {
|
|
1922
|
+
settlementId: string;
|
|
1923
|
+
note?: string;
|
|
1924
|
+
nowMs?: number;
|
|
1925
|
+
}): Promise<X402SettlementWriteOff | X402SettlementRepairRefusal>;
|
|
1926
|
+
}
|
|
1927
|
+
/** Runtime server handle used only by `/v1/credit`. */
|
|
1928
|
+
interface X402BatchSettlementServer {
|
|
1929
|
+
buildRequirements(amount: string): Promise<PaymentRequirementsV2[]>;
|
|
1930
|
+
accept<T>(args: {
|
|
1931
|
+
paymentHeader: string;
|
|
1932
|
+
amount: string;
|
|
1933
|
+
effectId: string;
|
|
1934
|
+
/**
|
|
1935
|
+
* Required on `refund`: the credit's unspent balance in the channel token's
|
|
1936
|
+
* own units. It is the ceiling the on-chain payout is sized to, so that a
|
|
1937
|
+
* drain returns what the caller is owed and nothing the DVM has earned
|
|
1938
|
+
* (internal-review). `commit` must re-assert it against the authoritative ledger
|
|
1939
|
+
* figure, since this one is read before the channel lock is taken.
|
|
1940
|
+
*/
|
|
1941
|
+
refundBoundNative?: number;
|
|
1942
|
+
commit: (funding: X402BatchFunding, tx?: CreditLedgerQuerier) => Promise<T>;
|
|
1943
|
+
complete?: (settlement: SettleResponse, committed: T, tx?: CreditLedgerQuerier) => Promise<T>;
|
|
1944
|
+
} & ({
|
|
1945
|
+
operation: "fund";
|
|
1946
|
+
expectedChannelId?: undefined;
|
|
1947
|
+
} | {
|
|
1948
|
+
operation: "refund";
|
|
1949
|
+
/**
|
|
1950
|
+
* The channel the credit being drained is bound to. Required, and
|
|
1951
|
+
* required in the *type*, because the payload the payer presents is
|
|
1952
|
+
* what revokes, settles and pays: a refund voucher naming another
|
|
1953
|
+
* channel would extinguish this credit's balance out of that channel's
|
|
1954
|
+
* escrow (internal-review). The fund path binds in the ledger
|
|
1955
|
+
* (`credit-ledger.ts`'s `rail_mismatch`) and the Tempo close binds in
|
|
1956
|
+
* its credential verifier; this is x402's.
|
|
1957
|
+
*/
|
|
1958
|
+
expectedChannelId: string;
|
|
1959
|
+
})): Promise<{
|
|
1960
|
+
ok: true;
|
|
1961
|
+
value: X402BatchAcceptance<T>;
|
|
1962
|
+
} | X402BatchRefusal>;
|
|
1963
|
+
getChannel(channelId: string): Promise<Channel | undefined>;
|
|
1964
|
+
/** Refresh one stored channel from the facilitator without settling or broadcasting anything. */
|
|
1965
|
+
observeChannel(channelId: string): Promise<Channel | undefined>;
|
|
1966
|
+
claimAndSettle(): Promise<void>;
|
|
1967
|
+
/**
|
|
1968
|
+
* Operator repair for a settlement wedged between chain and ledger
|
|
1969
|
+
* (internal-review). Absent without durable storage: with no settlement rows there
|
|
1970
|
+
* is no queue and nothing a repair could act on.
|
|
1971
|
+
*/
|
|
1972
|
+
repair?: X402SettlementRepair;
|
|
1973
|
+
/**
|
|
1974
|
+
* The durable settlement read the credit ledger gates spending on
|
|
1975
|
+
* (internal-review). Absent without durable storage, on the same reasoning as
|
|
1976
|
+
* {@link repair}: with no settlement rows nothing can be wedged, so there is
|
|
1977
|
+
* nothing to gate.
|
|
1978
|
+
*/
|
|
1979
|
+
settlementGate?: X402RefundSettlementGate;
|
|
1980
|
+
stop(): Promise<void>;
|
|
1981
|
+
}
|
|
1982
|
+
/** Options for constructing one DVM's batch-settlement server. */
|
|
1983
|
+
interface CreateX402BatchSettlementServerOpts {
|
|
1984
|
+
network: string;
|
|
1985
|
+
payTo: string;
|
|
1986
|
+
facilitator?: string;
|
|
1987
|
+
facilitatorAuth?: X402FacilitatorAuth;
|
|
1988
|
+
config: X402BatchSettlementConfig;
|
|
1989
|
+
/**
|
|
1990
|
+
* Chain RPC endpoint for settlement reconciliation; defaults to viem's bundled public one.
|
|
1991
|
+
*
|
|
1992
|
+
* Takes precedence over {@link X402BatchSettlementConfig.rpcUrl}, which is
|
|
1993
|
+
* where a builder configuring the rail declares it.
|
|
1994
|
+
*/
|
|
1995
|
+
rpcUrl?: string;
|
|
1996
|
+
db?: Pool;
|
|
1997
|
+
/**
|
|
1998
|
+
* Durable store to adopt instead of building one from {@link db} (internal-review).
|
|
1999
|
+
*
|
|
2000
|
+
* `createDVMServer` builds it at ledger construction, because the credit
|
|
2001
|
+
* ledger's spend gate has to exist whether or not this server is ever
|
|
2002
|
+
* constructed. Adopting it here keeps one `PostgresX402ChannelStorage` — and
|
|
2003
|
+
* so one settlement pool — per mount, which is what the internal-review reasoning on
|
|
2004
|
+
* {@link PostgresX402ChannelStorage} rests on. Ignored when the config
|
|
2005
|
+
* supplies its own {@link X402BatchSettlementConfig.storage}.
|
|
2006
|
+
*/
|
|
2007
|
+
channelStorage?: PostgresX402ChannelStorage;
|
|
2008
|
+
allowInMemory?: boolean;
|
|
2009
|
+
/**
|
|
2010
|
+
* Rail-native value the credit a channel funded has earned, in the channel
|
|
2011
|
+
* token's atomic units — `CreditLedgerLike.earnedNativeForX402Channel`.
|
|
2012
|
+
* `undefined` means the channel's credit could not be resolved.
|
|
2013
|
+
*
|
|
2014
|
+
* Required, not optional: it is the ceiling every claim is capped at
|
|
2015
|
+
* (internal-review), and a server that cannot derive one must claim nothing rather
|
|
2016
|
+
* than fall back to the deposit.
|
|
2017
|
+
*/
|
|
2018
|
+
earnedNative: (channelId: string) => Promise<number | undefined>;
|
|
2019
|
+
/** Bounded host-probe result reused during resource initialization. */
|
|
2020
|
+
supportedResponse?: SupportedResponse;
|
|
2021
|
+
/** Exact-chain evidence override for deterministic tests and custom RPC infrastructure. */
|
|
2022
|
+
settlementEvidenceReader?: X402SettlementEvidenceReader;
|
|
2023
|
+
/** Authoritative channel-view override for deterministic tests. */
|
|
2024
|
+
channelStateReader?: (channelId: string) => Promise<X402BatchChannelObservation>;
|
|
2025
|
+
/**
|
|
2026
|
+
* The payout tracker (internal-review). Attached with this server's settle scope
|
|
2027
|
+
* and routed every claim and settle, so the batch that lands at `pay_to` is
|
|
2028
|
+
* reported with the value claimed into it, and the pending snapshot can say
|
|
2029
|
+
* what is still on the channels and whether the settle is wedged.
|
|
2030
|
+
*
|
|
2031
|
+
* @internal Wired by `createDVMServer` from the platform reporter; not a
|
|
2032
|
+
* builder surface, and not part of the extractable SDK contract (internal-review).
|
|
2033
|
+
*/
|
|
2034
|
+
payout?: X402PayoutObserver;
|
|
2035
|
+
}
|
|
2036
|
+
/**
|
|
2037
|
+
* Initialize upstream's scheme and channel manager against dvmkit storage.
|
|
2038
|
+
* `resource.initialize()` performs the facilitator capability check and is
|
|
2039
|
+
* deliberately part of boot, so x402.org's missing authorizer refuses a
|
|
2040
|
+
* misconfigured server before it advertises a channel callers could fund.
|
|
2041
|
+
*/
|
|
2042
|
+
declare function createX402BatchSettlementServer(opts: CreateX402BatchSettlementServerOpts): Promise<X402BatchSettlementServer>;
|
|
2043
|
+
|
|
2044
|
+
/** Payment state from upfront token verification. */
|
|
2045
|
+
interface PaymentInfo {
|
|
2046
|
+
/**
|
|
2047
|
+
* Rail value this payment contributes to the job, in millisatoshis.
|
|
2048
|
+
*
|
|
2049
|
+
* For a credit-drawn job (internal-review) this is the **draw's** allocated share of
|
|
2050
|
+
* the credit's rail value, not the amount the caller handed over on this
|
|
2051
|
+
* request — under an explicit fund-and-draw the caller may have funded twenty
|
|
2052
|
+
* jobs' worth. That makes it the revenue basis for this job: mid-job top-ups
|
|
2053
|
+
* accumulate onto it, and the terminal books exactly what the job consumed.
|
|
2054
|
+
*/
|
|
2055
|
+
paidMsats: number;
|
|
2056
|
+
paymentMint?: string;
|
|
2057
|
+
/**
|
|
2058
|
+
* Rail that satisfied the upfront payment, if any. A {@link FundingMethod}
|
|
2059
|
+
* rather than a `PaymentMethod`: a draw against a Lightning-funded credit
|
|
2060
|
+
* reports `"lightning"` here (internal-review), which is what lets the revenue
|
|
2061
|
+
* ledger book it instead of silently dropping the row.
|
|
2062
|
+
*/
|
|
2063
|
+
paymentRail?: FundingMethod;
|
|
2064
|
+
receivedProofs: ProofLike[];
|
|
2065
|
+
/**
|
|
2066
|
+
* Settlement reference for the credit. x402 carries the EVM tx hash from
|
|
2067
|
+
* the facilitator (internal-review); mpp carries the credential's `challenge.id`,
|
|
2068
|
+
* HMAC-bound to the realm so cross-DVM collisions are impossible (internal-review);
|
|
2069
|
+
* cashu accumulator (internal-review) sets it to the `X-Cashu-Request-Id` UUID
|
|
2070
|
+
* (internal-review). Required by the platform revenue ledger for `tempo`/`x402`
|
|
2071
|
+
* rails; nullable for cashu's `tx_hash` column.
|
|
2072
|
+
*/
|
|
2073
|
+
paymentTxHash?: string;
|
|
2074
|
+
/** Chain transaction returned to a caller recovering this accepted request. */
|
|
2075
|
+
paymentTransactionHash?: string;
|
|
2076
|
+
/**
|
|
2077
|
+
* Rail-native payment amount in atomic units (internal-review). Sats for mpp,
|
|
2078
|
+
* USDC microunits for x402. Cashu leaves it unset — the existing
|
|
2079
|
+
* `paidMsats / 1000` derivation in the platform callback covers it.
|
|
2080
|
+
*/
|
|
2081
|
+
nativeAmount?: number;
|
|
2082
|
+
/** Native asset tag matching `revenue_events.native_asset` (internal-review). */
|
|
2083
|
+
nativeAsset?: "sats" | "usdc" | "usdc.e" | "usd-cents";
|
|
2084
|
+
/**
|
|
2085
|
+
* Discriminator written into `revenue_events.metadata.cashu_flow` so ops
|
|
2086
|
+
* queries can split per-call cashu rows by source path (internal-review).
|
|
2087
|
+
*/
|
|
2088
|
+
cashuFlow?: "p2pk_accumulator";
|
|
2089
|
+
/**
|
|
2090
|
+
* Base64-encoded x402 `SettleResponse` for the `X-PAYMENT-RESPONSE`
|
|
2091
|
+
* response header (internal-review). Set on the upfront-flow x402 success path;
|
|
2092
|
+
* `app.ts` attaches it to the 2xx response per x402 spec.
|
|
2093
|
+
*/
|
|
2094
|
+
x402SettleResponseHeader?: string;
|
|
2095
|
+
/** Exact-settlement admission retained until the matching upfront job row is durable. */
|
|
2096
|
+
x402ExactAdmission?: {
|
|
2097
|
+
intentId: string;
|
|
2098
|
+
jobId: string;
|
|
2099
|
+
replayed: boolean;
|
|
2100
|
+
};
|
|
2101
|
+
/**
|
|
2102
|
+
* Credit-ledger linkage (internal-review). Set whenever the payment funded and/or
|
|
2103
|
+
* drew the ledger — every paid job with a ledger wired. Persisted on the
|
|
2104
|
+
* job so the terminal funnel can settle/release the draw and the receipt
|
|
2105
|
+
* can countersign the `ReceiptCredit` block.
|
|
2106
|
+
*/
|
|
2107
|
+
creditId?: string;
|
|
2108
|
+
/** The draw placed for this job on `creditId` (internal-review). */
|
|
2109
|
+
drawId?: string;
|
|
2110
|
+
/** The draw's fiat amount, 1e-6 of {@link creditCurrency} (internal-review). */
|
|
2111
|
+
drawAmountMicro?: number;
|
|
2112
|
+
/** Currency the credit — and therefore the draw — is denominated in (internal-review). */
|
|
2113
|
+
creditCurrency?: string;
|
|
2114
|
+
/**
|
|
2115
|
+
* The funding leg, when this request actually moved money onto a credit
|
|
2116
|
+
* (internal-review). Reported to the platform as a **deposit**: a liability until
|
|
2117
|
+
* drawn, never summed into revenue. Absent on a pure draw against an
|
|
2118
|
+
* existing balance.
|
|
2119
|
+
*/
|
|
2120
|
+
funding?: CreditFundingReport;
|
|
2121
|
+
/** Signed proof and snapshot for an explicit fund-and-draw funding leg. */
|
|
2122
|
+
fundingReceipt?: FundingReceipt;
|
|
2123
|
+
fundingCredit?: CreditSnapshot;
|
|
2124
|
+
/**
|
|
2125
|
+
* Fiat micro of {@link creditCurrency} this payment funded that bought the
|
|
2126
|
+
* job **nothing** (internal-review) — a second payment for an ask the job's draw
|
|
2127
|
+
* already covers, or the overpaid tail of an oversized one. It is available
|
|
2128
|
+
* balance on {@link creditId}, drainable at once, and the caller has to be
|
|
2129
|
+
* told or they will re-send. Absent on every ordinary payment.
|
|
2130
|
+
*/
|
|
2131
|
+
unappliedMicro?: number;
|
|
2132
|
+
}
|
|
2133
|
+
/**
|
|
2134
|
+
* A funding event as the platform books it — one deposit row (internal-review, spec
|
|
2135
|
+
* §10). Keyed on `(dvmId, creditId, fundingId)`, so a retried report is a
|
|
2136
|
+
* no-op rather than a double-count.
|
|
2137
|
+
*/
|
|
2138
|
+
interface CreditFundingReport {
|
|
2139
|
+
creditId: string;
|
|
2140
|
+
/** The rail's own settlement reference — the deposit's idempotency key. */
|
|
2141
|
+
fundingId: string;
|
|
2142
|
+
rail: FundingMethod;
|
|
2143
|
+
/** Rail value received, in millisatoshis. */
|
|
2144
|
+
paidMsats: number;
|
|
2145
|
+
/** Gross Cashu proof face before its receiver input fee; Cashu only. */
|
|
2146
|
+
grossPaidMsats?: number;
|
|
2147
|
+
/** Standalone NUT-02 fee reserved by the received Cashu proofs; Cashu only. */
|
|
2148
|
+
cashuInputFeeMsats?: number;
|
|
2149
|
+
/** Rail-native atomic units; omitted for cashu (the platform derives from msats). */
|
|
2150
|
+
nativeAmount?: number;
|
|
2151
|
+
nativeAsset?: "sats" | "usdc" | "usdc.e" | "usd-cents";
|
|
2152
|
+
mint?: string;
|
|
2153
|
+
/** Fiat micro credited to the ledger, and the currency it's denominated in. */
|
|
2154
|
+
amountMicro: number;
|
|
2155
|
+
currency: string;
|
|
2156
|
+
fundedAt: number;
|
|
2157
|
+
/** Current credit expiry after this funding committed (epoch ms). */
|
|
2158
|
+
expiryMs: number;
|
|
2159
|
+
/** TIP-1034 voucher identity, present only for session-funded credit. */
|
|
2160
|
+
tempoSession?: {
|
|
2161
|
+
channelId: string;
|
|
2162
|
+
cumulativeAmount: string;
|
|
2163
|
+
};
|
|
2164
|
+
/** TIP-1034 chain binding, reported so the platform can route a close (internal-review). */
|
|
2165
|
+
tempoChannel?: TempoChannelReport;
|
|
2166
|
+
}
|
|
2167
|
+
/**
|
|
2168
|
+
* The chain identity of a TIP-1034 channel, as reported to the platform
|
|
2169
|
+
* alongside its deposit (internal-review). Read from the DVM's own durable channel
|
|
2170
|
+
* store, never from the caller's credential.
|
|
2171
|
+
*/
|
|
2172
|
+
interface TempoChannelReport {
|
|
2173
|
+
chainId: number;
|
|
2174
|
+
escrow: string;
|
|
2175
|
+
payee?: string;
|
|
2176
|
+
}
|
|
2177
|
+
/** Options for verifying an upfront payment on job submission. */
|
|
2178
|
+
interface UpfrontPaymentOpts {
|
|
2179
|
+
cashuToken?: string;
|
|
2180
|
+
requiredMsats?: number;
|
|
2181
|
+
mints?: string[];
|
|
2182
|
+
/** Base64-encoded x402 `PaymentPayload` from the `X-PAYMENT` header. */
|
|
2183
|
+
x402Payment?: string;
|
|
2184
|
+
x402Config?: X402Config;
|
|
2185
|
+
/** Durable exact-settlement coordinator. Production hosts wire this automatically. */
|
|
2186
|
+
x402ExactSettlement?: X402ExactSettlementServer;
|
|
2187
|
+
/** Exact generations the facilitator currently advertises. Defaults to both. */
|
|
2188
|
+
x402ExactVersions?: X402ExactVersionSupport;
|
|
2189
|
+
/** Reusable channel acceptor. Only consulted for fund-only `/v1/credit` requests. */
|
|
2190
|
+
x402BatchSettlement?: X402BatchSettlementServer;
|
|
2191
|
+
/**
|
|
2192
|
+
* Resource URL the client retried against. Bound into the rebuilt
|
|
2193
|
+
* `PaymentRequirements` when verifying x402, and surfaced on the 402 body
|
|
2194
|
+
* `accepts` array for x402 dual-serve (internal-review).
|
|
2195
|
+
*/
|
|
2196
|
+
x402Resource?: string;
|
|
2197
|
+
/** Either the structured credential object or its serialized `Authorization: Payment <…>` value. */
|
|
2198
|
+
mppCredential?: MppxCredential | string;
|
|
2199
|
+
mpp?: MppxServer;
|
|
2200
|
+
/**
|
|
2201
|
+
* Request path of the resource being paid for (e.g. `/v1/job`). Bound into
|
|
2202
|
+
* each issued mppx challenge's `meta.path` so a credential can't be replayed
|
|
2203
|
+
* against a different paid endpoint of the same DVM (internal-review).
|
|
2204
|
+
*/
|
|
2205
|
+
resourcePath?: string;
|
|
2206
|
+
/**
|
|
2207
|
+
* Whether `tempo/session` may be issued and newly accepted right now — the
|
|
2208
|
+
* platform close-observer and hosted settlement-readiness gates (internal-review,
|
|
2209
|
+
* internal-review), resolved per request by `DVMServer`. Absent means offered: a
|
|
2210
|
+
* self-hosted DVM operates these responsibilities itself.
|
|
2211
|
+
*
|
|
2212
|
+
* `false` refuses a session credential that would open a channel the platform
|
|
2213
|
+
* cannot safely protect or settle (internal-review, internal-review), and withholds session
|
|
2214
|
+
* challenges from every 402 this call emits, *except* on a credit already
|
|
2215
|
+
* bound to a TIP-1034 channel. That deposit is already exposed, so refusing
|
|
2216
|
+
* its vouchers only strands it; and since a channel-bound credit can be funded
|
|
2217
|
+
* no other way, withholding the challenge as well would have made the
|
|
2218
|
+
* acceptance carve-out unreachable through ordinary discovery.
|
|
2219
|
+
*/
|
|
2220
|
+
tempoSessionOffered?: boolean;
|
|
2221
|
+
/** Machine-readable cause when {@link tempoSessionOffered} is false. */
|
|
2222
|
+
tempoSessionUnavailableReason?: "observer_unhealthy" | "settlement_unhealthy";
|
|
2223
|
+
/**
|
|
2224
|
+
* Tracks consumed `(realm, challenge.id)` pairs so a credential that was
|
|
2225
|
+
* already accepted on the upfront path can't be replayed within its
|
|
2226
|
+
* `expires` window (internal-review). Optional: when absent, the upfront flow has
|
|
2227
|
+
* no per-process replay protection beyond mppx's own expiry check.
|
|
2228
|
+
*/
|
|
2229
|
+
consumedCredentialStore?: ConsumedCredentialStore;
|
|
2230
|
+
/** Cashu receive mode (internal-review / internal-review). Routes the receive path. */
|
|
2231
|
+
cashuMode?: CashuMode;
|
|
2232
|
+
/** Per-DVM Postgres pool — required for the `p2pk-accumulator` path. */
|
|
2233
|
+
accumulatorDb?: AccumulatorPool;
|
|
2234
|
+
/** DVM identifier — required for the `p2pk-accumulator` path. */
|
|
2235
|
+
dvmId?: string;
|
|
2236
|
+
/**
|
|
2237
|
+
* Active NUT-11 P2PK lock pubkeys — required for the `p2pk-accumulator`
|
|
2238
|
+
* path. Includes the current pubkey followed by retired pubkeys still
|
|
2239
|
+
* within the rotation grace window (internal-review). Caller resolves via
|
|
2240
|
+
* `loadLockPubkeyState` per-request so rotations propagate without a
|
|
2241
|
+
* server restart.
|
|
2242
|
+
*/
|
|
2243
|
+
lockPubkeys?: LockPubkey[];
|
|
2244
|
+
/** UUIDv4 from `X-Cashu-Request-Id` header. */
|
|
2245
|
+
requestId?: string;
|
|
2246
|
+
/**
|
|
2247
|
+
* Runtime mint-health tracker (internal-review). Receive path short-circuits with
|
|
2248
|
+
* `cashu_mint_sick` when the token's mint is currently flagged sick, so
|
|
2249
|
+
* agents retry against a healthy alternative without waiting for the
|
|
2250
|
+
* per-call mint timeout to fire inside `acceptAccumulatorPayment`.
|
|
2251
|
+
*/
|
|
2252
|
+
mintHealthTracker?: MintHealthTracker;
|
|
2253
|
+
/**
|
|
2254
|
+
* Host fx fetcher used to price the non-sat rails (x402 USDC, mppx fiat)
|
|
2255
|
+
* through the same BTC/USD source the quote path uses (internal-review). On a hosted
|
|
2256
|
+
* DVM this is the platform-served snapshot; self-hosted defaults to
|
|
2257
|
+
* CoinGecko-direct. Optional: a lazy default fetcher stands in when unset, so
|
|
2258
|
+
* the payment path never reaches `fetchBtcUsdRate`.
|
|
2259
|
+
*/
|
|
2260
|
+
fxFetcher?: FxFetcher;
|
|
2261
|
+
/**
|
|
2262
|
+
* Credit ledger every payment funds and draws through (internal-review, spec §1):
|
|
2263
|
+
* an attached rail payment is an implicit N=1 funding — `fund` the paid
|
|
2264
|
+
* amount + `draw` it, atomically with the rail commit. When absent (or
|
|
2265
|
+
* when the price can't be fiat-denominated), the payment is accepted with
|
|
2266
|
+
* legacy semantics and a structured warning — money is never refused
|
|
2267
|
+
* because the ledger is unavailable.
|
|
2268
|
+
*/
|
|
2269
|
+
creditLedger?: CreditLedgerLike;
|
|
2270
|
+
/**
|
|
2271
|
+
* Durable processed-payment markers for the x402/mpp rails (internal-review).
|
|
2272
|
+
* Those rails commit money outside dvmkit, so the marker row — written in
|
|
2273
|
+
* the same transaction as the ledger fund — is what makes a crash-replayed
|
|
2274
|
+
* credential unable to fund twice.
|
|
2275
|
+
*/
|
|
2276
|
+
processedPayments?: ProcessedPaymentStore;
|
|
2277
|
+
/**
|
|
2278
|
+
* Reporter-owned transactional outbox enqueue (internal-review). When present,
|
|
2279
|
+
* every funding awaits this inside the rail's commit transaction, so money
|
|
2280
|
+
* and its durable deposit report land together.
|
|
2281
|
+
*/
|
|
2282
|
+
enqueueCreditDeposit?: CreditDepositEnqueue;
|
|
2283
|
+
/**
|
|
2284
|
+
* The job price's fiat denomination, pinned by the caller from the same
|
|
2285
|
+
* quote surface that produced `requiredMsats` (internal-review). The ledger is
|
|
2286
|
+
* fiat-micro denominated; fund = draw = this amount in the exact-payment
|
|
2287
|
+
* case, so no fresh fx conversion happens on the verify hot path.
|
|
2288
|
+
*/
|
|
2289
|
+
priceFiat?: PriceFiat;
|
|
2290
|
+
/** Pre-allocated job id — recorded on the draw so replays can recover the job. */
|
|
2291
|
+
jobId?: string;
|
|
2292
|
+
/** Durable submit fingerprint paired with {@link jobId} for exact-x402 job admission. */
|
|
2293
|
+
requestFingerprint?: string;
|
|
2294
|
+
/** Fingerprint including the caller-auth envelope, reserved to this exact intent. */
|
|
2295
|
+
requestAuthFingerprint?: string;
|
|
2296
|
+
/** Verified caller pubkey from the signed-request envelope, when the DVM has auth. */
|
|
2297
|
+
callerPubkey?: string;
|
|
2298
|
+
/** Requester id — funder identity fallback for DVMs without descriptor auth. */
|
|
2299
|
+
requesterId?: string;
|
|
2300
|
+
/**
|
|
2301
|
+
* Explicit credit fields extracted from the signed body (spec §2). Their
|
|
2302
|
+
* signature coverage and structural validation happen in `app.ts` before
|
|
2303
|
+
* payment verification; this layer enforces ownership, the funding
|
|
2304
|
+
* commitment, and the draw itself.
|
|
2305
|
+
*/
|
|
2306
|
+
creditEnvelope?: CreditEnvelope;
|
|
2307
|
+
/**
|
|
2308
|
+
* Turn this verification into a **fund-only top-up** (internal-review): the rails
|
|
2309
|
+
* verify and commit exactly as they do for a job, but the ledger leg funds
|
|
2310
|
+
* without drawing — there is no job to pay for. Set by `POST /v1/credit`;
|
|
2311
|
+
* `requiredMsats` / `priceFiat` carry the requested top-up amount rather
|
|
2312
|
+
* than a job price.
|
|
2313
|
+
*/
|
|
2314
|
+
fundOnly?: FundOnlyRequest;
|
|
2315
|
+
/**
|
|
2316
|
+
* Credit lifetime to mint, in ms — the DVM's advertised `credit.ttl`.
|
|
2317
|
+
* Defaults to {@link IMPLICIT_CREDIT_TTL_MS}. Threading it keeps the
|
|
2318
|
+
* funding menu honest: a DVM advertising a 7-day TTL must not mint 30-day
|
|
2319
|
+
* credits.
|
|
2320
|
+
*/
|
|
2321
|
+
creditTtlMs?: number;
|
|
2322
|
+
}
|
|
2323
|
+
/** A fund-only top-up's identity: which credit, under which client-generated id. */
|
|
2324
|
+
interface FundOnlyRequest {
|
|
2325
|
+
/**
|
|
2326
|
+
* Target credit. `/v1/credit` always supplies one — either the caller's
|
|
2327
|
+
* `credit_id` or the deterministic id derived from `(pubkey, fund_id)` — so
|
|
2328
|
+
* a retry lands on the same credit and hits the `credit_fundings` key.
|
|
2329
|
+
*/
|
|
2330
|
+
creditId?: string;
|
|
2331
|
+
/**
|
|
2332
|
+
* Builder opt-in for one-payment stablecoin credit. Absent fails closed;
|
|
2333
|
+
* only `POST /v1/credit` supplies this policy input.
|
|
2334
|
+
*/
|
|
2335
|
+
allowOneShotStablecoin?: boolean;
|
|
2336
|
+
/** Client-generated idempotency key for this deposit (`credit_fundings` PK). */
|
|
2337
|
+
fundId: string;
|
|
2338
|
+
/**
|
|
2339
|
+
* SHA-256 hex over the raw funding artifact, signed by the caller (spec §2
|
|
2340
|
+
* condition 3). Checked before any rail work: it is what binds the artifact
|
|
2341
|
+
* presented on the header to the deposit named in the signed body.
|
|
2342
|
+
*/
|
|
2343
|
+
commitment: string;
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2346
|
+
/** Everything the issuer needs to sign for one DVM. */
|
|
2347
|
+
interface ReceiptIssuerOpts {
|
|
2348
|
+
/** 32-byte hex receipt secret — `DVMKIT_RECEIPT_KEY`, or an ephemeral dev key. */
|
|
2349
|
+
secret: string;
|
|
2350
|
+
/** DVM slug written into every receipt's `dvm` field. */
|
|
2351
|
+
dvm: string;
|
|
2352
|
+
/** Immutable DVM identity written into every signed receipt. */
|
|
2353
|
+
dvmId: string;
|
|
2354
|
+
}
|
|
2355
|
+
/**
|
|
2356
|
+
* Signs job receipts for one DVM (internal-review).
|
|
2357
|
+
*
|
|
2358
|
+
* Constructed only when a receipt key is wired, so the presence of an issuer
|
|
2359
|
+
* *is* the "this DVM emits receipts" signal that `/v1/info` advertises. Pure:
|
|
2360
|
+
* it holds the key and the slug and turns a terminal `JobRecord` plus an
|
|
2361
|
+
* allocated sequence number into signed bytes — sequence allocation and
|
|
2362
|
+
* persistence belong to the store, which is what makes issuance idempotent
|
|
2363
|
+
* across machines.
|
|
2364
|
+
*/
|
|
2365
|
+
declare class ReceiptIssuer {
|
|
2366
|
+
/** x-only pubkey this issuer signs under; the attested `receipt_pubkey`. */
|
|
2367
|
+
readonly pubkey: string;
|
|
2368
|
+
private readonly secret;
|
|
2369
|
+
private readonly dvm;
|
|
2370
|
+
private readonly dvmId;
|
|
2371
|
+
constructor(opts: ReceiptIssuerOpts);
|
|
2372
|
+
/**
|
|
2373
|
+
* Build and sign the receipt for a terminal job. `seq` comes from
|
|
2374
|
+
* `JobStore.claimReceiptSeq`, `issuedAt` defaults to now (tests pin it).
|
|
2375
|
+
*
|
|
2376
|
+
* Free jobs get receipts too, with `paid.msats: 0` — they still consume a
|
|
2377
|
+
* sequence number and still attest an outcome. Whether feedback is
|
|
2378
|
+
* paid-only is the feedback layer's call, not this one's.
|
|
2379
|
+
*
|
|
2380
|
+
* `credit` is the resolved draw block (internal-review) — present on every job
|
|
2381
|
+
* whose payment funded/drew the ledger, absent otherwise. Signature-
|
|
2382
|
+
* compatible either way: `canonicalize` omits absent keys.
|
|
2383
|
+
*/
|
|
2384
|
+
issue(record: JobRecord, seq: number, issuedAt?: number, credit?: ReceiptCredit): JobReceipt;
|
|
2385
|
+
/**
|
|
2386
|
+
* Countersign one reclaim event (internal-review). Unlike job receipts there is
|
|
2387
|
+
* no store-allocated sequence — the drain's own `ledger_seq` (taken under
|
|
2388
|
+
* the credit row lock, shared with draws) already orders it in the
|
|
2389
|
+
* per-credit evidence chain.
|
|
2390
|
+
*/
|
|
2391
|
+
issueDrain(args: {
|
|
2392
|
+
creditId: string;
|
|
2393
|
+
drainId: string;
|
|
2394
|
+
event: DrainReceiptEvent;
|
|
2395
|
+
method: string;
|
|
2396
|
+
amountMicro: number;
|
|
2397
|
+
balanceAfterMicro: number;
|
|
2398
|
+
ledgerSeq: number;
|
|
2399
|
+
callerPubkey: string;
|
|
2400
|
+
issuedAt?: number;
|
|
2401
|
+
}): DrainReceipt;
|
|
2402
|
+
/** Countersign the immutable tuple captured when a credit funding committed. */
|
|
2403
|
+
issueFunding(args: {
|
|
2404
|
+
creditId: string;
|
|
2405
|
+
fundId: string;
|
|
2406
|
+
fundedMicro: number;
|
|
2407
|
+
balanceAfterMicro: number;
|
|
2408
|
+
ledgerSeq: number;
|
|
2409
|
+
callerPubkey: string;
|
|
2410
|
+
issuedAt?: number;
|
|
2411
|
+
}): FundingReceipt;
|
|
2412
|
+
}
|
|
2413
|
+
|
|
2414
|
+
/** Stable reason why the configured x402 exact rail cannot be advertised. */
|
|
2415
|
+
type X402FacilitatorUnavailableReason = "probe_failed" | "unsupported_exact";
|
|
2416
|
+
/** Versioned settlement capabilities advertised by one facilitator/network pair. */
|
|
2417
|
+
interface X402FacilitatorSupport {
|
|
2418
|
+
/** True when at least one exact generation can settle. */
|
|
2419
|
+
available: boolean;
|
|
2420
|
+
exact: X402ExactVersionSupport;
|
|
2421
|
+
batchSettlement: boolean;
|
|
2422
|
+
facilitator: string;
|
|
2423
|
+
network: string;
|
|
2424
|
+
reason?: X402FacilitatorUnavailableReason;
|
|
2425
|
+
error?: string;
|
|
2426
|
+
}
|
|
2427
|
+
/** Live, synchronous capability view consumed while payment responses are rendered. */
|
|
2428
|
+
interface X402FacilitatorHealthLike {
|
|
2429
|
+
/** Prime or refresh the capability observation. */
|
|
2430
|
+
refresh(): Promise<void>;
|
|
2431
|
+
/** Return current support and begin a background refresh when its TTL has elapsed. */
|
|
2432
|
+
support(): X402FacilitatorSupport;
|
|
2433
|
+
/** Last successful raw response, reused by batch initialization without another request. */
|
|
2434
|
+
supportedResponse?(): SupportedResponse | undefined;
|
|
2435
|
+
}
|
|
2436
|
+
|
|
2437
|
+
/** Options for creating a JobManager. */
|
|
2438
|
+
interface JobManagerOpts {
|
|
2439
|
+
/** Environment variables for ctx.env. */
|
|
2440
|
+
env: Record<string, string>;
|
|
2441
|
+
/** KV store for ctx.store. */
|
|
2442
|
+
store: KVStore;
|
|
2443
|
+
/** Cashu mints accepted for payment. */
|
|
2444
|
+
mints?: string[];
|
|
2445
|
+
/** Persistent backing store for job records. Default: in-memory. */
|
|
2446
|
+
jobStore?: JobStore;
|
|
2447
|
+
/** When true, payment messages auto-credit without verification. */
|
|
2448
|
+
devMode?: boolean;
|
|
2449
|
+
/** Payment methods this DVM accepts. Default: derived by the server from configured rails. */
|
|
2450
|
+
paymentMethods?: PaymentMethod[];
|
|
2451
|
+
/** x402 stablecoin payment configuration. */
|
|
2452
|
+
x402?: X402Config;
|
|
2453
|
+
/** Durable exact-settlement coordinator shared with upfront verification. */
|
|
2454
|
+
x402ExactSettlement?: X402ExactSettlementServer;
|
|
2455
|
+
/** Live facilitator capabilities for version-specific mid-job payment handling. */
|
|
2456
|
+
x402FacilitatorHealth?: X402FacilitatorHealthLike;
|
|
2457
|
+
/** MPP multi-rail payment handle (built via `createMppFromOpts`). */
|
|
2458
|
+
mpp?: MppxServer;
|
|
2459
|
+
/** Cashu receive mode (internal-review). */
|
|
2460
|
+
cashuMode?: CashuMode;
|
|
2461
|
+
/** Builder's NUT-11 P2PK lock pubkey (internal-review). Required for accumulator mode. */
|
|
2462
|
+
lockPubkey?: string;
|
|
2463
|
+
/** Postgres pool for wallet accumulator persistence. */
|
|
2464
|
+
db?: Pool;
|
|
2465
|
+
/**
|
|
2466
|
+
* Agent-wallet (internal-review): per-DVM identifier used for the scheduler advisory
|
|
2467
|
+
* lock and the `(dvm_id, request_id)` replay key. Defaults to `DVMKIT_DVM_ID`
|
|
2468
|
+
* env at boot in `createDVMHost()`.
|
|
2469
|
+
*/
|
|
2470
|
+
dvmId?: string;
|
|
2471
|
+
/** Audience expected by persisted v2 caller statements. */
|
|
2472
|
+
authAudience?: SignedRequestAudience;
|
|
2473
|
+
/**
|
|
2474
|
+
* Shared fx fetcher used to convert USD-string descriptor prices and
|
|
2475
|
+
* fiat-form `requestPayment` calls into sats (internal-review). The SDK server owns
|
|
2476
|
+
* one per process so concurrent quotes share the 60s cache window.
|
|
2477
|
+
*/
|
|
2478
|
+
fxFetcher: FxFetcher;
|
|
2479
|
+
/**
|
|
2480
|
+
* Signs a receipt at every terminal transition (internal-review). Absent when the
|
|
2481
|
+
* DVM has no receipt key wired — jobs then terminate exactly as before and
|
|
2482
|
+
* `/v1/info` doesn't advertise `receipts`.
|
|
2483
|
+
*/
|
|
2484
|
+
receiptIssuer?: ReceiptIssuer;
|
|
2485
|
+
/**
|
|
2486
|
+
* Credit ledger for the terminal funnel (internal-review): a job carrying a
|
|
2487
|
+
* `creditId`/`drawId` settles its draw on success and releases it on
|
|
2488
|
+
* failure/cancel — this is how "no debit on job failure" is mechanically
|
|
2489
|
+
* real. The resolution runs before receipt issuance so the receipt's
|
|
2490
|
+
* `credit` block countersigns the post-resolution balance.
|
|
2491
|
+
*/
|
|
2492
|
+
creditLedger?: CreditLedgerLike;
|
|
2493
|
+
/**
|
|
2494
|
+
* Durable processed-payment markers (internal-review), threaded through to the
|
|
2495
|
+
* mid-job top-up path (internal-review) so an x402/mpp payment's marker, `fund`, and
|
|
2496
|
+
* `growDraw` commit together.
|
|
2497
|
+
*/
|
|
2498
|
+
processedPayments?: ProcessedPaymentStore;
|
|
2499
|
+
/**
|
|
2500
|
+
* Reporter-owned transactional deposit enqueue threaded into mid-job rail
|
|
2501
|
+
* commits (internal-review).
|
|
2502
|
+
*/
|
|
2503
|
+
enqueueCreditDeposit?: CreditDepositEnqueue;
|
|
2504
|
+
/** Reporter outbox inserted atomically with a terminal draw release (internal-review). */
|
|
2505
|
+
enqueueCreditDrawRelease?: CreditDrawReleaseEnqueue;
|
|
2506
|
+
/** Credit lifetime a top-up mints with, from the DVM's advertised `credit.ttl`. */
|
|
2507
|
+
creditTtlMs?: number;
|
|
2508
|
+
/**
|
|
2509
|
+
* The DVM's declared pricing currency (internal-review), which is the currency of
|
|
2510
|
+
* every credit it opens. Threaded into the job context so a mid-job ask on a
|
|
2511
|
+
* job that holds no credit yet is still pinned in the denomination its first
|
|
2512
|
+
* payment will open the credit in (internal-review). `DVMServer` passes its resolved
|
|
2513
|
+
* `pricingCurrency`; a host building a `JobManager` directly falls back to the
|
|
2514
|
+
* descriptor, and then to `"usd"`.
|
|
2515
|
+
*/
|
|
2516
|
+
pricingCurrency?: string;
|
|
2517
|
+
/**
|
|
2518
|
+
* Callback fired when a mid-job top-up moves money onto a credit (internal-review).
|
|
2519
|
+
* The platform books it as a **deposit** — a liability until drawn, never
|
|
2520
|
+
* summed into revenue — so a top-up that skipped this would leave the
|
|
2521
|
+
* outstanding-liability view short. Mirrors `DVMServer.reportCreditDeposit`,
|
|
2522
|
+
* which handles the upfront and `/v1/credit` legs.
|
|
2523
|
+
*/
|
|
2524
|
+
onCreditFunded?: (info: CreditDepositPayload) => void | Promise<void>;
|
|
2525
|
+
/**
|
|
2526
|
+
* Callback fired when a paid job completes (internal-review). Container-runtime DVMs
|
|
2527
|
+
* use this to report revenue to the platform via `RevenueReporter`. Mirrors
|
|
2528
|
+
* the isolate path's `IsolateJobManager.opts.onJobCompleted`.
|
|
2529
|
+
*/
|
|
2530
|
+
onJobCompleted?: (info: {
|
|
2531
|
+
dvmId: string;
|
|
2532
|
+
jobId: string;
|
|
2533
|
+
paidMsats: number;
|
|
2534
|
+
paymentMint?: string;
|
|
2535
|
+
rail: string;
|
|
2536
|
+
paymentTxHash?: string;
|
|
2537
|
+
nativeAmount?: number;
|
|
2538
|
+
nativeAsset?: string;
|
|
2539
|
+
cashuFlow?: string;
|
|
2540
|
+
creditId?: string;
|
|
2541
|
+
drawId?: string;
|
|
2542
|
+
drawAmountMicro?: number;
|
|
2543
|
+
creditCurrency?: string;
|
|
2544
|
+
settledAt?: number;
|
|
2545
|
+
fundingRef?: string;
|
|
2546
|
+
kind?: string;
|
|
2547
|
+
}) => void | Promise<void>;
|
|
2548
|
+
/**
|
|
2549
|
+
* Callback fired when the stale-job reaper force-fails a *paid* job — its
|
|
2550
|
+
* pending credit draw is released rather than settled, because only a
|
|
2551
|
+
* completed job settles a draw (internal-review). Container-runtime DVMs wire this
|
|
2552
|
+
* to a paid-job-death report → operator `notice` (internal-review). Never fires for
|
|
2553
|
+
* free jobs (`paidMsats === 0`). Fired fire-and-forget so a throw or a slow
|
|
2554
|
+
* report can't wedge the sweep.
|
|
2555
|
+
*/
|
|
2556
|
+
onPaidJobDeath?: (info: {
|
|
2557
|
+
dvmId: string;
|
|
2558
|
+
jobId: string;
|
|
2559
|
+
capability: string;
|
|
2560
|
+
paidMsats: number;
|
|
2561
|
+
rail: string;
|
|
2562
|
+
reason: "worker_died_mid_job" | "stale_no_terminal_status";
|
|
2563
|
+
nativeAmount?: number;
|
|
2564
|
+
nativeAsset?: string;
|
|
2565
|
+
paymentMint?: string;
|
|
2566
|
+
}) => void | Promise<void>;
|
|
2567
|
+
/**
|
|
2568
|
+
* Callback fired when a settled credit draw cannot book a revenue event
|
|
2569
|
+
* because no usable payment rail is available (internal-review). Container-runtime
|
|
2570
|
+
* DVMs auto-wire this to the platform's deduped operator notice. Fired
|
|
2571
|
+
* fire-and-forget so alert delivery cannot block terminal handling or draw
|
|
2572
|
+
* reconciliation.
|
|
2573
|
+
*/
|
|
2574
|
+
onRevenueSkippedNoRail?: (info: RevenueSkippedNoRailPayload) => void | Promise<void>;
|
|
2575
|
+
/**
|
|
2576
|
+
* Stale-job sweep threshold in ms (internal-review). Jobs in a non-terminal status
|
|
2577
|
+
* (`processing` / `working` / `awaiting-input`) whose `lastActivityAt` is
|
|
2578
|
+
* older than this are force-cancelled with reason `stale_no_terminal_status`,
|
|
2579
|
+
* so callers always reach a terminal status instead of polling forever after
|
|
2580
|
+
* a worker crash, OOM, or silent `messageAppender` failure. Default:
|
|
2581
|
+
* `2 × descriptor.idleTimeout + STALE_JOB_WATCHDOG_HEADROOM_MS`, matching
|
|
2582
|
+
* the internal-review acceptance criterion against scrape's `JOB_TIMEOUT_MS`.
|
|
2583
|
+
* Builders whose per-handler wall-clock bound exceeds 90s must override
|
|
2584
|
+
* this — otherwise the watchdog fires on a legitimate in-flight handler.
|
|
2585
|
+
* Set to `0` to disable (test seam).
|
|
2586
|
+
*/
|
|
2587
|
+
staleJobTimeoutMs?: number;
|
|
2588
|
+
/**
|
|
2589
|
+
* Sweep interval for the stale-job watchdog. Default: `min(60s, max(15s,
|
|
2590
|
+
* tightestActiveTimeout / 4))`. Tests override to fire deterministically.
|
|
2591
|
+
*/
|
|
2592
|
+
staleJobSweepIntervalMs?: number;
|
|
2593
|
+
/**
|
|
2594
|
+
* Worker-liveness watchdog in ms (internal-review). Jobs in `processing`/`working`
|
|
2595
|
+
* whose `lastActivityAt` is older than this are marked `failed` (dead
|
|
2596
|
+
* worker), separately from the longer `awaiting-input` idle timeout above.
|
|
2597
|
+
* Default: `descriptor.processingWatchdog * 1000` or
|
|
2598
|
+
* `DEFAULT_PROCESSING_WATCHDOG_MS`. Set to `0` to disable (test seam).
|
|
2599
|
+
*/
|
|
2600
|
+
processingWatchdogMs?: number;
|
|
2601
|
+
/**
|
|
2602
|
+
* Interval at which locally-active `processing`/`working` jobs have their
|
|
2603
|
+
* `lastActivityAt` bumped so the processing watchdog only fires on a truly
|
|
2604
|
+
* dead worker (internal-review). Default: `DEFAULT_HEARTBEAT_INTERVAL_MS`. Set to
|
|
2605
|
+
* `0` to disable (test seam). Also floors how far the internal-review clock guard
|
|
2606
|
+
* may compensate a backwards step on the processing arm — a cadence close to
|
|
2607
|
+
* `processingWatchdogMs` leaves it no room and it compensates nothing.
|
|
2608
|
+
*/
|
|
2609
|
+
heartbeatIntervalMs?: number;
|
|
2610
|
+
/**
|
|
2611
|
+
* Age in ms at which a `pending` credit draw whose `job_id` matches no row in
|
|
2612
|
+
* the job store is released as orphaned (internal-review). The draw commits before
|
|
2613
|
+
* the job row is persisted, so a crash or a fail-closed refusal in that
|
|
2614
|
+
* window strands a hold no terminal path can ever reach. Must stay
|
|
2615
|
+
* comfortably above the submit path's draw→persist latency. Set to `0` to
|
|
2616
|
+
* disable the watchdog entirely — stranded holds then stay stranded, so
|
|
2617
|
+
* treat it as a money knob, not a tuning one. Default:
|
|
2618
|
+
* {@link DEFAULT_ORPHAN_DRAW_AGE_MS}.
|
|
2619
|
+
*/
|
|
2620
|
+
orphanDrawAgeMs?: number;
|
|
2621
|
+
/**
|
|
2622
|
+
* Age in ms at which a `pending` credit draw whose job row **is** terminal is
|
|
2623
|
+
* reconciled against that row's outcome (internal-review) — settled for a
|
|
2624
|
+
* `completed` job, released for a `failed`/`cancelled` one. Covers the hole
|
|
2625
|
+
* `orphanDrawAgeMs` cannot: `resolveCreditDraw` throwing on a ledger error
|
|
2626
|
+
* leaves a terminal job whose hold no later path retries.
|
|
2627
|
+
*
|
|
2628
|
+
* Rides the same scan as the orphan arm, which has two consequences for the
|
|
2629
|
+
* value you set here. Setting `orphanDrawAgeMs` to `0` disables this arm too.
|
|
2630
|
+
* And the effective gate is `max(orphanDrawAgeMs, terminalDrawReconcileAgeMs)`
|
|
2631
|
+
* — the scan never returns a row younger than its own cutoff, so anything
|
|
2632
|
+
* below `orphanDrawAgeMs` is silently clamped up to it. That floor is
|
|
2633
|
+
* deliberate rather than a wart: honouring a narrower value would mean
|
|
2634
|
+
* widening the scan, which hands the orphan arm rows younger than
|
|
2635
|
+
* `orphanDrawAgeMs` that it must not release. It can only ever delay a
|
|
2636
|
+
* settle, never advance one, which is the safe direction for a debit.
|
|
2637
|
+
*
|
|
2638
|
+
* Set to `0` to disable this arm alone — a money knob, not a tuning one.
|
|
2639
|
+
* Default: {@link DEFAULT_TERMINAL_DRAW_RECONCILE_AGE_MS}.
|
|
2640
|
+
*/
|
|
2641
|
+
terminalDrawReconcileAgeMs?: number;
|
|
2642
|
+
/**
|
|
2643
|
+
* Sweep interval for the orphan-draw watchdog. Default:
|
|
2644
|
+
* {@link DEFAULT_ORPHAN_DRAW_SWEEP_INTERVAL_MS}. Set to `0` to disable — the
|
|
2645
|
+
* test seam, since suites drive `sweepOrphanDraws()` directly.
|
|
2646
|
+
*/
|
|
2647
|
+
orphanDrawSweepIntervalMs?: number;
|
|
2648
|
+
/**
|
|
2649
|
+
* Grace window in ms a reactivating machine waits for a live original handler
|
|
2650
|
+
* to win the single-execution claim before replaying an `awaiting-input` job
|
|
2651
|
+
* itself (internal-review). Default: `DEFAULT_REACTIVATION_CLAIM_GRACE_MS`. Widen if
|
|
2652
|
+
* cross-machine NOTIFY latency causes spurious double executions; lower in
|
|
2653
|
+
* tests for speed.
|
|
2654
|
+
*/
|
|
2655
|
+
reactivationClaimGraceMs?: number;
|
|
2656
|
+
}
|
|
2657
|
+
type ResolvedInput<I extends ZodLike | undefined> = I extends ZodLike<infer O> ? O : string;
|
|
2658
|
+
/**
|
|
2659
|
+
* A deposit this payment funded that the job row did **not** take (internal-review).
|
|
2660
|
+
*
|
|
2661
|
+
* Reached when two mid-job top-ups race on a free-then-paid job: each opens its
|
|
2662
|
+
* own implicit credit, and `verifyAndCredit`'s set-if-null bind keeps one. The
|
|
2663
|
+
* loser's money is real, owned by the caller, and reclaimable — but nothing
|
|
2664
|
+
* named it, so the caller would have to derive `imp:<rail>:<payment_id>` to
|
|
2665
|
+
* reach their own balance. This rides the 201 so they don't have to.
|
|
2666
|
+
*
|
|
2667
|
+
* The response is the fast surface, not the only one: the credit is opened
|
|
2668
|
+
* under the caller's verified pubkey, so `POST /v1/credit` op `balance` lists
|
|
2669
|
+
* it and op `drain` reclaims it once the hold resolves.
|
|
2670
|
+
*/
|
|
2671
|
+
interface UnappliedCredit {
|
|
2672
|
+
credit_id: string;
|
|
2673
|
+
credited_micro: number;
|
|
2674
|
+
credit_currency: string;
|
|
2675
|
+
display: string;
|
|
2676
|
+
hint: string;
|
|
2677
|
+
}
|
|
2678
|
+
/**
|
|
2679
|
+
* What {@link JobManager.processPayment} tells the route. `error` is a refusal
|
|
2680
|
+
* to serialise verbatim; otherwise the payment was accepted, optionally
|
|
2681
|
+
* carrying an {@link UnappliedCredit} to fold into the 201.
|
|
2682
|
+
*/
|
|
2683
|
+
type ProcessPaymentOutcome = {
|
|
2684
|
+
error: {
|
|
2685
|
+
body: Record<string, unknown>;
|
|
2686
|
+
status: number;
|
|
2687
|
+
};
|
|
2688
|
+
unappliedCredit?: undefined;
|
|
2689
|
+
} | {
|
|
2690
|
+
error?: undefined;
|
|
2691
|
+
unappliedCredit?: UnappliedCredit;
|
|
2692
|
+
};
|
|
2693
|
+
/**
|
|
2694
|
+
* Transport-agnostic job lifecycle manager.
|
|
2695
|
+
*
|
|
2696
|
+
* Handles job creation, handler invocation, message dispatch, persistence,
|
|
2697
|
+
* idle timeout, and durable replay. Shared by both the HTTP server and
|
|
2698
|
+
* relay provider.
|
|
2699
|
+
*/
|
|
2700
|
+
declare class JobManager<State, InputSchema extends ZodLike | undefined> {
|
|
2701
|
+
private readonly descriptor;
|
|
2702
|
+
private readonly opts;
|
|
2703
|
+
private readonly _activeJobs;
|
|
2704
|
+
private readonly jobStore;
|
|
2705
|
+
private readonly idleTimers;
|
|
2706
|
+
private readonly cleanupTimers;
|
|
2707
|
+
/** Per-job NOTIFY unsubscribe handles for the durable-status watch (internal-review). */
|
|
2708
|
+
private readonly statusWatchers;
|
|
2709
|
+
/** Job ids whose durable-status re-read is in flight — coalesces notify storms (internal-review). */
|
|
2710
|
+
private readonly statusChecksInFlight;
|
|
2711
|
+
/** Job ids that were notified mid-re-read and must be re-checked (internal-review). */
|
|
2712
|
+
private readonly statusChecksQueued;
|
|
2713
|
+
private readonly sessionId;
|
|
2714
|
+
private nextJobId;
|
|
2715
|
+
private accumulatorMonitor?;
|
|
2716
|
+
private readonly staleJobTimeoutMs;
|
|
2717
|
+
private readonly processingWatchdogMs;
|
|
2718
|
+
private readonly reactivationClaimGraceMs;
|
|
2719
|
+
/**
|
|
2720
|
+
* Worker-heartbeat cadence (internal-review). Also floors the internal-review clock
|
|
2721
|
+
* guard's compensation on the processing arm — see
|
|
2722
|
+
* {@link staleSweepCompensationCapMs}.
|
|
2723
|
+
*/
|
|
2724
|
+
private readonly heartbeatIntervalMs;
|
|
2725
|
+
private readonly orphanDrawAgeMs;
|
|
2726
|
+
private readonly terminalDrawReconcileAgeMs;
|
|
2727
|
+
private staleSweepTimer?;
|
|
2728
|
+
private heartbeatTimer?;
|
|
2729
|
+
private orphanDrawSweepTimer?;
|
|
2730
|
+
/** This manager holds {@link ORPHAN_SWEEP_CLAIMS} for its ledger. */
|
|
2731
|
+
private orphanSweepClaimed;
|
|
2732
|
+
/** Where a page-capped tick left off; cleared once a tick reaches the end. */
|
|
2733
|
+
private orphanSweepResumeFrom?;
|
|
2734
|
+
/**
|
|
2735
|
+
* A `Date.now()` reading that cannot regress within this manager's lifetime
|
|
2736
|
+
* (internal-review). Anchored at construction, so a manager already running when
|
|
2737
|
+
* the clock stepped is covered. The stale-job sweep's two cutoffs, the
|
|
2738
|
+
* orphan-draw age gate and the reactivation claim grace all read it; see
|
|
2739
|
+
* {@link createMonotonicClock} for what it costs and where it is clamped.
|
|
2740
|
+
*/
|
|
2741
|
+
private readonly monotonicNowMs;
|
|
2742
|
+
private sweepInFlight;
|
|
2743
|
+
private orphanSweepInFlight;
|
|
2744
|
+
private heartbeatInFlight;
|
|
2745
|
+
/**
|
|
2746
|
+
* The denomination of every credit this DVM opens (internal-review). Resolved once,
|
|
2747
|
+
* with the same boundary fallback `DVMServer` applies for direct JavaScript
|
|
2748
|
+
* callers that force an incomplete value past the descriptor contract.
|
|
2749
|
+
*/
|
|
2750
|
+
private readonly pricingCurrency;
|
|
2751
|
+
constructor(descriptor: DVMDescriptor<State, InputSchema>, opts: JobManagerOpts);
|
|
2752
|
+
/** Active in-memory jobs. */
|
|
2753
|
+
get activeJobs(): Map<string, ServerJob>;
|
|
2754
|
+
/** The backing job store. */
|
|
2755
|
+
get store(): JobStore;
|
|
2756
|
+
/**
|
|
2757
|
+
* True when this DVM will actually issue receipts (internal-review) — a key *and*
|
|
2758
|
+
* a store that can allocate sequence numbers and persist bytes write-once.
|
|
2759
|
+
* `/v1/info#receipts` reads this rather than the key alone, so the flag can
|
|
2760
|
+
* never promise something `issueReceipt` silently declines to do.
|
|
2761
|
+
*/
|
|
2762
|
+
get receiptsEnabled(): boolean;
|
|
2763
|
+
/** True when the named capability exists on this descriptor. */
|
|
2764
|
+
hasCapability(name: string): boolean;
|
|
2765
|
+
/** Names of every capability this descriptor exposes — used for error envelopes. */
|
|
2766
|
+
capabilityNames(): string[];
|
|
2767
|
+
/**
|
|
2768
|
+
* Resolve a capability's static `price` to msats (internal-review).
|
|
2769
|
+
*
|
|
2770
|
+
* `"$X.XX"` is parsed as USD and converted via the SDK's shared fx fetcher.
|
|
2771
|
+
* Returns `undefined` for dynamic-priced capabilities (those that declare
|
|
2772
|
+
* `onQuote`) — the caller falls back to `computeDynamicPrice` in that path.
|
|
2773
|
+
*/
|
|
2774
|
+
currentPriceMsats(capability: string): Promise<number | undefined>;
|
|
2775
|
+
/**
|
|
2776
|
+
* Translate a capability's static `price` into the fiat envelope used by
|
|
2777
|
+
* the per-capability `pricing.max` advertised on `/v1/info` (internal-review).
|
|
2778
|
+
* Returns `undefined` for dynamic-priced capabilities and for unknown
|
|
2779
|
+
* names (the route handler validates the name before invoking this).
|
|
2780
|
+
*/
|
|
2781
|
+
currentPricingMax(capability: string): {
|
|
2782
|
+
amount: number;
|
|
2783
|
+
currency: string;
|
|
2784
|
+
} | undefined;
|
|
2785
|
+
/**
|
|
2786
|
+
* Parse a request body through a capability's input schema (internal-review).
|
|
2787
|
+
*
|
|
2788
|
+
* Resolution order when a capability declares an `input` schema:
|
|
2789
|
+
* 1. `body.data` is preferred — agents pass structured fields directly
|
|
2790
|
+
* (CLI builds this from `--param k=v`).
|
|
2791
|
+
* 2. Fallback: `JSON.parse(body.input)` for clients still on the legacy
|
|
2792
|
+
* JSON-string contract.
|
|
2793
|
+
* 3. Neither usable → throw `MissingStructuredInputError` so the route can
|
|
2794
|
+
* return `invalid_input` with a hint pointing at `/v1/info`.
|
|
2795
|
+
*
|
|
2796
|
+
* When the capability has no `input` schema, returns `body.input` raw
|
|
2797
|
+
* (primitive path).
|
|
2798
|
+
*
|
|
2799
|
+
* The internal-review credit envelope is removed before the schema runs, on both
|
|
2800
|
+
* branches (internal-review) — it rides the signed body but is protocol-level, so a
|
|
2801
|
+
* top-level `.strict()` capability schema would otherwise reject every
|
|
2802
|
+
* explicit draw as an unknown key, before the envelope was even extracted.
|
|
2803
|
+
* The persisted `record.input` the internal-review reactivation path re-reads is the
|
|
2804
|
+
* pre-Zod wire form, so it comes through the second branch carrying them too.
|
|
2805
|
+
*/
|
|
2806
|
+
parseInput(body: {
|
|
2807
|
+
input?: string;
|
|
2808
|
+
data?: unknown;
|
|
2809
|
+
}, capability: string): unknown;
|
|
2810
|
+
/**
|
|
2811
|
+
* Look up the per-capability descriptor by name (internal-review). Throws when the
|
|
2812
|
+
* name doesn't exist — the route layer validates body.capability first, so
|
|
2813
|
+
* reaching this with an unknown name is a programmer error.
|
|
2814
|
+
*/
|
|
2815
|
+
private requireCapability;
|
|
2816
|
+
/**
|
|
2817
|
+
* Soft capability lookup — `undefined` when the name isn't defined. The
|
|
2818
|
+
* single place the `descriptor.capabilities` index cast lives; callers that
|
|
2819
|
+
* tolerate a missing capability (the route layer pre-validates, or the cap
|
|
2820
|
+
* was deleted from the descriptor after a job was persisted) route through
|
|
2821
|
+
* here instead of re-casting inline.
|
|
2822
|
+
*/
|
|
2823
|
+
private getCapability;
|
|
2824
|
+
/**
|
|
2825
|
+
* Allocate the id the next job will be created under (internal-review). The
|
|
2826
|
+
* submit path calls this BEFORE payment verification so the ledger draw
|
|
2827
|
+
* commits with its job linkage, then passes the id back through
|
|
2828
|
+
* `createJob`'s provenance. Ids allocated for requests whose payment is
|
|
2829
|
+
* refused are simply never used — the sequence has gaps, which nothing
|
|
2830
|
+
* reads meaning into.
|
|
2831
|
+
*/
|
|
2832
|
+
allocateJobId(): string;
|
|
2833
|
+
/**
|
|
2834
|
+
* Create a new job from a request body. `provenance` (internal-review) is what the
|
|
2835
|
+
* idempotent-replay path on `POST /v1/job` reads back: the raw `job_token`
|
|
2836
|
+
* to re-issue, and the fingerprint + caller pubkey a retry must reproduce
|
|
2837
|
+
* to be given it.
|
|
2838
|
+
*/
|
|
2839
|
+
createJob(body: {
|
|
2840
|
+
input: string;
|
|
2841
|
+
params?: Record<string, string>;
|
|
2842
|
+
}, requesterId: string, payment: PaymentInfo, requiredMsats: number | undefined, capability: string, requesterTokenHash?: string, provenance?: {
|
|
2843
|
+
requesterToken?: string;
|
|
2844
|
+
requestFingerprint?: string;
|
|
2845
|
+
requesterPubkey?: string;
|
|
2846
|
+
requestId?: string;
|
|
2847
|
+
authRequestPath?: string;
|
|
2848
|
+
/**
|
|
2849
|
+
* Pre-allocated id from {@link allocateJobId} (internal-review) — the submit
|
|
2850
|
+
* path allocates before payment verification so the ledger draw can
|
|
2851
|
+
* record the job it pays for.
|
|
2852
|
+
*/
|
|
2853
|
+
jobId?: string;
|
|
2854
|
+
}): ServerJob;
|
|
2855
|
+
/** Build an SDKJobContext and attach it to the job. */
|
|
2856
|
+
buildAndAttachContext(job: ServerJob, parsedInput: unknown): SDKJobContext<State, ResolvedInput<InputSchema>>;
|
|
2857
|
+
/** Start the handler for a job. */
|
|
2858
|
+
runHandler(job: ServerJob, sdkCtx: SDKJobContext<unknown, unknown>): void;
|
|
2859
|
+
/** Dispatch a validated incoming message to the appropriate handler or pending resolver. */
|
|
2860
|
+
dispatchMessage(job: ServerJob, type: MessageType, content: unknown): Promise<void>;
|
|
2861
|
+
/**
|
|
2862
|
+
* Persist a job to the backing store.
|
|
2863
|
+
*
|
|
2864
|
+
* Awaits the per-job appender tail (internal-review) so that:
|
|
2865
|
+
* 1. The snapshot's `messages` is consistent with the `job_messages` table
|
|
2866
|
+
* — no row is still in flight at snapshot time.
|
|
2867
|
+
* 2. For terminal saves, the `DELETE FROM job_messages` inside `save()`
|
|
2868
|
+
* can't race a still-pending `appendOutgoing` for the final yield
|
|
2869
|
+
* message (which would otherwise wipe the row before an in-flight SSE
|
|
2870
|
+
* subscriber's NOTIFY-driven fetch can see it).
|
|
2871
|
+
*/
|
|
2872
|
+
persistJob(job: ServerJob): Promise<void>;
|
|
2873
|
+
/**
|
|
2874
|
+
* Cross-machine terminal guard (internal-review). `buildContext`'s guard reads the
|
|
2875
|
+
* in-memory `job.status`, so on its own it only protects the machine running
|
|
2876
|
+
* the handler. On a multi-machine DVM a `DELETE /v1/job/:id` routinely lands
|
|
2877
|
+
* on a machine that isn't running the job — `app.ts` cancels it in the store
|
|
2878
|
+
* and this process learns of it only through the durable row.
|
|
2879
|
+
*
|
|
2880
|
+
* Adopting the durable status blocks the rest of the handler's writes via the
|
|
2881
|
+
* local guard, makes `handleJobTerminal` see a non-completed job so it skips
|
|
2882
|
+
* the revenue report, rejects a suspended handler's prompt/payment yields, and
|
|
2883
|
+
* fires `job.abort` so in-flight `ctx.fetch` calls tear down. Returns true when
|
|
2884
|
+
* the durable state won and the caller must skip its save.
|
|
2885
|
+
*
|
|
2886
|
+
* Two triggers: `watchDurableStatus`'s NOTIFY subscription (internal-review) fires
|
|
2887
|
+
* this within a round-trip of the remote cancel committing, which is what
|
|
2888
|
+
* actually stops the provider spend; `persistJob` calls it again on every save
|
|
2889
|
+
* as the backstop for the window where a cancel commits between a handler's
|
|
2890
|
+
* read and its write (the check-then-write it can still lose is covered by the
|
|
2891
|
+
* stores' terminal-sticky `save`, which keeps the row itself correct).
|
|
2892
|
+
*/
|
|
2893
|
+
private adoptDurableTerminal;
|
|
2894
|
+
/**
|
|
2895
|
+
* Watch the durable status of a locally-active job (internal-review).
|
|
2896
|
+
*
|
|
2897
|
+
* `ctx.signal` is raised by `dispatchMessage`, which only runs on the machine
|
|
2898
|
+
* holding the job in `activeJobs`. On a multi-machine DVM the `DELETE` usually
|
|
2899
|
+
* lands somewhere else, and that machine's store-only cancel (`cancelJob`)
|
|
2900
|
+
* commits the terminal status, appends the `cancel` message, and `pg_notify`s
|
|
2901
|
+
* in one transaction. This subscription is how the handler's machine hears it:
|
|
2902
|
+
* on each notify, re-read the durable status and adopt it if it went terminal.
|
|
2903
|
+
* Without it, the handler only notices at its next store write, and the
|
|
2904
|
+
* in-flight provider call — the spend the cancel was meant to stop — runs to
|
|
2905
|
+
* completion.
|
|
2906
|
+
*
|
|
2907
|
+
* Every locally-emitted message notifies this machine too, so the re-read is
|
|
2908
|
+
* coalesced: at most one `getCounters` in flight per job, and none once the
|
|
2909
|
+
* job is locally terminal.
|
|
2910
|
+
*/
|
|
2911
|
+
private watchDurableStatus;
|
|
2912
|
+
/**
|
|
2913
|
+
* Tear down a job's durable-status subscription (internal-review). Must be called
|
|
2914
|
+
* wherever a job leaves `activeJobs` — a leaked subscriber outlives the job on
|
|
2915
|
+
* the store's shared LISTEN connection. Idempotent.
|
|
2916
|
+
*/
|
|
2917
|
+
unwatchDurableStatus(jobId: string): void;
|
|
2918
|
+
/**
|
|
2919
|
+
* NOTIFY-driven durable-status re-read, coalesced per job (internal-review).
|
|
2920
|
+
*
|
|
2921
|
+
* A notify that arrives while a re-read is in flight is queued rather than
|
|
2922
|
+
* dropped: the in-flight read may have observed the row a moment *before* the
|
|
2923
|
+
* cancel committed, and dropping its notify would put the abort back where
|
|
2924
|
+
* this issue found it — waiting for the handler's next store write.
|
|
2925
|
+
*/
|
|
2926
|
+
private checkDurableTerminal;
|
|
2927
|
+
/**
|
|
2928
|
+
* Wire the Tx A appender when the store supports streaming.
|
|
2929
|
+
*
|
|
2930
|
+
* Tx A (internal-review): outgoing message + `pending_payment_msats` bump land in
|
|
2931
|
+
* the same transaction. The DB allocates the row's seq via
|
|
2932
|
+
* `UPDATE jobs SET next_seq = next_seq + 1 RETURNING` so concurrent inbound
|
|
2933
|
+
* traffic on different machines can never collide on the `(job_id, seq)` PK.
|
|
2934
|
+
* In-memory `job.seq` keeps its own counter for same-machine SSE listeners.
|
|
2935
|
+
*
|
|
2936
|
+
* Per-job serialisation (internal-review): chained through `job.messageAppenderTail`
|
|
2937
|
+
* so two synchronous `providerMessage` calls (e.g. `artifact` followed by
|
|
2938
|
+
* `complete`) commit their `appendOutgoing` transactions in call order.
|
|
2939
|
+
* Without this, the two transactions race for the `jobs` row lock and the
|
|
2940
|
+
* DB-side seq can be allocated in the opposite order — which both renumbers
|
|
2941
|
+
* the persisted messages and reorders the NOTIFY events feeding the
|
|
2942
|
+
* cross-machine SSE subscriber. The subscriber closes the stream on the
|
|
2943
|
+
* first yield message it sees, so a NOTIFY for `complete` arriving before
|
|
2944
|
+
* `artifact`'s NOTIFY silently drops the artifact.
|
|
2945
|
+
*/
|
|
2946
|
+
private wireMessageAppender;
|
|
2947
|
+
/** Start (or restart) the idle timer for a job. */
|
|
2948
|
+
startIdleTimer(job: ServerJob): void;
|
|
2949
|
+
/** Clear the idle timer for a job. */
|
|
2950
|
+
clearIdleTimer(id: string): void;
|
|
2951
|
+
/** Cancel a job due to idle timeout. Exposed for the reactivation path's idle-expiry guard (internal-review). */
|
|
2952
|
+
cancelJobIdle(job: ServerJob): Promise<void>;
|
|
2953
|
+
/**
|
|
2954
|
+
* Force-terminate stale non-terminal jobs. Two status-aware arms:
|
|
2955
|
+
* • `awaiting-input` past `staleJobTimeoutMs` → `cancelled`
|
|
2956
|
+
* (`stale_no_terminal_status`) — the caller never paid / responded.
|
|
2957
|
+
* • `processing`/`working` past `processingWatchdogMs` → `failed`
|
|
2958
|
+
* (`worker_died_mid_job`) — the worker died mid-job. The heartbeat keeps
|
|
2959
|
+
* live workers fresh, so a stale row here means a dead process.
|
|
2960
|
+
*
|
|
2961
|
+
* Fires on a timer (wired in the constructor) and also callable on demand
|
|
2962
|
+
* (tests, ops). The CAS in `cancelStaleJob` makes this safe to run
|
|
2963
|
+
* concurrently from multiple DVM machines. Scoped to capabilities this
|
|
2964
|
+
* JobManager owns so multi-mount hosts don't steal stuck rows from sibling
|
|
2965
|
+
* descriptors — the local activeJobs teardown only makes sense on the
|
|
2966
|
+
* JobManager that actually hosted the zombie handler.
|
|
2967
|
+
*
|
|
2968
|
+
* **Both cutoffs float off the wall clock (internal-review).** Each is one
|
|
2969
|
+
* `Date.now()` sample compared against `last_activity_at`, stamped from a
|
|
2970
|
+
* different sample at a different moment, so a backwards step between the
|
|
2971
|
+
* two makes every row look more recently active than it is: the query
|
|
2972
|
+
* matches nothing and both arms go silent for the length of the skew. The
|
|
2973
|
+
* dead worker's job keeps its `processing` status, its credit hold stays
|
|
2974
|
+
* `pending`, and the internal-review paid-job-death alert — wired to this reaper —
|
|
2975
|
+
* never fires. Same defect, same host class, as internal-review's orphan sweep.
|
|
2976
|
+
*
|
|
2977
|
+
* **The compensation is capped** — see {@link staleSweepCompensationCapMs}
|
|
2978
|
+
* for where each arm's cap comes from — which internal-review did not need to do. Its eagerness costs at worst an early release
|
|
2979
|
+
* of an unclaimed hold, refused outright for any live job row; ours
|
|
2980
|
+
* force-*fails* a running job. `cancelStaleJob`'s CAS does not cover that:
|
|
2981
|
+
* `expectedActivityBefore` is the same compensated threshold the query used,
|
|
2982
|
+
* and a live worker's post-step heartbeat writes the low, post-step
|
|
2983
|
+
* `Date.now()`, so `last_activity_at <= expectedActivityBefore` still holds.
|
|
2984
|
+
* The CAS guards a job checking in *between* the find and the claim with a
|
|
2985
|
+
* stamp above the cutoff — a race, not a clock.
|
|
2986
|
+
*
|
|
2987
|
+
* **Some blindness is inherent, and no cap choice removes it.** A backwards
|
|
2988
|
+
* step of `S` inverts the stamp ordering: everything stamped after it reads
|
|
2989
|
+
* `S` older than everything stamped before. A live worker heartbeating right
|
|
2990
|
+
* now stamps `wall - S`; a job genuinely idle for `age` stamps `wall - age`.
|
|
2991
|
+
* The stale row is the lower of the two — the one a cutoff reaches first —
|
|
2992
|
+
* only once `age` exceeds `S`. Below that the reaper cannot tell them apart,
|
|
2993
|
+
* so any cutoff that claimed the stale job would force-fail every live
|
|
2994
|
+
* worker on the host with it. The cap picks where on that curve to sit: on
|
|
2995
|
+
* the defaults a row must have been silent for `S + window / 2`, and a live
|
|
2996
|
+
* worker keeps five missed heartbeats of margin. The
|
|
2997
|
+
* alternative to a missed reap is mild and self-healing — the orphan sweep
|
|
2998
|
+
* backstops the hold, and the arm recovers when the wall catches up. The
|
|
2999
|
+
* alternative to a false reap is an irreversible force-fail of paid work.
|
|
3000
|
+
*
|
|
3001
|
+
* The stamps themselves stay wall-clock epoch-ms on purpose — they are
|
|
3002
|
+
* written by whichever machine touched the job and read by every other one,
|
|
3003
|
+
* so only the reading side can float.
|
|
3004
|
+
*/
|
|
3005
|
+
sweepStaleJobs(): Promise<{
|
|
3006
|
+
swept: number;
|
|
3007
|
+
}>;
|
|
3008
|
+
/**
|
|
3009
|
+
* How far one watchdog arm may lean on {@link monotonicNowMs} past the wall
|
|
3010
|
+
* clock to cover a backwards step (internal-review). The compensation is capped,
|
|
3011
|
+
* not free: a claimed row must still have been silent for
|
|
3012
|
+
* `windowMs - cap`, and unlike the orphan sweep, claiming eagerly here
|
|
3013
|
+
* force-fails a job that may well be alive.
|
|
3014
|
+
*
|
|
3015
|
+
* **Half the window** is the floor that governs a wide window — five missed
|
|
3016
|
+
* heartbeats on the processing defaults. On the `awaiting-input` arm it also
|
|
3017
|
+
* preserves a real invariant: with the derived
|
|
3018
|
+
* `2 x idleTimeout + headroom`, half is `idleTimeout + 45s`, so the sweeper
|
|
3019
|
+
* still cannot fire before the in-process idle timer would have. (A builder
|
|
3020
|
+
* who overrides `staleJobTimeoutMs` below `2 x idleTimeout` has already
|
|
3021
|
+
* opted out of that, guard or no guard.)
|
|
3022
|
+
*
|
|
3023
|
+
* **Two heartbeat intervals** is the floor that governs a window configured
|
|
3024
|
+
* close to the beat cadence — `processingWatchdog: 40` against the 30s
|
|
3025
|
+
* default beat, where half the window is less than one beat and a worker
|
|
3026
|
+
* heartbeating exactly on schedule would be reaped the moment the host's
|
|
3027
|
+
* clock stepped. That pair is marginal already; the guard must not make it
|
|
3028
|
+
* deterministic. The cap collapses to zero there, which is today's
|
|
3029
|
+
* behaviour: late, never wrong. The `awaiting-input` arm takes no such term
|
|
3030
|
+
* — nothing heartbeats it by design, and its refresh is an inbound caller
|
|
3031
|
+
* message on no cadence at all.
|
|
3032
|
+
*
|
|
3033
|
+
* **Why a magnitude and not a predicate.** The tempting sharper rule is to
|
|
3034
|
+
* compensate per row, fully, for any stamp that reads ahead of our wall —
|
|
3035
|
+
* one our own post-step heartbeat could not have written. It is unsafe: a
|
|
3036
|
+
* future-stamped row is equally what a live job heartbeated by a peer whose
|
|
3037
|
+
* clock runs fast looks like, and from the stamp alone the two are
|
|
3038
|
+
* indistinguishable. The same reasoning bounds the cap from the other side —
|
|
3039
|
+
* while our clock is stepped, the guard force-fails the live jobs of any
|
|
3040
|
+
* peer slower than `windowMs - cap`, which is 2.5 minutes of tolerated
|
|
3041
|
+
* disagreement on the defaults, far outside anything NTP-managed hosts show.
|
|
3042
|
+
*/
|
|
3043
|
+
private staleSweepCompensationCapMs;
|
|
3044
|
+
/**
|
|
3045
|
+
* Resolve `pending` credit draws the terminal funnel can no longer reach.
|
|
3046
|
+
* Two arms over one scan, distinguished by whether the draw's job row exists.
|
|
3047
|
+
*
|
|
3048
|
+
* **No job row (internal-review) — release.** internal-review allocates the job id and
|
|
3049
|
+
* commits the ledger draw, with its `job_id` linkage, before `recordReplay`
|
|
3050
|
+
* and `persistJob` write the job row. A crash, a `draw_conflict` 409, or a
|
|
3051
|
+
* fail-closed `replay_detected` 401 in that window leaves a committed hold
|
|
3052
|
+
* that **no** terminal path can ever reach: `issueReceipt` →
|
|
3053
|
+
* `resolveCreditDraw` keys off the `creditId`/`drawId` persisted on the job
|
|
3054
|
+
* row, so with no row there is no release path at all and the caller's
|
|
3055
|
+
* available balance stays reduced forever. Cosmetic for an implicit N=1
|
|
3056
|
+
* credit, a silent balance shrink for an explicit N>1 one.
|
|
3057
|
+
*
|
|
3058
|
+
* **Terminal job row (internal-review) — reconcile to its outcome.** The funnel
|
|
3059
|
+
* demonstrably fails: `resolveCreditDraw` throws on a ledger/DB error,
|
|
3060
|
+
* `issueReceipt` catches it, logs `credit_resolve_failed` and returns
|
|
3061
|
+
* nothing, and until now nothing retried. Same stranded hold, reached through
|
|
3062
|
+
* a different door — and for a `completed` job it strands the platform's
|
|
3063
|
+
* books too, since draws *are* the revenue events (internal-review): the caller is
|
|
3064
|
+
* charged nothing, the balance never moves, and outstanding liability
|
|
3065
|
+
* (`deposits − draw revenue`) overstates forever. So a late settle books, off
|
|
3066
|
+
* the committed draw row, through the ordinary reporter path.
|
|
3067
|
+
*
|
|
3068
|
+
* **A non-terminal job row is still never touched** — the stale-job sweep
|
|
3069
|
+
* above drives those to terminal first, and its `issueReceiptForStoredJob`
|
|
3070
|
+
* resolves the draw on the way. Those skipped holds are why the scan
|
|
3071
|
+
* keyset-paginates rather than re-reading one batch: they stay `pending`
|
|
3072
|
+
* indefinitely (an `awaiting-input` job on a 30-minute idle timeout sits well
|
|
3073
|
+
* past the orphan cutoff), so a fixed first page of them would starve every
|
|
3074
|
+
* real orphan behind it, on every tick, forever. The cursor advances over
|
|
3075
|
+
* every row examined; a tick that hits {@link ORPHAN_DRAW_SWEEP_MAX_PAGES}
|
|
3076
|
+
* logs `orphan_draw_sweep_truncated` and the next one resumes where it
|
|
3077
|
+
* stopped, so progress is bounded per tick but never blocked.
|
|
3078
|
+
*
|
|
3079
|
+
* Fires on a timer (wired in the constructor) and also callable on demand
|
|
3080
|
+
* (tests, ops). Both `settle` and `release` are idempotent under the credit
|
|
3081
|
+
* row's `FOR UPDATE`, so concurrent sweepers on several machines — and a
|
|
3082
|
+
* sweeper racing the in-line funnel — resolve the hold exactly once; the
|
|
3083
|
+
* losers see `replayed: true` and book nothing.
|
|
3084
|
+
*/
|
|
3085
|
+
sweepOrphanDraws(): Promise<{
|
|
3086
|
+
released: number;
|
|
3087
|
+
reconciled: number;
|
|
3088
|
+
}>;
|
|
3089
|
+
/**
|
|
3090
|
+
* The end-of-scan line for a tick that resolved nothing (internal-review, extended
|
|
3091
|
+
* by internal-review). Two shapes are worth a warning, and neither is the ordinary
|
|
3092
|
+
* one — the timer runs every 5 minutes per ledger in production.
|
|
3093
|
+
*
|
|
3094
|
+
* `examined > 0`: aged candidates were looked at and none moved. Usually
|
|
3095
|
+
* legitimate (their jobs are still running, or terminal inside the reconcile
|
|
3096
|
+
* grace period), but it is also what a silently skipped hold looks like.
|
|
3097
|
+
*
|
|
3098
|
+
* `examined === 0`: the query returned nothing. Ordinarily that means there
|
|
3099
|
+
* is nothing to do — but it is equally what a blinded scan looks like, which
|
|
3100
|
+
* is the case the internal-review gate could not reach. Probe for the oldest
|
|
3101
|
+
* `pending` hold at any age and speak up when one exists the scan should
|
|
3102
|
+
* have seen and didn't: already past the age gate outright, stamped ahead of
|
|
3103
|
+
* our clock (some other process's clock is fast), or hidden while our own
|
|
3104
|
+
* clock is held forward over a backwards step. A ledger holding nothing but
|
|
3105
|
+
* young draws stays quiet.
|
|
3106
|
+
*
|
|
3107
|
+
* The probe is diagnostics, so it is wrapped: a tick that swept cleanly must
|
|
3108
|
+
* never report failure because the extra read fell over. A probe that throws
|
|
3109
|
+
* says so on the same line rather than replacing the tick's outcome.
|
|
3110
|
+
*/
|
|
3111
|
+
private reportSweepOutcome;
|
|
3112
|
+
/**
|
|
3113
|
+
* Resolve one aged `pending` draw whose job row reached terminal (internal-review),
|
|
3114
|
+
* mirroring what `resolveCreditDraw` would have done in line. Returns true
|
|
3115
|
+
* when this call is the one that moved the draw — the caller counts it, and
|
|
3116
|
+
* only it books.
|
|
3117
|
+
*
|
|
3118
|
+
* **The settle is gated on the job row naming _this_ draw.** The scan finds
|
|
3119
|
+
* the draw by `credit_draws.job_id`, but the job's own `credit_id`/`draw_id`
|
|
3120
|
+
* are written by a *later* transaction than the one that committed the draw
|
|
3121
|
+
* (`verifyIncomingPayment` commits fund+draw; `verifyAndCredit` binds the
|
|
3122
|
+
* row), and a free-then-paid job binds set-if-null. A store failure or a
|
|
3123
|
+
* second concurrent top-up therefore leaves a hold tagged with the job whose
|
|
3124
|
+
* row points somewhere else, or nowhere. Settling that would debit the caller
|
|
3125
|
+
* a second time for one job — and invisibly, since `revenue_events`'
|
|
3126
|
+
* `UNIQUE (dvm_id, job_id, rail)` would drop the booking. So an unbound hold
|
|
3127
|
+
* is **released**, never settled: the job's payment was the draw it is bound
|
|
3128
|
+
* to, and this one bought nothing.
|
|
3129
|
+
*
|
|
3130
|
+
* Concurrency needs no guard of its own. `settle`/`release` run under the
|
|
3131
|
+
* credit row's `FOR UPDATE`, so exactly one caller — a sibling reconciler,
|
|
3132
|
+
* or the in-line funnel arriving late — sees `replayed: false`, which makes
|
|
3133
|
+
* the booking gate a true mutex rather than a hopeful one. The one race left
|
|
3134
|
+
* open is another machine's `handleJobTerminal` stalled between resolving the
|
|
3135
|
+
* draw and booking it, which would double-report; the platform's
|
|
3136
|
+
* `(dvm_id, credit_id, draw_id)` and `(dvm_id, job_id, rail)` unique indexes
|
|
3137
|
+
* absorb that.
|
|
3138
|
+
*
|
|
3139
|
+
* `invalid_draw_state` — the funnel having already resolved the draw to the
|
|
3140
|
+
* other state — is the expected loss, logged and swallowed like the orphan
|
|
3141
|
+
* arm's, so one contended hold can't strand the rest of the batch.
|
|
3142
|
+
*/
|
|
3143
|
+
private reconcileTerminalDraw;
|
|
3144
|
+
/**
|
|
3145
|
+
* Refresh `lastActivityAt` for jobs this process is actively running
|
|
3146
|
+
* (`processing`/`working`) so the processing watchdog only fires once the
|
|
3147
|
+
* worker is genuinely dead (internal-review). `awaiting-input` jobs are
|
|
3148
|
+
* deliberately excluded — their idle timeout must still elapse. Fires on the
|
|
3149
|
+
* heartbeat timer; also callable on demand for tests.
|
|
3150
|
+
*/
|
|
3151
|
+
heartbeatActiveJobs(): Promise<{
|
|
3152
|
+
beat: number;
|
|
3153
|
+
}>;
|
|
3154
|
+
/**
|
|
3155
|
+
* Single-execution claim for a reactivating machine (internal-review). Before
|
|
3156
|
+
* replaying an `awaiting-input` job, wait a bounded grace window for a live
|
|
3157
|
+
* original handler to win the `awaiting-input → processing` CAS via its
|
|
3158
|
+
* NOTIFY wake. Resolves:
|
|
3159
|
+
* • `false` — the row left `awaiting-input` during the window (the original
|
|
3160
|
+
* handler claimed it / drove it terminal). Stand down; do not replay.
|
|
3161
|
+
* • `true` — the window elapsed still `awaiting-input` (the original worker
|
|
3162
|
+
* is gone or has no live resolver) AND this machine won the claim CAS.
|
|
3163
|
+
* Replay for dead-worker recovery.
|
|
3164
|
+
*
|
|
3165
|
+
* Biasing the original handler to win kills the double execution (double
|
|
3166
|
+
* substrate spend, clobbered artifact) without heartbeating `awaiting-input`
|
|
3167
|
+
* (which would break the caller-input idle timeout). Non-streamable stores
|
|
3168
|
+
* have no cross-machine race, so the claim is a no-op `true`.
|
|
3169
|
+
*/
|
|
3170
|
+
claimAwaitingInputForReactivation(jobId: string): Promise<boolean>;
|
|
3171
|
+
/**
|
|
3172
|
+
* Record an inbound client message durably via Tx B (internal-review) and append
|
|
3173
|
+
* it to the in-memory `job.messages` array. Returns the DB-allocated seq
|
|
3174
|
+
* (`null` when the store isn't streamable — non-Postgres test fallback).
|
|
3175
|
+
*
|
|
3176
|
+
* Status starts as `pending-verification`; a follow-up Tx C call
|
|
3177
|
+
* (`processPayment` for payments, `markInboundVerified` for everything
|
|
3178
|
+
* else) transitions it to `verified` once the inbound is accepted.
|
|
3179
|
+
*/
|
|
3180
|
+
recordInboundPending(job: ServerJob, msgType: MessageType, msgContent: Record<string, unknown>): Promise<number | null>;
|
|
3181
|
+
/**
|
|
3182
|
+
* Tx C complement for non-payment inbound messages — flip the row to
|
|
3183
|
+
* `verified` so cross-machine readers see it via `subscribeMessages`.
|
|
3184
|
+
*/
|
|
3185
|
+
markInboundVerified(jobId: string, inboundSeq: number | null): Promise<void>;
|
|
3186
|
+
/**
|
|
3187
|
+
* Reactivate a suspended/event-driven job from the store. Builds the
|
|
3188
|
+
* in-memory `ServerJob`, wires the appender, builds the context, starts
|
|
3189
|
+
* the idle timer. The caller has already recorded the inbound via Tx B
|
|
3190
|
+
* and run Tx C (verify + credit); reactivation is now decoupled from
|
|
3191
|
+
* credit (internal-review, internal-review). The caller decides whether to run the
|
|
3192
|
+
* handler (Pattern A: status was `awaiting-input`) or to dispatch the
|
|
3193
|
+
* message to a custom handler (Pattern B).
|
|
3194
|
+
*
|
|
3195
|
+
* For DVMs that declare descriptor-level auth, the persisted
|
|
3196
|
+
* `record.input` carries the original signed envelope. We re-verify the
|
|
3197
|
+
* signature here (internal-review) so a DB-layer tamper — compromised admin, SQL
|
|
3198
|
+
* injection, malicious operator with DB access — can't silently feed an
|
|
3199
|
+
* attacker-supplied pubkey/envelope into the handler. The drift window
|
|
3200
|
+
* and replay store are deliberately skipped: the persisted timestamp is
|
|
3201
|
+
* from the original signing instant (long past the 5-min drift bound for
|
|
3202
|
+
* any long-running job), and the nonce was already committed at submission.
|
|
3203
|
+
* On failure: throws `SignedRequestError` ({@link AuthSchemaError} when
|
|
3204
|
+
* the capability has no input schema to verify against); the caller is
|
|
3205
|
+
* expected to mark the row failed and surface a 401 / 500 to the inbound
|
|
3206
|
+
* caller.
|
|
3207
|
+
*
|
|
3208
|
+
* What gets re-verified is the persisted row read back through
|
|
3209
|
+
* {@link signedRequestInput} — `app.ts` stores the pre-Zod wire form, so
|
|
3210
|
+
* those are the caller's own signed bytes, the same ones `/v1/quote` and the
|
|
3211
|
+
* submit checked (internal-review). That makes the tamper check strictly stronger
|
|
3212
|
+
* than the parsed form it replaced: an injected key the capability schema
|
|
3213
|
+
* doesn't declare used to be stripped before the signature was checked, so
|
|
3214
|
+
* it re-verified clean.
|
|
3215
|
+
*/
|
|
3216
|
+
reactivateJob(record: JobRecord): {
|
|
3217
|
+
job: ServerJob;
|
|
3218
|
+
sdkCtx: SDKJobContext<unknown, unknown>;
|
|
3219
|
+
};
|
|
3220
|
+
/**
|
|
3221
|
+
* Verify an inbound payment and apply the credit via Tx C (internal-review).
|
|
3222
|
+
* The inbound message must have been recorded via Tx B already
|
|
3223
|
+
* (`recordInboundPending` returned `inboundSeq`). For non-streamable
|
|
3224
|
+
* stores (`inboundSeq === null`), falls back to in-memory mutation via
|
|
3225
|
+
* `applyPaymentInfoToJob`.
|
|
3226
|
+
*
|
|
3227
|
+
* Dev-mode short-circuit: auto-credits the outstanding
|
|
3228
|
+
* `pendingPaymentMsats` without external verification. Gated on
|
|
3229
|
+
* `devModeSkipsPaymentVerification` — the same predicate the upfront path
|
|
3230
|
+
* reads, so a dev server wired to a mint verifies mid-job payments for real
|
|
3231
|
+
* (internal-review) — plus the explicit `dev_auto` opt-out the dev console uses,
|
|
3232
|
+
* which is the one caller that has no wallet to pay from. `dev_auto` loses to
|
|
3233
|
+
* any proof riding the same message: money on the wire always takes the rail,
|
|
3234
|
+
* so the flag can never leave a real token unspent against a credited job.
|
|
3235
|
+
*
|
|
3236
|
+
* Tx C is not atomic with the rail commit (internal-review). `verifyIncomingPayment`
|
|
3237
|
+
* commits the proofs *and* the ledger leg in one transaction and returns;
|
|
3238
|
+
* only then does `verifyAndCredit` write the job row, on its own connection.
|
|
3239
|
+
* Everything after that call therefore runs with the caller's money already
|
|
3240
|
+
* moved, which is why the write is retried rather than left to throw, and why
|
|
3241
|
+
* an exhausted retry answers with the credit it landed on instead of a bare
|
|
3242
|
+
* 500. See `creditInbound`.
|
|
3243
|
+
*/
|
|
3244
|
+
processPayment(job: ServerJob, body: {
|
|
3245
|
+
type: MessageType;
|
|
3246
|
+
content: Record<string, unknown>;
|
|
3247
|
+
}, inboundSeq: number | null, extra?: {
|
|
3248
|
+
lockPubkeys?: LockPubkey[];
|
|
3249
|
+
mintHealthTracker?: MintHealthTracker;
|
|
3250
|
+
}): Promise<ProcessPaymentOutcome>;
|
|
3251
|
+
/**
|
|
3252
|
+
* Tx C with a bounded retry (internal-review).
|
|
3253
|
+
*
|
|
3254
|
+
* By the time this runs on the verified path, the rail commit and the ledger
|
|
3255
|
+
* leg have already committed in a transaction this call is not part of — so a
|
|
3256
|
+
* throw here is money moved against a job row that records none of it, and
|
|
3257
|
+
* letting it propagate was the bug. A transient store error is the ordinary
|
|
3258
|
+
* cause and a second attempt clears it.
|
|
3259
|
+
*
|
|
3260
|
+
* The retry is safe by construction, not by convention: `verifyAndCredit`
|
|
3261
|
+
* CASes on `job_messages.status = 'pending-verification'` inside its own
|
|
3262
|
+
* transaction, so a call whose COMMIT actually landed before the ack was lost
|
|
3263
|
+
* finds the row already `verified` and returns `alreadyVerified: true`
|
|
3264
|
+
* carrying the counters and binding that first attempt committed. Re-applying
|
|
3265
|
+
* the delta is impossible either way.
|
|
3266
|
+
*
|
|
3267
|
+
* That argument covers every attempt but the last, whose error nothing
|
|
3268
|
+
* re-checks — and a connection-level fault is exactly the shape that loses
|
|
3269
|
+
* the ack on *all* of them, idempotent replays included (the no-op path
|
|
3270
|
+
* COMMITs too). So the exhausted path asks the row before reporting a
|
|
3271
|
+
* failure: `getVerifiedInbound` reads, transaction-free, whether a Tx C for
|
|
3272
|
+
* this seq committed. Without it a fully credited, fully bound job answers
|
|
3273
|
+
* `payment_unapplied` and invites a retry, and a caller who takes it pays
|
|
3274
|
+
* twice for one ask — the second payment growing the now-bound draw and
|
|
3275
|
+
* settling as revenue.
|
|
3276
|
+
*
|
|
3277
|
+
* The read runs on the pool that just failed those COMMITs, so it is retried
|
|
3278
|
+
* too, and a read that fails all the way through is reported as *unknown*
|
|
3279
|
+
* rather than as nothing: `CreditInboundExhaustedError.readError` is what
|
|
3280
|
+
* separates "checked the row, nothing landed" from "couldn't check", both on
|
|
3281
|
+
* the operator's log line and in the copy the caller acts on.
|
|
3282
|
+
*/
|
|
3283
|
+
private creditInbound;
|
|
3284
|
+
/**
|
|
3285
|
+
* Answer a payment whose rail and ledger legs committed but whose job row
|
|
3286
|
+
* could not be written (internal-review) — every retry spent, the money real.
|
|
3287
|
+
*
|
|
3288
|
+
* The refusal names the credit the money landed on so the caller can act on
|
|
3289
|
+
* it directly, mirroring the short mid-job pay's 402 (`payment.ts`). It is a
|
|
3290
|
+
* 500 rather than a 402 on purpose: the payment was valid and was accepted,
|
|
3291
|
+
* so telling the caller it was insufficient would be a lie, and this is a
|
|
3292
|
+
* fault an operator should see in their 5xx rate. `retryable` is true because
|
|
3293
|
+
* the ask is genuinely still outstanding.
|
|
3294
|
+
*
|
|
3295
|
+
* Nothing is released here. The hold is left to the internal-review reconciler,
|
|
3296
|
+
* which makes the same decision — unbound draw ⇒ release, never settle — but
|
|
3297
|
+
* under the credit row's `FOR UPDATE`, after the job is terminal, where it
|
|
3298
|
+
* cannot race an in-flight COMMIT. Releasing from here would also be
|
|
3299
|
+
* irreversible: a released draw can be neither re-grown nor re-opened, so one
|
|
3300
|
+
* bad call would silently de-ledger every later top-up on the job.
|
|
3301
|
+
*
|
|
3302
|
+
* Whether the row was *read* is load-bearing on both surfaces. When the
|
|
3303
|
+
* post-exhaustion read-back failed as well, `credit_job_row_unwritten` says so
|
|
3304
|
+
* (`row_checked: false` plus the read's own error) instead of implying an
|
|
3305
|
+
* operator can trust the row is clean, and the caller gets the `"unconfirmed"`
|
|
3306
|
+
* copy rather than a flat "pay it again" it could be charged twice for.
|
|
3307
|
+
*
|
|
3308
|
+
* That copy overrides `display` as well as `hint`, on every branch including
|
|
3309
|
+
* the ledger-less one. `display` is the sentence an agent relays to its human
|
|
3310
|
+
* (repo convention: JSON carries the words, not just the facts), so a body
|
|
3311
|
+
* whose `hint` says the outcome is unknown while its `display` still asserts
|
|
3312
|
+
* the ask is outstanding is read as "pay again" by the audience that acts on
|
|
3313
|
+
* it — and a payment that did land is charged on top.
|
|
3314
|
+
*/
|
|
3315
|
+
private reportUnappliedPayment;
|
|
3316
|
+
/**
|
|
3317
|
+
* Describe a deposit the job row never took, in the shape both the 201 and
|
|
3318
|
+
* the `payment_unapplied` 500 carry (internal-review). `undefined` when the payment
|
|
3319
|
+
* ran ledger-less (no credit ledger wired, or the top-up preflight skipped) —
|
|
3320
|
+
* there is no credit to name, so the 201 carries no `credit` block and the
|
|
3321
|
+
* 500 takes its words from `unappliedCopy` directly.
|
|
3322
|
+
*/
|
|
3323
|
+
private unappliedCredit;
|
|
3324
|
+
/** Book a mid-job top-up's funding as a deposit (internal-review, spec §10). */
|
|
3325
|
+
private reportCreditDeposit;
|
|
3326
|
+
/**
|
|
3327
|
+
* Internal: build the BuildContextOpts shared between `createJob` and
|
|
3328
|
+
* `reactivateJob`. Centralised so persistence/terminal callbacks and the
|
|
3329
|
+
* internal-review NOTIFY-driven cross-machine wake stay in one place.
|
|
3330
|
+
*/
|
|
3331
|
+
private buildContextOpts;
|
|
3332
|
+
/**
|
|
3333
|
+
* Internal: what unit `ctx.requestPayment`'s auto-credit gate may measure this
|
|
3334
|
+
* job's remaining pool in (internal-review).
|
|
3335
|
+
*
|
|
3336
|
+
* The regime is read off the DRAW, never off the job. Implicit N=1 is exactly
|
|
3337
|
+
* `credit_id === imp:<rail>:<draw_id>`, because `fundAndDraw` derives both
|
|
3338
|
+
* from the same rail payment id — a fact about a row that was written once
|
|
3339
|
+
* and never moves. The tempting alternative, comparing `job.payment_tx_hash`
|
|
3340
|
+
* against `drawSettlementRef` (what the mid-job preflight does to inherit
|
|
3341
|
+
* `drawBasis`), silently rots: Tx C overwrites that column on every mid-job
|
|
3342
|
+
* credit, and a payment whose ledger leg was skipped — a cross-rail top-up,
|
|
3343
|
+
* an fx outage, a draw already resolved — leaves the rail's own reference
|
|
3344
|
+
* there, so an explicit-draw job would read as implicit from then on.
|
|
3345
|
+
*
|
|
3346
|
+
* Never throws, and never answers `rail` on a guess. `rail` is claimed only
|
|
3347
|
+
* where `withDrawBasis` demonstrably left `PaymentInfo.paidMsats` alone — no
|
|
3348
|
+
* draw at all, or a draw carrying no rail value to overlay it with. A job that
|
|
3349
|
+
* drew and whose pool this can't read or denominate answers `unpriced`, which
|
|
3350
|
+
* the gate declines to auto-credit from: the msat figure it would otherwise
|
|
3351
|
+
* fall back on is a slice of the credit's rail value at a ratio pinned
|
|
3352
|
+
* whenever that credit was funded, which is the exact figure this issue exists
|
|
3353
|
+
* to stop spending against.
|
|
3354
|
+
*/
|
|
3355
|
+
private resolveDrawPool;
|
|
3356
|
+
/** Internal: structured warn for the one path that can't price a drawn job's pool. */
|
|
3357
|
+
private warnDrawPool;
|
|
3358
|
+
/**
|
|
3359
|
+
* Issue the signed receipt for a job that has reached a terminal status
|
|
3360
|
+
* (internal-review). Idempotent and safe to call from every terminal path — the
|
|
3361
|
+
* store owns both the sequence allocation and the write-once persist, so
|
|
3362
|
+
* two machines racing on the same job converge on identical bytes and burn
|
|
3363
|
+
* exactly one sequence number.
|
|
3364
|
+
*
|
|
3365
|
+
* Never throws. A receipt is evidence about a job, not part of delivering
|
|
3366
|
+
* it: if signing or persistence fails we log structured and serve the
|
|
3367
|
+
* response without one (same posture as the `cashu_refund_failed` branch in
|
|
3368
|
+
* `context.ts`). The gap is visible to callers — the `seq` series skips a
|
|
3369
|
+
* number — which is precisely the completeness signal receipts exist for.
|
|
3370
|
+
*/
|
|
3371
|
+
issueReceipt(record: JobRecord): Promise<JobReceipt | undefined>;
|
|
3372
|
+
/** Build the common settle/release args, adding the hosted release outbox when wired. */
|
|
3373
|
+
private drawResolutionArgs;
|
|
3374
|
+
/**
|
|
3375
|
+
* Settle or release a terminal job's credit draw (internal-review): `completed`
|
|
3376
|
+
* settles (the hold becomes a real debit); `failed`/`cancelled` releases
|
|
3377
|
+
* (the hold evaporates — "no debit on job failure", mechanically
|
|
3378
|
+
* superseding the internal-review/974 no-op refund for credit-paid jobs, including
|
|
3379
|
+
* the `ctx.fail` path and the stale-sweeper reap). Idempotent — a replayed
|
|
3380
|
+
* resolution returns the recorded state. Returns the `ReceiptCredit` block
|
|
3381
|
+
* for the receipt: `balance_after` is the recorded draw trajectory for a
|
|
3382
|
+
* settled draw, and the restored available balance for a released one.
|
|
3383
|
+
*/
|
|
3384
|
+
private resolveCreditDraw;
|
|
3385
|
+
/**
|
|
3386
|
+
* The one place a terminal job's status becomes a ledger verb (internal-review):
|
|
3387
|
+
* `completed` settles the hold into a real debit, `failed`/`cancelled`
|
|
3388
|
+
* release it. Read by the in-line funnel (`resolveCreditDraw`) and by the
|
|
3389
|
+
* late reconciler (`reconcileTerminalDraw`) — two paths that must never
|
|
3390
|
+
* disagree about what an outcome means for the caller's money.
|
|
3391
|
+
*/
|
|
3392
|
+
private settlesDraw;
|
|
3393
|
+
/**
|
|
3394
|
+
* Sign a locally-held terminal job's receipt and mirror it onto the
|
|
3395
|
+
* in-memory job, so the read paths served out of `activeJobs` during the
|
|
3396
|
+
* cleanup window hand back the same bytes as a cross-machine re-read.
|
|
3397
|
+
*/
|
|
3398
|
+
private attachReceipt;
|
|
3399
|
+
/**
|
|
3400
|
+
* Read-path repair for a terminal job carrying no receipt (internal-review), for
|
|
3401
|
+
* either an in-memory job or a store record. Free in the steady state — it
|
|
3402
|
+
* returns on the `receipt` check without touching the store — so it costs
|
|
3403
|
+
* only on the cases it exists for:
|
|
3404
|
+
*
|
|
3405
|
+
* - **A crash between the two store calls.** `claimReceiptSeq` commits the
|
|
3406
|
+
* sequence number to the job row before `saveReceipt` writes the bytes; a
|
|
3407
|
+
* process death in that window would otherwise strand that number
|
|
3408
|
+
* forever, and a permanent gap is indistinguishable from the deliberate
|
|
3409
|
+
* suppression `seq` exists to expose. Re-issuing here reuses the already
|
|
3410
|
+
* committed number (the claim is idempotent per job) rather than
|
|
3411
|
+
* allocating a second one.
|
|
3412
|
+
* - **A transient store failure** at the terminal: `issueReceipt` logs
|
|
3413
|
+
* `receipt_issue_failed` and returns nothing rather than failing the job,
|
|
3414
|
+
* so the next read retries.
|
|
3415
|
+
* - **Jobs that terminated before the DVM had a receipt key.** They pick one
|
|
3416
|
+
* up on first read, with `issued_at` reflecting when it was signed.
|
|
3417
|
+
*/
|
|
3418
|
+
ensureReceipt(target: ServerJob | JobRecord): Promise<void>;
|
|
3419
|
+
/**
|
|
3420
|
+
* Close out a terminal job whose caller hasn't already persisted it: sign
|
|
3421
|
+
* the receipt, then save the snapshot. The snapshot save never writes the
|
|
3422
|
+
* receipt column, so ordering only affects how soon a reader sees the
|
|
3423
|
+
* receipt — this way a caller polling immediately after the terminal
|
|
3424
|
+
* already finds it.
|
|
3425
|
+
*/
|
|
3426
|
+
private finalizeTerminal;
|
|
3427
|
+
/**
|
|
3428
|
+
* Issue a receipt for a job this process doesn't hold in `activeJobs` — a
|
|
3429
|
+
* cross-machine cancel, or a row the stale sweeper just reaped. Re-reads the
|
|
3430
|
+
* record so the receipt is built from the committed terminal row rather than
|
|
3431
|
+
* from whatever the caller happened to have in hand.
|
|
3432
|
+
*/
|
|
3433
|
+
issueReceiptForStoredJob(jobId: string): Promise<JobReceipt | undefined>;
|
|
3434
|
+
/** Handle job reaching terminal state (completed, failed, cancelled). */
|
|
3435
|
+
private handleJobTerminal;
|
|
3436
|
+
/**
|
|
3437
|
+
* Report a completed paid job's revenue (internal-review, re-keyed by internal-review).
|
|
3438
|
+
* Fire-and-forget — the `RevenueReporter` owns persistence and retry.
|
|
3439
|
+
*
|
|
3440
|
+
* For a **credit-backed** job the revenue event is the *settled draw* (spec
|
|
3441
|
+
* §10), and since internal-review the sats figure corrects the job's mirror of that
|
|
3442
|
+
* draw against the draw itself: `withDrawBasis` stamps the job when the
|
|
3443
|
+
* payment lands, but a Bitcoin credit's settle re-prices the draw against
|
|
3444
|
+
* the funding lots it consumed. The correction is a delta, because a job's
|
|
3445
|
+
* counter can carry legs no draw ever absorbed — except where the draw's
|
|
3446
|
+
* forecast was zero and the counter therefore never held a component to
|
|
3447
|
+
* correct. The event is dated at settlement rather than at report time. A
|
|
3448
|
+
* released draw — the failed/cancelled path — is deliberately unreachable
|
|
3449
|
+
* here, which is what makes "no debit on job failure" true in the books as
|
|
3450
|
+
* well as the ledger.
|
|
3451
|
+
*
|
|
3452
|
+
* For everything else (free-then-paid jobs, isolate-shaped flows, a DVM
|
|
3453
|
+
* whose price couldn't be fiat-denominated) nothing changes: the pre-credits
|
|
3454
|
+
* payload is reported verbatim.
|
|
3455
|
+
*
|
|
3456
|
+
* Takes the fields rather than a `ServerJob` so the internal-review reconciler — a
|
|
3457
|
+
* background sweep that holds no in-process job — books through this exact
|
|
3458
|
+
* path off the row it read. `paymentTxHash` is passed **verbatim**, never
|
|
3459
|
+
* recomputed as a `drawSettlementRef`: it is the same column the in-line
|
|
3460
|
+
* funnel reads, so the two bookings are byte-identical by construction and
|
|
3461
|
+
* the platform's `UNIQUE (rail, tx_hash)` dedupes them. Deriving it instead
|
|
3462
|
+
* would diverge on an implicit N=1 job (whose row carries the rail's own
|
|
3463
|
+
* reference) and on any job whose column a mid-job top-up overwrote — the
|
|
3464
|
+
* same rot `resolveDrawPool` documents as the reason not to read regime off
|
|
3465
|
+
* this column.
|
|
3466
|
+
*
|
|
3467
|
+
* Returns whether the report was handed to `onJobCompleted`, so a caller
|
|
3468
|
+
* that logs the booking says what actually happened rather than what it
|
|
3469
|
+
* assumed — the guards below still drop legitimate shapes (a free job, a
|
|
3470
|
+
* DVM with no reporter). Every drop that costs a booking logs first.
|
|
3471
|
+
*
|
|
3472
|
+
* The rail check sits **below** the draw re-read, not in the entry guard
|
|
3473
|
+
* (internal-review): a settled draw the terminal can't report under is a real debit
|
|
3474
|
+
* with no revenue event, permanently overstating outstanding liability
|
|
3475
|
+
* (`deposits − draw revenue`), and it used to return here in silence. Placed
|
|
3476
|
+
* after the re-read, `revenue_skipped_no_rail` can name the draw and its
|
|
3477
|
+
* amount, and can distinguish the four causes — see its `reason` below.
|
|
3478
|
+
*/
|
|
3479
|
+
private bookRevenue;
|
|
3480
|
+
/** Schedule job removal from activeJobs after a delay so clients can still poll final status. */
|
|
3481
|
+
scheduleCleanup(job: ServerJob): void;
|
|
3482
|
+
/** Clear all timers. Called on server shutdown to allow clean exit. */
|
|
3483
|
+
shutdown(): void;
|
|
3484
|
+
}
|
|
3485
|
+
|
|
3486
|
+
/** A configured Tempo operator fee-token balance reading. */
|
|
3487
|
+
interface TempoSettlementBalanceObservation {
|
|
3488
|
+
address: `0x${string}`;
|
|
3489
|
+
network: string;
|
|
3490
|
+
token: `0x${string}`;
|
|
3491
|
+
balanceMicro: bigint;
|
|
3492
|
+
lowBalanceMicro: bigint;
|
|
3493
|
+
checkedAt: number;
|
|
3494
|
+
/** Whether balance and any fee-rejection latch both permit new sessions. */
|
|
3495
|
+
ready: boolean;
|
|
3496
|
+
}
|
|
3497
|
+
/** A redacted operator-funded transaction rejection. */
|
|
3498
|
+
interface TempoSettlementFailureObservation {
|
|
3499
|
+
address: `0x${string}`;
|
|
3500
|
+
network: string;
|
|
3501
|
+
token: `0x${string}`;
|
|
3502
|
+
operation: "settle" | "close";
|
|
3503
|
+
errorClass: "InsufficientFundsError";
|
|
3504
|
+
checkedAt: number;
|
|
3505
|
+
balanceMicro?: bigint;
|
|
3506
|
+
channelId?: string;
|
|
3507
|
+
creditId?: string;
|
|
3508
|
+
drainId?: string;
|
|
3509
|
+
}
|
|
3510
|
+
/** Listener for platform reporting and builder-supplied observability. */
|
|
3511
|
+
interface TempoSettlementReadinessListener {
|
|
3512
|
+
onBalance?(observation: TempoSettlementBalanceObservation): Promise<void> | undefined;
|
|
3513
|
+
onFailure?(observation: TempoSettlementFailureObservation): Promise<void> | undefined;
|
|
3514
|
+
}
|
|
3515
|
+
/** Fleet-wide persisted state for one DVM/operator/token readiness epoch. */
|
|
3516
|
+
interface TempoSettlementFailureState {
|
|
3517
|
+
latched: boolean;
|
|
3518
|
+
version?: string;
|
|
3519
|
+
balanceMicro?: bigint;
|
|
3520
|
+
}
|
|
3521
|
+
/** Durable latch operations shared by every Machine serving a hosted DVM. */
|
|
3522
|
+
interface TempoSettlementReadinessStateStore {
|
|
3523
|
+
readFailure(): Promise<TempoSettlementFailureState>;
|
|
3524
|
+
latchFailure(): Promise<TempoSettlementFailureState>;
|
|
3525
|
+
captureFailureBalance(version: string, balanceMicro: bigint): Promise<boolean>;
|
|
3526
|
+
clearFailureIfRefilled(version: string, balanceMicro: bigint, lowBalanceMicro: bigint): Promise<boolean>;
|
|
3527
|
+
clearFailureAfterSuccessfulTransaction(version: string): Promise<boolean>;
|
|
3528
|
+
}
|
|
3529
|
+
/** Construction inputs for {@link TempoSettlementReadiness}. */
|
|
3530
|
+
interface TempoSettlementReadinessOptions {
|
|
3531
|
+
address: `0x${string}`;
|
|
3532
|
+
network: string;
|
|
3533
|
+
token: `0x${string}`;
|
|
3534
|
+
lowBalanceMicro?: bigint;
|
|
3535
|
+
balanceCheckIntervalMs?: number;
|
|
3536
|
+
readBalance(): Promise<bigint>;
|
|
3537
|
+
stateStore?: TempoSettlementReadinessStateStore;
|
|
3538
|
+
now?: () => number;
|
|
3539
|
+
}
|
|
3540
|
+
/**
|
|
3541
|
+
* Readiness gate for the account that funds Tempo session settlement.
|
|
3542
|
+
*
|
|
3543
|
+
* The timer is deliberately unref'd: it reports only while the Fly Machine is
|
|
3544
|
+
* awake and can never keep one awake or cause an external wake. An explicit
|
|
3545
|
+
* fee rejection is stronger evidence than a balance reading, so hosted DVMs
|
|
3546
|
+
* persist that latch in their shared database until a later successful operator
|
|
3547
|
+
* transaction or an observed refill clears it for every Machine.
|
|
3548
|
+
*/
|
|
3549
|
+
declare class TempoSettlementReadiness {
|
|
3550
|
+
private readonly opts;
|
|
3551
|
+
private readonly lowBalanceMicro;
|
|
3552
|
+
private readonly intervalMs;
|
|
3553
|
+
private readonly now;
|
|
3554
|
+
private readonly listeners;
|
|
3555
|
+
private timer;
|
|
3556
|
+
private inFlight;
|
|
3557
|
+
private lastBalanceMicro;
|
|
3558
|
+
private failureBalanceMicro;
|
|
3559
|
+
private failureVersion;
|
|
3560
|
+
private failureLatched;
|
|
3561
|
+
private failureStateKnown;
|
|
3562
|
+
private localFailureGeneration;
|
|
3563
|
+
private pendingFailureGeneration;
|
|
3564
|
+
private failurePersistence;
|
|
3565
|
+
constructor(opts: TempoSettlementReadinessOptions);
|
|
3566
|
+
/** Whether new Tempo session channels may be offered at this instant. */
|
|
3567
|
+
available(): boolean;
|
|
3568
|
+
/** Refresh shared failure state and hold the first balance read before gating. */
|
|
3569
|
+
ensureFresh(): Promise<void>;
|
|
3570
|
+
/** Add a reporting listener; the disposer removes only that listener. */
|
|
3571
|
+
subscribe(listener: TempoSettlementReadinessListener): () => void;
|
|
3572
|
+
/** Start the boot reading and in-process cadence. Idempotent. */
|
|
3573
|
+
start(): void;
|
|
3574
|
+
/** Stop the cadence without affecting any in-flight reading. */
|
|
3575
|
+
stop(): void;
|
|
3576
|
+
/** Read and report the configured fee-token balance now. */
|
|
3577
|
+
checkBalance(opts?: {
|
|
3578
|
+
successfulTransaction?: boolean;
|
|
3579
|
+
recoveryVersion?: string;
|
|
3580
|
+
}): Promise<TempoSettlementBalanceObservation>;
|
|
3581
|
+
/** Latch fleet-wide and report every redacted fee-funding rejection. */
|
|
3582
|
+
recordInsufficientFunds(failure: Omit<TempoSettlementFailureObservation, "address" | "network" | "token" | "errorClass" | "checkedAt" | "balanceMicro">): Promise<void>;
|
|
3583
|
+
/** Snapshot the failure version an operator-funded transaction may recover. */
|
|
3584
|
+
captureRecoveryVersion(): Promise<string | undefined>;
|
|
3585
|
+
/** Re-check after a landed transaction and recover only its preflight version. */
|
|
3586
|
+
recordSuccessfulTransaction(recoveryVersion?: string): Promise<void>;
|
|
3587
|
+
/** Exposed for timer-lifecycle assertions without reaching into Node internals. */
|
|
3588
|
+
timerHasRef(): boolean | undefined;
|
|
3589
|
+
private checkBalanceOnce;
|
|
3590
|
+
private captureFailureBalance;
|
|
3591
|
+
private refreshFailureState;
|
|
3592
|
+
private persistPendingFailure;
|
|
3593
|
+
private applyFailureState;
|
|
3594
|
+
private currentFailureState;
|
|
3595
|
+
private reportBalance;
|
|
3596
|
+
private notify;
|
|
3597
|
+
}
|
|
3598
|
+
|
|
3599
|
+
/**
|
|
3600
|
+
* Hono env for the SDK's DVM app. `unknownRoute` is set by
|
|
3601
|
+
* {@link unknownRouteNotFound} so the `dvm.request` span can classify a 404 as
|
|
3602
|
+
* scanner-probe noise vs a registered route's own 404 (internal-review).
|
|
3603
|
+
*
|
|
3604
|
+
* Exported as a type only, for route modules split out of this file
|
|
3605
|
+
* (`credit-routes.ts`) — a type-only import erases at compile time, so it
|
|
3606
|
+
* introduces no runtime cycle back into `app.ts`.
|
|
3607
|
+
*/
|
|
3608
|
+
interface AppEnv {
|
|
3609
|
+
Variables: {
|
|
3610
|
+
requesterId: string;
|
|
3611
|
+
unknownRoute?: boolean;
|
|
3612
|
+
clientCompatibility?: ClientCompatibility;
|
|
3613
|
+
};
|
|
3614
|
+
}
|
|
3615
|
+
|
|
3616
|
+
/**
|
|
3617
|
+
* What a terminal drain transition needs in order to book its platform drain
|
|
3618
|
+
* report (internal-review). Every field is optional because a DVM can legitimately
|
|
3619
|
+
* run without platform reporting — a `dvmctl dev` server, a self-hosted
|
|
3620
|
+
* builder, a test host — and a drain must still be fulfillable there.
|
|
3621
|
+
*/
|
|
3622
|
+
interface DrainReportWiring {
|
|
3623
|
+
/** The SDK's own Postgres, which holds both the ledger and the reporter's outbox. */
|
|
3624
|
+
db?: Pool;
|
|
3625
|
+
/** Platform DVM record ID, bound into every report. */
|
|
3626
|
+
dvmId?: string;
|
|
3627
|
+
/** Reporter-owned outbox write, run inside the transition's transaction. */
|
|
3628
|
+
enqueueCreditDrain?: CreditDrainEnqueue;
|
|
3629
|
+
}
|
|
3630
|
+
|
|
3631
|
+
/**
|
|
3632
|
+
* Signed body of a `POST /v1/credit` request (internal-review). The op discriminator
|
|
3633
|
+
* lives **inside** the signed bytes, so a network attacker can't rewrite a
|
|
3634
|
+
* `balance` read into a `drain`.
|
|
3635
|
+
*
|
|
3636
|
+
* Deliberately independent of `credit-envelope.ts`, which parses the credit
|
|
3637
|
+
* fields riding a `/v1/job` body: that shape pairs `credit_id` with a
|
|
3638
|
+
* mandatory `draw_id`, and a fund-only top-up has no draw. Two routes, two
|
|
3639
|
+
* schemas — sharing one would force a top-up to invent a draw id.
|
|
3640
|
+
*/
|
|
3641
|
+
declare const CREDIT_REQUEST_SCHEMA: z.ZodObject<{
|
|
3642
|
+
op: z.ZodEnum<{
|
|
3643
|
+
drain: "drain";
|
|
3644
|
+
balance: "balance";
|
|
3645
|
+
fund: "fund";
|
|
3646
|
+
}>;
|
|
3647
|
+
credit_id: z.ZodOptional<z.ZodString>;
|
|
3648
|
+
fund: z.ZodOptional<z.ZodObject<{
|
|
3649
|
+
amount_micro: z.ZodNumber;
|
|
3650
|
+
fund_id: z.ZodString;
|
|
3651
|
+
commitment: z.ZodOptional<z.ZodString>;
|
|
3652
|
+
method: z.ZodOptional<z.ZodEnum<{
|
|
3653
|
+
tempo: "tempo";
|
|
3654
|
+
lightning: "lightning";
|
|
3655
|
+
}>>;
|
|
3656
|
+
intent: z.ZodOptional<z.ZodEnum<{
|
|
3657
|
+
charge: "charge";
|
|
3658
|
+
session: "session";
|
|
3659
|
+
}>>;
|
|
3660
|
+
}, z.core.$strip>>;
|
|
3661
|
+
drain: z.ZodOptional<z.ZodObject<{
|
|
3662
|
+
drain_id: z.ZodString;
|
|
3663
|
+
method: z.ZodOptional<z.ZodString>;
|
|
3664
|
+
payout: z.ZodOptional<z.ZodObject<{
|
|
3665
|
+
refund_pubkey: z.ZodOptional<z.ZodString>;
|
|
3666
|
+
address: z.ZodOptional<z.ZodString>;
|
|
3667
|
+
}, z.core.$strip>>;
|
|
3668
|
+
}, z.core.$strip>>;
|
|
3669
|
+
pubkey: z.ZodString;
|
|
3670
|
+
signature: z.ZodString;
|
|
3671
|
+
timestamp: z.ZodNumber;
|
|
3672
|
+
nonce: z.ZodString;
|
|
3673
|
+
}, z.core.$strip>;
|
|
3674
|
+
/** Parsed `/v1/credit` body. */
|
|
3675
|
+
type CreditRequest = z.infer<typeof CREDIT_REQUEST_SCHEMA>;
|
|
3676
|
+
/** Everything the `/v1/credit` handler needs from the server that mounts it. */
|
|
3677
|
+
interface CreditRouteDeps {
|
|
3678
|
+
/** Resolved credit sizing, or `undefined` when this DVM didn't opt in. */
|
|
3679
|
+
menu: () => Promise<CreditMenu | undefined>;
|
|
3680
|
+
/** Resolved builder policy; absent and older config fail closed. */
|
|
3681
|
+
creditConfig: ResolvedCreditConfig | undefined;
|
|
3682
|
+
auth: DVMAuthScheme | undefined;
|
|
3683
|
+
/** Exact `/v1/credit` audience and operation expected by the auth gate. */
|
|
3684
|
+
authDomain?: SignedRequestDomain;
|
|
3685
|
+
ledger: CreditLedgerLike;
|
|
3686
|
+
fxFetcher: FxFetcher;
|
|
3687
|
+
/** Per-DVM + per-request rail wiring, shared with `/v1/job` (see `railVerifyOpts`). */
|
|
3688
|
+
railOpts: (c: Context<AppEnv>) => Promise<UpfrontPaymentOpts>;
|
|
3689
|
+
/**
|
|
3690
|
+
* Book a committed top-up as a platform deposit (internal-review). A fund-only
|
|
3691
|
+
* top-up moves real money onto a credit whose draws will each book revenue
|
|
3692
|
+
* later, so skipping this would report the spending without the money that
|
|
3693
|
+
* backed it — the DVM's outstanding liability would go negative.
|
|
3694
|
+
*/
|
|
3695
|
+
onDeposit: (funding: NonNullable<PaymentInfo["funding"]>) => void;
|
|
3696
|
+
/**
|
|
3697
|
+
* Platform drain-report wiring (internal-review), used by the cashu pickup — the
|
|
3698
|
+
* one terminal drain transition that lands on this caller-facing route.
|
|
3699
|
+
* Empty on a DVM with no platform reporting, which changes nothing about
|
|
3700
|
+
* the reclaim itself.
|
|
3701
|
+
*/
|
|
3702
|
+
drainReport: DrainReportWiring;
|
|
3703
|
+
/**
|
|
3704
|
+
* Receipt signer, when this DVM has a receipt key (internal-review). Drain ops
|
|
3705
|
+
* countersign every reclaim event with it (internal-review); absent, drains still
|
|
3706
|
+
* work — the ledger stays authoritative, the caller just gets no portable
|
|
3707
|
+
* evidence artifact.
|
|
3708
|
+
*/
|
|
3709
|
+
receiptIssuer: ReceiptIssuer | undefined;
|
|
3710
|
+
/**
|
|
3711
|
+
* The Lightning receive leg, when this DVM configured one (internal-review).
|
|
3712
|
+
* Absent ⇒ `method: "lightning"` is refused with `lightning_unavailable`
|
|
3713
|
+
* and the funding menu never advertised it in the first place.
|
|
3714
|
+
*
|
|
3715
|
+
* Only the *issue* path reaches through this. Settling goes via
|
|
3716
|
+
* {@link settleLightning}, which is where a settlement books its deposit.
|
|
3717
|
+
*/
|
|
3718
|
+
lightning: {
|
|
3719
|
+
receive: LightningReceive;
|
|
3720
|
+
} | undefined;
|
|
3721
|
+
/**
|
|
3722
|
+
* Apply any Lightning invoice this caller has paid since the last check, and
|
|
3723
|
+
* book the deposit for each one that credits (internal-review). A no-op when the
|
|
3724
|
+
* rail isn't configured. Runs before the read ops so a balance reflects a
|
|
3725
|
+
* top-up that settled out of band, and behind the fund poll so a sibling
|
|
3726
|
+
* invoice on the same credit is swept with it.
|
|
3727
|
+
*/
|
|
3728
|
+
settleLightning: (callerPubkey: string, creditId?: string) => Promise<void>;
|
|
3729
|
+
/** Hosted operator fee-token gate; absent on self-hosted DVMs. */
|
|
3730
|
+
tempoSettlementReadiness?: TempoSettlementReadiness;
|
|
3731
|
+
}
|
|
3732
|
+
/**
|
|
3733
|
+
* `POST /v1/credit` — the caller-facing surface of the credits settlement
|
|
3734
|
+
* layer (internal-review, spec §4/§7). Three ops on one signed, replay-protected
|
|
3735
|
+
* envelope, mirroring `/v1/job`'s auth discipline exactly:
|
|
3736
|
+
*
|
|
3737
|
+
* - **fund** — a top-up with no job attached. The rail artifact rides the same
|
|
3738
|
+
* headers as `/v1/job` (`X-Cashu` + `X-Cashu-Request-Id`, `X-PAYMENT`,
|
|
3739
|
+
* `Authorization: Payment …`) and a missing or bad one comes back as the
|
|
3740
|
+
* same 402 envelope, which is what lets a caller's existing resumable-submit
|
|
3741
|
+
* machinery drive a top-up with no new client code.
|
|
3742
|
+
* - **balance** — what do I hold here, including expired credits (their value
|
|
3743
|
+
* is still the caller's; spec §5), plus the funding menu, so the same read
|
|
3744
|
+
* also answers what may be added (internal-review).
|
|
3745
|
+
* - **drain** — reclaim the unspent balance (internal-review, spec §5 tier 3). One
|
|
3746
|
+
* op is both the request and the poll/pickup: the first call debits the
|
|
3747
|
+
* available balance into a `pending` liability; re-polling the same
|
|
3748
|
+
* `drain_id` returns the current state, delivers the parked Cashu token
|
|
3749
|
+
* when the builder's batch job has parked it, and is idempotent throughout.
|
|
3750
|
+
*/
|
|
3751
|
+
declare function handleCreditRequest(c: Context<AppEnv>, deps: CreditRouteDeps): Promise<Response>;
|
|
3752
|
+
|
|
3753
|
+
/**
|
|
3754
|
+
* Pool-shaped handle the lock needs — `connect`, since the lock and the unlock
|
|
3755
|
+
* must ride one pinned client, plus `pg`'s resolved config when it's there (a
|
|
3756
|
+
* hand-rolled pool shape won't have it) so the size check below can run.
|
|
3757
|
+
* Declared here rather than reusing `AccumulatorPool` so the stores can depend
|
|
3758
|
+
* on this module without an import cycle; every SDK pool type (`Pool`,
|
|
3759
|
+
* `AccumulatorPool`, `CreditLedgerPool`) structurally satisfies it.
|
|
3760
|
+
*/
|
|
3761
|
+
type InitLockPool = Pick<Pool, "connect"> & {
|
|
3762
|
+
readonly options?: {
|
|
3763
|
+
readonly max?: number;
|
|
3764
|
+
};
|
|
3765
|
+
};
|
|
3766
|
+
/**
|
|
3767
|
+
* The single session advisory-lock key serializing **all** SDK boot-time
|
|
3768
|
+
* schema work against concurrent creators — two machines cold-booting one DVM
|
|
3769
|
+
* against a fresh database, or parallel test files sharing one DB.
|
|
3770
|
+
*
|
|
3771
|
+
* Postgres' `IF NOT EXISTS` existence check is not atomic against a concurrent
|
|
3772
|
+
* creator: two sessions running the same `CREATE TABLE IF NOT EXISTS` at once
|
|
3773
|
+
* can raise `duplicate key value violates unique constraint
|
|
3774
|
+
* "pg_type_typname_nsp_index"` (23505) in the loser, which at SDK boot is an
|
|
3775
|
+
* unhandled throw that kills the machine (internal-review).
|
|
3776
|
+
*
|
|
3777
|
+
* Deliberately distinct from `PLATFORM_INIT_ADVISORY_LOCK` (17320917) — a
|
|
3778
|
+
* DVM's own database is not the platform database — and from the first-party
|
|
3779
|
+
* DVMs' own keys (cast 17320918, scrape 17320926, scribe 24700919). **A new
|
|
3780
|
+
* SDK store reuses this key rather than inventing a second one**: one key means
|
|
3781
|
+
* there is only ever one lock order, so the per-store lock-order inversion that
|
|
3782
|
+
* deadlocked internal-review can't recur.
|
|
3783
|
+
*/
|
|
3784
|
+
declare const SDK_INIT_ADVISORY_LOCK = 17320927;
|
|
3785
|
+
/**
|
|
3786
|
+
* Run `fn` holding {@link SDK_INIT_ADVISORY_LOCK} on `db`. Every SDK
|
|
3787
|
+
* boot-path `CREATE TABLE IF NOT EXISTS` goes through here.
|
|
3788
|
+
*
|
|
3789
|
+
* Overlapping callers on the same pool share **one** acquisition: the first
|
|
3790
|
+
* connects and locks, concurrent and nested callers join the held lock, and
|
|
3791
|
+
* the last one out unlocks and releases the client. That keeps internal-review's
|
|
3792
|
+
* deliberately-parallel store-init batch parallel — six `init()`s racing each
|
|
3793
|
+
* other would otherwise queue behind one another — and keeps the uncontended
|
|
3794
|
+
* cost at one extra round trip per boot phase rather than one per store.
|
|
3795
|
+
* Sharing is safe for the DDL, which is what this lock is for: it excludes
|
|
3796
|
+
* *other machines*, and within one process no two inits create the same table.
|
|
3797
|
+
* It is not by itself a mutex between same-pool callers — two of them
|
|
3798
|
+
* concurrently running one non-idempotent statement still race, so anything
|
|
3799
|
+
* riding this lock that isn't `CREATE … IF NOT EXISTS` must carry its own
|
|
3800
|
+
* conflict handling (`seedLockPubkeyState`'s `ON CONFLICT DO NOTHING`).
|
|
3801
|
+
*
|
|
3802
|
+
* Session-level (not `pg_advisory_xact_lock`) because the DDL runs in
|
|
3803
|
+
* autocommit with no enclosing transaction, so the lock and the unlock ride a
|
|
3804
|
+
* dedicated pinned client — advisory locks are session-scoped and the unlock
|
|
3805
|
+
* must land on the backend that holds it. Released in `finally` on every path
|
|
3806
|
+
* including throws: an SDK boot that died holding this lock would block every
|
|
3807
|
+
* sibling until its connection closed.
|
|
3808
|
+
*
|
|
3809
|
+
* The guarded DDL runs on the pool's *other* connections, so the pool must
|
|
3810
|
+
* allow at least two — `pg`'s default `max` is 10 and the host never narrows
|
|
3811
|
+
* it, but a builder-supplied `Pool({ max: 1 })` would starve boot. That one
|
|
3812
|
+
* deadlocks with no error and reads as a dead database, so it is refused up
|
|
3813
|
+
* front instead.
|
|
3814
|
+
*/
|
|
3815
|
+
declare function withSdkInitLock<T>(db: InitLockPool, fn: () => Promise<T>): Promise<T>;
|
|
3816
|
+
|
|
3817
|
+
/**
|
|
3818
|
+
* In-memory {@link CreditLedgerLike} for pool-less setups — devMode, direct
|
|
3819
|
+
* `createDVMServer` tests, and the in-process x402 money loop — mirroring the
|
|
3820
|
+
* `MemoryJobStore` precedent. Same semantics as the Postgres `CreditLedger`
|
|
3821
|
+
* (two-phase draws, replay-before-expiry, `draw_conflict`, gap-free
|
|
3822
|
+
* `ledger_seq`), minus durability and cross-machine safety: per-process only,
|
|
3823
|
+
* so production DVMs must use the Postgres ledger (spec §2 condition 2).
|
|
3824
|
+
* Every mutating verb is synchronous between awaits, so operations are
|
|
3825
|
+
* atomic in-process; the `tx` handles are accepted and ignored.
|
|
3826
|
+
*/
|
|
3827
|
+
declare class MemoryCreditLedger implements CreditLedgerLike {
|
|
3828
|
+
/**
|
|
3829
|
+
* Per-process and lost on restart, so a DVM backed by this ledger never
|
|
3830
|
+
* advertises the funding menu outside devMode (internal-review) — funding on one
|
|
3831
|
+
* machine and drawing on another would `credit_not_found`.
|
|
3832
|
+
*/
|
|
3833
|
+
readonly durable = false;
|
|
3834
|
+
private readonly credits;
|
|
3835
|
+
private readonly fundings;
|
|
3836
|
+
private readonly invoices;
|
|
3837
|
+
private readonly tempoLosses;
|
|
3838
|
+
private readonly x402Losses;
|
|
3839
|
+
private x402Settlements?;
|
|
3840
|
+
/** See {@link CreditLedger.useX402SettlementGate} — same contract (internal-review). */
|
|
3841
|
+
useX402SettlementGate(gate: X402RefundSettlementGate): void;
|
|
3842
|
+
fund(args: Parameters<CreditLedgerLike["fund"]>[0]): Promise<CreditSnapshot>;
|
|
3843
|
+
draw(args: Parameters<CreditLedgerLike["draw"]>[0]): Promise<DrawResult>;
|
|
3844
|
+
/** See {@link CreditLedger.blockingX402Refund} — same contract (internal-review). */
|
|
3845
|
+
blockingX402Refund(channelId: string | null | undefined, statuses: X402SettlementStatus[]): Promise<X402UnresolvedRefund | undefined>;
|
|
3846
|
+
/** The unresolved x402 refund that must stop an effect on `creditId` (internal-review). */
|
|
3847
|
+
private blockingRefundForCredit;
|
|
3848
|
+
growDraw(args: Parameters<CreditLedgerLike["growDraw"]>[0]): Promise<GrownDrawResult>;
|
|
3849
|
+
settle(args: Parameters<CreditLedgerLike["settle"]>[0]): Promise<DrawResolution>;
|
|
3850
|
+
release(args: Parameters<CreditLedgerLike["release"]>[0]): Promise<DrawResolution>;
|
|
3851
|
+
recordFunding(args: Parameters<CreditLedgerLike["recordFunding"]>[0]): Promise<FundingRecord>;
|
|
3852
|
+
getFunding(args: {
|
|
3853
|
+
creditId: string;
|
|
3854
|
+
fundId: string;
|
|
3855
|
+
}): Promise<FundingRecord | undefined>;
|
|
3856
|
+
completeFunding(args: Parameters<CreditLedgerLike["completeFunding"]>[0]): Promise<FundingRecord>;
|
|
3857
|
+
saveFundingReceipt(args: Parameters<CreditLedgerLike["saveFundingReceipt"]>[0]): Promise<FundingReceipt>;
|
|
3858
|
+
recordInvoice(args: Parameters<CreditLedgerLike["recordInvoice"]>[0]): Promise<CreditInvoiceRecord>;
|
|
3859
|
+
getInvoice(args: {
|
|
3860
|
+
creditId: string;
|
|
3861
|
+
fundId: string;
|
|
3862
|
+
}): Promise<CreditInvoiceRecord | undefined>;
|
|
3863
|
+
listPendingInvoices(args: {
|
|
3864
|
+
callerPubkey: string;
|
|
3865
|
+
creditId?: string;
|
|
3866
|
+
limit: number;
|
|
3867
|
+
}): Promise<CreditInvoiceRecord[]>;
|
|
3868
|
+
settleInvoice(args: Parameters<CreditLedgerLike["settleInvoice"]>[0]): Promise<InvoiceSettlement>;
|
|
3869
|
+
markInvoiceExpired(args: {
|
|
3870
|
+
creditId: string;
|
|
3871
|
+
fundId: string;
|
|
3872
|
+
}): Promise<CreditInvoiceRecord | undefined>;
|
|
3873
|
+
markInvoiceBlocked(args: {
|
|
3874
|
+
creditId: string;
|
|
3875
|
+
fundId: string;
|
|
3876
|
+
reason: string;
|
|
3877
|
+
}): Promise<CreditInvoiceRecord | undefined>;
|
|
3878
|
+
getInvoiceByPaymentHash(paymentHash: string): Promise<CreditInvoiceRecord | undefined>;
|
|
3879
|
+
listBlockedInvoices(args: {
|
|
3880
|
+
limit: number;
|
|
3881
|
+
after?: BlockedInvoiceCursor;
|
|
3882
|
+
includeResolved?: boolean;
|
|
3883
|
+
}): Promise<CreditInvoiceRecord[]>;
|
|
3884
|
+
reconcileBlockedInvoice(args: Parameters<CreditLedgerLike["reconcileBlockedInvoice"]>[0]): Promise<InvoiceReconciliation>;
|
|
3885
|
+
writeOffBlockedInvoice(args: {
|
|
3886
|
+
paymentHash: string;
|
|
3887
|
+
note?: string;
|
|
3888
|
+
nowMs?: number;
|
|
3889
|
+
}): Promise<InvoiceWriteOff | undefined>;
|
|
3890
|
+
private findByPaymentHash;
|
|
3891
|
+
private fundSync;
|
|
3892
|
+
private drawSync;
|
|
3893
|
+
/** Mid-job top-up growth (internal-review) — see `CreditLedger.growDraw` for the semantics. */
|
|
3894
|
+
private growDrawSync;
|
|
3895
|
+
get(creditId: string, opts?: {
|
|
3896
|
+
nowMs?: number;
|
|
3897
|
+
}): Promise<CreditSnapshot | undefined>;
|
|
3898
|
+
getForCaller(callerPubkey: string, opts?: {
|
|
3899
|
+
nowMs?: number;
|
|
3900
|
+
}): Promise<CreditSnapshot[]>;
|
|
3901
|
+
getDraw(args: {
|
|
3902
|
+
creditId: string;
|
|
3903
|
+
drawId: string;
|
|
3904
|
+
}): Promise<DrawRecord | undefined>;
|
|
3905
|
+
getDrawByJobId(jobId: string): Promise<DrawRecord | undefined>;
|
|
3906
|
+
listPendingDraws(creditId: string): Promise<DrawRecord[]>;
|
|
3907
|
+
listStalePendingDraws(args: {
|
|
3908
|
+
createdBeforeMs: number;
|
|
3909
|
+
limit: number;
|
|
3910
|
+
after?: StalePendingDrawCursor;
|
|
3911
|
+
}): Promise<DrawRecord[]>;
|
|
3912
|
+
/** Settled draws' native value for the credit an x402 channel funded (internal-review). */
|
|
3913
|
+
earnedNativeForX402Channel(channelId: string): Promise<number | undefined>;
|
|
3914
|
+
getCreditByX402Channel(channelId: string): Promise<CreditSnapshot | undefined>;
|
|
3915
|
+
terminalizeTempoCredits(evidence: TempoCreditLossEvidence, _tx?: CreditLedgerQuerier): Promise<TempoCreditLoss[]>;
|
|
3916
|
+
listTempoCreditLosses(args?: {
|
|
3917
|
+
limit?: number;
|
|
3918
|
+
}): Promise<TempoCreditLoss[]>;
|
|
3919
|
+
terminalizeX402Credit(evidence: X402CreditLossEvidence, _tx?: CreditLedgerQuerier): Promise<X402CreditLoss[]>;
|
|
3920
|
+
listX402CreditLosses(args?: {
|
|
3921
|
+
limit?: number;
|
|
3922
|
+
}): Promise<X402CreditLoss[]>;
|
|
3923
|
+
requestDrain(args: {
|
|
3924
|
+
creditId: string;
|
|
3925
|
+
drainId: string;
|
|
3926
|
+
callerPubkey: string;
|
|
3927
|
+
method: DrainMethod;
|
|
3928
|
+
payout: Record<string, string>;
|
|
3929
|
+
requireNoPending?: boolean;
|
|
3930
|
+
tx?: CreditLedgerQuerier;
|
|
3931
|
+
nowMs?: number;
|
|
3932
|
+
}): Promise<DrainRequestResult>;
|
|
3933
|
+
bitcoinDepositLiability(): Promise<BitcoinDepositLiability>;
|
|
3934
|
+
listFundingLots(creditId: string): Promise<FundingLot[]>;
|
|
3935
|
+
getDrain(args: {
|
|
3936
|
+
creditId: string;
|
|
3937
|
+
drainId: string;
|
|
3938
|
+
}): Promise<CreditDrainRecord | undefined>;
|
|
3939
|
+
listPendingDrains(): Promise<CreditDrainRecord[]>;
|
|
3940
|
+
listChannelDrains(args: {
|
|
3941
|
+
limit: number;
|
|
3942
|
+
after?: ChannelDrainCursor;
|
|
3943
|
+
createdBeforeMs: number;
|
|
3944
|
+
rail: "tempo";
|
|
3945
|
+
includeResolved?: boolean;
|
|
3946
|
+
}): Promise<CreditDrainRecord[]>;
|
|
3947
|
+
writeOffDrain(args: {
|
|
3948
|
+
creditId: string;
|
|
3949
|
+
drainId: string;
|
|
3950
|
+
note?: string;
|
|
3951
|
+
nowMs?: number;
|
|
3952
|
+
}): Promise<DrainWriteOff | undefined>;
|
|
3953
|
+
clearDrainWriteOff(args: {
|
|
3954
|
+
creditId: string;
|
|
3955
|
+
drainId: string;
|
|
3956
|
+
}): Promise<void>;
|
|
3957
|
+
releaseDrain(args: {
|
|
3958
|
+
creditId: string;
|
|
3959
|
+
drainId: string;
|
|
3960
|
+
tx?: CreditLedgerQuerier;
|
|
3961
|
+
nowMs?: number;
|
|
3962
|
+
}): Promise<DrainReleaseResult>;
|
|
3963
|
+
parkDrain(args: {
|
|
3964
|
+
creditId: string;
|
|
3965
|
+
drainId: string;
|
|
3966
|
+
token: string;
|
|
3967
|
+
fulfilment?: DrainFulfilment;
|
|
3968
|
+
tx?: CreditLedgerQuerier;
|
|
3969
|
+
nowMs?: number;
|
|
3970
|
+
}): Promise<CreditDrainRecord>;
|
|
3971
|
+
markDrainPickedUp(args: {
|
|
3972
|
+
creditId: string;
|
|
3973
|
+
drainId: string;
|
|
3974
|
+
tx?: CreditLedgerQuerier;
|
|
3975
|
+
nowMs?: number;
|
|
3976
|
+
}): Promise<DrainTransitionResult>;
|
|
3977
|
+
markDrainSent(args: {
|
|
3978
|
+
creditId: string;
|
|
3979
|
+
drainId: string;
|
|
3980
|
+
sentRef: Record<string, unknown>;
|
|
3981
|
+
tx?: CreditLedgerQuerier;
|
|
3982
|
+
nowMs?: number;
|
|
3983
|
+
}): Promise<DrainTransitionResult>;
|
|
3984
|
+
appendDrainReceipt(args: {
|
|
3985
|
+
creditId: string;
|
|
3986
|
+
drainId: string;
|
|
3987
|
+
receipt: Record<string, unknown>;
|
|
3988
|
+
tx?: CreditLedgerQuerier;
|
|
3989
|
+
}): Promise<void>;
|
|
3990
|
+
private requireDrain;
|
|
3991
|
+
private transitionRefusal;
|
|
3992
|
+
private resolve;
|
|
3993
|
+
private requireCredit;
|
|
3994
|
+
private snapshot;
|
|
3995
|
+
}
|
|
3996
|
+
|
|
3997
|
+
/** Postgres-backed JobStore with cross-machine message streaming via LISTEN/NOTIFY. */
|
|
3998
|
+
declare class PostgresJobStore implements StreamableJobStore, ReceiptIssuingStore {
|
|
3999
|
+
private pool;
|
|
4000
|
+
private listenChannel;
|
|
4001
|
+
private messageSubscribers;
|
|
4002
|
+
private notifySubscribers;
|
|
4003
|
+
private requestIdClaimLocks;
|
|
4004
|
+
constructor(pool: Pool);
|
|
4005
|
+
/** Run the CREATE TABLE migration. Call once at startup. */
|
|
4006
|
+
init(): Promise<void>;
|
|
4007
|
+
/** The boot DDL itself — always runs under {@link withSdkInitLock} (internal-review). */
|
|
4008
|
+
private createTables;
|
|
4009
|
+
get(id: string): Promise<JobRecord | undefined>;
|
|
4010
|
+
/**
|
|
4011
|
+
* internal-review. Indexed by `idx_jobs_payment_tx_hash`. `paymentTxHash` is unique
|
|
4012
|
+
* per settlement in practice — the accumulator's
|
|
4013
|
+
* `UNIQUE(dvm_id, request_id, proof_secret)` is what makes a second job under
|
|
4014
|
+
* the same request id impossible — but order by `created_at` so a
|
|
4015
|
+
* hypothetical duplicate resolves to the job the caller actually paid for.
|
|
4016
|
+
*/
|
|
4017
|
+
findJobByPaymentTxHash(txHash: string): Promise<JobRecord | undefined>;
|
|
4018
|
+
findJobByRequestId(requestId: string, requesterPubkey: string): Promise<JobRecord | undefined>;
|
|
4019
|
+
claimRequestId(claim: RequestIdClaim): Promise<RequestIdClaimResult | undefined>;
|
|
4020
|
+
resumeRequestId(claim: RequestIdClaim): Promise<RequestIdClaimResult | undefined>;
|
|
4021
|
+
private acquireRequestIdClaim;
|
|
4022
|
+
releaseRequestIdClaim(claim: RequestIdClaim): Promise<void>;
|
|
4023
|
+
save(record: JobRecord): Promise<void>;
|
|
4024
|
+
delete(id: string): Promise<void>;
|
|
4025
|
+
claimReceiptSeq(jobId: string): Promise<number>;
|
|
4026
|
+
saveReceipt(jobId: string, receipt: JobReceipt): Promise<JobReceipt | undefined>;
|
|
4027
|
+
appendOutgoing(jobId: string, message: OutgoingMessage, opts?: AppendOutgoingOptions): Promise<number>;
|
|
4028
|
+
recordInbound(jobId: string, message: OutgoingMessage): Promise<number>;
|
|
4029
|
+
verifyAndCredit(jobId: string, seq: number, credit: PaymentCreditDelta | null): Promise<VerifyAndCreditResult>;
|
|
4030
|
+
getVerifiedInbound(jobId: string, seq: number): Promise<VerifyAndCreditResult | undefined>;
|
|
4031
|
+
markInboundFailed(jobId: string, seq: number, _reason: string): Promise<void>;
|
|
4032
|
+
findStaleJobs(processingThresholdMs: number, awaitingThresholdMs: number, limit: number, capabilities: string[] | null): Promise<JobRecord[]>;
|
|
4033
|
+
heartbeatActiveJobs(jobIds: string[], now: number): Promise<void>;
|
|
4034
|
+
cancelStaleJob(jobId: string, expectedActivityBefore: number, reason: string, terminalStatus: "failed" | "cancelled"): Promise<boolean>;
|
|
4035
|
+
cancelJob(jobId: string, reason: string): Promise<boolean>;
|
|
4036
|
+
getCounters(jobId: string): Promise<JobCounters | undefined>;
|
|
4037
|
+
claimForProcessing(jobId: string): Promise<boolean>;
|
|
4038
|
+
tryReactivationLock<T>(jobId: string, fn: () => Promise<T>): Promise<{
|
|
4039
|
+
acquired: true;
|
|
4040
|
+
result: T;
|
|
4041
|
+
} | {
|
|
4042
|
+
acquired: false;
|
|
4043
|
+
}>;
|
|
4044
|
+
subscribeMessages(jobId: string, afterSeq: number, onMessage: (msg: Message) => void): Promise<() => void>;
|
|
4045
|
+
subscribeNotifications(jobId: string, onNotify: () => void): Promise<() => void>;
|
|
4046
|
+
getMessages(jobId: string, afterSeq: number): Promise<Message[]>;
|
|
4047
|
+
initStreaming(): Promise<void>;
|
|
4048
|
+
shutdownStreaming(): Promise<void>;
|
|
4049
|
+
private dispatch;
|
|
4050
|
+
/**
|
|
4051
|
+
* Drive a non-terminal job to `terminalStatus` and append its final `cancel`
|
|
4052
|
+
* message in one transaction. Backs both `cancelStaleJob` (which passes an
|
|
4053
|
+
* `expectedActivityBefore` cutoff, so the flip only lands while the row is
|
|
4054
|
+
* still stale — a live worker that heartbeats in between keeps its job) and
|
|
4055
|
+
* `cancelJob` (which passes `null`: an operator/caller cancel is
|
|
4056
|
+
* unconditional, gated only on the row still being non-terminal).
|
|
4057
|
+
*
|
|
4058
|
+
* The `SELECT … FOR UPDATE` makes the whole thing a CAS: two machines racing
|
|
4059
|
+
* to terminate the same job serialise on the row lock, and the loser's
|
|
4060
|
+
* status filter no longer matches. `pg_notify` fires inside the tx so the
|
|
4061
|
+
* machine running the handler wakes as soon as the terminal status is
|
|
4062
|
+
* visible.
|
|
4063
|
+
*/
|
|
4064
|
+
private terminateJob;
|
|
4065
|
+
private fetchCountersTx;
|
|
4066
|
+
/**
|
|
4067
|
+
* The credit binding on the job row, read inside Tx C (internal-review). Used on the
|
|
4068
|
+
* paths that don't get it from a `RETURNING` — the `alreadyVerified` skip and
|
|
4069
|
+
* a non-payment inbound.
|
|
4070
|
+
*/
|
|
4071
|
+
private fetchCreditBindingTx;
|
|
4072
|
+
}
|
|
4073
|
+
|
|
4074
|
+
/**
|
|
4075
|
+
* Postgres-backed KVStore. Requires the `pg` package as a peer dependency.
|
|
4076
|
+
*
|
|
4077
|
+
* Expired rows are filtered on read but never deleted. A periodic cleanup
|
|
4078
|
+
* mechanism (e.g. `DELETE FROM kv_store WHERE expires_at IS NOT NULL AND
|
|
4079
|
+
* expires_at < now()`) should be added before production use to prevent
|
|
4080
|
+
* unbounded table growth.
|
|
4081
|
+
*/
|
|
4082
|
+
declare class PostgresKVStore implements KVStore {
|
|
4083
|
+
private pool;
|
|
4084
|
+
constructor(pool: Pool);
|
|
4085
|
+
/** Run the CREATE TABLE migration. Call once at startup. */
|
|
4086
|
+
init(): Promise<void>;
|
|
4087
|
+
/** The boot DDL itself — always runs under {@link withSdkInitLock} (internal-review). */
|
|
4088
|
+
private createTables;
|
|
4089
|
+
get<T = unknown>(key: string): Promise<T | undefined>;
|
|
4090
|
+
set(key: string, value: unknown, opts?: {
|
|
4091
|
+
ttl?: number;
|
|
4092
|
+
}): Promise<void>;
|
|
4093
|
+
delete(key: string): Promise<void>;
|
|
4094
|
+
list(prefix?: string): Promise<string[]>;
|
|
4095
|
+
}
|
|
4096
|
+
|
|
4097
|
+
/** Options for `PostgresReplayStore`. */
|
|
4098
|
+
interface PostgresReplayStoreOpts {
|
|
4099
|
+
/** Table name. Defaults to `"signed_request_replays"`. Must match `/^[a-zA-Z_][a-zA-Z0-9_]*$/`. */
|
|
4100
|
+
tableName?: string;
|
|
4101
|
+
/**
|
|
4102
|
+
* Retention window in seconds — rows older than `now - windowSeconds` are
|
|
4103
|
+
* eligible for the sampled-in-line GC sweep. Defaults to 600 (10 minutes),
|
|
4104
|
+
* matching the SDK's signed-request retention default.
|
|
4105
|
+
*/
|
|
4106
|
+
windowSeconds?: number;
|
|
4107
|
+
/**
|
|
4108
|
+
* Probability (per `checkAndRecord` call) of firing a fire-and-forget GC
|
|
4109
|
+
* sweep. Defaults to 0.01 — every ~100 writes runs one DELETE. Set to 0 to
|
|
4110
|
+
* disable GC entirely (rely on an external sweeper).
|
|
4111
|
+
*/
|
|
4112
|
+
gcSampleRate?: number;
|
|
4113
|
+
}
|
|
4114
|
+
/**
|
|
4115
|
+
* Postgres-backed replay store implementing {@link SignedRequestReplayStore}.
|
|
4116
|
+
*
|
|
4117
|
+
* Cross-machine safe: relies on the PK on `(pubkey, timestamp, nonce)` with
|
|
4118
|
+
* `INSERT ... ON CONFLICT DO NOTHING` to detect duplicates atomically across
|
|
4119
|
+
* concurrent inserts from multiple machines. The consumer hands in a `Pool`
|
|
4120
|
+
* (matching `PostgresJobStore` / `PostgresKVStore`) and manages its lifecycle.
|
|
4121
|
+
*
|
|
4122
|
+
* Multi-machine deploys of secp256k1-auth'd DVMs SHOULD use this store to
|
|
4123
|
+
* close the per-process replay window — see {@link SignedRequestReplayStore}
|
|
4124
|
+
* for the threat model.
|
|
4125
|
+
*/
|
|
4126
|
+
declare class PostgresReplayStore implements SignedRequestReplayStore {
|
|
4127
|
+
private pool;
|
|
4128
|
+
private tableName;
|
|
4129
|
+
private windowSeconds;
|
|
4130
|
+
private gcSampleRate;
|
|
4131
|
+
constructor(pool: Pool, opts?: PostgresReplayStoreOpts);
|
|
4132
|
+
/** Run the CREATE TABLE migration. Idempotent — safe to call repeatedly. */
|
|
4133
|
+
init(): Promise<void>;
|
|
4134
|
+
/** The boot DDL itself — always runs under {@link withSdkInitLock} (internal-review). */
|
|
4135
|
+
private createTables;
|
|
4136
|
+
checkAndRecord(pubkey: string, timestamp: number, nonce: string, now: number): Promise<boolean>;
|
|
4137
|
+
private gcSweep;
|
|
4138
|
+
}
|
|
4139
|
+
|
|
4140
|
+
/** Protocol-owned request field carried inside a signed request body. */
|
|
4141
|
+
declare const REQUEST_ID_FIELD = "request_id";
|
|
4142
|
+
/** Auth-schema fields the protocol owns rather than the builder input schema. */
|
|
4143
|
+
declare function protocolEnvelopeIgnoreFields(schema: unknown): readonly string[];
|
|
4144
|
+
/** Remove protocol-owned credit and recovery fields before builder input parsing. */
|
|
4145
|
+
declare function stripProtocolEnvelope(schema: unknown, data: unknown): unknown;
|
|
4146
|
+
/** Read and validate the optional recovery id from a signed request body. */
|
|
4147
|
+
declare function requestIdFromEnvelope(data: unknown): string | undefined;
|
|
4148
|
+
/** True when the request body carries a request id field but its value is invalid. */
|
|
4149
|
+
declare function hasInvalidRequestId(data: unknown): boolean;
|
|
4150
|
+
|
|
4151
|
+
/**
|
|
4152
|
+
* Convenience one-liner over {@link createDVMHost} (internal-review). For
|
|
4153
|
+
* single-DVM hosts, `serve(dvm, opts)` is equivalent to
|
|
4154
|
+
* `createDVMHost(opts).mount(dvm).serve()`. Multi-DVM hosts go through
|
|
4155
|
+
* `createDVMHost` directly so each mount can carry its own prefix.
|
|
4156
|
+
*
|
|
4157
|
+
* This is **shorthand-over-host**, not a parallel code path — every wiring
|
|
4158
|
+
* step (Cashu/MPP/x402 envelope resolution, platform reporter, db, fx) flows
|
|
4159
|
+
* through `createDVMHost`. First-party DVMs that use `serve(...)` are NOT
|
|
4160
|
+
* exposed to the wiring-drift bug class fixed in internal-review/485/511, which was
|
|
4161
|
+
* about bypassing the host entirely. There is no migration to do.
|
|
4162
|
+
*/
|
|
4163
|
+
declare function serve<State, InputSchema extends ZodLike | undefined = undefined>(descriptor: DVMDescriptor<State, InputSchema>, opts?: DVMHostOpts): Promise<{
|
|
4164
|
+
url: string;
|
|
4165
|
+
close: () => Promise<void>;
|
|
4166
|
+
}>;
|
|
4167
|
+
|
|
4168
|
+
export { BlockedInvoiceCursor, type BuildCreditMenuArgs, type BuilderIdentity, CREDIT_ENVELOPE_KEYS, CREDIT_REQUEST_SCHEMA, ClientCompatibility, ClientCompatibilityGate, type ConsumedCredentialStore, type CreateX402BatchSettlementServerOpts, type CreditEnvelope, CreditEnvelopeError, type CreditFundCommitment, CreditInvoiceRecord, CreditLedgerLike, CreditLedgerQuerier, type CreditMenu, type CreditRequest, type CreditRouteDeps, CreditSnapshot, type CreditTerms, DEFAULT_INVOICE_TTL_SECONDS, DVMAuthScheme, type DVMHost, type DVMHostOpts, DrawRecord, DrawResolution, DrawResult, FundingRecord, GrownDrawResult, IMPLICIT_CREDIT_TTL_MS, type InitLockPool, InvoiceReconciliation, InvoiceSettlement, InvoiceWriteOff, JobManager, type JobManagerOpts, JobRecord, JobStore, LightningReceive, type LightningReceiveConfig, MIN_INVOICE_TTL_SECONDS, MemoryConsumedCredentialStore, type MemoryConsumedCredentialStoreOpts, MemoryCreditLedger, MemoryProcessedPaymentStore, MemoryX402ExactSettlementStore, type MountOpts, type OwnerDisplay, type PlatformReporterOpts, PostgresJobStore, PostgresKVStore, PostgresProcessedPaymentStore, PostgresReplayStore, type PostgresReplayStoreOpts, PostgresX402ChannelStorage, PostgresX402ExactSettlementStore, type PriceFiat, type ProcessedPaymentQuerier, type ProcessedPaymentRail, type ProcessedPaymentRecord, ProcessedPaymentReplayError, type ProcessedPaymentStore, REQUEST_ID_FIELD, type ResolvedCashuOpts, RevenueSkippedNoRailPayload, SDK_INIT_ADVISORY_LOCK, SignedRequestAudience, SignedRequestDomain, SignedRequestReplayStore, StalePendingDrawCursor, StreamableJobStore, type X402BatchAcceptance, type X402BatchFunding, type X402BatchRefusal, type X402BatchSettlementServer, type X402ExactAcceptance, X402ExactIntentConflictError, type X402ExactSettlementAttempt, type X402ExactSettlementChainEvidence, type X402ExactSettlementEffect, X402ExactSettlementEvidenceMissingError, type X402ExactSettlementEvidenceReader, type X402ExactSettlementIntent, X402ExactSettlementNotReadyError, X402ExactSettlementServer, type X402ExactSettlementServerOpts, type X402ExactSettlementStatus, type X402ExactSettlementStore, type X402SettlementChainEvidence, type X402SettlementEvidenceReader, X402SettlementSubmissionError, X402_BATCH_AUTO_SETTLEMENT, X402_BATCH_MIN_WITHDRAW_DELAY_SECONDS, X402_BATCH_SETTLEMENT_NETWORK, attachCreditMenu, buildCreditMenu, createDVMHost, createX402BatchSettlementServer, creditEnvelopeIgnoreFields, drawSettlementRef, extractCreditEnvelope, fundingCommitment, handleCreditRequest, hasInvalidRequestId, protocolEnvelopeIgnoreFields, requestIdFromEnvelope, resolveCashuOptsFromEnv, selectPrimaryCredit, serve, stripCreditEnvelope, stripProtocolEnvelope, toCreditView, withSdkInitLock };
|