@dvmkit/sdk 0.3.4-rc.10 → 0.5.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-2KGG5ORW.js → chunk-7ZUKL4IV.js} +3 -3
- package/dist/{chunk-KXZUCCEY.js → chunk-A4YFWZ7R.js} +4 -55
- package/dist/{chunk-C2AGARLM.js → chunk-B2IROPQF.js} +450 -83
- package/dist/{chunk-MKI6OVW4.js → chunk-CZULM42S.js} +0 -51
- package/dist/{chunk-UZC4A4DM.js → chunk-EYTHFFFU.js} +2489 -213
- package/dist/{chunk-M4QDXD2A.js → chunk-FYRR2UEZ.js} +1 -1
- package/dist/{chunk-4UZN2GP5.js → chunk-GAOAYHZA.js} +209 -33
- package/dist/{chunk-YYQG4R7T.js → chunk-GFY5FRX3.js} +6 -4
- package/dist/{chunk-Z4BNLUZF.js → chunk-IWQHUSMO.js} +3 -105
- package/dist/{chunk-4J32DTLT.js → chunk-IXR5RFK7.js} +82 -4
- package/dist/{chunk-2SD2W2BG.js → chunk-JH3SX3LY.js} +252 -26
- package/dist/chunk-KA6VN5FK.js +245 -0
- package/dist/{chunk-YRDA7EWN.js → chunk-LE74UEV7.js} +61 -16
- package/dist/chunk-LIRAHP7B.js +53 -0
- package/dist/{chunk-MG67KXU7.js → chunk-MZRJR2CW.js} +4 -2
- package/dist/chunk-OFVV37XC.js +57 -0
- package/dist/{chunk-A4CM2BD3.js → chunk-PAIM66YA.js} +91 -2
- package/dist/{chunk-JDFLRNAZ.js → chunk-PB4FYZET.js} +67 -7
- package/dist/{chunk-BQ2NMWKE.js → chunk-PLG4XER6.js} +1 -1
- package/dist/chunk-RLTSCKRI.js +109 -0
- package/dist/{chunk-VAMSJRJT.js → chunk-TUA7PYH7.js} +27 -105
- package/dist/{chunk-O3QDFPKM.js → chunk-TZ2WKCDD.js} +1 -1
- package/dist/{chunk-Z3EHBW2G.js → chunk-UOCZXO2Z.js} +7 -5
- package/dist/{chunk-EAMZZ2B4.js → chunk-X7EZ5CLI.js} +59 -0
- package/dist/{chunk-CE7XEHYC.js → chunk-Z4KLECAC.js} +1 -1
- package/dist/{chunk-7X6UDVCQ.js → chunk-Z4ZT5LNT.js} +66 -2
- package/dist/{credit-ledger-A35WH2TI.js → credit-ledger-MI2H7NEC.js} +3 -1
- package/dist/{credit-menu-0-UtJ3hh.d.ts → credit-menu-CAfqe8IY.d.ts} +133 -7
- package/dist/{fx-DhyT1Cjh.d.ts → fx-D9nsN3n4.d.ts} +1 -1
- package/dist/index.d.ts +6 -6
- package/dist/index.js +4 -3
- package/dist/internal/caller.d.ts +80 -13
- package/dist/internal/caller.js +56 -54
- package/dist/internal/index.js +1 -1
- package/dist/internal/server.d.ts +11 -85
- package/dist/internal/server.js +21 -18
- package/dist/{job-store-D3CcZp_g.d.ts → job-store-Bx-VFRjg.d.ts} +1 -1
- package/dist/{lightning-backend-BhsyrBLa.d.ts → lightning-backend-BAx2Y8pP.d.ts} +1 -1
- package/dist/{memory-credit-ledger-2LBZVMKJ.js → memory-credit-ledger-W2BAUYJP.js} +4 -2
- package/dist/{mpp-setup-7CGBKNUP.js → mpp-setup-RF5FXGGV.js} +5 -1
- package/dist/{payout-reporter-ZMOA37GN.js → payout-reporter-3Y2U55ZX.js} +1 -1
- package/dist/{postgres-job-store-MSD2HZOI.js → postgres-job-store-VLXLWXUQ.js} +1 -1
- package/dist/{revenue-reporter-EKJOGQYR.js → revenue-reporter-MAERPYGK.js} +1 -1
- package/dist/server/index.d.ts +14 -10
- package/dist/server/index.js +57 -19
- package/dist/{step-cache-v2yhyhSZ.d.ts → step-cache-QG_h5-3G.d.ts} +1769 -1549
- package/dist/{tempo-charge-store-4WX7FPOV.js → tempo-charge-store-Q3D7KYMH.js} +1 -1
- package/dist/tempo-lifecycle-ZBBLFBMH.js +15 -0
- package/dist/{tempo-session-store-LCSDHB7G.js → tempo-session-store-ZRNCOX7P.js} +1 -1
- package/dist/{tempo-wallet-RCGBBAJH.js → tempo-wallet-B5XXV3YH.js} +4 -3
- package/dist/testing/index.d.ts +3 -3
- package/dist/{usd-Cu9G6xv2.d.ts → usd-EwRqCPF-.d.ts} +1 -1
- package/dist/{x402-S47Q77UE.js → x402-R6HPBY5D.js} +18 -14
- package/package.json +1 -1
- package/dist/chunk-YHN3WMMF.js +0 -122
- package/dist/tempo-lifecycle-TJ62A7UX.js +0 -13
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import {
|
|
2
|
+
parseStoreValue,
|
|
3
|
+
stringifyStoreValue
|
|
4
|
+
} from "./chunk-YG7G4DPZ.js";
|
|
5
|
+
import {
|
|
6
|
+
withSdkInitLock
|
|
7
|
+
} from "./chunk-COYKJRMM.js";
|
|
8
|
+
import {
|
|
9
|
+
diagnosticConsole
|
|
10
|
+
} from "./chunk-ETXDTUJO.js";
|
|
11
|
+
|
|
12
|
+
// src/sdk/server/tempo-charge-store.ts
|
|
13
|
+
import { Credential } from "mppx";
|
|
14
|
+
import { createHash } from "crypto";
|
|
15
|
+
import { keccak256 } from "viem";
|
|
16
|
+
var DEFAULT_TABLE_NAME = "mpp_tempo_charge_replays";
|
|
17
|
+
var DEFAULT_RETENTION_MS = 60 * 60 * 1e3;
|
|
18
|
+
var DEFAULT_GC_SAMPLE_RATE = 0.01;
|
|
19
|
+
var VALID_IDENTIFIER = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
20
|
+
function replayKeyFor(credential) {
|
|
21
|
+
const payload = credential.payload;
|
|
22
|
+
if (payload.type === "proof") return `mppx:charge:proof:${credential.challenge.id}`;
|
|
23
|
+
if (payload.type === "hash" && /^0x[0-9a-fA-F]{64}$/.test(payload.hash ?? "")) {
|
|
24
|
+
return `mppx:charge:${payload.hash.toLowerCase()}`;
|
|
25
|
+
}
|
|
26
|
+
if (payload.type === "transaction" && /^0x[0-9a-fA-F]+$/.test(payload.signature ?? "")) {
|
|
27
|
+
return `mppx:charge:${keccak256(payload.signature).toLowerCase()}`;
|
|
28
|
+
}
|
|
29
|
+
throw new Error("Hosted Tempo charge credential has no supported replay key");
|
|
30
|
+
}
|
|
31
|
+
function canonicalCredential(credential) {
|
|
32
|
+
const serialized = Credential.serialize(credential);
|
|
33
|
+
const encoded = serialized.slice(serialized.indexOf(" ") + 1);
|
|
34
|
+
const wire = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
|
|
35
|
+
const sorted = (value) => {
|
|
36
|
+
if (Array.isArray(value)) return value.map(sorted);
|
|
37
|
+
if (value && typeof value === "object") {
|
|
38
|
+
return Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([key, field]) => [key, sorted(field)]));
|
|
39
|
+
}
|
|
40
|
+
return value;
|
|
41
|
+
};
|
|
42
|
+
return JSON.stringify(sorted(wire));
|
|
43
|
+
}
|
|
44
|
+
var PostgresTempoChargeStore = class {
|
|
45
|
+
hostingCleanupFence;
|
|
46
|
+
useHostingCleanupFence(fence) {
|
|
47
|
+
this.hostingCleanupFence = fence;
|
|
48
|
+
}
|
|
49
|
+
async reserveHostedCharge(serializedCredential, intent) {
|
|
50
|
+
if (!this.hostingCleanupFence) return;
|
|
51
|
+
const credential = Credential.deserialize(serializedCredential);
|
|
52
|
+
if (credential.challenge.intent !== "charge" || credential.challenge.method !== "tempo") {
|
|
53
|
+
throw new Error("Hosted Tempo charge credential is not a Tempo charge");
|
|
54
|
+
}
|
|
55
|
+
const challengeId = credential.challenge.id;
|
|
56
|
+
const replayKey = replayKeyFor(credential);
|
|
57
|
+
const artifactHash = createHash("sha256").update(serializedCredential).digest("hex");
|
|
58
|
+
await this.hostingCleanupFence.withNewIntent({
|
|
59
|
+
...intent,
|
|
60
|
+
challengeId,
|
|
61
|
+
paymentArtifactHash: artifactHash
|
|
62
|
+
}, async (tx) => {
|
|
63
|
+
if (intent.paymentSeq !== void 0 && intent.jobId) {
|
|
64
|
+
const original = await tx.query(
|
|
65
|
+
`SELECT data->'content'->'tempo_credential' AS credential FROM job_messages
|
|
66
|
+
WHERE job_id=$1 AND seq=$2 AND status='pending-verification'`,
|
|
67
|
+
[intent.jobId, intent.paymentSeq]
|
|
68
|
+
);
|
|
69
|
+
const recorded = original.rows[0]?.credential;
|
|
70
|
+
if (!recorded || canonicalCredential(recorded) !== canonicalCredential(credential)) {
|
|
71
|
+
throw new Error("Hosted Tempo charge differs from the original inbound payment");
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const prior = await tx.query(
|
|
75
|
+
`SELECT admission_id,job_id,payment_seq,replay_key,payment_artifact_hash FROM sdk_hosted_tempo_charge_intents
|
|
76
|
+
WHERE challenge_id=$1 FOR UPDATE`,
|
|
77
|
+
[challengeId]
|
|
78
|
+
);
|
|
79
|
+
if (prior.rows[0]) {
|
|
80
|
+
const row = prior.rows[0];
|
|
81
|
+
if (row.admission_id !== (intent.admissionId ?? null) || row.job_id !== (intent.jobId ?? null) || row.payment_seq !== (intent.paymentSeq ?? null) || row.replay_key !== replayKey || row.payment_artifact_hash !== artifactHash) {
|
|
82
|
+
throw new Error("Hosted Tempo charge identity conflicts with the original payment");
|
|
83
|
+
}
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
await tx.query(
|
|
87
|
+
`INSERT INTO sdk_hosted_tempo_charge_intents
|
|
88
|
+
(challenge_id,admission_id,job_id,payment_seq,replay_key,payment_artifact_hash,status)
|
|
89
|
+
VALUES ($1,$2,$3,$4,$5,$6,'pending')`,
|
|
90
|
+
[
|
|
91
|
+
challengeId,
|
|
92
|
+
intent.admissionId ?? null,
|
|
93
|
+
intent.jobId ?? null,
|
|
94
|
+
intent.paymentSeq ?? null,
|
|
95
|
+
replayKey,
|
|
96
|
+
artifactHash
|
|
97
|
+
]
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
async completeHostedCharge(challengeId) {
|
|
102
|
+
if (!this.hostingCleanupFence) return;
|
|
103
|
+
await this.pool.query(
|
|
104
|
+
`UPDATE sdk_hosted_tempo_charge_intents SET status='complete'
|
|
105
|
+
WHERE challenge_id=$1 AND status='pending' AND (
|
|
106
|
+
EXISTS (SELECT 1 FROM processed_payments p
|
|
107
|
+
WHERE p.payment_id=$1 AND p.rail='tempo' AND p.dvm_id=$2
|
|
108
|
+
AND p.job_id IS NOT DISTINCT FROM sdk_hosted_tempo_charge_intents.job_id)
|
|
109
|
+
)`,
|
|
110
|
+
[challengeId, this.hostingCleanupFence.dvmId]
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
pool;
|
|
114
|
+
tableName;
|
|
115
|
+
retentionMs;
|
|
116
|
+
gcSampleRate;
|
|
117
|
+
now;
|
|
118
|
+
constructor(pool, opts) {
|
|
119
|
+
const tableName = opts?.tableName ?? DEFAULT_TABLE_NAME;
|
|
120
|
+
if (!VALID_IDENTIFIER.test(tableName)) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
`PostgresTempoChargeStore: tableName must match ${VALID_IDENTIFIER.source}, got "${tableName}"`
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
this.pool = pool;
|
|
126
|
+
this.tableName = tableName;
|
|
127
|
+
this.retentionMs = opts?.retentionMs ?? DEFAULT_RETENTION_MS;
|
|
128
|
+
this.gcSampleRate = opts?.gcSampleRate ?? DEFAULT_GC_SAMPLE_RATE;
|
|
129
|
+
this.now = opts?.now ?? (() => Date.now());
|
|
130
|
+
this.get = this.get.bind(this);
|
|
131
|
+
this.put = this.put.bind(this);
|
|
132
|
+
this.delete = this.delete.bind(this);
|
|
133
|
+
this.update = this.update.bind(this);
|
|
134
|
+
}
|
|
135
|
+
/** Run the CREATE TABLE migration. Idempotent — safe to call repeatedly. */
|
|
136
|
+
async init() {
|
|
137
|
+
await withSdkInitLock(this.pool, () => this.createTables());
|
|
138
|
+
}
|
|
139
|
+
/** Read one stored value, or `null` when absent — mppx's miss sentinel. */
|
|
140
|
+
async get(key) {
|
|
141
|
+
const { rows } = await this.pool.query(
|
|
142
|
+
`SELECT value_json FROM ${this.tableName} WHERE store_key = $1`,
|
|
143
|
+
[key]
|
|
144
|
+
);
|
|
145
|
+
return rows[0] ? parseStoreValue(rows[0].value_json) : null;
|
|
146
|
+
}
|
|
147
|
+
async put(key, value) {
|
|
148
|
+
await this.update(key, () => ({ op: "set", value, result: void 0 }));
|
|
149
|
+
}
|
|
150
|
+
/** Delete one stored value — mppx's compensating release on a failed broadcast. */
|
|
151
|
+
async delete(key) {
|
|
152
|
+
await this.update(key, () => ({ op: "delete", result: void 0 }));
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Advisory locking covers the absent-row first write before the row lock.
|
|
156
|
+
* Both competing presentations of one credential and the shared sponsor
|
|
157
|
+
* budget need this serialization. The transaction contains no chain call.
|
|
158
|
+
*/
|
|
159
|
+
async update(key, fn) {
|
|
160
|
+
const client = await this.pool.connect();
|
|
161
|
+
try {
|
|
162
|
+
await client.query("BEGIN");
|
|
163
|
+
await client.query(`SELECT pg_advisory_xact_lock(hashtextextended($1, 2036))`, [key]);
|
|
164
|
+
const { rows } = await client.query(
|
|
165
|
+
`SELECT value_json FROM ${this.tableName} WHERE store_key = $1 FOR UPDATE`,
|
|
166
|
+
[key]
|
|
167
|
+
);
|
|
168
|
+
const current = rows[0] ? parseStoreValue(rows[0].value_json) : null;
|
|
169
|
+
const change = fn(current);
|
|
170
|
+
const now = this.now();
|
|
171
|
+
if (change.op === "set") {
|
|
172
|
+
if (this.hostingCleanupFence) {
|
|
173
|
+
const pending = await client.query(
|
|
174
|
+
`SELECT EXISTS (
|
|
175
|
+
SELECT 1 FROM sdk_hosted_tempo_charge_intents i
|
|
176
|
+
LEFT JOIN sdk_hosting_admissions a ON a.id=i.admission_id
|
|
177
|
+
LEFT JOIN jobs j ON j.id=i.job_id
|
|
178
|
+
WHERE i.status='pending' AND i.replay_key=$3 AND
|
|
179
|
+
((a.dvm_id=$1 AND a.scope=$2) OR j.dvm_id=$1)
|
|
180
|
+
) AS reserved`,
|
|
181
|
+
[this.hostingCleanupFence.dvmId, this.hostingCleanupFence.scope, key]
|
|
182
|
+
);
|
|
183
|
+
if (!pending.rows[0]?.reserved) {
|
|
184
|
+
await this.hostingCleanupFence.assertNewIntent(client);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
await client.query(
|
|
188
|
+
`INSERT INTO ${this.tableName} (store_key, value_json, expires_at)
|
|
189
|
+
VALUES ($1, $2, $3)
|
|
190
|
+
ON CONFLICT (store_key) DO UPDATE
|
|
191
|
+
SET value_json = EXCLUDED.value_json, expires_at = EXCLUDED.expires_at`,
|
|
192
|
+
[
|
|
193
|
+
key,
|
|
194
|
+
stringifyStoreValue(change.value, "Tempo charge store value"),
|
|
195
|
+
now + this.retentionMs
|
|
196
|
+
]
|
|
197
|
+
);
|
|
198
|
+
} else if (change.op === "delete") {
|
|
199
|
+
await client.query(`DELETE FROM ${this.tableName} WHERE store_key = $1`, [key]);
|
|
200
|
+
}
|
|
201
|
+
await client.query("COMMIT");
|
|
202
|
+
if (change.op !== "noop") this.maybeSweep(now);
|
|
203
|
+
return change.result;
|
|
204
|
+
} catch (error) {
|
|
205
|
+
await client.query("ROLLBACK").catch(() => void 0);
|
|
206
|
+
throw error;
|
|
207
|
+
} finally {
|
|
208
|
+
client.release();
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
async createTables() {
|
|
212
|
+
await this.pool.query(`
|
|
213
|
+
CREATE TABLE IF NOT EXISTS ${this.tableName} (
|
|
214
|
+
store_key TEXT PRIMARY KEY,
|
|
215
|
+
value_json TEXT NOT NULL,
|
|
216
|
+
expires_at BIGINT NOT NULL
|
|
217
|
+
);
|
|
218
|
+
CREATE INDEX IF NOT EXISTS ${this.tableName}_expires_at_idx
|
|
219
|
+
ON ${this.tableName} (expires_at);
|
|
220
|
+
CREATE TABLE IF NOT EXISTS sdk_hosted_tempo_charge_intents (
|
|
221
|
+
challenge_id TEXT PRIMARY KEY,
|
|
222
|
+
admission_id TEXT,
|
|
223
|
+
job_id TEXT,
|
|
224
|
+
payment_seq INTEGER,
|
|
225
|
+
replay_key TEXT,
|
|
226
|
+
payment_artifact_hash TEXT,
|
|
227
|
+
status TEXT NOT NULL CHECK (status IN ('pending','complete')),
|
|
228
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT clock_timestamp()
|
|
229
|
+
);
|
|
230
|
+
ALTER TABLE sdk_hosted_tempo_charge_intents
|
|
231
|
+
ADD COLUMN IF NOT EXISTS replay_key TEXT,
|
|
232
|
+
ADD COLUMN IF NOT EXISTS payment_artifact_hash TEXT;
|
|
233
|
+
`);
|
|
234
|
+
}
|
|
235
|
+
maybeSweep(now) {
|
|
236
|
+
if (this.gcSampleRate <= 0 || Math.random() >= this.gcSampleRate) return;
|
|
237
|
+
this.pool.query(`DELETE FROM ${this.tableName} WHERE expires_at < $1`, [now]).catch((err) => {
|
|
238
|
+
diagnosticConsole.error("PostgresTempoChargeStore GC sweep failed:", err);
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
export {
|
|
244
|
+
PostgresTempoChargeStore
|
|
245
|
+
};
|
|
@@ -1,31 +1,27 @@
|
|
|
1
1
|
import {
|
|
2
2
|
fundingReachableAt,
|
|
3
|
-
info,
|
|
4
3
|
listCredits,
|
|
5
4
|
maxAdvertisedMicro,
|
|
6
5
|
withinAdvertisedTolerance
|
|
7
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-B2IROPQF.js";
|
|
8
7
|
import {
|
|
9
8
|
authorizeProtectedRemoval,
|
|
10
9
|
isValidEvmPrivateKey,
|
|
11
10
|
loadConfig,
|
|
12
11
|
updateConfig
|
|
13
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-MZRJR2CW.js";
|
|
14
13
|
import {
|
|
15
14
|
X402_DEFAULT_NETWORK,
|
|
16
15
|
X402_V1_VERSION,
|
|
17
16
|
X402_VERSION,
|
|
18
|
-
assertSupportedX402Network,
|
|
19
17
|
buildPaymentRequirements,
|
|
20
18
|
chainIdFromCaip2,
|
|
21
19
|
encodePayment,
|
|
22
20
|
usdcContractFor,
|
|
23
21
|
usdcDomainNameFor,
|
|
24
22
|
usdcDomainVersionFor,
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
x402SupportedNetworksHint
|
|
28
|
-
} from "./chunk-Z4BNLUZF.js";
|
|
23
|
+
x402NetworkToCaip2
|
|
24
|
+
} from "./chunk-IWQHUSMO.js";
|
|
29
25
|
import {
|
|
30
26
|
costAnchor,
|
|
31
27
|
fetchBtcUsdRate,
|
|
@@ -34,18 +30,25 @@ import {
|
|
|
34
30
|
getLastKnownBtcRate,
|
|
35
31
|
msatsToUsdc,
|
|
36
32
|
usdcToMsats
|
|
37
|
-
} from "./chunk-
|
|
33
|
+
} from "./chunk-PAIM66YA.js";
|
|
38
34
|
import {
|
|
39
|
-
DvmError,
|
|
40
35
|
X402_CHANNELS_FILE,
|
|
41
36
|
ensureConfigDir,
|
|
42
37
|
resolveRpcHttpTransportOptions,
|
|
43
38
|
resolveRpcOverride
|
|
44
|
-
} from "./chunk-
|
|
39
|
+
} from "./chunk-CZULM42S.js";
|
|
45
40
|
import {
|
|
46
41
|
redactUrl,
|
|
47
42
|
redactUrlsInText
|
|
48
43
|
} from "./chunk-FUJ36YDV.js";
|
|
44
|
+
import {
|
|
45
|
+
assertSupportedX402Network,
|
|
46
|
+
x402NetworkByCaip2,
|
|
47
|
+
x402SupportedNetworksHint
|
|
48
|
+
} from "./chunk-RLTSCKRI.js";
|
|
49
|
+
import {
|
|
50
|
+
DvmError
|
|
51
|
+
} from "./chunk-LIRAHP7B.js";
|
|
49
52
|
|
|
50
53
|
// src/lib/x402/asset.ts
|
|
51
54
|
function resolveX402Asset(network = X402_DEFAULT_NETWORK) {
|
|
@@ -68,6 +71,42 @@ function x402WrongAssetHint(asset) {
|
|
|
68
71
|
return `This rail reads only ${asset.label} (${asset.address}) on ${asset.networkLabel}. ${wrongToken} USDC on a different chain is invisible here too \u2014 this wallet reads ${asset.networkLabel} alone. If you funded the wrong one, swap it to ${asset.label} on ${asset.networkLabel}, or bridge the funds across to ${asset.networkLabel}; either way they land where this rail can spend them.`;
|
|
69
72
|
}
|
|
70
73
|
|
|
74
|
+
// src/lib/output.ts
|
|
75
|
+
import { writeSync } from "fs";
|
|
76
|
+
var _traceId = null;
|
|
77
|
+
function setOutputTraceId(id) {
|
|
78
|
+
_traceId = id;
|
|
79
|
+
}
|
|
80
|
+
function getOutputTraceId() {
|
|
81
|
+
return _traceId;
|
|
82
|
+
}
|
|
83
|
+
function json(data) {
|
|
84
|
+
const out = _traceId !== null && typeof data === "object" && data !== null && !Array.isArray(data) && !("trace_id" in data) ? { trace_id: _traceId, ...data } : data;
|
|
85
|
+
writeAllSync(1, JSON.stringify(out) + "\n");
|
|
86
|
+
}
|
|
87
|
+
function info(msg) {
|
|
88
|
+
writeAllSync(2, msg + "\n");
|
|
89
|
+
}
|
|
90
|
+
function raw(msg) {
|
|
91
|
+
writeAllSync(1, msg + "\n");
|
|
92
|
+
}
|
|
93
|
+
function progress(event) {
|
|
94
|
+
writeAllSync(2, JSON.stringify(event) + "\n");
|
|
95
|
+
}
|
|
96
|
+
function writeAllSync(fd, text) {
|
|
97
|
+
const buf = Buffer.from(text, "utf8");
|
|
98
|
+
let offset = 0;
|
|
99
|
+
while (offset < buf.length) {
|
|
100
|
+
try {
|
|
101
|
+
offset += writeSync(fd, buf, offset, buf.length - offset);
|
|
102
|
+
} catch (err) {
|
|
103
|
+
const code = err.code;
|
|
104
|
+
if (code === "EAGAIN" || code === "EINTR") continue;
|
|
105
|
+
throw err;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
71
110
|
// src/lib/btc-rate.ts
|
|
72
111
|
async function resolveBtcUsdRate(purpose) {
|
|
73
112
|
try {
|
|
@@ -596,18 +635,18 @@ function ensureX402ChannelFile() {
|
|
|
596
635
|
function loadFile() {
|
|
597
636
|
if (!existsSync(X402_CHANNELS_FILE)) return { ...EMPTY, channels: {}, pending: [], exact: [] };
|
|
598
637
|
try {
|
|
599
|
-
const
|
|
600
|
-
if (
|
|
638
|
+
const raw2 = JSON.parse(readFileSync(X402_CHANNELS_FILE, "utf8"));
|
|
639
|
+
if (raw2.version !== 1 || !raw2.channels || typeof raw2.channels !== "object" || Array.isArray(raw2.channels) || !Array.isArray(raw2.pending)) {
|
|
601
640
|
throw new Error("unsupported x402 channel store shape");
|
|
602
641
|
}
|
|
603
642
|
return {
|
|
604
643
|
version: 1,
|
|
605
|
-
channels:
|
|
606
|
-
pending:
|
|
644
|
+
channels: raw2.channels,
|
|
645
|
+
pending: raw2.pending,
|
|
607
646
|
// Tolerated rather than required, unlike its two siblings: every file
|
|
608
647
|
// written before lacks the key, and failing closed on it would
|
|
609
648
|
// read as a corrupt journal on a store that is perfectly intact.
|
|
610
|
-
exact: Array.isArray(
|
|
649
|
+
exact: Array.isArray(raw2.exact) ? raw2.exact : []
|
|
611
650
|
};
|
|
612
651
|
} catch (cause) {
|
|
613
652
|
throw new DvmError(
|
|
@@ -982,6 +1021,12 @@ async function priceableRate(btcRate) {
|
|
|
982
1021
|
export {
|
|
983
1022
|
resolveX402Asset,
|
|
984
1023
|
x402WrongAssetHint,
|
|
1024
|
+
setOutputTraceId,
|
|
1025
|
+
getOutputTraceId,
|
|
1026
|
+
json,
|
|
1027
|
+
info,
|
|
1028
|
+
raw,
|
|
1029
|
+
progress,
|
|
985
1030
|
resolveBtcUsdRate,
|
|
986
1031
|
tryResolveBtcUsdRate,
|
|
987
1032
|
staleRateNotice,
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// src/lib/errors.ts
|
|
2
|
+
var DvmError = class extends Error {
|
|
3
|
+
constructor(code, message, hint, data, display, proseSources) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.code = code;
|
|
6
|
+
this.hint = hint;
|
|
7
|
+
this.data = data;
|
|
8
|
+
this.display = display;
|
|
9
|
+
this.proseSources = proseSources;
|
|
10
|
+
this.name = "DvmError";
|
|
11
|
+
}
|
|
12
|
+
code;
|
|
13
|
+
hint;
|
|
14
|
+
data;
|
|
15
|
+
display;
|
|
16
|
+
proseSources;
|
|
17
|
+
};
|
|
18
|
+
function providerProseSources(...sources) {
|
|
19
|
+
const provenance = {};
|
|
20
|
+
for (const source of sources) {
|
|
21
|
+
if (!source) continue;
|
|
22
|
+
for (const field of ["message", "hint", "display"]) {
|
|
23
|
+
if (typeof source[field] === "string") provenance[field] = "provider";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return Object.keys(provenance).length > 0 ? provenance : void 0;
|
|
27
|
+
}
|
|
28
|
+
function isAbortError(err) {
|
|
29
|
+
return err instanceof Error && (err.name === "AbortError" || err.name === "TimeoutError");
|
|
30
|
+
}
|
|
31
|
+
function transportErrorCode(err) {
|
|
32
|
+
let current = err;
|
|
33
|
+
const seen = /* @__PURE__ */ new Set();
|
|
34
|
+
for (let depth = 0; depth < 5 && isErrorLike(current) && !seen.has(current); depth++) {
|
|
35
|
+
seen.add(current);
|
|
36
|
+
const code = current.code;
|
|
37
|
+
if (typeof code === "string" && /^(?:E[A-Z0-9_]{1,63}|UND_ERR_[A-Z0-9_]{1,63})$/.test(code)) {
|
|
38
|
+
return code;
|
|
39
|
+
}
|
|
40
|
+
current = current.cause;
|
|
41
|
+
}
|
|
42
|
+
return void 0;
|
|
43
|
+
}
|
|
44
|
+
function isErrorLike(value) {
|
|
45
|
+
return (typeof value === "object" || typeof value === "function") && value !== null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export {
|
|
49
|
+
DvmError,
|
|
50
|
+
providerProseSources,
|
|
51
|
+
isAbortError,
|
|
52
|
+
transportErrorCode
|
|
53
|
+
};
|
|
@@ -3,9 +3,11 @@ import {
|
|
|
3
3
|
CONFIG_BACKUP_FILE,
|
|
4
4
|
CONFIG_FILE,
|
|
5
5
|
CONFIG_PINNED_FILE,
|
|
6
|
-
DvmError,
|
|
7
6
|
ensureConfigDir
|
|
8
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-CZULM42S.js";
|
|
8
|
+
import {
|
|
9
|
+
DvmError
|
|
10
|
+
} from "./chunk-LIRAHP7B.js";
|
|
9
11
|
|
|
10
12
|
// src/lib/evm-private-key.ts
|
|
11
13
|
import { secp256k1 } from "@noble/curves/secp256k1.js";
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// src/lib/cast-auth/canonical-json.ts
|
|
2
|
+
function canonicalize(value) {
|
|
3
|
+
return JSON.stringify(sortDeep(value));
|
|
4
|
+
}
|
|
5
|
+
function canonicaliseForSigning(value) {
|
|
6
|
+
return new TextEncoder().encode(canonicalize(value));
|
|
7
|
+
}
|
|
8
|
+
function sortDeep(value) {
|
|
9
|
+
if (value === null) return null;
|
|
10
|
+
if (typeof value === "string" || typeof value === "boolean") return value;
|
|
11
|
+
if (typeof value === "number") {
|
|
12
|
+
if (!Number.isFinite(value)) {
|
|
13
|
+
throw new TypeError(`canonicalize: non-finite number is not JSON-serialisable`);
|
|
14
|
+
}
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
if (Array.isArray(value)) return value.map(sortDeep);
|
|
18
|
+
if (typeof value === "object") {
|
|
19
|
+
const sorted = {};
|
|
20
|
+
for (const key of Object.keys(value).sort()) {
|
|
21
|
+
const v = value[key];
|
|
22
|
+
if (v === void 0) continue;
|
|
23
|
+
sorted[key] = sortDeep(v);
|
|
24
|
+
}
|
|
25
|
+
return sorted;
|
|
26
|
+
}
|
|
27
|
+
throw new TypeError(`canonicalize: unsupported value of type ${typeof value}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// src/lib/cast-auth/verify.ts
|
|
31
|
+
import { schnorr } from "@noble/curves/secp256k1.js";
|
|
32
|
+
function verifyCanonicalSignedRequest(payload, signatureHex, pubkeyHex) {
|
|
33
|
+
if (!/^[0-9a-f]{64}$/i.test(pubkeyHex) || !/^[0-9a-f]{128}$/i.test(signatureHex)) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
const data = canonicaliseForSigning(payload);
|
|
37
|
+
const signature = hexToBytes(signatureHex);
|
|
38
|
+
const pubkey = hexToBytes(pubkeyHex);
|
|
39
|
+
try {
|
|
40
|
+
return schnorr.verify(signature, data, pubkey);
|
|
41
|
+
} catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function hexToBytes(hex) {
|
|
46
|
+
const out = new Uint8Array(hex.length / 2);
|
|
47
|
+
for (let i = 0; i < out.length; i++) {
|
|
48
|
+
out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export {
|
|
54
|
+
canonicalize,
|
|
55
|
+
canonicaliseForSigning,
|
|
56
|
+
verifyCanonicalSignedRequest
|
|
57
|
+
};
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
BTC_RATE_FILE,
|
|
3
3
|
ensureConfigSubdir
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-CZULM42S.js";
|
|
5
|
+
import {
|
|
6
|
+
canonicaliseForSigning
|
|
7
|
+
} from "./chunk-OFVV37XC.js";
|
|
5
8
|
import {
|
|
6
9
|
formatFiat
|
|
7
10
|
} from "./chunk-AT6V3SY7.js";
|
|
@@ -232,6 +235,82 @@ async function fetchFromApi() {
|
|
|
232
235
|
return rate;
|
|
233
236
|
}
|
|
234
237
|
|
|
238
|
+
// src/lib/receipt.ts
|
|
239
|
+
import { schnorr } from "@noble/curves/secp256k1.js";
|
|
240
|
+
import { sha256 } from "@noble/hashes/sha2.js";
|
|
241
|
+
import { bytesToHex, hexToBytes } from "@noble/hashes/utils.js";
|
|
242
|
+
function computeResultHash(summary, artifactContents) {
|
|
243
|
+
const artifactHashes = artifactContents.map(
|
|
244
|
+
(content) => bytesToHex(sha256(canonicaliseForSigning(content)))
|
|
245
|
+
);
|
|
246
|
+
return bytesToHex(
|
|
247
|
+
sha256(
|
|
248
|
+
canonicaliseForSigning({
|
|
249
|
+
summary: summary ?? null,
|
|
250
|
+
artifact_hashes: artifactHashes
|
|
251
|
+
})
|
|
252
|
+
)
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
function signReceipt(unsigned, secretHex) {
|
|
256
|
+
const bytes = canonicaliseForSigning(unsigned);
|
|
257
|
+
const signature = bytesToHex(schnorr.sign(bytes, hexToBytes(secretHex)));
|
|
258
|
+
return { ...unsigned, signature };
|
|
259
|
+
}
|
|
260
|
+
function verifyReceipt(receipt) {
|
|
261
|
+
try {
|
|
262
|
+
const { signature, ...unsigned } = receipt;
|
|
263
|
+
const bytes = canonicaliseForSigning(unsigned);
|
|
264
|
+
return schnorr.verify(hexToBytes(signature), bytes, hexToBytes(receipt.receipt_pubkey));
|
|
265
|
+
} catch {
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
function isSignedJobReceipt(value) {
|
|
270
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
271
|
+
const r = value;
|
|
272
|
+
const paid = r.paid;
|
|
273
|
+
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";
|
|
274
|
+
}
|
|
275
|
+
function signDrainReceipt(unsigned, secretHex) {
|
|
276
|
+
const bytes = canonicaliseForSigning(unsigned);
|
|
277
|
+
const signature = bytesToHex(schnorr.sign(bytes, hexToBytes(secretHex)));
|
|
278
|
+
return { ...unsigned, signature };
|
|
279
|
+
}
|
|
280
|
+
function signFundingReceipt(unsigned, secretHex) {
|
|
281
|
+
const bytes = canonicaliseForSigning(unsigned);
|
|
282
|
+
const signature = bytesToHex(schnorr.sign(bytes, hexToBytes(secretHex)));
|
|
283
|
+
return { ...unsigned, signature };
|
|
284
|
+
}
|
|
285
|
+
function isFundingReceipt(value) {
|
|
286
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
287
|
+
const r = value;
|
|
288
|
+
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";
|
|
289
|
+
}
|
|
290
|
+
function verifyFundingReceipt(receipt) {
|
|
291
|
+
try {
|
|
292
|
+
const { signature, ...unsigned } = receipt;
|
|
293
|
+
const bytes = canonicaliseForSigning(unsigned);
|
|
294
|
+
return schnorr.verify(hexToBytes(signature), bytes, hexToBytes(receipt.receipt_pubkey));
|
|
295
|
+
} catch {
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
function isDrainReceipt(value) {
|
|
300
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
301
|
+
const r = value;
|
|
302
|
+
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";
|
|
303
|
+
}
|
|
304
|
+
function verifyDrainReceipt(receipt) {
|
|
305
|
+
try {
|
|
306
|
+
const { signature, ...unsigned } = receipt;
|
|
307
|
+
const bytes = canonicaliseForSigning(unsigned);
|
|
308
|
+
return schnorr.verify(hexToBytes(signature), bytes, hexToBytes(receipt.receipt_pubkey));
|
|
309
|
+
} catch {
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
235
314
|
export {
|
|
236
315
|
IMPLICIT_CREDIT_TTL_MS,
|
|
237
316
|
COINGECKO_SIMPLE_PRICE_URL,
|
|
@@ -254,5 +333,15 @@ export {
|
|
|
254
333
|
fetchBtcUsdRate,
|
|
255
334
|
getLastKnownBtcRate,
|
|
256
335
|
formatStaleness,
|
|
257
|
-
formatAmountBestEffort
|
|
336
|
+
formatAmountBestEffort,
|
|
337
|
+
computeResultHash,
|
|
338
|
+
signReceipt,
|
|
339
|
+
verifyReceipt,
|
|
340
|
+
isSignedJobReceipt,
|
|
341
|
+
signDrainReceipt,
|
|
342
|
+
signFundingReceipt,
|
|
343
|
+
isFundingReceipt,
|
|
344
|
+
verifyFundingReceipt,
|
|
345
|
+
isDrainReceipt,
|
|
346
|
+
verifyDrainReceipt
|
|
258
347
|
};
|