agentic-wallet-mcp 0.8.0 → 0.9.0
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/CHANGELOG.md +49 -0
- package/README.md +3 -1
- package/dist/server-bundle.cjs +329 -42
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,55 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
8
8
|
> Entries for 0.5.0 and earlier were reconstructed from commit history when this file was
|
|
9
9
|
> introduced in 0.6.0, so they summarise each release rather than being exhaustive.
|
|
10
10
|
|
|
11
|
+
## [0.9.0] — 24 August 2026
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **Paymaster-sponsored gas for `request_ai_birthcert_verification`.** When the resource server
|
|
16
|
+
offers a sponsored payment option (`extra.gasModel: "facilitator"`), the wallet now prefers it by
|
|
17
|
+
default — the ms-zetrix paymaster covers network gas, so a wallet holding the payment token but
|
|
18
|
+
zero ZTX can still complete the flow. Handles the server's asynchronous `202 Accepted` settlement
|
|
19
|
+
response: the wallet retries with the returned payment receipt (never re-sending the original
|
|
20
|
+
payment header) until the settlement confirms or the retry budget is exhausted, without ever
|
|
21
|
+
losing the receipt to a mid-retry crash or transient error.
|
|
22
|
+
- **Automatic self-pay fallback.** If sponsorship is refused before any money moved (the
|
|
23
|
+
paymaster pool is exhausted, rate-limited, or the network/asset isn't sponsorable), the wallet
|
|
24
|
+
falls back to self-pay automatically. It never falls back on an indeterminate outcome — a
|
|
25
|
+
payment that may still be settling is never retried as a fresh payment, which would risk paying
|
|
26
|
+
twice.
|
|
27
|
+
- **`gasPayer` parameter** on `request_ai_birthcert_verification` (`"sponsored"` | `"self"`) —
|
|
28
|
+
overrides the deployment default for a single call. An unrecognised value is treated as absent
|
|
29
|
+
rather than silently forcing self-pay.
|
|
30
|
+
- **`GAS_PREFERENCE`** env var (default `sponsored`) — deployment-wide default gas payer.
|
|
31
|
+
- **`MAX_SETTLEMENT_ATTEMPTS`** env var (default `20`) — cap on retry attempts while polling a
|
|
32
|
+
queued sponsored settlement.
|
|
33
|
+
|
|
34
|
+
### Changed
|
|
35
|
+
|
|
36
|
+
- Existing self-pay behaviour is unchanged — a resource server that only quotes self-pay (no
|
|
37
|
+
sponsored option) behaves identically to before this release.
|
|
38
|
+
|
|
39
|
+
## [0.8.1] — 19 August 2026
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
|
|
43
|
+
- **A payment blocked by the spending cap, or rejected for insufficient balance, now names the
|
|
44
|
+
real token and shows a human-readable amount** — e.g. `10000 (0.01 JMYR)` instead of a bare raw
|
|
45
|
+
integer that got mislabeled as ZTX once relayed. Affects `pay_and_fetch`, `subscribe_and_issue`,
|
|
46
|
+
and `request_ai_birthcert_verification`, since all three share the same payment step.
|
|
47
|
+
- **A wallet holding a ZTP20 token (e.g. JMYR) but zero ZTX no longer fails with a raw, opaque
|
|
48
|
+
error when trying to pay.** The wallet now checks its own ZTX gas balance before attempting the
|
|
49
|
+
payment and reports a clear "send some ZTX first" message — working around a bug in
|
|
50
|
+
`x402-zetrix-client` where its own gas check runs after an on-chain call that can itself fail
|
|
51
|
+
unhelpfully on a zero-gas account.
|
|
52
|
+
|
|
53
|
+
### Changed
|
|
54
|
+
|
|
55
|
+
- The known-but-never-confirmed mainnet SSIVC host is now a named, exported constant
|
|
56
|
+
(`UNVERIFIED_MAINNET_SSIVC_BASE_URL`) instead of only living in a comment — still not wired in by
|
|
57
|
+
default (`SSIVC_BASE_URL` remains the way to enable it on mainnet), but easier to flip on once
|
|
58
|
+
confirmed reachable.
|
|
59
|
+
|
|
11
60
|
## [0.8.0] — 17 August 2026
|
|
12
61
|
|
|
13
62
|
### Added
|
package/README.md
CHANGED
|
@@ -27,7 +27,7 @@ VC issuance → identity proof → pay-per-use).
|
|
|
27
27
|
| `create_holder_account` | Onboarding: mint an HSM account (the MCP already auto-creates one at startup if `ZETRIX_ADDRESS` is omitted — see Environment below). Always checks for an existing account first — if one is active for this session, returns `{ alreadyExists: true, existing }` without creating anything; pass `confirmNew: true` (after asking the user) to mint a new one anyway | `{ password, label?, purpose?, confirmNew? }` | `{ created, alreadyExists, existing?, zetrixAddress?, holderDid?, publicKeyHex?, message }` |
|
|
28
28
|
| `get_template_schema` | **Free** read of a VC template's declared attribute schema. Call before `subscribe_and_issue` to learn which attributes it requires | `{ templateId }` | `{ templateId, schema: { required, optional } }` or `{ templateId, error }` |
|
|
29
29
|
| `query_contract` | Read-only query against any Zetrix contract — call an arbitrary method and return its raw result. No signing, no state change | `{ contractAddress, method, params? }` | `{ ok: true, result }` or `{ ok: false, error }` |
|
|
30
|
-
| `request_ai_birthcert_verification` | Start a **Verified** AI Birthcert issuance session with myid (MyDigital ID owner verification) — distinct from `subscribe_and_issue`'s self-declared Basic AI Birthcert | `{ agentName, agentPurpose?, evidenceAssuranceLevel?, ownerType?, ownerVerified? }` | `{ sessionId, verificationUrl, expiresAt }` |
|
|
30
|
+
| `request_ai_birthcert_verification` | Start a **Verified** AI Birthcert issuance session with myid (MyDigital ID owner verification) — distinct from `subscribe_and_issue`'s self-declared Basic AI Birthcert. Optional `gasPayer` (`"sponsored"` \| `"self"`) overrides the gas payer for this call only — precedence is per-call `gasPayer` > `GAS_PREFERENCE` config > `sponsored` hardcoded default. If a sponsored quote is refused before any money moved, the wallet automatically falls back to paying gas itself; it never falls back once payment is merely pending/indeterminate | `{ agentName, agentPurpose?, evidenceAssuranceLevel?, ownerType?, ownerVerified?, gasPayer? }` | `{ sessionId, verificationUrl, expiresAt }` |
|
|
31
31
|
| `check_ai_birthcert_verification` | Poll the most recently requested Verified AI Birthcert session; on `status: "issued"`, also fetches, verifies, and caches the credential | (none) | `{ status: "pending" \| "issued" \| "no_session", vcId?, vc?, cacheError? }` |
|
|
32
32
|
|
|
33
33
|
> **VCs are cached locally**, keyed by `templateId`, under `~/.agentic-wallet-mcp/vc-cache/`
|
|
@@ -180,6 +180,8 @@ Node ≥ 18 required (built-in `fetch`).
|
|
|
180
180
|
| `ZETRIX_WALLET_STATE_DIR` | no | Where the wallet keeps `account.json` and its VC cache. Defaults to `~/.agentic-wallet-mcp` |
|
|
181
181
|
| `SSIVC_BASE_URL` | no | myid's SSIVC API base URL, for the Verified AI Birthcert flow (`request_ai_birthcert_verification`/`check_ai_birthcert_verification`). **Auto-derived per network** — testnet `https://ssivc-api-uat.myegdev.com/api`, mainnet `https://verifyid-api.zetrix.com/api`. Override only if either changes |
|
|
182
182
|
| `AI_BIRTHCERT_VERIFIED_TEMPLATE_ID` | no | The Verified AI Birthcert's on-chain `did:zid:...` template id. Auto-derived per network by default — **the mainnet default is unverified**, so override this explicitly once the mainnet template id is confirmed |
|
|
183
|
+
| `GAS_PREFERENCE` | no | `sponsored` or `self` — the default gas payer for the Verified AI Birthcert flow (`request_ai_birthcert_verification`). **Defaults to `sponsored`; any unrecognised value also falls back to `sponsored`** rather than throwing. Sponsorship only actually engages when the resource server's x402 quote offers a sponsored option (`extra.gasModel: "facilitator"`) — a server that only quotes self-pay behaves unchanged. **Sponsorship is testnet-only today**; override per-call with the tool's `gasPayer` parameter |
|
|
184
|
+
| `MAX_SETTLEMENT_ATTEMPTS` | no | Cap on retry attempts while polling a sponsored payment for settlement after SSIVC returns `202 Accepted` (queued). **Defaults to `20`; a non-positive or unparseable value also falls back to `20`** |
|
|
183
185
|
|
|
184
186
|
\* sensitive — never logged, never returned in a tool result, and never a tool parameter.
|
|
185
187
|
|
package/dist/server-bundle.cjs
CHANGED
|
@@ -12537,6 +12537,7 @@ var require_dist2 = __commonJS({
|
|
|
12537
12537
|
// src/index.ts
|
|
12538
12538
|
var index_exports = {};
|
|
12539
12539
|
__export(index_exports, {
|
|
12540
|
+
asPayRequest: () => asPayRequest,
|
|
12540
12541
|
buildToolList: () => buildToolList
|
|
12541
12542
|
});
|
|
12542
12543
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -12548,7 +12549,7 @@ var import_node_path6 = require("node:path");
|
|
|
12548
12549
|
// package.json
|
|
12549
12550
|
var package_default = {
|
|
12550
12551
|
name: "agentic-wallet-mcp",
|
|
12551
|
-
version: "0.
|
|
12552
|
+
version: "0.9.0",
|
|
12552
12553
|
description: "Agent-facing MCP wallet for Zetrix \u2014 orchestrates x401 identity proof, x402 payment, and MBI VC issuance",
|
|
12553
12554
|
keywords: [
|
|
12554
12555
|
"mcp",
|
|
@@ -21496,9 +21497,18 @@ var import_node_path = require("node:path");
|
|
|
21496
21497
|
|
|
21497
21498
|
// src/payment-guard.ts
|
|
21498
21499
|
var PaymentCapError = class extends Error {
|
|
21499
|
-
|
|
21500
|
+
/**
|
|
21501
|
+
* Set only when the failure is "requested amount exceeds the configured cap" — carries the raw
|
|
21502
|
+
* asset identifier and raw base-unit amounts so a caller with symbol/decimals resolution (e.g.
|
|
21503
|
+
* index.ts's `pay`) can rebuild a human-readable message without re-deriving these numbers.
|
|
21504
|
+
* Undefined for configuration-shaped failures (missing cap entry, malformed input), which have
|
|
21505
|
+
* no amount to render more legibly.
|
|
21506
|
+
*/
|
|
21507
|
+
detail;
|
|
21508
|
+
constructor(message, detail) {
|
|
21500
21509
|
super(message);
|
|
21501
21510
|
this.name = "PaymentCapError";
|
|
21511
|
+
this.detail = detail;
|
|
21502
21512
|
}
|
|
21503
21513
|
};
|
|
21504
21514
|
function isNonNegativeIntegerString(v) {
|
|
@@ -21538,7 +21548,10 @@ function assertWithinPaymentCap(accept, caps) {
|
|
|
21538
21548
|
const required2 = BigInt(requiredRaw);
|
|
21539
21549
|
const cap = BigInt(capRaw);
|
|
21540
21550
|
if (required2 > cap) {
|
|
21541
|
-
throw new PaymentCapError(
|
|
21551
|
+
throw new PaymentCapError(
|
|
21552
|
+
`payment blocked: requested ${required2} ${asset || "(unknown asset)"} exceeds configured MAX_PAYMENT_AMOUNT ${cap}`,
|
|
21553
|
+
{ asset, requiredRaw: required2.toString(), capRaw: cap.toString() }
|
|
21554
|
+
);
|
|
21542
21555
|
}
|
|
21543
21556
|
}
|
|
21544
21557
|
|
|
@@ -21611,7 +21624,14 @@ function loadConfig(env) {
|
|
|
21611
21624
|
const v = opt("SSIVC_BASE_URL") ?? deriveSsivcBaseUrl(network);
|
|
21612
21625
|
return v ? stripTrailingSlash(v) : void 0;
|
|
21613
21626
|
})(),
|
|
21614
|
-
aiBirthcertVerifiedTemplateId: opt("AI_BIRTHCERT_VERIFIED_TEMPLATE_ID") ?? deriveAiBirthcertVerifiedTemplateId(network)
|
|
21627
|
+
aiBirthcertVerifiedTemplateId: opt("AI_BIRTHCERT_VERIFIED_TEMPLATE_ID") ?? deriveAiBirthcertVerifiedTemplateId(network),
|
|
21628
|
+
// Unrecognised values fall back to the default rather than throwing — a typo here must not
|
|
21629
|
+
// brick startup, and 'sponsored' is always attemptable thanks to Task 5's self-pay fallback.
|
|
21630
|
+
gasPreference: env.GAS_PREFERENCE === "self" ? "self" : "sponsored",
|
|
21631
|
+
maxSettlementAttempts: (() => {
|
|
21632
|
+
const attempts = Number(env.MAX_SETTLEMENT_ATTEMPTS);
|
|
21633
|
+
return Number.isInteger(attempts) && attempts > 0 ? attempts : 20;
|
|
21634
|
+
})()
|
|
21615
21635
|
};
|
|
21616
21636
|
}
|
|
21617
21637
|
|
|
@@ -21645,6 +21665,23 @@ async function resolveAssetSymbol(asset, query) {
|
|
|
21645
21665
|
const info = await fetchTokenInfo(asset, query);
|
|
21646
21666
|
return info?.symbol ?? asset;
|
|
21647
21667
|
}
|
|
21668
|
+
var ZTX_DECIMALS = 6;
|
|
21669
|
+
async function resolveAssetInfo(asset, query) {
|
|
21670
|
+
if (asset === "ZTX") return { symbol: "ZTX", decimals: ZTX_DECIMALS };
|
|
21671
|
+
if (asset === "") return { symbol: "", decimals: 0 };
|
|
21672
|
+
const info = await fetchTokenInfo(asset, query);
|
|
21673
|
+
return info ?? { symbol: asset, decimals: 0 };
|
|
21674
|
+
}
|
|
21675
|
+
function formatHumanAmount(raw, decimals) {
|
|
21676
|
+
if (!/^\d+$/.test(raw) || decimals <= 0) return raw;
|
|
21677
|
+
const base = 10n ** BigInt(decimals);
|
|
21678
|
+
const value = BigInt(raw);
|
|
21679
|
+
const wholePart = value / base;
|
|
21680
|
+
const fracPart = value % base;
|
|
21681
|
+
if (fracPart === 0n) return wholePart.toString();
|
|
21682
|
+
const fracStr = fracPart.toString().padStart(decimals, "0").replace(/0+$/, "");
|
|
21683
|
+
return `${wholePart}.${fracStr}`;
|
|
21684
|
+
}
|
|
21648
21685
|
|
|
21649
21686
|
// src/clients/contract-query-client.ts
|
|
21650
21687
|
async function queryContract(input, query) {
|
|
@@ -21673,7 +21710,7 @@ async function queryContract(input, query) {
|
|
|
21673
21710
|
}
|
|
21674
21711
|
|
|
21675
21712
|
// src/clients/token-balance-client.ts
|
|
21676
|
-
var
|
|
21713
|
+
var ZTX_DECIMALS2 = 6;
|
|
21677
21714
|
function parseNativeBalance(res) {
|
|
21678
21715
|
if (res?.errorCode !== 0) throw new Error(`getInfo failed with errorCode ${res?.errorCode}`);
|
|
21679
21716
|
if (res.result === void 0 || res.result === null) throw new Error("getInfo returned no result");
|
|
@@ -21707,7 +21744,7 @@ async function queryTokenBalance(deps, token) {
|
|
|
21707
21744
|
const symbol = token.toUpperCase();
|
|
21708
21745
|
if (symbol === "ZTX") {
|
|
21709
21746
|
try {
|
|
21710
|
-
return { token: symbol, balance: await deps.fetchNativeBalance(deps.address), decimals:
|
|
21747
|
+
return { token: symbol, balance: await deps.fetchNativeBalance(deps.address), decimals: ZTX_DECIMALS2 };
|
|
21711
21748
|
} catch {
|
|
21712
21749
|
return { token: symbol, error: "query_failed" };
|
|
21713
21750
|
}
|
|
@@ -23135,6 +23172,7 @@ var SsivcError = class extends Error {
|
|
|
23135
23172
|
this.kind = kind;
|
|
23136
23173
|
}
|
|
23137
23174
|
};
|
|
23175
|
+
var DEFAULT_RETRY_AFTER_SECONDS = 15;
|
|
23138
23176
|
var SESSIONS_PATH = "/v2/verify/ai-birthcert/sessions";
|
|
23139
23177
|
var SsivcClient = class {
|
|
23140
23178
|
baseUrl;
|
|
@@ -23159,16 +23197,22 @@ var SsivcClient = class {
|
|
|
23159
23197
|
async createSessionPaid(body, paymentHeader) {
|
|
23160
23198
|
const res = await this.fetch("POST", SESSIONS_PATH, body, paymentHeader);
|
|
23161
23199
|
if (!res.ok) throw await this.error(res, "createSession (paid) failed");
|
|
23200
|
+
const paymentReceipt = res.headers.get("x-payment-response");
|
|
23201
|
+
if (!paymentReceipt) {
|
|
23202
|
+
throw new SsivcError("SSIVC createSession succeeded (2xx) but returned no X-Payment-Response settlement receipt header", res.status);
|
|
23203
|
+
}
|
|
23204
|
+
if (res.status === 202) {
|
|
23205
|
+
const header = Number(res.headers.get("retry-after"));
|
|
23206
|
+
const retryAfterSeconds = Number.isFinite(header) && header > 0 ? header : DEFAULT_RETRY_AFTER_SECONDS;
|
|
23207
|
+
return { kind: "queued", paymentReceipt, retryAfterSeconds };
|
|
23208
|
+
}
|
|
23162
23209
|
const env = await res.json();
|
|
23163
23210
|
const data = env.data;
|
|
23164
23211
|
if (!data || typeof data !== "object" || !data.sessionId || !data.verification_url || !data.expiresAt) {
|
|
23165
23212
|
throw new SsivcError("SSIVC createSession succeeded (2xx) but returned a malformed data envelope", res.status);
|
|
23166
23213
|
}
|
|
23167
|
-
const paymentReceipt = res.headers.get("x-payment-response");
|
|
23168
|
-
if (!paymentReceipt) {
|
|
23169
|
-
throw new SsivcError("SSIVC createSession succeeded (2xx) but returned no X-Payment-Response settlement receipt header", res.status);
|
|
23170
|
-
}
|
|
23171
23214
|
return {
|
|
23215
|
+
kind: "settled",
|
|
23172
23216
|
session: { sessionId: data.sessionId, verificationUrl: data.verification_url, expiresAt: data.expiresAt },
|
|
23173
23217
|
paymentReceipt
|
|
23174
23218
|
};
|
|
@@ -23294,6 +23338,43 @@ function canonicalizeJson(value) {
|
|
|
23294
23338
|
return JSON.stringify(value);
|
|
23295
23339
|
}
|
|
23296
23340
|
|
|
23341
|
+
// src/accept-selection.ts
|
|
23342
|
+
function extraOf(accept) {
|
|
23343
|
+
const extra = accept.extra;
|
|
23344
|
+
if (!extra || typeof extra !== "object" || Array.isArray(extra)) return {};
|
|
23345
|
+
return extra;
|
|
23346
|
+
}
|
|
23347
|
+
function assetOf(accept) {
|
|
23348
|
+
return String(accept.asset ?? "");
|
|
23349
|
+
}
|
|
23350
|
+
function declaresFacilitator(accept) {
|
|
23351
|
+
return extraOf(accept).gasModel === "facilitator";
|
|
23352
|
+
}
|
|
23353
|
+
function isSponsored(accept) {
|
|
23354
|
+
if (!declaresFacilitator(accept)) return false;
|
|
23355
|
+
const prepareEndpoint = extraOf(accept).prepareEndpoint;
|
|
23356
|
+
if (typeof prepareEndpoint !== "string" || prepareEndpoint === "") return false;
|
|
23357
|
+
const asset = assetOf(accept);
|
|
23358
|
+
return asset !== "" && asset !== "ZTX";
|
|
23359
|
+
}
|
|
23360
|
+
function isUnusable(accept) {
|
|
23361
|
+
return declaresFacilitator(accept) && !isSponsored(accept);
|
|
23362
|
+
}
|
|
23363
|
+
function orderAccepts(accepts, prefer) {
|
|
23364
|
+
const usable = accepts.filter((a) => !isUnusable(a));
|
|
23365
|
+
const sponsored = usable.filter(isSponsored);
|
|
23366
|
+
const selfPay = usable.filter((a) => !isSponsored(a));
|
|
23367
|
+
return prefer === "sponsored" ? [...sponsored, ...selfPay] : [...selfPay, ...sponsored];
|
|
23368
|
+
}
|
|
23369
|
+
function prepareBaseUrl(prepareEndpoint) {
|
|
23370
|
+
return prepareEndpoint.replace(/\/+$/, "").replace(/\/prepare$/, "");
|
|
23371
|
+
}
|
|
23372
|
+
function needsNativeGasCheck(accept) {
|
|
23373
|
+
const asset = String(accept.asset ?? "");
|
|
23374
|
+
if (asset === "" || asset === "ZTX") return false;
|
|
23375
|
+
return !isSponsored(accept);
|
|
23376
|
+
}
|
|
23377
|
+
|
|
23297
23378
|
// src/orchestrator/verify-ai-birthcert.ts
|
|
23298
23379
|
function subjectMatches(vc, holderDid) {
|
|
23299
23380
|
if (typeof vc !== "object" || vc === null) return false;
|
|
@@ -23321,6 +23402,7 @@ async function getConfirmedStatus(deps, sessionId) {
|
|
|
23321
23402
|
async function decidePriorSession(deps, agentName) {
|
|
23322
23403
|
const stored = await deps.sessionStore.get();
|
|
23323
23404
|
if (!stored) return { kind: "pay_fresh" };
|
|
23405
|
+
if (stored.sessionId === "") return { kind: "replay_receipt", receipt: stored.paymentReceipt };
|
|
23324
23406
|
if (stored.agentName !== agentName) {
|
|
23325
23407
|
const otherStatus = await getConfirmedStatus(deps, stored.sessionId);
|
|
23326
23408
|
if (otherStatus === "gone") return { kind: "replay_receipt", receipt: stored.paymentReceipt };
|
|
@@ -23356,12 +23438,107 @@ function withRequestLock(fn) {
|
|
|
23356
23438
|
}
|
|
23357
23439
|
var NoPaymentOptionsError = class extends Error {
|
|
23358
23440
|
};
|
|
23359
|
-
|
|
23360
|
-
|
|
23361
|
-
|
|
23362
|
-
|
|
23363
|
-
|
|
23364
|
-
|
|
23441
|
+
var DEFAULT_MAX_SETTLEMENT_ATTEMPTS = 20;
|
|
23442
|
+
var MAX_RETRY_DELAY_MS = 6e4;
|
|
23443
|
+
var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
23444
|
+
var SettlementStillQueuedError = class extends Error {
|
|
23445
|
+
constructor(message, paymentReceipt) {
|
|
23446
|
+
super(message);
|
|
23447
|
+
this.paymentReceipt = paymentReceipt;
|
|
23448
|
+
this.name = "SettlementStillQueuedError";
|
|
23449
|
+
}
|
|
23450
|
+
};
|
|
23451
|
+
var SettlementOutcomeUnknownError = class extends Error {
|
|
23452
|
+
constructor(message, paymentReceipt) {
|
|
23453
|
+
super(message);
|
|
23454
|
+
this.paymentReceipt = paymentReceipt;
|
|
23455
|
+
this.name = "SettlementOutcomeUnknownError";
|
|
23456
|
+
}
|
|
23457
|
+
};
|
|
23458
|
+
async function resolveSettlement(deps, buildBody, initialOutcome, onQueued) {
|
|
23459
|
+
const sleep = deps.sleep ?? defaultSleep;
|
|
23460
|
+
const maxAttempts = deps.maxSettlementAttempts ?? DEFAULT_MAX_SETTLEMENT_ATTEMPTS;
|
|
23461
|
+
let outcome = initialOutcome;
|
|
23462
|
+
let lastPersistedReceipt;
|
|
23463
|
+
if (outcome.kind === "queued") {
|
|
23464
|
+
lastPersistedReceipt = outcome.paymentReceipt;
|
|
23465
|
+
await onQueued(outcome.paymentReceipt);
|
|
23466
|
+
}
|
|
23467
|
+
for (let attempt = 0; outcome.kind === "queued" && attempt < maxAttempts; attempt++) {
|
|
23468
|
+
const delayMs = Math.min(outcome.retryAfterSeconds * 1e3, MAX_RETRY_DELAY_MS);
|
|
23469
|
+
await sleep(delayMs);
|
|
23470
|
+
const receiptSent = outcome.paymentReceipt;
|
|
23471
|
+
try {
|
|
23472
|
+
outcome = await deps.ssivc.createSessionWithReceipt(await buildBody(), receiptSent);
|
|
23473
|
+
} catch (err) {
|
|
23474
|
+
throw new SettlementOutcomeUnknownError(
|
|
23475
|
+
`the settlement outcome for payment receipt ${receiptSent} could not be determined \u2014 the receipt-retry call itself failed (${err instanceof Error ? err.message : String(err)})`,
|
|
23476
|
+
receiptSent
|
|
23477
|
+
);
|
|
23478
|
+
}
|
|
23479
|
+
if (outcome.kind === "queued" && outcome.paymentReceipt !== lastPersistedReceipt) {
|
|
23480
|
+
lastPersistedReceipt = outcome.paymentReceipt;
|
|
23481
|
+
await onQueued(outcome.paymentReceipt);
|
|
23482
|
+
}
|
|
23483
|
+
}
|
|
23484
|
+
if (outcome.kind === "queued") {
|
|
23485
|
+
throw new SettlementStillQueuedError(
|
|
23486
|
+
"the sponsored payment is being processed and has not settled yet \u2014 no funds are lost and the receipt has been kept; run request_ai_birthcert_verification again shortly to resume",
|
|
23487
|
+
outcome.paymentReceipt
|
|
23488
|
+
);
|
|
23489
|
+
}
|
|
23490
|
+
return { session: outcome.session, paymentReceipt: outcome.paymentReceipt };
|
|
23491
|
+
}
|
|
23492
|
+
var DEFINITIVE_PREPARE_REFUSALS = /* @__PURE__ */ new Set([
|
|
23493
|
+
461411,
|
|
23494
|
+
// X402_UNSUPPORTED_NETWORK — this network is not sponsored
|
|
23495
|
+
461414,
|
|
23496
|
+
// X402_RATE_LIMIT_EXCEEDED — pool rate limit, or the per-address active-prepare cap (default 5)
|
|
23497
|
+
461415
|
|
23498
|
+
// X402_UNSUPPORTED_ASSET — sponsorship is ZTP-20 only
|
|
23499
|
+
]);
|
|
23500
|
+
function facilitatorErrorCode(err) {
|
|
23501
|
+
const m = /"errorCode"\s*:\s*(\d{6})/.exec(err instanceof Error ? err.message : String(err));
|
|
23502
|
+
return m ? Number(m[1]) : void 0;
|
|
23503
|
+
}
|
|
23504
|
+
var PrepareStageError = class extends Error {
|
|
23505
|
+
constructor(cause) {
|
|
23506
|
+
super("prepare stage failed");
|
|
23507
|
+
this.cause = cause;
|
|
23508
|
+
this.name = "PrepareStageError";
|
|
23509
|
+
}
|
|
23510
|
+
};
|
|
23511
|
+
function isDefinitiveSponsorshipFailure(err) {
|
|
23512
|
+
if (!(err instanceof PrepareStageError)) return false;
|
|
23513
|
+
const cause = err.cause;
|
|
23514
|
+
if (cause instanceof SettlementStillQueuedError) return false;
|
|
23515
|
+
const code = facilitatorErrorCode(cause);
|
|
23516
|
+
if (code === void 0) return false;
|
|
23517
|
+
return DEFINITIVE_PREPARE_REFUSALS.has(code);
|
|
23518
|
+
}
|
|
23519
|
+
async function attemptCandidate(deps, accept, buildBody, onQueued) {
|
|
23520
|
+
let xPayment;
|
|
23521
|
+
try {
|
|
23522
|
+
xPayment = await deps.pay(accept);
|
|
23523
|
+
} catch (err) {
|
|
23524
|
+
throw new PrepareStageError(err);
|
|
23525
|
+
}
|
|
23526
|
+
const initialOutcome = await deps.ssivc.createSessionSettle(await buildBody(), xPayment);
|
|
23527
|
+
return resolveSettlement(deps, buildBody, initialOutcome, onQueued);
|
|
23528
|
+
}
|
|
23529
|
+
async function payAndCreateSession(deps, buildBody, onQueued) {
|
|
23530
|
+
const challenge = await deps.ssivc.createSessionChallenge(await buildBody());
|
|
23531
|
+
const candidates = orderAccepts(challenge.accepts, deps.gasPreference ?? "sponsored");
|
|
23532
|
+
const primary = candidates[0];
|
|
23533
|
+
if (!primary) throw new NoPaymentOptionsError("SSIVC 402 returned no usable payment options");
|
|
23534
|
+
try {
|
|
23535
|
+
return await attemptCandidate(deps, primary, buildBody, onQueued);
|
|
23536
|
+
} catch (err) {
|
|
23537
|
+
const fallback = candidates[1];
|
|
23538
|
+
if (!fallback || !isSponsored(primary) || isSponsored(fallback)) throw err;
|
|
23539
|
+
if (!isDefinitiveSponsorshipFailure(err)) throw err;
|
|
23540
|
+
return await attemptCandidate(deps, fallback, buildBody, onQueued);
|
|
23541
|
+
}
|
|
23365
23542
|
}
|
|
23366
23543
|
async function requestAiBirthcertVerification(deps, input) {
|
|
23367
23544
|
if (!input.agentName || !input.agentName.trim()) {
|
|
@@ -23374,31 +23551,85 @@ async function requestAiBirthcertVerificationLocked(deps, agentName, input) {
|
|
|
23374
23551
|
const decision = await decidePriorSession(deps, agentName);
|
|
23375
23552
|
if (decision.kind === "still_pending") return decision.result;
|
|
23376
23553
|
if (decision.kind === "blocked") return { error: decision.message };
|
|
23377
|
-
const
|
|
23378
|
-
|
|
23379
|
-
|
|
23380
|
-
|
|
23381
|
-
|
|
23382
|
-
|
|
23383
|
-
|
|
23554
|
+
const buildBody = async () => {
|
|
23555
|
+
const fields = {
|
|
23556
|
+
publicKey: deps.publicKeyHex,
|
|
23557
|
+
address: deps.address,
|
|
23558
|
+
timestamp: isoSeconds(deps.now()),
|
|
23559
|
+
agentName,
|
|
23560
|
+
id: agentName,
|
|
23561
|
+
ownerReference: deps.holderDid
|
|
23562
|
+
};
|
|
23563
|
+
if (input.agentPurpose) fields.agentPurpose = input.agentPurpose;
|
|
23564
|
+
if (input.evidenceAssuranceLevel) fields.evidenceAssuranceLevel = input.evidenceAssuranceLevel;
|
|
23565
|
+
if (input.ownerType) fields.ownerType = input.ownerType;
|
|
23566
|
+
if (input.ownerVerified) fields.ownerVerified = input.ownerVerified;
|
|
23567
|
+
const digestHex = (0, import_node_crypto4.createHash)("sha256").update(canonicalizeJson(fields), "utf8").digest("hex");
|
|
23568
|
+
const { signBlob: signedData } = await deps.signHexBlob(digestHex);
|
|
23569
|
+
return { ...fields, signedData };
|
|
23570
|
+
};
|
|
23571
|
+
const persistQueuedReceipt = async (receipt) => {
|
|
23572
|
+
try {
|
|
23573
|
+
await deps.sessionStore.set({
|
|
23574
|
+
sessionId: "",
|
|
23575
|
+
agentName,
|
|
23576
|
+
createdAt: deps.now().toISOString(),
|
|
23577
|
+
verificationUrl: "",
|
|
23578
|
+
paymentReceipt: receipt
|
|
23579
|
+
});
|
|
23580
|
+
} catch {
|
|
23581
|
+
}
|
|
23384
23582
|
};
|
|
23385
|
-
|
|
23386
|
-
if (input.evidenceAssuranceLevel) fields.evidenceAssuranceLevel = input.evidenceAssuranceLevel;
|
|
23387
|
-
if (input.ownerType) fields.ownerType = input.ownerType;
|
|
23388
|
-
if (input.ownerVerified) fields.ownerVerified = input.ownerVerified;
|
|
23389
|
-
const digestHex = (0, import_node_crypto4.createHash)("sha256").update(canonicalizeJson(fields), "utf8").digest("hex");
|
|
23390
|
-
const { signBlob: signedData } = await deps.signHexBlob(digestHex);
|
|
23391
|
-
const body = { ...fields, signedData };
|
|
23583
|
+
const requestedGasPayer = input.gasPayer === "self" || input.gasPayer === "sponsored" ? input.gasPayer : void 0;
|
|
23392
23584
|
let paid;
|
|
23393
23585
|
try {
|
|
23394
|
-
|
|
23395
|
-
|
|
23586
|
+
if (decision.kind === "replay_receipt") {
|
|
23587
|
+
let initialOutcome;
|
|
23588
|
+
try {
|
|
23589
|
+
initialOutcome = await deps.ssivc.createSessionWithReceipt(await buildBody(), decision.receipt);
|
|
23590
|
+
} catch (err) {
|
|
23591
|
+
throw new SettlementOutcomeUnknownError(
|
|
23592
|
+
`the settlement outcome for payment receipt ${decision.receipt} could not be determined \u2014 the receipt-replay call itself failed (${err instanceof Error ? err.message : String(err)})`,
|
|
23593
|
+
decision.receipt
|
|
23594
|
+
);
|
|
23595
|
+
}
|
|
23596
|
+
paid = await resolveSettlement(deps, buildBody, initialOutcome, persistQueuedReceipt);
|
|
23597
|
+
} else {
|
|
23598
|
+
paid = await payAndCreateSession(
|
|
23599
|
+
{ ...deps, gasPreference: requestedGasPayer ?? deps.gasPreference },
|
|
23600
|
+
buildBody,
|
|
23601
|
+
persistQueuedReceipt
|
|
23602
|
+
);
|
|
23603
|
+
}
|
|
23604
|
+
} catch (rawErr) {
|
|
23605
|
+
const err = rawErr instanceof PrepareStageError ? rawErr.cause : rawErr;
|
|
23396
23606
|
if (err instanceof PaymentReadinessError) return { error: `insufficient funds: ${err.message}` };
|
|
23397
23607
|
if (err instanceof PaymentCapError) return { error: err.message };
|
|
23398
23608
|
if (err instanceof NoPaymentOptionsError) return { error: err.message };
|
|
23609
|
+
if (err instanceof SettlementStillQueuedError) {
|
|
23610
|
+
try {
|
|
23611
|
+
await deps.sessionStore.set({
|
|
23612
|
+
sessionId: "",
|
|
23613
|
+
agentName,
|
|
23614
|
+
createdAt: deps.now().toISOString(),
|
|
23615
|
+
verificationUrl: "",
|
|
23616
|
+
paymentReceipt: err.paymentReceipt
|
|
23617
|
+
});
|
|
23618
|
+
} catch {
|
|
23619
|
+
return {
|
|
23620
|
+
error: err.message + ` (could not save the receipt locally \u2014 keep this value to resume manually: ${err.paymentReceipt})`
|
|
23621
|
+
};
|
|
23622
|
+
}
|
|
23623
|
+
return { error: err.message };
|
|
23624
|
+
}
|
|
23399
23625
|
if (err instanceof SsivcError && err.kind === "blob_already_settled") {
|
|
23400
23626
|
return { error: `payment already settled for this attempt: ${err.message}` };
|
|
23401
23627
|
}
|
|
23628
|
+
if (err instanceof SettlementOutcomeUnknownError) {
|
|
23629
|
+
return {
|
|
23630
|
+
error: `could not determine whether the sponsored settlement succeeded or failed for payment receipt ${err.paymentReceipt} (${err.message}) \u2014 this is NOT a confirmed failure, so the receipt has been kept as-is and no new payment has been attempted; this requires manual investigation by an operator before retrying, rather than calling request_ai_birthcert_verification again`
|
|
23631
|
+
};
|
|
23632
|
+
}
|
|
23402
23633
|
throw err;
|
|
23403
23634
|
}
|
|
23404
23635
|
await deps.sessionStore.set({
|
|
@@ -23418,6 +23649,12 @@ async function checkAiBirthcertVerification(deps) {
|
|
|
23418
23649
|
message: "No verification session found for this wallet \u2014 call request_ai_birthcert_verification first."
|
|
23419
23650
|
};
|
|
23420
23651
|
}
|
|
23652
|
+
if (stored.sessionId === "") {
|
|
23653
|
+
return {
|
|
23654
|
+
status: "no_session",
|
|
23655
|
+
message: "a sponsored payment is still settling and no verification session exists yet \u2014 the payment receipt has been kept, so calling request_ai_birthcert_verification again resumes it and will not pay twice."
|
|
23656
|
+
};
|
|
23657
|
+
}
|
|
23421
23658
|
const status = await deps.ssivc.getSession(stored.sessionId);
|
|
23422
23659
|
if (status.status !== "issued" || !status.vcId) return status;
|
|
23423
23660
|
if (deps.verifiedTemplateId && deps.cache) {
|
|
@@ -23594,7 +23831,12 @@ function buildToolList() {
|
|
|
23594
23831
|
agentPurpose: { type: "string", description: 'Optional \u2014 what this agent does, e.g. "Negotiate and settle supplier invoices".' },
|
|
23595
23832
|
evidenceAssuranceLevel: { type: "string", description: 'Optional \u2014 assurance level of the identity evidence, e.g. "high".' },
|
|
23596
23833
|
ownerType: { type: "string", description: `Optional \u2014 the owner's type, e.g. "Individual".` },
|
|
23597
|
-
ownerVerified: { type: "string", description: 'Optional \u2014 whether the owner is already verified, as the string "true" or "false".' }
|
|
23834
|
+
ownerVerified: { type: "string", description: 'Optional \u2014 whether the owner is already verified, as the string "true" or "false".' },
|
|
23835
|
+
gasPayer: {
|
|
23836
|
+
type: "string",
|
|
23837
|
+
enum: ["sponsored", "self"],
|
|
23838
|
+
description: `Who pays network gas. "sponsored" (default) asks the platform paymaster to cover gas, so this wallet needs no ZTX. "self" pays gas from this wallet's own ZTX balance. Omit to use the configured default.`
|
|
23839
|
+
}
|
|
23598
23840
|
},
|
|
23599
23841
|
required: ["agentName"]
|
|
23600
23842
|
}
|
|
@@ -23623,7 +23865,9 @@ function buildToolList() {
|
|
|
23623
23865
|
}
|
|
23624
23866
|
function asPayRequest(accept) {
|
|
23625
23867
|
const extra = accept.extra ?? {};
|
|
23626
|
-
|
|
23868
|
+
const prepareEndpoint = extra.prepareEndpoint;
|
|
23869
|
+
const normalizedExtra = typeof prepareEndpoint === "string" && prepareEndpoint !== "" ? { ...extra, prepareEndpoint: prepareBaseUrl(prepareEndpoint) } : extra;
|
|
23870
|
+
return { ...accept, extra: { gasModel: "client", ...normalizedExtra } };
|
|
23627
23871
|
}
|
|
23628
23872
|
async function main() {
|
|
23629
23873
|
const fileEnv = loadConfigFileEnv(process.argv, (p) => (0, import_node_fs.readFileSync)(p, "utf8"));
|
|
@@ -23705,13 +23949,53 @@ async function main() {
|
|
|
23705
23949
|
const vcCache = createFsVcCache((0, import_node_path6.join)(config2.stateDir, "vc-cache", cacheScope));
|
|
23706
23950
|
const mbi = new MbiClient(config2.mbiBaseUrl);
|
|
23707
23951
|
const messageSigner = (message) => be.signMessage(message, zetrixAddress, hsmPassword);
|
|
23708
|
-
const
|
|
23709
|
-
|
|
23710
|
-
|
|
23711
|
-
|
|
23712
|
-
|
|
23713
|
-
|
|
23714
|
-
|
|
23952
|
+
const formatAssetAmount = async (asset, raw) => {
|
|
23953
|
+
const { symbol, decimals } = await resolveAssetInfo(asset, contractQuery);
|
|
23954
|
+
const label = symbol || "(unknown asset)";
|
|
23955
|
+
const human = formatHumanAmount(raw, decimals);
|
|
23956
|
+
return human === raw ? `${raw} ${label}` : `${raw} (${human} ${label})`;
|
|
23957
|
+
};
|
|
23958
|
+
const pay = async (accept) => {
|
|
23959
|
+
const rawAsset = String(accept.asset ?? "");
|
|
23960
|
+
try {
|
|
23961
|
+
assertWithinPaymentCap(accept, config2.maxPaymentAmount);
|
|
23962
|
+
} catch (err) {
|
|
23963
|
+
if (err instanceof PaymentCapError && err.detail) {
|
|
23964
|
+
const { asset, requiredRaw, capRaw } = err.detail;
|
|
23965
|
+
throw new PaymentCapError(
|
|
23966
|
+
`payment blocked: requested ${await formatAssetAmount(asset, requiredRaw)} exceeds configured MAX_PAYMENT_AMOUNT ${await formatAssetAmount(asset, capRaw)}`,
|
|
23967
|
+
err.detail
|
|
23968
|
+
);
|
|
23969
|
+
}
|
|
23970
|
+
throw err;
|
|
23971
|
+
}
|
|
23972
|
+
if (needsNativeGasCheck(accept)) {
|
|
23973
|
+
const gasBalance = await fetchNativeBalance(zetrixAddress).catch(() => null);
|
|
23974
|
+
if (gasBalance === "0") {
|
|
23975
|
+
const { symbol } = await resolveAssetInfo(rawAsset, contractQuery);
|
|
23976
|
+
throw new PaymentReadinessError(
|
|
23977
|
+
`this wallet has 0 ZTX to pay network gas \u2014 the ${symbol || rawAsset} balance is separate from gas, and every transaction costs a small amount of ZTX regardless of which token is being paid. Send some ZTX to ${zetrixAddress} first, then retry.`,
|
|
23978
|
+
{ asset: "ZTX", required: "unknown", available: "0", reason: "gas" }
|
|
23979
|
+
);
|
|
23980
|
+
}
|
|
23981
|
+
}
|
|
23982
|
+
try {
|
|
23983
|
+
return await payWithReadinessCheck(
|
|
23984
|
+
rawAsset,
|
|
23985
|
+
() => import_x402_zetrix_client2.PaymentEngine.pay(asPayRequest(accept), walletCfg, node, {}, walletBeSignerFn),
|
|
23986
|
+
activated
|
|
23987
|
+
);
|
|
23988
|
+
} catch (err) {
|
|
23989
|
+
if (err instanceof PaymentReadinessError && err.shortfall.reason !== "not_activated") {
|
|
23990
|
+
const { asset, required: required2, available, reason } = err.shortfall;
|
|
23991
|
+
const label = reason === "gas" ? "ZTX for gas" : (await resolveAssetInfo(asset, contractQuery)).symbol || asset;
|
|
23992
|
+
throw new PaymentReadinessError(
|
|
23993
|
+
`insufficient ${label} \u2014 required ${await formatAssetAmount(asset, required2)}, available ${await formatAssetAmount(asset, available)}`,
|
|
23994
|
+
err.shortfall
|
|
23995
|
+
);
|
|
23996
|
+
}
|
|
23997
|
+
throw err;
|
|
23998
|
+
}
|
|
23715
23999
|
};
|
|
23716
24000
|
const verifyAiBirthcert = config2.ssivcBaseUrl ? (() => {
|
|
23717
24001
|
const ssivcSessionStore = createFsSsivcSessionStore((0, import_node_path6.join)(config2.stateDir, "ssivc-session.json"));
|
|
@@ -23730,7 +24014,9 @@ async function main() {
|
|
|
23730
24014
|
sessionStore: ssivcSessionStore,
|
|
23731
24015
|
verifiedTemplateId: config2.aiBirthcertVerifiedTemplateId,
|
|
23732
24016
|
cache: vcCache,
|
|
23733
|
-
quarantine: downloadQuarantine
|
|
24017
|
+
quarantine: downloadQuarantine,
|
|
24018
|
+
gasPreference: config2.gasPreference,
|
|
24019
|
+
maxSettlementAttempts: config2.maxSettlementAttempts
|
|
23734
24020
|
};
|
|
23735
24021
|
return {
|
|
23736
24022
|
request: (input) => requestAiBirthcertVerification(verifyAiBirthcertDeps, input),
|
|
@@ -23825,5 +24111,6 @@ if (process.env.NODE_ENV !== "test") {
|
|
|
23825
24111
|
}
|
|
23826
24112
|
// Annotate the CommonJS export names for ESM import in node:
|
|
23827
24113
|
0 && (module.exports = {
|
|
24114
|
+
asPayRequest,
|
|
23828
24115
|
buildToolList
|
|
23829
24116
|
});
|