@dvmkit/sdk 0.1.0-rc.1 → 0.1.0-rc.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -2
- package/dist/{revenue-reporter-M35KP6V7.js → chunk-2ABMGUDS.js} +78 -2
- package/dist/chunk-5SO7ZVOH.js +375 -0
- package/dist/chunk-66HGCPBU.js +25 -0
- package/dist/{chunk-KQAJVVZT.js → chunk-AZBXSXQT.js} +27 -287
- package/dist/chunk-C2DC4FKR.js +21424 -0
- package/dist/{chunk-H25M54MI.js → chunk-C3MTFLC6.js} +16 -0
- package/dist/{chunk-NTK5DJ6R.js → chunk-EXHBXA4U.js} +11 -1
- package/dist/chunk-FT7IM66W.js +1557 -0
- package/dist/{chunk-KXWROQGK.js → chunk-FUJ36YDV.js} +1 -24
- package/dist/{tempo-charge-store-6GJEMNUU.js → chunk-JZWELPFH.js} +1 -0
- package/dist/{chunk-RPXHKMYE.js → chunk-LWUR4CGG.js} +348 -34
- package/dist/chunk-RHP3BRTH.js +1090 -0
- package/dist/{tempo-session-store-FTEEGZXA.js → chunk-RU7SXHLO.js} +2 -1
- package/dist/{chunk-7IH5SG2A.js → chunk-TKA6ZP4M.js} +62 -41
- package/dist/chunk-TVI4V7GF.js +283 -0
- package/dist/chunk-X3IKFWJA.js +754 -0
- package/dist/{chunk-DCNT4PJS.js → chunk-XY5Y5REG.js} +6 -258
- package/dist/chunk-XYTSDAPH.js +232 -0
- package/dist/chunk-YD3TZNXV.js +1042 -0
- package/dist/{credit-ledger-EDMEZSA2.js → credit-ledger-ED6JXKVD.js} +2 -2
- package/dist/credit-menu-DONAtGVf.d.ts +5076 -0
- package/dist/{ssrf-BdHsrrIb.d.ts → fx-Bq4cvn16.d.ts} +37 -119
- package/dist/index.d.ts +8 -66
- package/dist/index.js +11 -219
- package/dist/internal/index.d.ts +5745 -0
- package/dist/internal/index.js +6379 -0
- package/dist/{job-store-C5n6bhap.d.ts → job-store-m2pYmvbr.d.ts} +1772 -31
- package/dist/{memory-credit-ledger-7TTZDSRS.js → memory-credit-ledger-XJ5VQEVP.js} +3 -3
- package/dist/payout-reporter-3UB5WRCV.js +13 -0
- package/dist/revenue-reporter-JIKUPXOK.js +7 -0
- package/dist/server/index.d.ts +14 -3530
- package/dist/server/index.js +327 -20636
- package/dist/ssrf-DbFkpDv0.d.ts +118 -0
- package/dist/tempo-charge-store-RIFTALZK.js +8 -0
- package/dist/tempo-session-store-DALMRIWN.js +11 -0
- package/dist/testing/index.d.ts +3 -2
- package/dist/testing/index.js +3 -2
- package/dist/usd-DjVAPMlf.d.ts +97 -0
- package/dist/x402-5EVIUSEP.js +81 -0
- package/package.json +6 -2
- package/dist/x402-35VLYFKZ.js +0 -1272
package/README.md
CHANGED
|
@@ -8,18 +8,26 @@ Use the Node.js release in `.nvmrc` (Node 22). Install with `npm ci`, then run t
|
|
|
8
8
|
|
|
9
9
|
`DATABASE_URL` is optional locally. Without it the database suite is skipped rather than passed, and `verify` says so in its summary, because a skipped suite verified nothing and must not be reported as green. Point `DATABASE_URL` at a scratch Postgres to run it, and pass `npm run verify -- --require-database` to make a missing one a failure, which is what CI does.
|
|
10
10
|
|
|
11
|
-
The public
|
|
11
|
+
The public surface is exactly `@dvmkit/sdk`, `@dvmkit/sdk/server`, and `@dvmkit/sdk/testing`. There is a fourth entry point, `@dvmkit/sdk/internal`, which is not part of it; see below. Documentation is at [dvmkit.ai](https://dvmkit.ai/docs/sdk).
|
|
12
12
|
|
|
13
13
|
## Compatibility and versioning
|
|
14
14
|
|
|
15
15
|
This is pre-1.0 alpha software and the promises match that. Breaking changes can land in any release, they are announced in the release notes for the release that carries them, and there is no long-term support release. Security fixes are made on the latest published release rather than backported.
|
|
16
16
|
|
|
17
|
-
The public surface is exactly three entry points: `@dvmkit/sdk`, `@dvmkit/sdk/server`, and `@dvmkit/sdk/testing`. Nothing reachable only through a deep path into `dist/` is public, and `npm run check:api` pins every exported declaration name against `api-surface.json`, so a change to that surface is a deliberate, reviewed act rather than a side effect.
|
|
17
|
+
The public surface is exactly three entry points: `@dvmkit/sdk`, `@dvmkit/sdk/server`, and `@dvmkit/sdk/testing`. Nothing reachable only through a deep path into `dist/` is public, nothing under `@dvmkit/sdk/internal` is public, and `npm run check:api` pins every exported declaration name against `api-surface.json`, so a change to that surface is a deliberate, reviewed act rather than a side effect.
|
|
18
18
|
|
|
19
19
|
Wire compatibility with callers is separate from the package version and is declared per endpoint. A server built on this SDK answers `DVM-Protocol-Version: 1`, and an individual endpoint may require a set of named capability tokens the caller advertises, a minimum caller version, or both. Capability tokens are additive, so an endpoint that declares no requirement stays reachable by a caller that sends no compatibility headers at all. A caller that cannot meet what an endpoint declares gets HTTP 426 naming the required capabilities and, where one is declared, the minimum version. The protocol is specified at [dvmkit.ai/docs/protocol](https://dvmkit.ai/docs/protocol).
|
|
20
20
|
|
|
21
21
|
If an upgrade broke something for you, an [API compatibility report](https://github.com/dvmkit/sdk/issues/new/choose) is the most useful thing you can send us. It tells us which parts of the published surface people actually build on, which no test of ours can.
|
|
22
22
|
|
|
23
|
+
## Internal entry point
|
|
24
|
+
|
|
25
|
+
`@dvmkit/sdk/internal` exists for one consumer: the dvmkit platform, which runs on this source instead of keeping a fork of it. Do not build on it.
|
|
26
|
+
|
|
27
|
+
It carries no stability promise of any kind. A name under it can change meaning, change signature, or disappear in any release, including a patch release, and that is not a breaking change: it needs no `!` marker, no `BREAKING CHANGE:` footer, and no note in the release notes. It is not documented, its contents track what the platform happens to need, and it is excluded from everything the three public entry points promise above.
|
|
28
|
+
|
|
29
|
+
The names in it are still pinned in `api-surface.json`, so a change to them is visible in review rather than silent; the pin records what moved, it does not promise that it will not move. If something under `./internal` is genuinely useful to a third party, the way to get it is to ask for it: graduating a name to `.`, `./server` or `./testing` is a deliberate, reviewed `feat`, and only then does it acquire the compatibility promises on this page.
|
|
30
|
+
|
|
23
31
|
## Contributing
|
|
24
32
|
|
|
25
33
|
**This repository does not accept unsolicited pull requests.** One opened without an invitation is closed automatically, with a pointer to where the report belongs.
|
|
@@ -8,7 +8,9 @@ var ENDPOINTS = {
|
|
|
8
8
|
revenue: "/_internal/job-revenue",
|
|
9
9
|
deposit: "/_internal/credit-deposit",
|
|
10
10
|
release: "/_internal/credit-draw-release",
|
|
11
|
-
drain: "/_internal/credit-drain"
|
|
11
|
+
drain: "/_internal/credit-drain",
|
|
12
|
+
payout: "/_internal/payout",
|
|
13
|
+
expiry_release: "/_internal/credit-expiry-release"
|
|
12
14
|
};
|
|
13
15
|
var RevenueReporter = class _RevenueReporter {
|
|
14
16
|
// 24h of failed retries
|
|
@@ -83,6 +85,79 @@ var RevenueReporter = class _RevenueReporter {
|
|
|
83
85
|
async enqueueCreditDrain(tx, payload) {
|
|
84
86
|
await this.insertPending(tx, "drain", payload);
|
|
85
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* Join a payout to the transaction that commits the movement it reports
|
|
90
|
+
* (internal-review) — the batch close, the accumulator mark, the drain booking.
|
|
91
|
+
* No network I/O, only the durable queue row through `tx`, the discipline
|
|
92
|
+
* {@link enqueueCreditDrain} set and for the same reason: the money has
|
|
93
|
+
* already moved on a rail this ledger does not own, so the platform row is
|
|
94
|
+
* the only record the builder's dashboard has of it, and a row lost between
|
|
95
|
+
* the fact and the queue would understate "paid out" forever. Without `tx`
|
|
96
|
+
* the row is queued on the pool, for callers with no fact of their own to
|
|
97
|
+
* commit. Idempotent on `(dvmId, payoutId)` platform-side, so the retry
|
|
98
|
+
* loop redelivers freely.
|
|
99
|
+
*/
|
|
100
|
+
async enqueuePayout(payload, tx) {
|
|
101
|
+
await this.insertPending(tx ?? this.db, "payout", payload);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Report one landed payout on its own and try to deliver it at once — the
|
|
105
|
+
* live Tempo settlement, which has no dvmkit transaction to join. Durable
|
|
106
|
+
* like {@link report}.
|
|
107
|
+
*/
|
|
108
|
+
async reportPayout(payload) {
|
|
109
|
+
return this.enqueue("payout", payload, payload.payoutId);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Report one rail's pending-pool snapshot (internal-review). Best-effort and
|
|
113
|
+
* fire-and-forget, deliberately unlike {@link reportPayout}: a snapshot is
|
|
114
|
+
* replaced by the next tick, so queueing a stale one behind an outage would
|
|
115
|
+
* only deliver figures the platform already has newer ones for.
|
|
116
|
+
*/
|
|
117
|
+
async reportPayoutPending(payload) {
|
|
118
|
+
const url = `${this.platformUrl}/_internal/payout-pending`;
|
|
119
|
+
try {
|
|
120
|
+
const res = await fetch(url, {
|
|
121
|
+
method: "POST",
|
|
122
|
+
headers: {
|
|
123
|
+
"Content-Type": "application/json",
|
|
124
|
+
Authorization: `Bearer ${this.platformToken}`
|
|
125
|
+
},
|
|
126
|
+
body: JSON.stringify(payload),
|
|
127
|
+
signal: AbortSignal.timeout(1e4)
|
|
128
|
+
});
|
|
129
|
+
if (!res.ok) {
|
|
130
|
+
console.warn(
|
|
131
|
+
JSON.stringify({
|
|
132
|
+
level: "payout_pending_report_failed",
|
|
133
|
+
rail: payload.rail,
|
|
134
|
+
status: res.status
|
|
135
|
+
})
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
} catch (err) {
|
|
139
|
+
console.warn(
|
|
140
|
+
JSON.stringify({
|
|
141
|
+
level: "payout_pending_report_failed",
|
|
142
|
+
rail: payload.rail,
|
|
143
|
+
error: err instanceof Error ? err.message : String(err)
|
|
144
|
+
})
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Join a credit-expiry release — or the revival that reverses one — to the
|
|
150
|
+
* transaction that made it true (internal-review).
|
|
151
|
+
*
|
|
152
|
+
* Same discipline as {@link enqueueCreditDrain}: no network I/O, only the
|
|
153
|
+
* durable queue row written through `tx`. A release whose row was lost after
|
|
154
|
+
* the sweep committed would understate committed value forever; a lost
|
|
155
|
+
* reversal would overstate it, which is the worse direction — the builder
|
|
156
|
+
* would see money as theirs that a revived credit can still buy work with.
|
|
157
|
+
*/
|
|
158
|
+
async enqueueCreditExpiryRelease(tx, payload) {
|
|
159
|
+
await this.insertPending(tx, "expiry_release", payload);
|
|
160
|
+
}
|
|
86
161
|
/**
|
|
87
162
|
* Report a reaper-force-failed paid job to the platform (internal-review) so the
|
|
88
163
|
* operator receives an ambient notice that a DVM died or wedged mid-job.
|
|
@@ -325,7 +400,7 @@ var RevenueReporter = class _RevenueReporter {
|
|
|
325
400
|
[Date.now()]
|
|
326
401
|
);
|
|
327
402
|
for (const row of rows) {
|
|
328
|
-
const label = row.payload.jobId ?? row.payload.creditId ?? row.id;
|
|
403
|
+
const label = row.payload.jobId ?? row.payload.creditId ?? row.payload.payoutId ?? row.id;
|
|
329
404
|
try {
|
|
330
405
|
await this.attemptDelivery(row.id, row.payload, row.kind, label);
|
|
331
406
|
} catch {
|
|
@@ -430,6 +505,7 @@ var RevenueReporter = class _RevenueReporter {
|
|
|
430
505
|
);
|
|
431
506
|
}
|
|
432
507
|
};
|
|
508
|
+
|
|
433
509
|
export {
|
|
434
510
|
RevenueReporter
|
|
435
511
|
};
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
import {
|
|
2
|
+
formatFiat
|
|
3
|
+
} from "./chunk-AT6V3SY7.js";
|
|
4
|
+
|
|
5
|
+
// src/lib/errors.ts
|
|
6
|
+
var DvmError = class extends Error {
|
|
7
|
+
constructor(code, message, hint, data, display, proseSources) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.code = code;
|
|
10
|
+
this.hint = hint;
|
|
11
|
+
this.data = data;
|
|
12
|
+
this.display = display;
|
|
13
|
+
this.proseSources = proseSources;
|
|
14
|
+
this.name = "DvmError";
|
|
15
|
+
}
|
|
16
|
+
code;
|
|
17
|
+
hint;
|
|
18
|
+
data;
|
|
19
|
+
display;
|
|
20
|
+
proseSources;
|
|
21
|
+
};
|
|
22
|
+
function providerProseSources(...sources) {
|
|
23
|
+
const provenance = {};
|
|
24
|
+
for (const source of sources) {
|
|
25
|
+
if (!source) continue;
|
|
26
|
+
for (const field of ["message", "hint", "display"]) {
|
|
27
|
+
if (typeof source[field] === "string") provenance[field] = "provider";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return Object.keys(provenance).length > 0 ? provenance : void 0;
|
|
31
|
+
}
|
|
32
|
+
function transportErrorCode(err) {
|
|
33
|
+
let current = err;
|
|
34
|
+
const seen = /* @__PURE__ */ new Set();
|
|
35
|
+
for (let depth = 0; depth < 5 && isErrorLike(current) && !seen.has(current); depth++) {
|
|
36
|
+
seen.add(current);
|
|
37
|
+
const code = current.code;
|
|
38
|
+
if (typeof code === "string" && /^(?:E[A-Z0-9_]{1,63}|UND_ERR_[A-Z0-9_]{1,63})$/.test(code)) {
|
|
39
|
+
return code;
|
|
40
|
+
}
|
|
41
|
+
current = current.cause;
|
|
42
|
+
}
|
|
43
|
+
return void 0;
|
|
44
|
+
}
|
|
45
|
+
function isErrorLike(value) {
|
|
46
|
+
return (typeof value === "object" || typeof value === "function") && value !== null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// src/lib/credit-implicit.ts
|
|
50
|
+
var IMPLICIT_CREDIT_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
51
|
+
|
|
52
|
+
// src/lib/fx-coingecko.ts
|
|
53
|
+
var COINGECKO_SIMPLE_PRICE_URL = "https://api.coingecko.com/api/v3/simple/price?ids=bitcoin,usd-coin&vs_currencies=usd";
|
|
54
|
+
var COINGECKO_BTC_USD_URL = "https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd";
|
|
55
|
+
var DEFAULT_TIMEOUT_MS = 5e3;
|
|
56
|
+
var DEFAULT_USER_AGENT = "dvmkit-platform/1.0 (+https://dvmkit.com)";
|
|
57
|
+
async function fetchCoinGeckoSpot(opts) {
|
|
58
|
+
const body = await fetchCoinGeckoBody(opts, COINGECKO_SIMPLE_PRICE_URL);
|
|
59
|
+
const btc = body.bitcoin?.usd;
|
|
60
|
+
const usdc = body["usd-coin"]?.usd;
|
|
61
|
+
if (!isPositiveNumber(btc)) {
|
|
62
|
+
throw new Error("coingecko response missing bitcoin.usd");
|
|
63
|
+
}
|
|
64
|
+
if (!isPositiveNumber(usdc)) {
|
|
65
|
+
throw new Error("coingecko response missing usd-coin.usd");
|
|
66
|
+
}
|
|
67
|
+
return { btc_usd: btc, usdc_usd: usdc, as_of: (/* @__PURE__ */ new Date()).toISOString() };
|
|
68
|
+
}
|
|
69
|
+
async function fetchCoinGeckoBtcUsd(opts) {
|
|
70
|
+
const body = await fetchCoinGeckoBody(opts, COINGECKO_BTC_USD_URL);
|
|
71
|
+
const btc = body.bitcoin?.usd;
|
|
72
|
+
if (!isPositiveNumber(btc)) {
|
|
73
|
+
throw new Error("coingecko response missing bitcoin.usd");
|
|
74
|
+
}
|
|
75
|
+
return { btc_usd: btc, as_of: (/* @__PURE__ */ new Date()).toISOString() };
|
|
76
|
+
}
|
|
77
|
+
async function fetchCoinGeckoBody(opts, defaultUrl) {
|
|
78
|
+
const url = opts?.url ?? defaultUrl;
|
|
79
|
+
const fetchImpl = opts?.fetchImpl ?? fetch;
|
|
80
|
+
const timeoutMs = opts?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
81
|
+
const userAgent = opts?.userAgent ?? DEFAULT_USER_AGENT;
|
|
82
|
+
const res = await fetchImpl(url, {
|
|
83
|
+
headers: { "User-Agent": userAgent, Accept: "application/json" },
|
|
84
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
85
|
+
});
|
|
86
|
+
if (!res.ok) {
|
|
87
|
+
throw new Error(`coingecko returned HTTP ${res.status.toString()}`);
|
|
88
|
+
}
|
|
89
|
+
return await res.json();
|
|
90
|
+
}
|
|
91
|
+
function isPositiveNumber(v) {
|
|
92
|
+
return typeof v === "number" && Number.isFinite(v) && v > 0;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// src/lib/currency.ts
|
|
96
|
+
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
97
|
+
import { dirname } from "path";
|
|
98
|
+
|
|
99
|
+
// src/defaults.ts
|
|
100
|
+
import { homedir } from "os";
|
|
101
|
+
import { join } from "path";
|
|
102
|
+
var CONFIG_DIR = process.env.DVM_CONFIG_DIR ?? join(homedir(), ".dvm");
|
|
103
|
+
var CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
104
|
+
var CONFIG_BACKUP_FILE = join(CONFIG_DIR, "config.json.bak");
|
|
105
|
+
var CONFIG_AUDIT_FILE = join(CONFIG_DIR, "config-audit.jsonl");
|
|
106
|
+
var CONFIG_PINNED_FILE = join(CONFIG_DIR, "config.json.pinned");
|
|
107
|
+
var JOBS_FILE = join(CONFIG_DIR, "jobs.json");
|
|
108
|
+
var REQUEST_INTENTS_DIR = join(CONFIG_DIR, "request-intents");
|
|
109
|
+
var FAVORITES_FILE = join(CONFIG_DIR, "favorites.json");
|
|
110
|
+
var MESSAGES_DIR = join(CONFIG_DIR, "messages");
|
|
111
|
+
var RECEIPTS_FILE = join(CONFIG_DIR, "receipts.jsonl");
|
|
112
|
+
var CREDITS_FILE = join(CONFIG_DIR, "credits.json");
|
|
113
|
+
var X402_CHANNELS_FILE = join(CONFIG_DIR, "x402-channels.json");
|
|
114
|
+
var TEMPO_CHANNELS_FILE = join(CONFIG_DIR, "tempo-channels.json");
|
|
115
|
+
var FEEDBACK_NUDGES_FILE = join(CONFIG_DIR, "feedback-nudges.json");
|
|
116
|
+
var RECOVERY_MNEMONIC_FILE = join(CONFIG_DIR, "recovery.mnemonic");
|
|
117
|
+
var WALLET_FILE = join(CONFIG_DIR, "wallet.json");
|
|
118
|
+
var AGENT_MNEMONIC_FILE = join(CONFIG_DIR, "wallet.mnemonic");
|
|
119
|
+
var IDENTITIES_FILE = join(CONFIG_DIR, "identities.json");
|
|
120
|
+
var PAIRINGS_DIR = join(CONFIG_DIR, "pairings");
|
|
121
|
+
var BUDGET_FILE = join(CONFIG_DIR, "budget.json");
|
|
122
|
+
var BTC_RATE_FILE = join(CONFIG_DIR, "btc-rate.json");
|
|
123
|
+
var ADMIN_DIR = join(CONFIG_DIR, "admin");
|
|
124
|
+
var PROXY_STATE_FILE = join(ADMIN_DIR, "proxy.json");
|
|
125
|
+
var BUILDER_CONFIG_DIR = process.env.DVMKIT_CONFIG_DIR ?? join(homedir(), ".dvmkit");
|
|
126
|
+
var BUILDER_FILE = join(BUILDER_CONFIG_DIR, "builder.json");
|
|
127
|
+
var BUILDER_MNEMONIC_FILE = join(BUILDER_CONFIG_DIR, "recovery.mnemonic");
|
|
128
|
+
var BUILDER_KEY_FILE = join(BUILDER_CONFIG_DIR, "builder.key");
|
|
129
|
+
var MELT_PENDING_LOG_FILE = join(BUILDER_CONFIG_DIR, "melt-pending.log");
|
|
130
|
+
var DRAIN_PARK_LOG_FILE = join(BUILDER_CONFIG_DIR, "drain-park.log");
|
|
131
|
+
var DRAIN_HOLD_STATE_FILE = join(BUILDER_CONFIG_DIR, "drain-hold.json");
|
|
132
|
+
var MELT_SUCCESS_FILE = join(BUILDER_CONFIG_DIR, "melt-success.json");
|
|
133
|
+
|
|
134
|
+
// src/lib/config-dir.ts
|
|
135
|
+
import { chmodSync, mkdirSync, statSync } from "fs";
|
|
136
|
+
var CONFIG_DIR_MODE = 448;
|
|
137
|
+
function ensureConfigDir() {
|
|
138
|
+
ensureConfigSubdir(CONFIG_DIR);
|
|
139
|
+
}
|
|
140
|
+
function ensureConfigSubdir(path) {
|
|
141
|
+
mkdirSync(path, { recursive: true, mode: CONFIG_DIR_MODE });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// src/lib/currency.ts
|
|
145
|
+
function msatsToUsd(msats, rate) {
|
|
146
|
+
return msats / 1e11 * rate;
|
|
147
|
+
}
|
|
148
|
+
function usdToMsats(usd, rate) {
|
|
149
|
+
return Math.round(usd / rate * 1e11);
|
|
150
|
+
}
|
|
151
|
+
function msatsToUsdCents(msats, btcPriceUsd) {
|
|
152
|
+
const usd = msatsToUsd(msats, btcPriceUsd);
|
|
153
|
+
return Math.round(usd * 100);
|
|
154
|
+
}
|
|
155
|
+
function msatsToUsdc(msats, btcUsdRate) {
|
|
156
|
+
const usd = msatsToUsd(msats, btcUsdRate);
|
|
157
|
+
return Math.ceil(usd * 1e6);
|
|
158
|
+
}
|
|
159
|
+
function usdcToMsats(usdc, btcUsdRate) {
|
|
160
|
+
return usdToMsats(usdc, btcUsdRate);
|
|
161
|
+
}
|
|
162
|
+
function formatSats(msats) {
|
|
163
|
+
if (msats === 0) return "0 sats";
|
|
164
|
+
if (msats < 1e3) return `${msats.toLocaleString("en-US")} msats`;
|
|
165
|
+
if (msats % 1e3 === 0) return `${(msats / 1e3).toLocaleString("en-US")} sats`;
|
|
166
|
+
return `${Math.round(msats / 1e3 * 100) / 100} sats`;
|
|
167
|
+
}
|
|
168
|
+
function formatAmount(msats, rate, opts) {
|
|
169
|
+
const usd = msatsToUsd(msats, rate);
|
|
170
|
+
const base = `~$${formatUsd(usd)} (${formatSats(msats)})`;
|
|
171
|
+
return opts?.anchor === false ? base : `${base} \u2014 ${costAnchor(usd)}`;
|
|
172
|
+
}
|
|
173
|
+
function formatPaymentAmount(msats, fiat, opts) {
|
|
174
|
+
if (!fiat || fiat.amount <= 0) return formatAmountBestEffort(msats, opts);
|
|
175
|
+
const base = `${formatFiat(fiat.amount, fiat.currency)} (${formatSats(msats)})`;
|
|
176
|
+
return opts?.anchor === false ? base : `${base} \u2014 ${costAnchor(fiat.amount, fiat.currency)}`;
|
|
177
|
+
}
|
|
178
|
+
var COST_ANCHOR_BANDS = {
|
|
179
|
+
usd: [0.01, 0.1, 1, 10],
|
|
180
|
+
eur: [0.01, 0.1, 1, 10],
|
|
181
|
+
gbp: [0.01, 0.1, 1, 10],
|
|
182
|
+
jpy: [1, 10, 100, 1500]
|
|
183
|
+
};
|
|
184
|
+
function costAnchor(amount, currency = "usd") {
|
|
185
|
+
const bands = COST_ANCHOR_BANDS[currency] ?? COST_ANCHOR_BANDS.usd;
|
|
186
|
+
if (amount < bands[0]) return "fraction of a cent";
|
|
187
|
+
if (amount <= bands[1]) return "comparable to an API call";
|
|
188
|
+
if (amount <= bands[2]) return "comparable to a ChatGPT query";
|
|
189
|
+
if (amount <= bands[3]) return "comparable to a coffee";
|
|
190
|
+
return "significant purchase \u2014 review carefully";
|
|
191
|
+
}
|
|
192
|
+
function monetaryJson(msats, rate) {
|
|
193
|
+
const usd = msatsToUsd(msats, rate);
|
|
194
|
+
return {
|
|
195
|
+
msats,
|
|
196
|
+
usd: Math.round(usd * 100) / 100,
|
|
197
|
+
anchor: costAnchor(usd),
|
|
198
|
+
display: formatAmount(msats, rate)
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
function formatUsd(usd) {
|
|
202
|
+
if (usd === 0) return "0.00";
|
|
203
|
+
if (usd >= 0.01) return usd.toFixed(2);
|
|
204
|
+
const digits = Math.max(2, -Math.floor(Math.log10(usd)) + 1);
|
|
205
|
+
return usd.toFixed(digits);
|
|
206
|
+
}
|
|
207
|
+
function formatUsdcMicrounits(microunits) {
|
|
208
|
+
const value = typeof microunits === "bigint" ? microunits : BigInt(microunits);
|
|
209
|
+
const whole = value / 1000000n;
|
|
210
|
+
const fraction = (value % 1000000n).toString().padStart(6, "0").replace(/0+$/, "");
|
|
211
|
+
return `${whole}.${fraction.padEnd(2, "0") || "00"}`;
|
|
212
|
+
}
|
|
213
|
+
function parseBudget(budget, rate) {
|
|
214
|
+
const value = assertValidBudget(budget);
|
|
215
|
+
return budget.startsWith("$") ? usdToMsats(value, rate) : value;
|
|
216
|
+
}
|
|
217
|
+
function assertValidBudget(budget) {
|
|
218
|
+
const value = budget.startsWith("$") ? parseFloat(budget.slice(1)) : parseInt(budget, 10);
|
|
219
|
+
if (isNaN(value)) throw new Error(`Invalid budget: ${budget}`);
|
|
220
|
+
return value;
|
|
221
|
+
}
|
|
222
|
+
async function fetchBtcUsdRate() {
|
|
223
|
+
if (memRate && Date.now() - memTime < CACHE_TTL) {
|
|
224
|
+
return memRate;
|
|
225
|
+
}
|
|
226
|
+
const disk = readDiskCache();
|
|
227
|
+
if (disk) {
|
|
228
|
+
memRate = disk;
|
|
229
|
+
memTime = Date.now();
|
|
230
|
+
return disk;
|
|
231
|
+
}
|
|
232
|
+
const rate = await fetchFromApi();
|
|
233
|
+
memRate = rate;
|
|
234
|
+
memTime = Date.now();
|
|
235
|
+
writeDiskCache(rate);
|
|
236
|
+
return rate;
|
|
237
|
+
}
|
|
238
|
+
function getLastKnownBtcRate() {
|
|
239
|
+
if (memRate && memTime > 0) return { rate: memRate, cachedAt: memTime };
|
|
240
|
+
return readDiskCacheStale();
|
|
241
|
+
}
|
|
242
|
+
function formatStaleness(cachedAt, now = Date.now()) {
|
|
243
|
+
const ageMs = Math.max(0, now - cachedAt);
|
|
244
|
+
const seconds = Math.floor(ageMs / 1e3);
|
|
245
|
+
if (seconds < 60) return `${seconds}s ago`;
|
|
246
|
+
const minutes = Math.floor(seconds / 60);
|
|
247
|
+
if (minutes < 60) return `${minutes} min ago`;
|
|
248
|
+
const hours = Math.floor(minutes / 60);
|
|
249
|
+
if (hours < 24) return `${hours} hour${hours === 1 ? "" : "s"} ago`;
|
|
250
|
+
const days = Math.floor(hours / 24);
|
|
251
|
+
return `${days} day${days === 1 ? "" : "s"} ago`;
|
|
252
|
+
}
|
|
253
|
+
function formatAmountBestEffort(msats, opts) {
|
|
254
|
+
const m = msats ?? 0;
|
|
255
|
+
const cached = getLastKnownBtcRate();
|
|
256
|
+
if (cached === null) return `${Math.round(m / 1e3)} sats`;
|
|
257
|
+
const base = formatAmount(m, cached.rate, opts);
|
|
258
|
+
if (Date.now() - cached.cachedAt < CACHE_TTL) return base;
|
|
259
|
+
return `${base} (rate cached ${formatStaleness(cached.cachedAt)})`;
|
|
260
|
+
}
|
|
261
|
+
var CACHE_TTL = 5 * 60 * 1e3;
|
|
262
|
+
var CACHE_FILE = BTC_RATE_FILE;
|
|
263
|
+
var memRate = null;
|
|
264
|
+
var memTime = 0;
|
|
265
|
+
function readDiskCache() {
|
|
266
|
+
try {
|
|
267
|
+
if (!existsSync(CACHE_FILE)) return null;
|
|
268
|
+
const data = JSON.parse(readFileSync(CACHE_FILE, "utf-8"));
|
|
269
|
+
if (Date.now() - data.timestamp >= CACHE_TTL) return null;
|
|
270
|
+
if (!isPlausibleBtcUsdRate(data.rate)) return null;
|
|
271
|
+
return data.rate;
|
|
272
|
+
} catch {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
function readDiskCacheStale() {
|
|
277
|
+
try {
|
|
278
|
+
if (!existsSync(CACHE_FILE)) return null;
|
|
279
|
+
const data = JSON.parse(readFileSync(CACHE_FILE, "utf-8"));
|
|
280
|
+
if (!isPlausibleBtcUsdRate(data.rate)) return null;
|
|
281
|
+
return { rate: data.rate, cachedAt: data.timestamp };
|
|
282
|
+
} catch {
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
function writeDiskCache(rate) {
|
|
287
|
+
try {
|
|
288
|
+
ensureConfigSubdir(dirname(CACHE_FILE));
|
|
289
|
+
writeFileSync(CACHE_FILE, JSON.stringify({ rate, timestamp: Date.now() }));
|
|
290
|
+
} catch {
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
var MAX_PLAUSIBLE_BTC_USD = 1e7;
|
|
294
|
+
function isPlausibleBtcUsdRate(rate) {
|
|
295
|
+
return typeof rate === "number" && Number.isFinite(rate) && rate > 0 && rate < MAX_PLAUSIBLE_BTC_USD;
|
|
296
|
+
}
|
|
297
|
+
async function fetchFromApi() {
|
|
298
|
+
const spot = await fetchCoinGeckoBtcUsd();
|
|
299
|
+
const rate = spot.btc_usd;
|
|
300
|
+
if (!isPlausibleBtcUsdRate(rate)) {
|
|
301
|
+
throw new Error(`implausible BTC/USD rate from upstream: ${String(rate)}`);
|
|
302
|
+
}
|
|
303
|
+
return rate;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// src/lib/payment-rails/rpc.ts
|
|
307
|
+
function resolveRpcOverride(envValue, bundled) {
|
|
308
|
+
const override = envValue?.trim() ?? "";
|
|
309
|
+
return override.length > 0 ? { url: override, source: "override" } : { url: bundled, source: "bundled" };
|
|
310
|
+
}
|
|
311
|
+
function resolveRpcHttpTransportOptions(source, envValue) {
|
|
312
|
+
if (source !== "override" || !envValue?.trim()) return void 0;
|
|
313
|
+
let parsed;
|
|
314
|
+
try {
|
|
315
|
+
parsed = JSON.parse(envValue);
|
|
316
|
+
} catch {
|
|
317
|
+
throw new Error("RPC headers must be a JSON object of non-empty string values.");
|
|
318
|
+
}
|
|
319
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
320
|
+
throw new Error("RPC headers must be a JSON object of non-empty string values.");
|
|
321
|
+
const headers = {};
|
|
322
|
+
for (const [name, value] of Object.entries(parsed)) {
|
|
323
|
+
if (!/^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/.test(name) || typeof value !== "string" || value.length === 0 || /[\r\n]/.test(value))
|
|
324
|
+
throw new Error("RPC headers must be a JSON object of non-empty string values.");
|
|
325
|
+
headers[name] = value;
|
|
326
|
+
}
|
|
327
|
+
if (Object.keys(headers).length === 0)
|
|
328
|
+
throw new Error("RPC headers must be a JSON object of non-empty string values.");
|
|
329
|
+
return { fetchOptions: { headers } };
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export {
|
|
333
|
+
DvmError,
|
|
334
|
+
providerProseSources,
|
|
335
|
+
transportErrorCode,
|
|
336
|
+
IMPLICIT_CREDIT_TTL_MS,
|
|
337
|
+
CONFIG_FILE,
|
|
338
|
+
CONFIG_BACKUP_FILE,
|
|
339
|
+
CONFIG_AUDIT_FILE,
|
|
340
|
+
CONFIG_PINNED_FILE,
|
|
341
|
+
RECEIPTS_FILE,
|
|
342
|
+
CREDITS_FILE,
|
|
343
|
+
X402_CHANNELS_FILE,
|
|
344
|
+
TEMPO_CHANNELS_FILE,
|
|
345
|
+
WALLET_FILE,
|
|
346
|
+
AGENT_MNEMONIC_FILE,
|
|
347
|
+
IDENTITIES_FILE,
|
|
348
|
+
BUDGET_FILE,
|
|
349
|
+
BUILDER_CONFIG_DIR,
|
|
350
|
+
BUILDER_FILE,
|
|
351
|
+
BUILDER_KEY_FILE,
|
|
352
|
+
ensureConfigDir,
|
|
353
|
+
COINGECKO_SIMPLE_PRICE_URL,
|
|
354
|
+
fetchCoinGeckoSpot,
|
|
355
|
+
msatsToUsd,
|
|
356
|
+
usdToMsats,
|
|
357
|
+
msatsToUsdCents,
|
|
358
|
+
msatsToUsdc,
|
|
359
|
+
usdcToMsats,
|
|
360
|
+
formatSats,
|
|
361
|
+
formatAmount,
|
|
362
|
+
formatPaymentAmount,
|
|
363
|
+
costAnchor,
|
|
364
|
+
monetaryJson,
|
|
365
|
+
formatUsd,
|
|
366
|
+
formatUsdcMicrounits,
|
|
367
|
+
parseBudget,
|
|
368
|
+
assertValidBudget,
|
|
369
|
+
fetchBtcUsdRate,
|
|
370
|
+
getLastKnownBtcRate,
|
|
371
|
+
formatStaleness,
|
|
372
|
+
formatAmountBestEffort,
|
|
373
|
+
resolveRpcOverride,
|
|
374
|
+
resolveRpcHttpTransportOptions
|
|
375
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// src/lib/caller-loggers.ts
|
|
2
|
+
var callerLoggers = buildNoopLoggers();
|
|
3
|
+
function initCallerLoggers(nextLoggers) {
|
|
4
|
+
callerLoggers = nextLoggers;
|
|
5
|
+
}
|
|
6
|
+
function buildNoopLoggers() {
|
|
7
|
+
const noopLogger = {
|
|
8
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function -- standalone caller has no exporter
|
|
9
|
+
info() {
|
|
10
|
+
},
|
|
11
|
+
async span(_name, _attrs, fn) {
|
|
12
|
+
return fn();
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
return {
|
|
16
|
+
cashu: noopLogger,
|
|
17
|
+
lightning: noopLogger,
|
|
18
|
+
x402: noopLogger
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export {
|
|
23
|
+
callerLoggers,
|
|
24
|
+
initCallerLoggers
|
|
25
|
+
};
|