@dvmkit/sdk 0.6.0-rc.1 → 0.7.0-rc.10
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 +39 -2
- package/dist/{chunk-PLG4XER6.js → chunk-2NUFE2PG.js} +47 -5
- package/dist/{chunk-7ZUKL4IV.js → chunk-3E4C6W7W.js} +16 -5
- package/dist/{chunk-HHBDBMEO.js → chunk-53SNRGQQ.js} +375 -158
- package/dist/{chunk-IWQHUSMO.js → chunk-62FI3NJF.js} +78 -2
- package/dist/chunk-6HJEVCVI.js +17 -0
- package/dist/chunk-A262WVYX.js +6 -0
- package/dist/{chunk-RLTSCKRI.js → chunk-BPYMEMQX.js} +50 -3
- package/dist/{chunk-IV6Q7NRW.js → chunk-C5RIQWG4.js} +1 -1
- package/dist/{chunk-GAOAYHZA.js → chunk-FBQVOOYF.js} +37 -6
- package/dist/{chunk-Z4ZT5LNT.js → chunk-GEI4MAXE.js} +3 -3
- package/dist/{chunk-FYRR2UEZ.js → chunk-GLIAR7RN.js} +1 -1
- package/dist/{chunk-X7EZ5CLI.js → chunk-HCATFJDD.js} +3 -3
- package/dist/{chunk-BYCEKO65.js → chunk-HPCXF3DV.js} +220 -32
- package/dist/{chunk-JH3SX3LY.js → chunk-IRXIH775.js} +6 -6
- package/dist/{chunk-QR2EF2NI.js → chunk-KCQDXIJU.js} +1 -42
- package/dist/chunk-MDW7ZR7V.js +47 -0
- package/dist/{chunk-KA6VN5FK.js → chunk-MQYM4T66.js} +3 -3
- package/dist/{chunk-P6QON7ER.js → chunk-NV7Z7377.js} +1 -1
- package/dist/{chunk-OQA7IHI4.js → chunk-OBBVDKLE.js} +484 -234
- package/dist/{chunk-EQLA4FFY.js → chunk-OHUXCAOM.js} +17 -6
- package/dist/{chunk-FI4CST2D.js → chunk-PZ5IAWFD.js} +5 -7
- package/dist/{chunk-IXR5RFK7.js → chunk-R3TQSXWT.js} +191 -5
- package/dist/{chunk-VHNPVGSA.js → chunk-T5DIXOLR.js} +3 -3
- package/dist/{chunk-RCM3P2HE.js → chunk-UKD3R2KU.js} +55 -4
- package/dist/chunk-WMGXJQI3.js +120 -0
- package/dist/{chunk-N5RZ5HVB.js → chunk-X6S2CHFM.js} +2 -2
- package/dist/{chunk-ETXDTUJO.js → chunk-ZCNPIILZ.js} +112 -1
- package/dist/{content-retention-BTGSQRfP.d.ts → content-retention-D2K4MKNO.d.ts} +1 -1
- package/dist/{credit-ledger-MI2H7NEC.js → credit-ledger-MFIIOSZH.js} +4 -5
- package/dist/{credit-menu-BAsrRu3z.d.ts → credit-menu-D2t1-Tf6.d.ts} +13 -6
- package/dist/{fx-BgDmyTdd.d.ts → fx-Ql4nvdbh.d.ts} +10 -3
- package/dist/index.d.ts +5 -5
- package/dist/index.js +248 -16
- package/dist/internal/caller.d.ts +4586 -7318
- package/dist/internal/caller.js +4431 -8127
- package/dist/internal/index.js +1 -1
- package/dist/internal/server.d.ts +7 -7
- package/dist/internal/server.js +30 -33
- package/dist/{job-store-BVeF5_Jq.d.ts → job-store-DCvJQUX0.d.ts} +1 -1
- package/dist/{lightning-backend-40fIzz1W.d.ts → lightning-backend-CY5ML3km.d.ts} +4 -2
- package/dist/memory-credit-ledger-44NE4PZC.js +11 -0
- package/dist/{mpp-setup-RF5FXGGV.js → mpp-setup-HRWUBULX.js} +4 -2
- package/dist/{payout-reporter-3Y2U55ZX.js → payout-reporter-7P66T77I.js} +6 -4
- package/dist/{postgres-consumed-credential-store-BUVCAPLS.js → postgres-consumed-credential-store-IDGPFKIL.js} +3 -3
- package/dist/{postgres-job-store-VHLQBJZC.js → postgres-job-store-UKNJ2F6Z.js} +2 -2
- package/dist/{postgres-replay-store-ARZSQ45X.js → postgres-replay-store-PMA4ADXN.js} +2 -2
- package/dist/{pricing-XT2IAPNE.js → pricing-52XJHJ6K.js} +12 -12
- package/dist/{revenue-reporter-MAERPYGK.js → revenue-reporter-HV7RMS54.js} +2 -2
- package/dist/server/index.d.ts +12 -11
- package/dist/server/index.js +49 -53
- package/dist/{step-cache-CPdH1hei.d.ts → step-cache-BOWAn_BH.d.ts} +84 -7
- package/dist/{tempo-charge-store-Q3D7KYMH.js → tempo-charge-store-P2Z4RMU7.js} +2 -2
- package/dist/{tempo-session-store-ZRNCOX7P.js → tempo-session-store-VSP7KLBS.js} +3 -3
- package/dist/testing/index.d.ts +3 -3
- package/dist/testing/index.js +7 -6
- package/dist/{usd-AWlrwlM5.d.ts → usd-MAtMp3DB.d.ts} +1 -1
- package/dist/{x402-R6HPBY5D.js → x402-3IQ3HXSG.js} +9 -29
- package/package.json +3 -2
- package/dist/chunk-AT6V3SY7.js +0 -102
- package/dist/chunk-B2IROPQF.js +0 -1844
- package/dist/chunk-CZULM42S.js +0 -143
- package/dist/chunk-GFY5FRX3.js +0 -218
- package/dist/chunk-HCLVYZUZ.js +0 -244
- package/dist/chunk-LE74UEV7.js +0 -1071
- package/dist/chunk-LIRAHP7B.js +0 -53
- package/dist/chunk-MZRJR2CW.js +0 -776
- package/dist/chunk-OFVV37XC.js +0 -57
- package/dist/chunk-PAIM66YA.js +0 -347
- package/dist/chunk-UOCZXO2Z.js +0 -1663
- package/dist/memory-credit-ledger-W2BAUYJP.js +0 -12
- package/dist/tempo-lifecycle-ZBBLFBMH.js +0 -15
- package/dist/tempo-wallet-B5XXV3YH.js +0 -40
|
@@ -1,34 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
computeResultHash,
|
|
3
|
-
costAnchor,
|
|
4
|
-
formatUsd,
|
|
5
|
-
isDrainReceipt,
|
|
6
|
-
isFundingReceipt,
|
|
7
|
-
msatsToUsd,
|
|
8
|
-
verifyDrainReceipt,
|
|
9
|
-
verifyFundingReceipt,
|
|
10
|
-
verifyReceipt
|
|
11
|
-
} from "./chunk-PAIM66YA.js";
|
|
12
|
-
import {
|
|
13
|
-
AGENT_MNEMONIC_FILE,
|
|
14
|
-
BUILDER_CONFIG_DIR,
|
|
15
|
-
BUILDER_FILE,
|
|
16
|
-
BUILDER_KEY_FILE,
|
|
17
|
-
WALLET_FILE,
|
|
18
|
-
ensureConfigDir
|
|
19
|
-
} from "./chunk-CZULM42S.js";
|
|
20
1
|
import {
|
|
21
2
|
canonicaliseForSigning
|
|
22
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-UKD3R2KU.js";
|
|
23
4
|
import {
|
|
24
5
|
callerLoggers
|
|
25
6
|
} from "./chunk-66HGCPBU.js";
|
|
26
7
|
import {
|
|
27
8
|
DvmError
|
|
28
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-BPYMEMQX.js";
|
|
29
10
|
import {
|
|
30
11
|
diagnosticConsole
|
|
31
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-ZCNPIILZ.js";
|
|
32
13
|
|
|
33
14
|
// src/lib/cashu/lock-pubkey.ts
|
|
34
15
|
function toLockPubkey(raw) {
|
|
@@ -40,45 +21,257 @@ function isX402SettlementReconciliationReason(value) {
|
|
|
40
21
|
return value === "settlement_not_on_chain" || value === "chain_unreachable" || value === "settlement_unbookmarked" || value === "facilitator_channel_state_unavailable";
|
|
41
22
|
}
|
|
42
23
|
|
|
43
|
-
// src/lib/
|
|
44
|
-
var
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
24
|
+
// src/lib/credit-implicit.ts
|
|
25
|
+
var IMPLICIT_CREDIT_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
26
|
+
|
|
27
|
+
// src/lib/payment-rails/tempo-wire.ts
|
|
28
|
+
var TEMPO_SESSION_UPDATED_REASON = "Tempo session updated; attach the next voucher to fund this credit.";
|
|
29
|
+
|
|
30
|
+
// src/defaults.ts
|
|
31
|
+
import { homedir } from "os";
|
|
32
|
+
import { join } from "path";
|
|
33
|
+
var CONFIG_DIR = process.env.DVM_CONFIG_DIR ?? join(homedir(), ".dvm");
|
|
34
|
+
var CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
35
|
+
var CONFIG_BACKUP_FILE = join(CONFIG_DIR, "config.json.bak");
|
|
36
|
+
var CONFIG_AUDIT_FILE = join(CONFIG_DIR, "config-audit.jsonl");
|
|
37
|
+
var CONFIG_PINNED_FILE = join(CONFIG_DIR, "config.json.pinned");
|
|
38
|
+
var JOBS_FILE = join(CONFIG_DIR, "jobs.json");
|
|
39
|
+
var REQUEST_INTENTS_DIR = join(CONFIG_DIR, "request-intents");
|
|
40
|
+
var FAVORITES_FILE = join(CONFIG_DIR, "favorites.json");
|
|
41
|
+
var MESSAGES_DIR = join(CONFIG_DIR, "messages");
|
|
42
|
+
var RECEIPTS_FILE = join(CONFIG_DIR, "receipts.jsonl");
|
|
43
|
+
var CREDITS_FILE = join(CONFIG_DIR, "credits.json");
|
|
44
|
+
var X402_CHANNELS_FILE = join(CONFIG_DIR, "x402-channels.json");
|
|
45
|
+
var TEMPO_CHANNELS_FILE = join(CONFIG_DIR, "tempo-channels.json");
|
|
46
|
+
var FEEDBACK_NUDGES_FILE = join(CONFIG_DIR, "feedback-nudges.json");
|
|
47
|
+
var RECOVERY_MNEMONIC_FILE = join(CONFIG_DIR, "recovery.mnemonic");
|
|
48
|
+
var WALLET_FILE = join(CONFIG_DIR, "wallet.json");
|
|
49
|
+
var AGENT_MNEMONIC_FILE = join(CONFIG_DIR, "wallet.mnemonic");
|
|
50
|
+
var IDENTITIES_FILE = join(CONFIG_DIR, "identities.json");
|
|
51
|
+
var PAIRINGS_DIR = join(CONFIG_DIR, "pairings");
|
|
52
|
+
var BUDGET_FILE = join(CONFIG_DIR, "budget.json");
|
|
53
|
+
var BTC_RATE_FILE = join(CONFIG_DIR, "btc-rate.json");
|
|
54
|
+
var ADMIN_DIR = join(CONFIG_DIR, "admin");
|
|
55
|
+
var PROXY_STATE_FILE = join(ADMIN_DIR, "proxy.json");
|
|
56
|
+
var BUILDER_CONFIG_DIR = process.env.DVMKIT_CONFIG_DIR ?? join(homedir(), ".dvmkit");
|
|
57
|
+
var BUILDER_FILE = join(BUILDER_CONFIG_DIR, "builder.json");
|
|
58
|
+
var BUILDER_MNEMONIC_FILE = join(BUILDER_CONFIG_DIR, "recovery.mnemonic");
|
|
59
|
+
var BUILDER_KEY_FILE = join(BUILDER_CONFIG_DIR, "builder.key");
|
|
60
|
+
var MELT_PENDING_LOG_FILE = join(BUILDER_CONFIG_DIR, "melt-pending.log");
|
|
61
|
+
var DRAIN_PARK_LOG_FILE = join(BUILDER_CONFIG_DIR, "drain-park.log");
|
|
62
|
+
var DRAIN_HOLD_STATE_FILE = join(BUILDER_CONFIG_DIR, "drain-hold.json");
|
|
63
|
+
var MELT_SUCCESS_FILE = join(BUILDER_CONFIG_DIR, "melt-success.json");
|
|
64
|
+
|
|
65
|
+
// src/lib/config-dir.ts
|
|
66
|
+
import { chmodSync, mkdirSync, statSync } from "fs";
|
|
67
|
+
var CONFIG_DIR_MODE = 448;
|
|
68
|
+
function ensureConfigDir() {
|
|
69
|
+
ensureConfigSubdir(CONFIG_DIR);
|
|
70
|
+
}
|
|
71
|
+
function ensureConfigSubdir(path) {
|
|
72
|
+
mkdirSync(path, { recursive: true, mode: CONFIG_DIR_MODE });
|
|
73
|
+
}
|
|
74
|
+
function repairConfigDirMode() {
|
|
75
|
+
const base = {
|
|
76
|
+
path: CONFIG_DIR,
|
|
77
|
+
exists: false,
|
|
78
|
+
modeOctal: null,
|
|
79
|
+
repaired: false,
|
|
80
|
+
skippedNonPosix: process.platform === "win32"
|
|
81
|
+
};
|
|
82
|
+
if (base.skippedNonPosix) return base;
|
|
83
|
+
let mode;
|
|
84
|
+
try {
|
|
85
|
+
mode = statSync(CONFIG_DIR).mode & 511;
|
|
86
|
+
} catch (err) {
|
|
87
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
88
|
+
const missing = err.code === "ENOENT";
|
|
89
|
+
return missing ? base : { ...base, error: message };
|
|
54
90
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
91
|
+
if ((mode & 63) === 0) {
|
|
92
|
+
return { ...base, exists: true, modeOctal: toOctal(mode) };
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
chmodSync(CONFIG_DIR, CONFIG_DIR_MODE);
|
|
96
|
+
} catch (err) {
|
|
97
|
+
return {
|
|
98
|
+
...base,
|
|
99
|
+
exists: true,
|
|
100
|
+
modeOctal: toOctal(mode),
|
|
101
|
+
error: err instanceof Error ? err.message : String(err)
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
...base,
|
|
106
|
+
exists: true,
|
|
107
|
+
modeOctal: toOctal(CONFIG_DIR_MODE),
|
|
108
|
+
previousModeOctal: toOctal(mode),
|
|
109
|
+
repaired: true
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function toOctal(mode) {
|
|
113
|
+
return "0" + mode.toString(8).padStart(3, "0");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// src/lib/fx-coingecko.ts
|
|
117
|
+
var COINGECKO_SIMPLE_PRICE_URL = "https://api.coingecko.com/api/v3/simple/price?ids=bitcoin,usd-coin&vs_currencies=usd";
|
|
118
|
+
var COINGECKO_BTC_USD_URL = "https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd";
|
|
119
|
+
var DEFAULT_TIMEOUT_MS = 5e3;
|
|
120
|
+
var DEFAULT_USER_AGENT = "dvmkit-platform/1.0 (+https://dvmkit.com)";
|
|
121
|
+
async function fetchCoinGeckoSpot(opts) {
|
|
122
|
+
const body = await fetchCoinGeckoBody(opts, COINGECKO_SIMPLE_PRICE_URL);
|
|
123
|
+
const btc = body.bitcoin?.usd;
|
|
124
|
+
const usdc = body["usd-coin"]?.usd;
|
|
125
|
+
if (!isPositiveNumber(btc)) {
|
|
126
|
+
throw new Error("coingecko response missing bitcoin.usd");
|
|
127
|
+
}
|
|
128
|
+
if (!isPositiveNumber(usdc)) {
|
|
129
|
+
throw new Error("coingecko response missing usd-coin.usd");
|
|
130
|
+
}
|
|
131
|
+
return { btc_usd: btc, usdc_usd: usdc, as_of: (/* @__PURE__ */ new Date()).toISOString() };
|
|
132
|
+
}
|
|
133
|
+
async function fetchCoinGeckoBtcUsd(opts) {
|
|
134
|
+
const body = await fetchCoinGeckoBody(opts, COINGECKO_BTC_USD_URL);
|
|
135
|
+
const btc = body.bitcoin?.usd;
|
|
136
|
+
if (!isPositiveNumber(btc)) {
|
|
137
|
+
throw new Error("coingecko response missing bitcoin.usd");
|
|
138
|
+
}
|
|
139
|
+
return { btc_usd: btc, as_of: (/* @__PURE__ */ new Date()).toISOString() };
|
|
140
|
+
}
|
|
141
|
+
async function fetchCoinGeckoBody(opts, defaultUrl) {
|
|
142
|
+
const url = opts?.url ?? defaultUrl;
|
|
143
|
+
const fetchImpl = opts?.fetchImpl ?? fetch;
|
|
144
|
+
const timeoutMs = opts?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
145
|
+
const userAgent = opts?.userAgent ?? DEFAULT_USER_AGENT;
|
|
146
|
+
const res = await fetchImpl(url, {
|
|
147
|
+
headers: { "User-Agent": userAgent, Accept: "application/json" },
|
|
148
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
149
|
+
});
|
|
150
|
+
if (!res.ok) {
|
|
151
|
+
throw new Error(`coingecko returned HTTP ${res.status.toString()}`);
|
|
152
|
+
}
|
|
153
|
+
return await res.json();
|
|
154
|
+
}
|
|
155
|
+
function isPositiveNumber(v) {
|
|
156
|
+
return typeof v === "number" && Number.isFinite(v) && v > 0;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// src/lib/currency.ts
|
|
160
|
+
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
161
|
+
import { dirname } from "path";
|
|
162
|
+
var COST_ANCHOR_BANDS = {
|
|
163
|
+
usd: [0.01, 0.1, 1, 10],
|
|
164
|
+
eur: [0.01, 0.1, 1, 10],
|
|
165
|
+
gbp: [0.01, 0.1, 1, 10],
|
|
166
|
+
jpy: [1, 10, 100, 1500]
|
|
167
|
+
};
|
|
168
|
+
function costAnchor(amount, currency = "usd") {
|
|
169
|
+
const bands = COST_ANCHOR_BANDS[currency] ?? COST_ANCHOR_BANDS.usd;
|
|
170
|
+
if (amount < bands[0]) return "fraction of a cent";
|
|
171
|
+
if (amount <= bands[1]) return "comparable to an API call";
|
|
172
|
+
if (amount <= bands[2]) return "comparable to a ChatGPT query";
|
|
173
|
+
if (amount <= bands[3]) return "comparable to a coffee";
|
|
174
|
+
return "significant purchase \u2014 review carefully";
|
|
175
|
+
}
|
|
176
|
+
function formatUsd(usd) {
|
|
177
|
+
if (usd === 0) return "0.00";
|
|
178
|
+
if (usd >= 0.01) return usd.toFixed(2);
|
|
179
|
+
const digits = Math.max(2, -Math.floor(Math.log10(usd)) + 1);
|
|
180
|
+
return usd.toFixed(digits);
|
|
181
|
+
}
|
|
182
|
+
function formatUsdcMicrounits(microunits) {
|
|
183
|
+
const value = typeof microunits === "bigint" ? microunits : BigInt(microunits);
|
|
184
|
+
const whole = value / 1000000n;
|
|
185
|
+
const fraction = (value % 1000000n).toString().padStart(6, "0").replace(/0+$/, "");
|
|
186
|
+
return `${whole}.${fraction.padEnd(2, "0") || "00"}`;
|
|
187
|
+
}
|
|
188
|
+
async function fetchBtcUsdRate() {
|
|
189
|
+
if (memRate && Date.now() - memTime < CACHE_TTL) {
|
|
190
|
+
return memRate;
|
|
191
|
+
}
|
|
192
|
+
const disk = readDiskCache();
|
|
193
|
+
if (disk) {
|
|
194
|
+
memRate = disk;
|
|
195
|
+
memTime = Date.now();
|
|
196
|
+
return disk;
|
|
197
|
+
}
|
|
198
|
+
const rate = await fetchFromApi();
|
|
199
|
+
memRate = rate;
|
|
200
|
+
memTime = Date.now();
|
|
201
|
+
writeDiskCache(rate);
|
|
202
|
+
return rate;
|
|
203
|
+
}
|
|
204
|
+
function getLastKnownBtcRate() {
|
|
205
|
+
if (memRate && memTime > 0) return { rate: memRate, cachedAt: memTime };
|
|
206
|
+
return readDiskCacheStale();
|
|
207
|
+
}
|
|
208
|
+
function formatStaleness(cachedAt, now = Date.now()) {
|
|
209
|
+
const ageMs = Math.max(0, now - cachedAt);
|
|
210
|
+
const seconds = Math.floor(ageMs / 1e3);
|
|
211
|
+
if (seconds < 60) return `${seconds}s ago`;
|
|
212
|
+
const minutes = Math.floor(seconds / 60);
|
|
213
|
+
if (minutes < 60) return `${minutes} min ago`;
|
|
214
|
+
const hours = Math.floor(minutes / 60);
|
|
215
|
+
if (hours < 24) return `${hours} hour${hours === 1 ? "" : "s"} ago`;
|
|
216
|
+
const days = Math.floor(hours / 24);
|
|
217
|
+
return `${days} day${days === 1 ? "" : "s"} ago`;
|
|
218
|
+
}
|
|
219
|
+
var CACHE_TTL = 5 * 60 * 1e3;
|
|
220
|
+
var CACHE_FILE = BTC_RATE_FILE;
|
|
221
|
+
var memRate = null;
|
|
222
|
+
var memTime = 0;
|
|
223
|
+
function readDiskCache() {
|
|
58
224
|
try {
|
|
59
|
-
|
|
225
|
+
if (!existsSync(CACHE_FILE)) return null;
|
|
226
|
+
const data = JSON.parse(readFileSync(CACHE_FILE, "utf-8"));
|
|
227
|
+
if (Date.now() - data.timestamp >= CACHE_TTL) return null;
|
|
228
|
+
if (!isPlausibleBtcUsdRate(data.rate)) return null;
|
|
229
|
+
return data.rate;
|
|
60
230
|
} catch {
|
|
61
|
-
return
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
function readDiskCacheStale() {
|
|
235
|
+
try {
|
|
236
|
+
if (!existsSync(CACHE_FILE)) return null;
|
|
237
|
+
const data = JSON.parse(readFileSync(CACHE_FILE, "utf-8"));
|
|
238
|
+
if (!isPlausibleBtcUsdRate(data.rate)) return null;
|
|
239
|
+
return { rate: data.rate, cachedAt: data.timestamp };
|
|
240
|
+
} catch {
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
function writeDiskCache(rate) {
|
|
245
|
+
try {
|
|
246
|
+
ensureConfigSubdir(dirname(CACHE_FILE));
|
|
247
|
+
writeFileSync(CACHE_FILE, JSON.stringify({ rate, timestamp: Date.now() }));
|
|
248
|
+
} catch {
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
var MAX_PLAUSIBLE_BTC_USD = 1e7;
|
|
252
|
+
function isPlausibleBtcUsdRate(rate) {
|
|
253
|
+
return typeof rate === "number" && Number.isFinite(rate) && rate > 0 && rate < MAX_PLAUSIBLE_BTC_USD;
|
|
254
|
+
}
|
|
255
|
+
async function fetchFromApi() {
|
|
256
|
+
const spot = await fetchCoinGeckoBtcUsd();
|
|
257
|
+
const rate = spot.btc_usd;
|
|
258
|
+
if (!isPlausibleBtcUsdRate(rate)) {
|
|
259
|
+
throw new Error(`implausible BTC/USD rate from upstream: ${String(rate)}`);
|
|
62
260
|
}
|
|
63
|
-
|
|
64
|
-
if (LOOPBACK_HOSTS.has(host)) return true;
|
|
65
|
-
if (host.endsWith(".localhost")) return true;
|
|
66
|
-
return TEST_MINT_HOSTS.has(host);
|
|
261
|
+
return rate;
|
|
67
262
|
}
|
|
68
|
-
var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "0.0.0.0", "::1"]);
|
|
69
|
-
var TEST_MINT_HOSTS = /* @__PURE__ */ new Set(["testnut.cashu.space", "dvmkit-testmint.fly.dev"]);
|
|
70
263
|
|
|
71
264
|
// src/lib/secret-backup.ts
|
|
72
|
-
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
265
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
73
266
|
function backupSecretFile(file, nextContent) {
|
|
74
|
-
const content =
|
|
267
|
+
const content = existsSync2(file) ? readFileSync2(file) : null;
|
|
75
268
|
if (content === null) return null;
|
|
76
269
|
if (content.toString("utf-8").trim() === nextContent?.trim()) return null;
|
|
77
270
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
78
271
|
for (let n = 0; n < 100; n++) {
|
|
79
272
|
const backup = n === 0 ? `${file}.${stamp}.bak` : `${file}.${stamp}-${String(n)}.bak`;
|
|
80
273
|
try {
|
|
81
|
-
|
|
274
|
+
writeFileSync2(backup, content, { mode: 384, flag: "wx" });
|
|
82
275
|
return backup;
|
|
83
276
|
} catch (err) {
|
|
84
277
|
if (err.code !== "EEXIST") throw err;
|
|
@@ -89,20 +282,20 @@ function backupSecretFile(file, nextContent) {
|
|
|
89
282
|
|
|
90
283
|
// src/lib/wallet-agent/agent-mnemonic-file.ts
|
|
91
284
|
import {
|
|
92
|
-
chmodSync,
|
|
93
|
-
existsSync as
|
|
94
|
-
readFileSync as
|
|
285
|
+
chmodSync as chmodSync2,
|
|
286
|
+
existsSync as existsSync3,
|
|
287
|
+
readFileSync as readFileSync3,
|
|
95
288
|
renameSync,
|
|
96
289
|
unlinkSync,
|
|
97
|
-
writeFileSync as
|
|
290
|
+
writeFileSync as writeFileSync3
|
|
98
291
|
} from "fs";
|
|
99
292
|
function readAgentMnemonic() {
|
|
100
|
-
if (!
|
|
101
|
-
const raw =
|
|
293
|
+
if (!existsSync3(AGENT_MNEMONIC_FILE)) return null;
|
|
294
|
+
const raw = readFileSync3(AGENT_MNEMONIC_FILE, "utf-8");
|
|
102
295
|
return raw.trim().toLowerCase().split(/\s+/).join(" ");
|
|
103
296
|
}
|
|
104
297
|
function assertAgentMnemonicReplaceable(opts = {}) {
|
|
105
|
-
if (
|
|
298
|
+
if (existsSync3(AGENT_MNEMONIC_FILE) && !opts.force) {
|
|
106
299
|
throw new DvmError(
|
|
107
300
|
"agent_mnemonic_exists",
|
|
108
301
|
`An agent wallet recovery mnemonic already exists at ${AGENT_MNEMONIC_FILE}.`,
|
|
@@ -116,7 +309,7 @@ function writeAgentMnemonic(mnemonic, opts = {}) {
|
|
|
116
309
|
ensureConfigDir();
|
|
117
310
|
const backupFile = backupSecretFile(AGENT_MNEMONIC_FILE, normalised);
|
|
118
311
|
const tmp = `${AGENT_MNEMONIC_FILE}.tmp`;
|
|
119
|
-
|
|
312
|
+
writeFileSync3(tmp, normalised + "\n", { mode: 384 });
|
|
120
313
|
try {
|
|
121
314
|
renameSync(tmp, AGENT_MNEMONIC_FILE);
|
|
122
315
|
} catch (err) {
|
|
@@ -127,13 +320,13 @@ function writeAgentMnemonic(mnemonic, opts = {}) {
|
|
|
127
320
|
throw err;
|
|
128
321
|
}
|
|
129
322
|
try {
|
|
130
|
-
|
|
323
|
+
chmodSync2(AGENT_MNEMONIC_FILE, 384);
|
|
131
324
|
} catch {
|
|
132
325
|
}
|
|
133
326
|
return { backupFile };
|
|
134
327
|
}
|
|
135
328
|
function deleteAgentMnemonic() {
|
|
136
|
-
if (!
|
|
329
|
+
if (!existsSync3(AGENT_MNEMONIC_FILE)) return { backupFile: null };
|
|
137
330
|
const backupFile = backupSecretFile(AGENT_MNEMONIC_FILE);
|
|
138
331
|
unlinkSync(AGENT_MNEMONIC_FILE);
|
|
139
332
|
return { backupFile };
|
|
@@ -195,7 +388,7 @@ function mnemonicFingerprint(mnemonic) {
|
|
|
195
388
|
}
|
|
196
389
|
|
|
197
390
|
// src/lib/wallet-agent/store.ts
|
|
198
|
-
import { existsSync as
|
|
391
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4, renameSync as renameSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
199
392
|
import lockfile from "proper-lockfile";
|
|
200
393
|
var LOCK_FILE = `${WALLET_FILE}.lock`;
|
|
201
394
|
var DEFAULT_LOCK_STALE_MS = 3e4;
|
|
@@ -204,9 +397,22 @@ var DEFAULT_LOCK_RETRY_INTERVAL_MS = 1e3;
|
|
|
204
397
|
var WALLET_VERSION = 9;
|
|
205
398
|
var RECENT_SPENDS_CAP = 10;
|
|
206
399
|
var DEFAULT_AGENT_MINT_URL = "https://mint.coinos.io";
|
|
400
|
+
var MAX_STORABLE_JOB_ID_LENGTH = 512;
|
|
401
|
+
function isStorableJobId(value) {
|
|
402
|
+
return typeof value === "string" && value.length > 0 && value.length <= MAX_STORABLE_JOB_ID_LENGTH && // eslint-disable-next-line no-control-regex -- control characters are what this rejects
|
|
403
|
+
!/[\u0000-\u001f\u007f-\u009f]/u.test(value);
|
|
404
|
+
}
|
|
405
|
+
function loadWalletConfiguredMints() {
|
|
406
|
+
return loadAgentWallet()?.mints ?? [];
|
|
407
|
+
}
|
|
207
408
|
function loadAgentWallet() {
|
|
208
|
-
|
|
209
|
-
|
|
409
|
+
const wallet = parseAgentWallet();
|
|
410
|
+
if (wallet) normalizeAcceptedUnresolved(wallet);
|
|
411
|
+
return wallet;
|
|
412
|
+
}
|
|
413
|
+
function parseAgentWallet() {
|
|
414
|
+
if (!existsSync4(WALLET_FILE)) return null;
|
|
415
|
+
const raw = readFileSync4(WALLET_FILE, "utf-8");
|
|
210
416
|
let parsed;
|
|
211
417
|
try {
|
|
212
418
|
parsed = JSON.parse(raw);
|
|
@@ -285,10 +491,11 @@ function loadAgentWallet() {
|
|
|
285
491
|
return parsed;
|
|
286
492
|
}
|
|
287
493
|
function saveAgentWallet(wallet) {
|
|
494
|
+
assertAcceptedUnresolvedStorable(wallet);
|
|
288
495
|
ensureConfigDir();
|
|
289
496
|
const tmp = `${WALLET_FILE}.tmp`;
|
|
290
497
|
const data = JSON.stringify(wallet, null, 2) + "\n";
|
|
291
|
-
|
|
498
|
+
writeFileSync4(tmp, data, { mode: 384 });
|
|
292
499
|
try {
|
|
293
500
|
renameSync2(tmp, WALLET_FILE);
|
|
294
501
|
} catch (err) {
|
|
@@ -300,7 +507,7 @@ function saveAgentWallet(wallet) {
|
|
|
300
507
|
}
|
|
301
508
|
}
|
|
302
509
|
function walletExists() {
|
|
303
|
-
return
|
|
510
|
+
return existsSync4(WALLET_FILE);
|
|
304
511
|
}
|
|
305
512
|
async function withWalletLock(fn, opts = {}) {
|
|
306
513
|
ensureConfigDir();
|
|
@@ -453,6 +660,62 @@ function migrateV8ToV9(parsed) {
|
|
|
453
660
|
created_at: typeof parsed.created_at === "string" ? parsed.created_at : (/* @__PURE__ */ new Date()).toISOString()
|
|
454
661
|
};
|
|
455
662
|
}
|
|
663
|
+
var ACCEPTED_UNRESOLVED_KINDS = /* @__PURE__ */ new Set([
|
|
664
|
+
"identity_mismatch",
|
|
665
|
+
"body_unreadable",
|
|
666
|
+
"router_identity_conflict"
|
|
667
|
+
]);
|
|
668
|
+
var OBSERVED_JOB_ID_FIELDS = ["header_job_id", "body_job_id", "accepted_job_id"];
|
|
669
|
+
var PLACEHOLDER_KIND = "body_unreadable";
|
|
670
|
+
var PLACEHOLDER_OBSERVED_AT = (/* @__PURE__ */ new Date(0)).toISOString();
|
|
671
|
+
function assertAcceptedUnresolvedStorable(wallet) {
|
|
672
|
+
const entries = wallet.pending_submissions;
|
|
673
|
+
if (!Array.isArray(entries)) return;
|
|
674
|
+
for (const entry of entries) {
|
|
675
|
+
const record = entry?.accepted_unresolved;
|
|
676
|
+
if (record === void 0 || isStorableAcceptedUnresolved(record)) continue;
|
|
677
|
+
throw new DvmError(
|
|
678
|
+
"accepted_unresolved_invalid",
|
|
679
|
+
`Refusing to save pending submission ${String(entry?.request_id)}: its accepted_unresolved record is outside the stored contract.`,
|
|
680
|
+
"Use one of the three kinds, an ISO-8601 observed_at, and job IDs and ambiguity_id that pass isStorableJobId; omit an observed ID that does not. The wallet file was not changed."
|
|
681
|
+
);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
function isStorableAcceptedUnresolved(value) {
|
|
685
|
+
if (!isPlainRecord(value)) return false;
|
|
686
|
+
return ACCEPTED_UNRESOLVED_KINDS.has(value.kind) && isIsoTimestamp(value.observed_at) && isStorableJobId(value.ambiguity_id) && OBSERVED_JOB_ID_FIELDS.every((field) => value[field] === void 0 || isStorableJobId(value[field]));
|
|
687
|
+
}
|
|
688
|
+
function normalizeAcceptedUnresolved(wallet) {
|
|
689
|
+
const entries = wallet.pending_submissions;
|
|
690
|
+
if (!Array.isArray(entries)) return;
|
|
691
|
+
for (const entry of entries) {
|
|
692
|
+
if (!entry || entry.accepted_unresolved === void 0) continue;
|
|
693
|
+
const stored = entry.accepted_unresolved;
|
|
694
|
+
if (isStorableAcceptedUnresolved(stored)) continue;
|
|
695
|
+
const record = isPlainRecord(stored) ? { ...stored } : {};
|
|
696
|
+
if (!ACCEPTED_UNRESOLVED_KINDS.has(record.kind)) record.kind = PLACEHOLDER_KIND;
|
|
697
|
+
if (!isIsoTimestamp(record.observed_at)) record.observed_at = PLACEHOLDER_OBSERVED_AT;
|
|
698
|
+
if (!isStorableJobId(record.ambiguity_id)) {
|
|
699
|
+
record.ambiguity_id = isStorableJobId(entry.request_id) ? entry.request_id : "missing-ambiguity";
|
|
700
|
+
}
|
|
701
|
+
for (const field of OBSERVED_JOB_ID_FIELDS) {
|
|
702
|
+
if (record[field] !== void 0 && !isStorableJobId(record[field])) delete record[field];
|
|
703
|
+
}
|
|
704
|
+
entry.accepted_unresolved = record;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
function isPlainRecord(value) {
|
|
708
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
709
|
+
}
|
|
710
|
+
var ISO_TIMESTAMP = /^(\d{4})-(\d{2})-(\d{2})T([01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d{1,9})?(?:Z|[+-](?:[01]\d|2[0-3]):[0-5]\d)$/;
|
|
711
|
+
function isIsoTimestamp(value) {
|
|
712
|
+
if (typeof value !== "string") return false;
|
|
713
|
+
const match = ISO_TIMESTAMP.exec(value);
|
|
714
|
+
if (!match) return false;
|
|
715
|
+
const [year, month, day] = [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
716
|
+
const date = new Date(Date.UTC(year, month - 1, day));
|
|
717
|
+
return date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day;
|
|
718
|
+
}
|
|
456
719
|
function isCounterMap(value) {
|
|
457
720
|
if (typeof value !== "object" || value === null) return false;
|
|
458
721
|
for (const inner of Object.values(value)) {
|
|
@@ -1059,36 +1322,47 @@ function buildAdminAuthHeader(args) {
|
|
|
1059
1322
|
};
|
|
1060
1323
|
}
|
|
1061
1324
|
|
|
1325
|
+
// src/lib/job-identity-header.ts
|
|
1326
|
+
var JOB_IDENTITY_HEADER = "dvm-job-identity-v1";
|
|
1327
|
+
var JOB_IDENTITY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
|
1328
|
+
function isJobIdentityValue(value) {
|
|
1329
|
+
return typeof value === "string" && JOB_IDENTITY_PATTERN.test(value);
|
|
1330
|
+
}
|
|
1331
|
+
function parseJobIdentityHeader(value) {
|
|
1332
|
+
if (value === null) return { kind: "absent" };
|
|
1333
|
+
return isJobIdentityValue(value) ? { kind: "valid", jobId: value } : { kind: "invalid" };
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1062
1336
|
// src/lib/builder-identity.ts
|
|
1063
1337
|
import {
|
|
1064
|
-
chmodSync as
|
|
1065
|
-
existsSync as
|
|
1066
|
-
mkdirSync as
|
|
1067
|
-
readFileSync as
|
|
1338
|
+
chmodSync as chmodSync4,
|
|
1339
|
+
existsSync as existsSync6,
|
|
1340
|
+
mkdirSync as mkdirSync3,
|
|
1341
|
+
readFileSync as readFileSync6,
|
|
1068
1342
|
renameSync as renameSync4,
|
|
1069
|
-
statSync,
|
|
1343
|
+
statSync as statSync2,
|
|
1070
1344
|
unlinkSync as unlinkSync4,
|
|
1071
|
-
writeFileSync as
|
|
1345
|
+
writeFileSync as writeFileSync6
|
|
1072
1346
|
} from "fs";
|
|
1073
1347
|
import { schnorr as schnorr3 } from "@noble/curves/secp256k1.js";
|
|
1074
1348
|
import { sha256 as sha2563 } from "@noble/hashes/sha2.js";
|
|
1075
1349
|
|
|
1076
1350
|
// src/lib/builder-store.ts
|
|
1077
1351
|
import {
|
|
1078
|
-
chmodSync as
|
|
1079
|
-
existsSync as
|
|
1080
|
-
mkdirSync,
|
|
1081
|
-
readFileSync as
|
|
1352
|
+
chmodSync as chmodSync3,
|
|
1353
|
+
existsSync as existsSync5,
|
|
1354
|
+
mkdirSync as mkdirSync2,
|
|
1355
|
+
readFileSync as readFileSync5,
|
|
1082
1356
|
renameSync as renameSync3,
|
|
1083
1357
|
unlinkSync as unlinkSync3,
|
|
1084
|
-
writeFileSync as
|
|
1358
|
+
writeFileSync as writeFileSync5
|
|
1085
1359
|
} from "fs";
|
|
1086
1360
|
var CURRENT_VERSION = 2;
|
|
1087
1361
|
function loadBuilderStore() {
|
|
1088
|
-
if (!
|
|
1362
|
+
if (!existsSync5(BUILDER_FILE)) {
|
|
1089
1363
|
return { version: CURRENT_VERSION, dvms: {} };
|
|
1090
1364
|
}
|
|
1091
|
-
const raw =
|
|
1365
|
+
const raw = readFileSync5(BUILDER_FILE, "utf-8");
|
|
1092
1366
|
let parsed;
|
|
1093
1367
|
try {
|
|
1094
1368
|
parsed = JSON.parse(raw);
|
|
@@ -1124,7 +1398,7 @@ function loadIdentityPubkey() {
|
|
|
1124
1398
|
return loadBuilderStore().identity_pubkey ?? null;
|
|
1125
1399
|
}
|
|
1126
1400
|
function setIdentityPubkey(pubkey) {
|
|
1127
|
-
|
|
1401
|
+
mkdirSync2(BUILDER_CONFIG_DIR, { recursive: true, mode: 448 });
|
|
1128
1402
|
const store = loadBuilderStore();
|
|
1129
1403
|
store.identity_pubkey = pubkey;
|
|
1130
1404
|
writeBuilderStore(store);
|
|
@@ -1134,7 +1408,7 @@ function writeBuilderStore(store) {
|
|
|
1134
1408
|
backupSecretFile(BUILDER_FILE, content);
|
|
1135
1409
|
const tmp = `${BUILDER_FILE}.tmp`;
|
|
1136
1410
|
try {
|
|
1137
|
-
|
|
1411
|
+
writeFileSync5(tmp, content, { mode: 384 });
|
|
1138
1412
|
renameSync3(tmp, BUILDER_FILE);
|
|
1139
1413
|
} catch (err) {
|
|
1140
1414
|
try {
|
|
@@ -1144,7 +1418,7 @@ function writeBuilderStore(store) {
|
|
|
1144
1418
|
throw err;
|
|
1145
1419
|
}
|
|
1146
1420
|
try {
|
|
1147
|
-
|
|
1421
|
+
chmodSync3(BUILDER_FILE, 384);
|
|
1148
1422
|
} catch {
|
|
1149
1423
|
}
|
|
1150
1424
|
}
|
|
@@ -1180,9 +1454,9 @@ function generateIdentity() {
|
|
|
1180
1454
|
return { pubkey: bytesToHex4(publicKey), secret: bytesToHex4(secretKey) };
|
|
1181
1455
|
}
|
|
1182
1456
|
function loadIdentitySecret() {
|
|
1183
|
-
if (!
|
|
1457
|
+
if (!existsSync6(BUILDER_KEY_FILE)) return null;
|
|
1184
1458
|
warnAndTightenLooseKeyMode();
|
|
1185
|
-
const raw =
|
|
1459
|
+
const raw = readFileSync6(BUILDER_KEY_FILE, "utf-8").trim();
|
|
1186
1460
|
if (!/^[0-9a-f]{64}$/.test(raw)) {
|
|
1187
1461
|
throw new DvmError(
|
|
1188
1462
|
"identity_secret_malformed",
|
|
@@ -1201,9 +1475,9 @@ function writeIdentity(keypair, opts = {}) {
|
|
|
1201
1475
|
"Pass --force to overwrite. Any DVMs deployed under the old key will need to be re-deployed to refresh their /v1/info attestation."
|
|
1202
1476
|
);
|
|
1203
1477
|
}
|
|
1204
|
-
|
|
1478
|
+
mkdirSync3(BUILDER_CONFIG_DIR, { recursive: true, mode: 448 });
|
|
1205
1479
|
const tmp = `${BUILDER_KEY_FILE}.tmp`;
|
|
1206
|
-
|
|
1480
|
+
writeFileSync6(tmp, keypair.secret + "\n", { mode: 384 });
|
|
1207
1481
|
try {
|
|
1208
1482
|
renameSync4(tmp, BUILDER_KEY_FILE);
|
|
1209
1483
|
} catch (err) {
|
|
@@ -1214,7 +1488,7 @@ function writeIdentity(keypair, opts = {}) {
|
|
|
1214
1488
|
throw err;
|
|
1215
1489
|
}
|
|
1216
1490
|
try {
|
|
1217
|
-
|
|
1491
|
+
chmodSync4(BUILDER_KEY_FILE, 384);
|
|
1218
1492
|
} catch {
|
|
1219
1493
|
}
|
|
1220
1494
|
setIdentityPubkey(keypair.pubkey);
|
|
@@ -1248,7 +1522,7 @@ function verifyAttestation(payload, signatureHex, pubkeyHex) {
|
|
|
1248
1522
|
function warnAndTightenLooseKeyMode() {
|
|
1249
1523
|
if (process.platform === "win32") return;
|
|
1250
1524
|
try {
|
|
1251
|
-
const mode =
|
|
1525
|
+
const mode = statSync2(BUILDER_KEY_FILE).mode & 511;
|
|
1252
1526
|
if ((mode & 63) === 0) return;
|
|
1253
1527
|
diagnosticConsole.warn(
|
|
1254
1528
|
{
|
|
@@ -1259,7 +1533,7 @@ function warnAndTightenLooseKeyMode() {
|
|
|
1259
1533
|
mode: mode.toString(8).padStart(3, "0")
|
|
1260
1534
|
}
|
|
1261
1535
|
);
|
|
1262
|
-
|
|
1536
|
+
chmodSync4(BUILDER_KEY_FILE, 384);
|
|
1263
1537
|
} catch {
|
|
1264
1538
|
}
|
|
1265
1539
|
}
|
|
@@ -1279,6 +1553,82 @@ function hexToBytes4(hex) {
|
|
|
1279
1553
|
return out;
|
|
1280
1554
|
}
|
|
1281
1555
|
|
|
1556
|
+
// src/lib/receipt.ts
|
|
1557
|
+
import { schnorr as schnorr4 } from "@noble/curves/secp256k1.js";
|
|
1558
|
+
import { sha256 as sha2564 } from "@noble/hashes/sha2.js";
|
|
1559
|
+
import { bytesToHex as bytesToHex5, hexToBytes as hexToBytes5 } from "@noble/hashes/utils.js";
|
|
1560
|
+
function computeResultHash(summary, artifactContents) {
|
|
1561
|
+
const artifactHashes = artifactContents.map(
|
|
1562
|
+
(content) => bytesToHex5(sha2564(canonicaliseForSigning(content)))
|
|
1563
|
+
);
|
|
1564
|
+
return bytesToHex5(
|
|
1565
|
+
sha2564(
|
|
1566
|
+
canonicaliseForSigning({
|
|
1567
|
+
summary: summary ?? null,
|
|
1568
|
+
artifact_hashes: artifactHashes
|
|
1569
|
+
})
|
|
1570
|
+
)
|
|
1571
|
+
);
|
|
1572
|
+
}
|
|
1573
|
+
function signReceipt(unsigned, secretHex) {
|
|
1574
|
+
const bytes = canonicaliseForSigning(unsigned);
|
|
1575
|
+
const signature = bytesToHex5(schnorr4.sign(bytes, hexToBytes5(secretHex)));
|
|
1576
|
+
return { ...unsigned, signature };
|
|
1577
|
+
}
|
|
1578
|
+
function verifyReceipt(receipt) {
|
|
1579
|
+
try {
|
|
1580
|
+
const { signature, ...unsigned } = receipt;
|
|
1581
|
+
const bytes = canonicaliseForSigning(unsigned);
|
|
1582
|
+
return schnorr4.verify(hexToBytes5(signature), bytes, hexToBytes5(receipt.receipt_pubkey));
|
|
1583
|
+
} catch {
|
|
1584
|
+
return false;
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
function isSignedJobReceipt(value) {
|
|
1588
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
1589
|
+
const r = value;
|
|
1590
|
+
const paid = r.paid;
|
|
1591
|
+
return r.v === 1 && typeof r.job_id === "string" && typeof r.dvm_id === "string" && typeof r.dvm === "string" && typeof r.capability === "string" && (r.outcome === "completed" || r.outcome === "failed" || r.outcome === "cancelled") && typeof r.seq === "number" && typeof r.issued_at === "number" && typeof r.receipt_pubkey === "string" && typeof r.signature === "string" && !!paid && typeof paid === "object" && !Array.isArray(paid) && typeof paid.msats === "number";
|
|
1592
|
+
}
|
|
1593
|
+
function signDrainReceipt(unsigned, secretHex) {
|
|
1594
|
+
const bytes = canonicaliseForSigning(unsigned);
|
|
1595
|
+
const signature = bytesToHex5(schnorr4.sign(bytes, hexToBytes5(secretHex)));
|
|
1596
|
+
return { ...unsigned, signature };
|
|
1597
|
+
}
|
|
1598
|
+
function signFundingReceipt(unsigned, secretHex) {
|
|
1599
|
+
const bytes = canonicaliseForSigning(unsigned);
|
|
1600
|
+
const signature = bytesToHex5(schnorr4.sign(bytes, hexToBytes5(secretHex)));
|
|
1601
|
+
return { ...unsigned, signature };
|
|
1602
|
+
}
|
|
1603
|
+
function isFundingReceipt(value) {
|
|
1604
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
1605
|
+
const r = value;
|
|
1606
|
+
return r.v === 1 && r.kind === "credit_funding" && typeof r.credit_id === "string" && typeof r.fund_id === "string" && typeof r.funded_micro === "number" && typeof r.balance_after_micro === "number" && typeof r.ledger_seq === "number" && typeof r.issued_at === "number" && typeof r.caller_pubkey === "string" && typeof r.dvm_id === "string" && typeof r.dvm === "string" && typeof r.receipt_pubkey === "string" && typeof r.signature === "string";
|
|
1607
|
+
}
|
|
1608
|
+
function verifyFundingReceipt(receipt) {
|
|
1609
|
+
try {
|
|
1610
|
+
const { signature, ...unsigned } = receipt;
|
|
1611
|
+
const bytes = canonicaliseForSigning(unsigned);
|
|
1612
|
+
return schnorr4.verify(hexToBytes5(signature), bytes, hexToBytes5(receipt.receipt_pubkey));
|
|
1613
|
+
} catch {
|
|
1614
|
+
return false;
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
function isDrainReceipt(value) {
|
|
1618
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
1619
|
+
const r = value;
|
|
1620
|
+
return r.v === 1 && r.kind === "credit_drain" && typeof r.credit_id === "string" && typeof r.drain_id === "string" && (r.event === "requested" || r.event === "parked" || r.event === "picked_up" || r.event === "sent" || r.event === "released") && typeof r.method === "string" && typeof r.amount_micro === "number" && typeof r.issued_at === "number" && typeof r.caller_pubkey === "string" && typeof r.dvm_id === "string" && typeof r.dvm === "string" && typeof r.receipt_pubkey === "string" && typeof r.signature === "string";
|
|
1621
|
+
}
|
|
1622
|
+
function verifyDrainReceipt(receipt) {
|
|
1623
|
+
try {
|
|
1624
|
+
const { signature, ...unsigned } = receipt;
|
|
1625
|
+
const bytes = canonicaliseForSigning(unsigned);
|
|
1626
|
+
return schnorr4.verify(hexToBytes5(signature), bytes, hexToBytes5(receipt.receipt_pubkey));
|
|
1627
|
+
} catch {
|
|
1628
|
+
return false;
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1282
1632
|
// src/lib/receipt-verify.ts
|
|
1283
1633
|
function attestedDvmIdentity(builder) {
|
|
1284
1634
|
const builderPubkey = authenticatedBuilderPubkey(builder);
|
|
@@ -1424,90 +1774,6 @@ function weakestReceiptVerdict(statuses) {
|
|
|
1424
1774
|
if (statuses.includes("verified_unattested")) return "verified_unattested";
|
|
1425
1775
|
return "verified";
|
|
1426
1776
|
}
|
|
1427
|
-
function drainReceiptsDisplay(status, sound, total) {
|
|
1428
|
-
const tally = `${String(sound)} of ${String(total)}`;
|
|
1429
|
-
switch (status) {
|
|
1430
|
-
case "verified":
|
|
1431
|
-
return `Verified reclaim receipts (${tally}): signed by the key this DVM's builder identity attests.`;
|
|
1432
|
-
case "verified_unattested":
|
|
1433
|
-
return `Reclaim receipts, signed but unattested (${tally}): the signature holds, but this DVM publishes no builder identity to vouch for the signing key.`;
|
|
1434
|
-
case "invalid":
|
|
1435
|
-
return `UNVERIFIED reclaim receipts: only ${tally} hold up \u2014 the refund is still owed, but this evidence is not sound.`;
|
|
1436
|
-
case "absent":
|
|
1437
|
-
return "No reclaim receipts: this DVM returned no signed evidence of the drain.";
|
|
1438
|
-
}
|
|
1439
|
-
}
|
|
1440
|
-
function drainReceiptsHint(status) {
|
|
1441
|
-
switch (status) {
|
|
1442
|
-
case "verified":
|
|
1443
|
-
return "The refund evidence chains to this DVM's published builder identity, and is stored locally with that identity pinned beside it \u2014 so an unhonoured refund stays provable against the builder, not against an anonymous key.";
|
|
1444
|
-
case "verified_unattested":
|
|
1445
|
-
return "The receipts are signed, but nothing vouches for the signing key \u2014 expected for a locally-run or development DVM, not for a deployed one. Stored either way; the refund is owed regardless.";
|
|
1446
|
-
case "invalid":
|
|
1447
|
-
return "The refund is unaffected \u2014 this is a problem with the proof, not the money. Keep this output, and treat the reclaim as unattested when deciding how much to leave on deposit at this DVM.";
|
|
1448
|
-
case "absent":
|
|
1449
|
-
return "This DVM countersigned nothing, so the only artifact behind this reclaim is the drain request the caller signed. Weaker evidence than a countersigned drain, and worth weighing before funding here again.";
|
|
1450
|
-
}
|
|
1451
|
-
}
|
|
1452
|
-
function receiptDisplay(receipt, status, rate = 0) {
|
|
1453
|
-
const what = `${receipt.dvm}/${receipt.capability} (DVM ID ${receipt.dvm_id})`;
|
|
1454
|
-
const when = receiptIssuedAtIso(receipt)?.slice(0, 10);
|
|
1455
|
-
return `${VERDICT_PREFIX[status]}: paid ${formatPaid(receipt.paid.msats, rate)} for ${what}, ${receipt.outcome}${when ? ` ${when}` : ""}.`;
|
|
1456
|
-
}
|
|
1457
|
-
function receiptIssuedAtIso(receipt) {
|
|
1458
|
-
const seconds = receipt.issued_at;
|
|
1459
|
-
return typeof seconds === "number" ? epochMsToIso(seconds * 1e3) : null;
|
|
1460
|
-
}
|
|
1461
|
-
function epochMsToIso(ms) {
|
|
1462
|
-
if (typeof ms !== "number" || !Number.isFinite(ms) || Math.abs(ms) > MAX_TIMESTAMP_MS) {
|
|
1463
|
-
return null;
|
|
1464
|
-
}
|
|
1465
|
-
return new Date(ms).toISOString();
|
|
1466
|
-
}
|
|
1467
|
-
function receiptHint(status, jobId) {
|
|
1468
|
-
const verifyCmd = jobId ? `'dvm receipts verify ${jobId}'` : "'dvm receipts verify <job-id>'";
|
|
1469
|
-
switch (status) {
|
|
1470
|
-
case "verified":
|
|
1471
|
-
return `Proof of purchase is stored locally. Re-check it any time with ${verifyCmd}, or hand over the raw bundle with 'dvm receipts export'.`;
|
|
1472
|
-
case "verified_unattested":
|
|
1473
|
-
return `The receipt's signature is valid, but this DVM publishes no builder identity to vouch for the signing key \u2014 expected for a locally-run or development DVM, not for a deployed one. Stored either way; inspect it with ${verifyCmd}.`;
|
|
1474
|
-
case "invalid":
|
|
1475
|
-
return `The job itself is unaffected \u2014 this is a problem with the proof, not the result. Run ${verifyCmd} to see which check failed before relying on this receipt as evidence.`;
|
|
1476
|
-
case "absent":
|
|
1477
|
-
return "This DVM does not issue signed receipts, so there is no proof of purchase to store. Nothing went wrong.";
|
|
1478
|
-
}
|
|
1479
|
-
}
|
|
1480
|
-
function receiptFailureDetail(reason) {
|
|
1481
|
-
switch (reason) {
|
|
1482
|
-
case "job_id_mismatch":
|
|
1483
|
-
return "The receipt names a different job than the one it was collected for \u2014 it is not proof of this job, whoever signed it.";
|
|
1484
|
-
case "drain_id_mismatch":
|
|
1485
|
-
return "The receipt names a different reclaim than the one it is filed under \u2014 it is not evidence of this drain, whoever signed it.";
|
|
1486
|
-
case "credit_id_mismatch":
|
|
1487
|
-
return "The receipt names a different credit than the one this reclaim was against \u2014 it is not evidence of this refund, whoever signed it.";
|
|
1488
|
-
case "fund_id_mismatch":
|
|
1489
|
-
return "The receipt names a different funding operation than the caller submitted.";
|
|
1490
|
-
case "funding_amount_mismatch":
|
|
1491
|
-
return "The signed funded amount differs from the provider's funding response.";
|
|
1492
|
-
case "funding_balance_mismatch":
|
|
1493
|
-
return "The signed post-funding balance differs from the provider's fresh balance response.";
|
|
1494
|
-
case "funding_ledger_seq_mismatch":
|
|
1495
|
-
return "The signed funding sequence differs from the provider's fresh balance response.";
|
|
1496
|
-
case "caller_pubkey_mismatch":
|
|
1497
|
-
return "The funding receipt names a different caller than the key that submitted the funding.";
|
|
1498
|
-
case "signature_invalid":
|
|
1499
|
-
return "The receipt's signature does not verify under the key it names \u2014 the bytes were altered, or it was never signed by that key.";
|
|
1500
|
-
case "attestation_invalid":
|
|
1501
|
-
return "The DVM's published builder attestation does not verify under its own builder pubkey.";
|
|
1502
|
-
case "dvm_identity_mismatch":
|
|
1503
|
-
return "The receipt names a different immutable DVM ID than the builder attestation, so it is not evidence for this provider.";
|
|
1504
|
-
case "receipt_key_mismatch":
|
|
1505
|
-
return "The receipt was signed by a different key than the one the DVM's builder attested.";
|
|
1506
|
-
case "result_hash_mismatch":
|
|
1507
|
-
return "The receipt attests a different result than the one delivered.";
|
|
1508
|
-
}
|
|
1509
|
-
}
|
|
1510
|
-
var MAX_TIMESTAMP_MS = 864e13;
|
|
1511
1777
|
function checkJobId(receipt, expectedJobId) {
|
|
1512
1778
|
if (expectedJobId === void 0) return "unchecked";
|
|
1513
1779
|
return receipt.job_id === expectedJobId ? "ok" : "mismatch";
|
|
@@ -1516,12 +1782,6 @@ function checkDrainField(actual, expected) {
|
|
|
1516
1782
|
if (expected === void 0 || actual === void 0) return "unchecked";
|
|
1517
1783
|
return actual === expected ? "ok" : "mismatch";
|
|
1518
1784
|
}
|
|
1519
|
-
var VERDICT_PREFIX = {
|
|
1520
|
-
verified: "Verified receipt",
|
|
1521
|
-
verified_unattested: "Receipt (signed, unattested)",
|
|
1522
|
-
invalid: "UNVERIFIED receipt",
|
|
1523
|
-
absent: "No receipt"
|
|
1524
|
-
};
|
|
1525
1785
|
function checkResultHash(receipt, delivered) {
|
|
1526
1786
|
if (!delivered) return "unchecked";
|
|
1527
1787
|
if (receipt.result_hash === null) return "unchecked";
|
|
@@ -1539,14 +1799,6 @@ function isTrustAnchor(value) {
|
|
|
1539
1799
|
const anchor = value;
|
|
1540
1800
|
return typeof anchor.pubkey === "string" && typeof anchor.signature === "string" && !!anchor.attestation && typeof anchor.attestation === "object" && !Array.isArray(anchor.attestation);
|
|
1541
1801
|
}
|
|
1542
|
-
function formatPaid(msats, rate) {
|
|
1543
|
-
if (msats === 0) return "nothing (free job)";
|
|
1544
|
-
const sats = msats % 1e3 === 0 ? msats / 1e3 : Math.round(msats / 1e3 * 100) / 100;
|
|
1545
|
-
const satsLabel = `${sats.toLocaleString("en-US")} sats`;
|
|
1546
|
-
if (rate <= 0) return satsLabel;
|
|
1547
|
-
const usd = msatsToUsd(msats, rate);
|
|
1548
|
-
return `~$${formatUsd(usd)} (${satsLabel}, ${costAnchor(usd)})`;
|
|
1549
|
-
}
|
|
1550
1802
|
|
|
1551
1803
|
// src/lib/request-id.ts
|
|
1552
1804
|
var MAX_REQUEST_ID_LENGTH = 128;
|
|
@@ -1583,54 +1835,44 @@ function createMonotonicClock() {
|
|
|
1583
1835
|
};
|
|
1584
1836
|
}
|
|
1585
1837
|
|
|
1586
|
-
// src/lib/cashu/verify.ts
|
|
1587
|
-
import { getTokenMetadata } from "@cashu/cashu-ts";
|
|
1588
|
-
function verifyCashuToken(token, acceptedMints) {
|
|
1589
|
-
let mint, unit, totalMsats;
|
|
1590
|
-
try {
|
|
1591
|
-
const meta = getTokenMetadata(token);
|
|
1592
|
-
mint = meta.mint;
|
|
1593
|
-
unit = meta.unit;
|
|
1594
|
-
totalMsats = meta.amount.toNumber() * 1e3;
|
|
1595
|
-
} catch {
|
|
1596
|
-
throw new DvmError(
|
|
1597
|
-
"cashu_invalid_token",
|
|
1598
|
-
"The Cashu token could not be decoded.",
|
|
1599
|
-
"Ensure the token is a valid cashuA... or cashuB... string."
|
|
1600
|
-
);
|
|
1601
|
-
}
|
|
1602
|
-
const normalised = acceptedMints.map(normaliseMintUrl);
|
|
1603
|
-
if (!normalised.includes(normaliseMintUrl(mint))) {
|
|
1604
|
-
throw new DvmError(
|
|
1605
|
-
"cashu_mint_not_accepted",
|
|
1606
|
-
`Mint ${mint} is not accepted by this provider.`,
|
|
1607
|
-
`Accepted mints: ${acceptedMints.join(", ")}`
|
|
1608
|
-
);
|
|
1609
|
-
}
|
|
1610
|
-
return { mint, totalMsats, unit };
|
|
1611
|
-
}
|
|
1612
|
-
function normaliseMintUrl(url) {
|
|
1613
|
-
try {
|
|
1614
|
-
const u = new URL(url);
|
|
1615
|
-
u.hostname = u.hostname.toLowerCase();
|
|
1616
|
-
u.pathname = u.pathname.replace(/\/+/g, "/").replace(/\/+$/, "") || "/";
|
|
1617
|
-
return u.toString().replace(/\/$/, "");
|
|
1618
|
-
} catch {
|
|
1619
|
-
return url.toLowerCase().replace(/\/+$/, "");
|
|
1620
|
-
}
|
|
1621
|
-
}
|
|
1622
|
-
|
|
1623
|
-
// src/lib/payment-rails/tempo-wire.ts
|
|
1624
|
-
var TEMPO_SESSION_UPDATED_REASON = "Tempo session updated; attach the next voucher to fund this credit.";
|
|
1625
|
-
|
|
1626
1838
|
export {
|
|
1627
1839
|
toLockPubkey,
|
|
1628
1840
|
isX402SettlementReconciliationReason,
|
|
1629
|
-
|
|
1630
|
-
normaliseMintUrl,
|
|
1841
|
+
IMPLICIT_CREDIT_TTL_MS,
|
|
1631
1842
|
TEMPO_SESSION_UPDATED_REASON,
|
|
1632
|
-
|
|
1633
|
-
|
|
1843
|
+
CONFIG_DIR,
|
|
1844
|
+
CONFIG_FILE,
|
|
1845
|
+
CONFIG_BACKUP_FILE,
|
|
1846
|
+
CONFIG_AUDIT_FILE,
|
|
1847
|
+
CONFIG_PINNED_FILE,
|
|
1848
|
+
JOBS_FILE,
|
|
1849
|
+
REQUEST_INTENTS_DIR,
|
|
1850
|
+
FAVORITES_FILE,
|
|
1851
|
+
MESSAGES_DIR,
|
|
1852
|
+
RECEIPTS_FILE,
|
|
1853
|
+
CREDITS_FILE,
|
|
1854
|
+
X402_CHANNELS_FILE,
|
|
1855
|
+
TEMPO_CHANNELS_FILE,
|
|
1856
|
+
FEEDBACK_NUDGES_FILE,
|
|
1857
|
+
RECOVERY_MNEMONIC_FILE,
|
|
1858
|
+
WALLET_FILE,
|
|
1859
|
+
AGENT_MNEMONIC_FILE,
|
|
1860
|
+
IDENTITIES_FILE,
|
|
1861
|
+
PAIRINGS_DIR,
|
|
1862
|
+
BUDGET_FILE,
|
|
1863
|
+
BTC_RATE_FILE,
|
|
1864
|
+
ensureConfigDir,
|
|
1865
|
+
ensureConfigSubdir,
|
|
1866
|
+
repairConfigDirMode,
|
|
1867
|
+
COINGECKO_SIMPLE_PRICE_URL,
|
|
1868
|
+
fetchCoinGeckoSpot,
|
|
1869
|
+
fetchCoinGeckoBtcUsd,
|
|
1870
|
+
costAnchor,
|
|
1871
|
+
formatUsd,
|
|
1872
|
+
formatUsdcMicrounits,
|
|
1873
|
+
fetchBtcUsdRate,
|
|
1874
|
+
getLastKnownBtcRate,
|
|
1875
|
+
formatStaleness,
|
|
1634
1876
|
backupSecretFile,
|
|
1635
1877
|
readAgentMnemonic,
|
|
1636
1878
|
assertAgentMnemonicReplaceable,
|
|
@@ -1644,6 +1886,8 @@ export {
|
|
|
1644
1886
|
WALLET_VERSION,
|
|
1645
1887
|
RECENT_SPENDS_CAP,
|
|
1646
1888
|
DEFAULT_AGENT_MINT_URL,
|
|
1889
|
+
isStorableJobId,
|
|
1890
|
+
loadWalletConfiguredMints,
|
|
1647
1891
|
loadAgentWallet,
|
|
1648
1892
|
saveAgentWallet,
|
|
1649
1893
|
walletExists,
|
|
@@ -1670,6 +1914,9 @@ export {
|
|
|
1670
1914
|
ADMIN_STATE_UNAVAILABLE,
|
|
1671
1915
|
buildAdminChallenge,
|
|
1672
1916
|
buildAdminAuthHeader,
|
|
1917
|
+
JOB_IDENTITY_HEADER,
|
|
1918
|
+
isJobIdentityValue,
|
|
1919
|
+
parseJobIdentityHeader,
|
|
1673
1920
|
generateIdentity,
|
|
1674
1921
|
loadIdentitySecret,
|
|
1675
1922
|
writeIdentity,
|
|
@@ -1677,6 +1924,16 @@ export {
|
|
|
1677
1924
|
buildAttestation,
|
|
1678
1925
|
signAttestation,
|
|
1679
1926
|
verifyAttestation,
|
|
1927
|
+
computeResultHash,
|
|
1928
|
+
signReceipt,
|
|
1929
|
+
verifyReceipt,
|
|
1930
|
+
isSignedJobReceipt,
|
|
1931
|
+
signDrainReceipt,
|
|
1932
|
+
signFundingReceipt,
|
|
1933
|
+
isFundingReceipt,
|
|
1934
|
+
verifyFundingReceipt,
|
|
1935
|
+
isDrainReceipt,
|
|
1936
|
+
verifyDrainReceipt,
|
|
1680
1937
|
attestedDvmIdentity,
|
|
1681
1938
|
authenticatedBuilderPubkey,
|
|
1682
1939
|
verifyJobReceipt,
|
|
@@ -1684,13 +1941,6 @@ export {
|
|
|
1684
1941
|
verifyDrainReceiptChain,
|
|
1685
1942
|
verifyFundingReceiptChain,
|
|
1686
1943
|
weakestReceiptVerdict,
|
|
1687
|
-
drainReceiptsDisplay,
|
|
1688
|
-
drainReceiptsHint,
|
|
1689
|
-
receiptDisplay,
|
|
1690
|
-
receiptIssuedAtIso,
|
|
1691
|
-
epochMsToIso,
|
|
1692
|
-
receiptHint,
|
|
1693
|
-
receiptFailureDetail,
|
|
1694
1944
|
isPublicRequestId,
|
|
1695
1945
|
parseTraceparent,
|
|
1696
1946
|
createMonotonicClock
|