agentic-wallet-mcp 0.9.2 → 0.10.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 +40 -0
- package/README.md +5 -4
- package/dist/server-bundle.cjs +454 -99
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,46 @@ 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.10.0] — 14 September 2026
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **`credential_preflight` — a free readiness check to call before asking the user for anything.**
|
|
16
|
+
Reports the live fee and which side pays gas, the balances that matter, whether the spending
|
|
17
|
+
limit permits it, and (for a template credential) the attributes the template requires. Every
|
|
18
|
+
reason it is not ready is listed *together*, so one round of fixes is enough rather than
|
|
19
|
+
discovering a low balance and a too-low spending cap one failed payment at a time. It also
|
|
20
|
+
reports what it could **not** check, so a clean result is not mistaken for a guarantee — notably,
|
|
21
|
+
it cannot tell whether an agent name is still free, because that is decided at issuance.
|
|
22
|
+
- **Quote-only mode for the Verified AI Birthcert**, so its price and gas model can be seen without
|
|
23
|
+
starting a session or paying.
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- **A dry run can no longer issue a credential.** `subscribe_and_issue({ dryRun: true })` now prices
|
|
28
|
+
through an endpoint that cannot issue, and stops before the call that can. Previously, against a
|
|
29
|
+
credential the issuer grants for free, asking what something cost *created it* — a real,
|
|
30
|
+
permanently-registered credential, which also displaced whatever the wallet already held for that
|
|
31
|
+
template. A dry run now signs nothing, issues nothing, and never touches the local credential
|
|
32
|
+
store.
|
|
33
|
+
- **A credential that is currently free is no longer reported as unaffordable.** The issuer states
|
|
34
|
+
separately whether a quoted amount will actually be charged; both `credential_preflight` and the
|
|
35
|
+
dry run now read it. When issuance is free, the amount is reported as indicative rather than as a
|
|
36
|
+
charge, and neither the balance nor the spending cap blocks it. Network gas is unaffected and can
|
|
37
|
+
still block, because gas is not the credential fee.
|
|
38
|
+
- **Where that answer is unknown it is reported as unknown, never as free** — an older issuer
|
|
39
|
+
deployment does not state it, and treating silence as "free" would under-report a real cost.
|
|
40
|
+
- **The default spending cap now permits the credential fee on mainnet**, scoped to credential
|
|
41
|
+
issuance rather than widened generally.
|
|
42
|
+
- **Balance reads during a preflight run concurrently**, so the fee balance and the native-gas
|
|
43
|
+
balance arrive together instead of one after the other.
|
|
44
|
+
|
|
45
|
+
### Fixed
|
|
46
|
+
|
|
47
|
+
- **The outbound `User-Agent` sent to payment facilitators no longer advertises a non-public
|
|
48
|
+
host.** It is transmitted to third parties on every prepare request; the underlying client
|
|
49
|
+
dependency has been updated to one that reports its public project URL.
|
|
50
|
+
|
|
11
51
|
## [0.9.2] — 3 September 2026
|
|
12
52
|
|
|
13
53
|
### Fixed
|
package/README.md
CHANGED
|
@@ -20,15 +20,16 @@ VC issuance → identity proof → pay-per-use).
|
|
|
20
20
|
|
|
21
21
|
| Tool | Does | Input | Output (shape) |
|
|
22
22
|
|---|---|---|---|
|
|
23
|
-
| `wallet_status` | Report holder DID/address/network + held VCs (client-supplied, or the local cache); optionally
|
|
23
|
+
| `wallet_status` | Report holder DID/address/network + held VCs (client-supplied, or the local cache); optionally one token balance (`token`) or several in one call (`tokens`). Each balance carries `balance` (raw base units), `decimals` and `display` — the same amount in whole tokens with its symbol. Quote `display`; a raw count beside a ticker is wrong by orders of magnitude. A token may be named by ticker **or** contract address | `{ heldCredentials?, token?, tokens? }` | `{ holderDid, zetrixAddress, network, credentials, balances?, tokenBalance?, tokenBalances? }` |
|
|
24
|
+
| `credential_preflight` | **Free.** The first call for any credential — composes the quote, the balances and the spending cap into one answer, before a single application field is collected. Spends nothing, starts nothing. `blockers` lists *every* reason the wallet is not ready, at once; `notChecked` lists what preflight cannot know — agent-name availability is decided by myid at issuance, after payment, so a `ready` result is never a reservation | `{ credential, templateId? }` | `{ credential, ready, fee?, balances, cap?, schema?, blockers, notChecked }` |
|
|
24
25
|
| `prove_identity` | Answer an x401 `PROOF-REQUEST` → return the `PROOF-RESPONSE` header to replay | `{ proofRequest, vc?, revealAttribute?, issuerKeys? }` | `{ proofResponseHeader, verified, presentationId }` |
|
|
25
26
|
| `pay_and_fetch` | Fetch a URL, auto-pay with x402 (self-pay via Wallet BE) on `402` | `{ url, method?, headers?, body? }` | `{ status, body, paymentMade, amountPaid, amountPaidHuman, asset }` |
|
|
26
27
|
| `subscribe_and_issue` | Reuse a cached VC if still valid, else pay x402 → MBI issues → return the VC | `{ templateId, attributes, expirationDate?, dryRun?, forceReissue? }` | `{ issued, vcId, vc, txHash, fromCache?, schema?, originalPayment?, paymentAttempted?, recovery? }` |
|
|
27
28
|
| `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
29
|
| `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
30
|
| `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. 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
|
-
| `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? }` |
|
|
31
|
+
| `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. `dryRun: true` quotes instead of paying: it stops after the 402 challenge, pays nothing, creates no session, and returns `{ quote }` — the asset, the amount and which side pays gas. Quote and paid request send byte-identical signed bodies | `{ agentName, agentPurpose?, evidenceAssuranceLevel?, ownerType?, ownerVerified?, gasPayer?, dryRun? }` | `{ sessionId, verificationUrl, expiresAt }` or `{ quote: { asset, maxAmountRequired, payTo?, gasModel } }` |
|
|
32
|
+
| `check_ai_birthcert_verification` | **Free.** Poll the most recently requested Verified AI Birthcert session; on `status: "issued"`, also fetches, verifies, and caches the credential. While the session is still open it replays the stored `verificationUrl` alongside SSIVC's live `expiresAt` — SSIVC issues the link only once, at creation, so this store is the only place it survives. That makes *"where is my link?"* answerable without touching the paid tool. No link is returned once `status` is `"issued"`: the link is spent | (none) | `{ status: "pending" \| "issued" \| "no_session", verificationUrl?, expiresAt?, vcId?, vc?, cacheError? }` |
|
|
32
33
|
|
|
33
34
|
> **VCs are cached locally**, keyed by `templateId`, under `~/.agentic-wallet-mcp/vc-cache/`
|
|
34
35
|
> (scoped per network + holder — different identities or networks never share a cache).
|
|
@@ -176,7 +177,7 @@ Node ≥ 18 required (built-in `fetch`).
|
|
|
176
177
|
| `OID4VP_BASE_URL` | no | OID4VP verifier base URL override — auto-derived from `ZETRIX_NETWORK` by the x401 SDK when not set |
|
|
177
178
|
| `ZETRIX_NODE_HOST` / `ZETRIX_NODE_PORT` | no | RPC node override (auto-derived from network) |
|
|
178
179
|
| `ZID_RESOLVER_BASE_URL` | no | ZID resolver override (auto-derived from network: sandbox for testnet, prod for mainnet) |
|
|
179
|
-
| `MAX_PAYMENT_AMOUNT` | no** | Per-asset x402 auto-pay cap — JSON `{ "<asset>": "<maxRawUnits>", "*": "<fallback>" }`. `pay_and_fetch`/`subscribe_and_issue` are asset-agnostic: the resource server's 402 challenge may quote the native ZETRIX token (asset code `ZTX`) **or** a ZTP20 token (e.g. `JMYR`) — cap whichever assets you expect. **
|
|
180
|
+
| `MAX_PAYMENT_AMOUNT` | no** | Per-asset x402 auto-pay cap — JSON `{ "<asset>": "<maxRawUnits>", "*": "<fallback>" }`. `pay_and_fetch`/`subscribe_and_issue` are asset-agnostic: the resource server's 402 challenge may quote the native ZETRIX token (asset code `ZTX`) **or** a ZTP20 token (e.g. `JMYR`) — cap whichever assets you expect. **Either the ticker or the contract address works** — the challenge identifies a ZTP20 token by its contract address, and the wallet resolves a known ticker (e.g. `JMYR`) to it. If both are written for the same asset, the contract address wins. An unrecognised ticker still matches nothing and falls through to `"*"`. e.g. `{"ZTX":"1000000000","ZTX3WeinXtt28YMyr4vUZ14ddTgEMGeuc1e6b":"5000000","*":"0"}`. **Default when unset:** both networks allow exactly the AI Birthcert fee (1 JMYR), keyed to that network's JMYR contract, and refuse everything else. **The cap is per call, not cumulative** — so an unconfigured wallet, mainnet included, can pay that fee once per call with no overall ceiling. Set this explicitly to lock a wallet down. |
|
|
180
181
|
| `ZETRIX_WALLET_STATE_DIR` | no | Where the wallet keeps `account.json` and its VC cache. Defaults to `~/.agentic-wallet-mcp` |
|
|
181
182
|
| `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
183
|
| `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 |
|
package/dist/server-bundle.cjs
CHANGED
|
@@ -12147,7 +12147,7 @@ var require_prepare_client = __commonJS({
|
|
|
12147
12147
|
method: "POST",
|
|
12148
12148
|
headers: {
|
|
12149
12149
|
"Content-Type": "application/json",
|
|
12150
|
-
"User-Agent": "x402-zetrix-js/1.0 (+https://
|
|
12150
|
+
"User-Agent": "x402-zetrix-js/1.0 (+https://github.com/Zetrix-Chain/x402-zetrix-js)"
|
|
12151
12151
|
},
|
|
12152
12152
|
body: JSON.stringify(params),
|
|
12153
12153
|
signal: controller.signal
|
|
@@ -12538,6 +12538,7 @@ var require_dist2 = __commonJS({
|
|
|
12538
12538
|
var index_exports = {};
|
|
12539
12539
|
__export(index_exports, {
|
|
12540
12540
|
asPayRequest: () => asPayRequest,
|
|
12541
|
+
buildPayers: () => buildPayers,
|
|
12541
12542
|
buildToolList: () => buildToolList
|
|
12542
12543
|
});
|
|
12543
12544
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -12549,7 +12550,7 @@ var import_node_path6 = require("node:path");
|
|
|
12549
12550
|
// package.json
|
|
12550
12551
|
var package_default = {
|
|
12551
12552
|
name: "agentic-wallet-mcp",
|
|
12552
|
-
version: "0.
|
|
12553
|
+
version: "0.10.0",
|
|
12553
12554
|
description: "Agent-facing MCP wallet for Zetrix \u2014 orchestrates x401 identity proof, x402 payment, and MBI VC issuance",
|
|
12554
12555
|
keywords: [
|
|
12555
12556
|
"mcp",
|
|
@@ -12592,7 +12593,7 @@ var package_default = {
|
|
|
12592
12593
|
dependencies: {
|
|
12593
12594
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
12594
12595
|
"x401-zetrix-client": "^0.2.1",
|
|
12595
|
-
"x402-zetrix-client": "^0.2.
|
|
12596
|
+
"x402-zetrix-client": "^0.2.4",
|
|
12596
12597
|
"zetrix-sdk-nodejs": "^1.0.3"
|
|
12597
12598
|
},
|
|
12598
12599
|
devDependencies: {
|
|
@@ -21511,10 +21512,15 @@ var PaymentCapError = class extends Error {
|
|
|
21511
21512
|
this.detail = detail;
|
|
21512
21513
|
}
|
|
21513
21514
|
};
|
|
21515
|
+
function formatCapRefusal(detail, renderRequired = detail.requiredRaw, renderCap = detail.capRaw) {
|
|
21516
|
+
const asset = detail.asset || "(unknown asset)";
|
|
21517
|
+
const base = `payment blocked: this call needs ${renderRequired} of asset "${asset}", but the limit that applies is ${renderCap}`;
|
|
21518
|
+
return detail.matchedKey === detail.asset ? `${base}, set for that asset.` : `${base}. No limit is set for this asset, so the "*" fallback applies \u2014 set a limit keyed by "${asset}" to allow it.`;
|
|
21519
|
+
}
|
|
21514
21520
|
function isNonNegativeIntegerString(v) {
|
|
21515
21521
|
return /^\d+$/.test(v);
|
|
21516
21522
|
}
|
|
21517
|
-
function parsePaymentCaps(json) {
|
|
21523
|
+
function parsePaymentCaps(json, opts = {}) {
|
|
21518
21524
|
if (json === void 0) return void 0;
|
|
21519
21525
|
let parsed;
|
|
21520
21526
|
try {
|
|
@@ -21532,12 +21538,43 @@ function parsePaymentCaps(json) {
|
|
|
21532
21538
|
}
|
|
21533
21539
|
caps[asset] = raw;
|
|
21534
21540
|
}
|
|
21535
|
-
return caps;
|
|
21541
|
+
return opts.resolveSymbol ? expandSymbolKeys(caps, opts.resolveSymbol, opts.onWarn) : caps;
|
|
21542
|
+
}
|
|
21543
|
+
function expandSymbolKeys(caps, resolveSymbol, onWarn) {
|
|
21544
|
+
const out = {};
|
|
21545
|
+
const pendingSymbols = [];
|
|
21546
|
+
for (const [key, value] of Object.entries(caps)) {
|
|
21547
|
+
const address = key === "*" || key === "ZTX" ? void 0 : resolveSymbol(key);
|
|
21548
|
+
if (address === void 0) out[key] = value;
|
|
21549
|
+
else pendingSymbols.push([key, address, value]);
|
|
21550
|
+
}
|
|
21551
|
+
for (const [symbol, address, value] of pendingSymbols) {
|
|
21552
|
+
if (out[address] !== void 0) continue;
|
|
21553
|
+
out[address] = value;
|
|
21554
|
+
const previous = caps["*"] ?? "0";
|
|
21555
|
+
if (BigInt(value) > BigInt(previous)) {
|
|
21556
|
+
onWarn?.(
|
|
21557
|
+
`MAX_PAYMENT_AMOUNT: the limit written as "${symbol}" now applies to ${address} and permits ${value}. Until this version a ticker matched nothing, so that asset was limited to ${previous}. If you did not intend to allow ${value}, change or remove the "${symbol}" entry.`
|
|
21558
|
+
);
|
|
21559
|
+
}
|
|
21560
|
+
}
|
|
21561
|
+
return out;
|
|
21562
|
+
}
|
|
21563
|
+
function describePaymentCap(asset, requiredRaw, caps) {
|
|
21564
|
+
if (caps === void 0) return { asset, capRaw: null, matchedKey: null, wouldPass: true };
|
|
21565
|
+
const { capRaw, matchedKey } = resolveCap(asset, caps);
|
|
21566
|
+
if (capRaw === void 0) return { asset, capRaw: null, matchedKey: null, wouldPass: false };
|
|
21567
|
+
const wouldPass = isNonNegativeIntegerString(requiredRaw) && BigInt(requiredRaw) <= BigInt(capRaw);
|
|
21568
|
+
return { asset, capRaw, matchedKey, wouldPass };
|
|
21569
|
+
}
|
|
21570
|
+
function resolveCap(asset, caps) {
|
|
21571
|
+
const explicit = caps[asset];
|
|
21572
|
+
return explicit !== void 0 ? { capRaw: explicit, matchedKey: asset } : { capRaw: caps["*"], matchedKey: "*" };
|
|
21536
21573
|
}
|
|
21537
21574
|
function assertWithinPaymentCap(accept, caps) {
|
|
21538
21575
|
if (caps === void 0) return;
|
|
21539
21576
|
const asset = accept.asset ?? "";
|
|
21540
|
-
const capRaw =
|
|
21577
|
+
const { capRaw, matchedKey } = resolveCap(asset, caps);
|
|
21541
21578
|
if (capRaw === void 0) {
|
|
21542
21579
|
throw new PaymentCapError(`payment blocked: no MAX_PAYMENT_AMOUNT entry for asset "${asset}" and no "*" fallback configured`);
|
|
21543
21580
|
}
|
|
@@ -21548,10 +21585,13 @@ function assertWithinPaymentCap(accept, caps) {
|
|
|
21548
21585
|
const required2 = BigInt(requiredRaw);
|
|
21549
21586
|
const cap = BigInt(capRaw);
|
|
21550
21587
|
if (required2 > cap) {
|
|
21551
|
-
|
|
21552
|
-
|
|
21553
|
-
|
|
21554
|
-
|
|
21588
|
+
const detail = {
|
|
21589
|
+
asset,
|
|
21590
|
+
requiredRaw: required2.toString(),
|
|
21591
|
+
capRaw: cap.toString(),
|
|
21592
|
+
matchedKey
|
|
21593
|
+
};
|
|
21594
|
+
throw new PaymentCapError(formatCapRefusal(detail), detail);
|
|
21555
21595
|
}
|
|
21556
21596
|
}
|
|
21557
21597
|
|
|
@@ -21583,10 +21623,22 @@ function deriveAiBirthcertVerifiedTemplateId(network) {
|
|
|
21583
21623
|
var TOKEN_REGISTRY = {
|
|
21584
21624
|
JMYR: { testnet: "ZTX3WeinXtt28YMyr4vUZ14ddTgEMGeuc1e6b", mainnet: "ZTX3NCkXBqbyJWjZZxciQez945Lu6tGAcjNJr" }
|
|
21585
21625
|
};
|
|
21626
|
+
function defaultPaymentCaps(network) {
|
|
21627
|
+
const jmyr = resolveTokenAddress("JMYR", network);
|
|
21628
|
+
return isTestnet(network) && jmyr ? { [jmyr]: "1000000", "*": "0" } : { "*": "0" };
|
|
21629
|
+
}
|
|
21630
|
+
function defaultCredentialIssuanceCaps(network) {
|
|
21631
|
+
const jmyr = resolveTokenAddress("JMYR", network);
|
|
21632
|
+
return jmyr ? { [jmyr]: "1000000", "*": "0" } : { "*": "0" };
|
|
21633
|
+
}
|
|
21634
|
+
var KNOWN_NETWORKS = ["zetrix:testnet", "zetrix:mainnet"];
|
|
21635
|
+
function isTestnet(network) {
|
|
21636
|
+
return network.includes("testnet");
|
|
21637
|
+
}
|
|
21586
21638
|
function resolveTokenAddress(symbol, network) {
|
|
21587
21639
|
const entry = TOKEN_REGISTRY[symbol.toUpperCase()];
|
|
21588
21640
|
if (!entry) return void 0;
|
|
21589
|
-
return network
|
|
21641
|
+
return isTestnet(network) ? entry.testnet : entry.mainnet;
|
|
21590
21642
|
}
|
|
21591
21643
|
function loadConfig(env) {
|
|
21592
21644
|
const req = (key, hint) => {
|
|
@@ -21599,7 +21651,17 @@ function loadConfig(env) {
|
|
|
21599
21651
|
return v && v.trim() ? v.trim() : void 0;
|
|
21600
21652
|
};
|
|
21601
21653
|
const network = opt("ZETRIX_NETWORK") ?? "zetrix:testnet";
|
|
21654
|
+
if (!KNOWN_NETWORKS.includes(network)) {
|
|
21655
|
+
throw new Error(
|
|
21656
|
+
`agentic-wallet-mcp: unrecognised ZETRIX_NETWORK "${network}" \u2014 expected one of ${KNOWN_NETWORKS.join(", ")}. Values are matched exactly and are case-sensitive; anything unrecognised would otherwise resolve to MAINNET addresses and grant a real spending allowance.`
|
|
21657
|
+
);
|
|
21658
|
+
}
|
|
21602
21659
|
const oid4vpBaseUrlOverride = opt("OID4VP_BASE_URL");
|
|
21660
|
+
const explicitCaps = parsePaymentCaps(opt("MAX_PAYMENT_AMOUNT"), {
|
|
21661
|
+
resolveSymbol: (symbol) => resolveTokenAddress(symbol, network),
|
|
21662
|
+
onWarn: (message) => process.stderr.write(`agentic-wallet-mcp: ${message}
|
|
21663
|
+
`)
|
|
21664
|
+
});
|
|
21603
21665
|
return {
|
|
21604
21666
|
walletBeUrl: stripTrailingSlash(opt("WALLET_BE_URL") ?? deriveWalletBeUrl(network)),
|
|
21605
21667
|
oid4vpBaseUrl: oid4vpBaseUrlOverride ? stripTrailingSlash(oid4vpBaseUrlOverride) : void 0,
|
|
@@ -21619,7 +21681,11 @@ function loadConfig(env) {
|
|
|
21619
21681
|
// Fail closed: an unset cap means "spend nothing", not "spend anything". A wallet that starts
|
|
21620
21682
|
// with no configuration at all must not be able to auto-pay a hostile x402 challenge. Raising
|
|
21621
21683
|
// it is a deliberate act.
|
|
21622
|
-
|
|
21684
|
+
//
|
|
21685
|
+
// An explicit MAX_PAYMENT_AMOUNT governs BOTH caps below — a user who sets a limit means it
|
|
21686
|
+
// everywhere. Only the defaults differ, and only because the two surfaces carry different risk.
|
|
21687
|
+
maxPaymentAmount: explicitCaps ?? defaultPaymentCaps(network),
|
|
21688
|
+
credentialIssuanceCaps: explicitCaps ?? defaultCredentialIssuanceCaps(network),
|
|
21623
21689
|
ssivcBaseUrl: (() => {
|
|
21624
21690
|
const v = opt("SSIVC_BASE_URL") ?? deriveSsivcBaseUrl(network);
|
|
21625
21691
|
return v ? stripTrailingSlash(v) : void 0;
|
|
@@ -21738,27 +21804,42 @@ async function fetchZTP20BalanceStrict(contractAddress, address, query) {
|
|
|
21738
21804
|
if (typeof balance !== "string") {
|
|
21739
21805
|
throw new Error("balanceOf returned no balance field");
|
|
21740
21806
|
}
|
|
21807
|
+
if (!/^\d+$/.test(balance)) {
|
|
21808
|
+
throw new Error(`balanceOf returned a non-numeric balance: ${JSON.stringify(balance)}`);
|
|
21809
|
+
}
|
|
21741
21810
|
return balance;
|
|
21742
21811
|
}
|
|
21812
|
+
function looksLikeContractAddress(token) {
|
|
21813
|
+
return /^ZTX[0-9A-Za-z]{30,}$/.test(token);
|
|
21814
|
+
}
|
|
21743
21815
|
async function queryTokenBalance(deps, token) {
|
|
21744
21816
|
const symbol = token.toUpperCase();
|
|
21745
21817
|
if (symbol === "ZTX") {
|
|
21746
21818
|
try {
|
|
21747
|
-
|
|
21819
|
+
const balance2 = await deps.fetchNativeBalance(deps.address);
|
|
21820
|
+
return { token: symbol, balance: balance2, decimals: ZTX_DECIMALS2, display: renderDisplay(balance2, ZTX_DECIMALS2, symbol) };
|
|
21748
21821
|
} catch {
|
|
21749
21822
|
return { token: symbol, error: "query_failed" };
|
|
21750
21823
|
}
|
|
21751
21824
|
}
|
|
21752
|
-
const
|
|
21825
|
+
const registered = deps.resolveTokenAddress(symbol);
|
|
21826
|
+
const byAddress = registered === null && looksLikeContractAddress(token);
|
|
21827
|
+
const contractAddress = registered ?? (byAddress ? token : null);
|
|
21753
21828
|
if (!contractAddress) return { token: symbol, error: "unknown_token" };
|
|
21829
|
+
const asked = byAddress ? token : symbol;
|
|
21754
21830
|
let balance;
|
|
21755
21831
|
try {
|
|
21756
21832
|
balance = await fetchZTP20BalanceStrict(contractAddress, deps.address, deps.query);
|
|
21757
21833
|
} catch {
|
|
21758
|
-
return { token:
|
|
21834
|
+
return { token: asked, error: "query_failed" };
|
|
21759
21835
|
}
|
|
21760
21836
|
const info = await fetchTokenInfo(contractAddress, deps.query);
|
|
21761
|
-
|
|
21837
|
+
const label = byAddress ? info?.symbol ?? asked : symbol;
|
|
21838
|
+
const decimals = info?.decimals ?? null;
|
|
21839
|
+
return { token: label, balance, decimals, display: renderDisplay(balance, decimals, label) };
|
|
21840
|
+
}
|
|
21841
|
+
function renderDisplay(raw, decimals, label) {
|
|
21842
|
+
return `${decimals === null ? raw : formatHumanAmount(raw, decimals)} ${label}`;
|
|
21762
21843
|
}
|
|
21763
21844
|
|
|
21764
21845
|
// src/clients/template-info-client.ts
|
|
@@ -22032,6 +22113,24 @@ var MbiClient = class _MbiClient {
|
|
|
22032
22113
|
if (!res.ok) throw await this.error(res, "apply (phase 2) failed");
|
|
22033
22114
|
return this.unwrap(res);
|
|
22034
22115
|
}
|
|
22116
|
+
/**
|
|
22117
|
+
* POST /v1/vc/pay/quote — the price for a template, with no issuance and no payment record.
|
|
22118
|
+
*
|
|
22119
|
+
* Unlike `applyChallenge`, this cannot mint anything: MBI resolves the template, builds the
|
|
22120
|
+
* canonical sign payload, and reads its configured price. Notably a FREE template does not
|
|
22121
|
+
* short-circuit into synchronous issuance here, which is what makes this safe to call purely to
|
|
22122
|
+
* find out a cost — `applyChallenge` is not (see its docstring).
|
|
22123
|
+
*
|
|
22124
|
+
* `data` is required to be non-empty by MBI's DTO but does not affect the price, which comes from
|
|
22125
|
+
* MBI's own asset config keyed by the resolved template. A caller pricing a credential before it
|
|
22126
|
+
* has collected any attribute therefore passes a throwaway value, and must not present it as
|
|
22127
|
+
* anything the user supplied.
|
|
22128
|
+
*/
|
|
22129
|
+
async quote(templateId, data) {
|
|
22130
|
+
const res = await this.fetch("POST", "/v1/vc/pay/quote", { templateId, data });
|
|
22131
|
+
if (!res.ok) throw await this.error(res, "quote failed");
|
|
22132
|
+
return this.unwrap(res);
|
|
22133
|
+
}
|
|
22035
22134
|
/** Idempotent recovery — GET /v1/vc/pay/status/{paymentId}. */
|
|
22036
22135
|
async getStatus(paymentId) {
|
|
22037
22136
|
const res = await this.fetch("GET", `/v1/vc/pay/status/${encodeURIComponent(paymentId)}`);
|
|
@@ -22169,6 +22268,43 @@ function payAndFetch(payer, req) {
|
|
|
22169
22268
|
return payer(req);
|
|
22170
22269
|
}
|
|
22171
22270
|
|
|
22271
|
+
// src/accept-selection.ts
|
|
22272
|
+
function extraOf(accept) {
|
|
22273
|
+
const extra = accept.extra;
|
|
22274
|
+
if (!extra || typeof extra !== "object" || Array.isArray(extra)) return {};
|
|
22275
|
+
return extra;
|
|
22276
|
+
}
|
|
22277
|
+
function assetOf(accept) {
|
|
22278
|
+
return String(accept.asset ?? "");
|
|
22279
|
+
}
|
|
22280
|
+
function declaresFacilitator(accept) {
|
|
22281
|
+
return extraOf(accept).gasModel === "facilitator";
|
|
22282
|
+
}
|
|
22283
|
+
function isSponsored(accept) {
|
|
22284
|
+
if (!declaresFacilitator(accept)) return false;
|
|
22285
|
+
const prepareEndpoint = extraOf(accept).prepareEndpoint;
|
|
22286
|
+
if (typeof prepareEndpoint !== "string" || prepareEndpoint === "") return false;
|
|
22287
|
+
const asset = assetOf(accept);
|
|
22288
|
+
return asset !== "" && asset !== "ZTX";
|
|
22289
|
+
}
|
|
22290
|
+
function isUnusable(accept) {
|
|
22291
|
+
return declaresFacilitator(accept) && !isSponsored(accept);
|
|
22292
|
+
}
|
|
22293
|
+
function orderAccepts(accepts, prefer) {
|
|
22294
|
+
const usable = accepts.filter((a) => !isUnusable(a));
|
|
22295
|
+
const sponsored = usable.filter(isSponsored);
|
|
22296
|
+
const selfPay = usable.filter((a) => !isSponsored(a));
|
|
22297
|
+
return prefer === "sponsored" ? [...sponsored, ...selfPay] : [...selfPay, ...sponsored];
|
|
22298
|
+
}
|
|
22299
|
+
function prepareBaseUrl(prepareEndpoint) {
|
|
22300
|
+
return prepareEndpoint.replace(/\/+$/, "").replace(/\/prepare$/, "");
|
|
22301
|
+
}
|
|
22302
|
+
function needsNativeGasCheck(accept) {
|
|
22303
|
+
const asset = String(accept.asset ?? "");
|
|
22304
|
+
if (asset === "" || asset === "ZTX") return false;
|
|
22305
|
+
return !isSponsored(accept);
|
|
22306
|
+
}
|
|
22307
|
+
|
|
22172
22308
|
// src/zetrix-hex.ts
|
|
22173
22309
|
function zetrixHexStringToBytes(s) {
|
|
22174
22310
|
const up = s.toUpperCase();
|
|
@@ -22359,6 +22495,45 @@ async function subscribeAndIssue(deps, opts) {
|
|
|
22359
22495
|
};
|
|
22360
22496
|
}
|
|
22361
22497
|
}
|
|
22498
|
+
if (opts.dryRun) {
|
|
22499
|
+
if (!deps.mbi.quote) {
|
|
22500
|
+
return {
|
|
22501
|
+
issued: false,
|
|
22502
|
+
reason: "dry run unavailable \u2014 this wallet has no MBI quote client, and pricing any other way risks issuing the credential",
|
|
22503
|
+
...schema ? { schema } : {}
|
|
22504
|
+
};
|
|
22505
|
+
}
|
|
22506
|
+
let quoted;
|
|
22507
|
+
try {
|
|
22508
|
+
quoted = await deps.mbi.quote(opts.templateId, attributes);
|
|
22509
|
+
} catch (err) {
|
|
22510
|
+
if (err instanceof MbiError) {
|
|
22511
|
+
return { issued: false, reason: err.message, httpStatus: err.httpStatus, ...schema ? { schema } : {} };
|
|
22512
|
+
}
|
|
22513
|
+
throw err;
|
|
22514
|
+
}
|
|
22515
|
+
const quotedAccept = quoted.accepts?.[0];
|
|
22516
|
+
if (!quotedAccept) {
|
|
22517
|
+
return { issued: false, reason: "MBI quoted no payment options", ...schema ? { schema } : {} };
|
|
22518
|
+
}
|
|
22519
|
+
const quotedRaw = String(quotedAccept.asset ?? "");
|
|
22520
|
+
const quotedAsset = deps.resolveSymbol ? await deps.resolveSymbol(quotedRaw) : quotedRaw;
|
|
22521
|
+
const reason = quoted.paymentRequired === false ? "dry run \u2014 issuance is free right now, no payment required" : quoted.paymentRequired === true ? "dry run \u2014 quoted only, no payment made" : "dry run \u2014 quoted only, no payment made. This MBI does not report whether issuance is free, so the quoted amount may not actually be charged.";
|
|
22522
|
+
return {
|
|
22523
|
+
issued: false,
|
|
22524
|
+
reason,
|
|
22525
|
+
quote: {
|
|
22526
|
+
asset: quotedAsset,
|
|
22527
|
+
maxAmountRequired: quotedAccept.maxAmountRequired,
|
|
22528
|
+
payTo: quotedAccept.payTo,
|
|
22529
|
+
// credential_preflight reads a missing gasModel as self-pay (preflight.ts), so a
|
|
22530
|
+
// sponsored template credential must say so explicitly rather than fall through as absent.
|
|
22531
|
+
gasModel: isSponsored(quotedAccept) ? "sponsored" : "self",
|
|
22532
|
+
...quoted.paymentRequired === void 0 ? {} : { paymentRequired: quoted.paymentRequired }
|
|
22533
|
+
},
|
|
22534
|
+
...schema ? { schema } : {}
|
|
22535
|
+
};
|
|
22536
|
+
}
|
|
22362
22537
|
const data = JSON.stringify([{ templateId: opts.templateId, metadata: attributes }]);
|
|
22363
22538
|
const blob = zetrixHexStringToBytes(data).toString("hex");
|
|
22364
22539
|
const { signBlob: signData, publicKey } = await deps.sign(blob);
|
|
@@ -22394,26 +22569,11 @@ async function subscribeAndIssue(deps, opts) {
|
|
|
22394
22569
|
txHash: issued2.txHash,
|
|
22395
22570
|
paidAsset: "none",
|
|
22396
22571
|
amountPaid: "0",
|
|
22397
|
-
...schema ? { schema } : {}
|
|
22398
|
-
...opts.dryRun ? { reason: "this template requires no payment \u2014 MBI issues synchronously at phase 1, so dryRun could not prevent this issuance" } : {}
|
|
22572
|
+
...schema ? { schema } : {}
|
|
22399
22573
|
};
|
|
22400
22574
|
}
|
|
22401
22575
|
const accept = challenge.accepts[0];
|
|
22402
22576
|
if (!accept) return { issued: false, reason: "MBI 402 returned no payment options", ...schema ? { schema } : {} };
|
|
22403
|
-
if (opts.dryRun) {
|
|
22404
|
-
const quotedRaw = String(accept.asset ?? "");
|
|
22405
|
-
const quotedAsset = deps.resolveSymbol ? await deps.resolveSymbol(quotedRaw) : quotedRaw;
|
|
22406
|
-
return {
|
|
22407
|
-
issued: false,
|
|
22408
|
-
reason: "dry run \u2014 quoted only, no payment made",
|
|
22409
|
-
quote: {
|
|
22410
|
-
asset: quotedAsset,
|
|
22411
|
-
maxAmountRequired: accept.maxAmountRequired,
|
|
22412
|
-
payTo: accept.payTo
|
|
22413
|
-
},
|
|
22414
|
-
...schema ? { schema } : {}
|
|
22415
|
-
};
|
|
22416
|
-
}
|
|
22417
22577
|
let xPayment;
|
|
22418
22578
|
try {
|
|
22419
22579
|
xPayment = await deps.pay(accept);
|
|
@@ -22874,6 +23034,102 @@ function validateTemplateAttributes(templateId, network, attributes) {
|
|
|
22874
23034
|
return errors;
|
|
22875
23035
|
}
|
|
22876
23036
|
|
|
23037
|
+
// src/orchestrator/preflight.ts
|
|
23038
|
+
var VERIFIED_AI_BIRTHCERT = "verified_ai_birthcert";
|
|
23039
|
+
var NATIVE = "ZTX";
|
|
23040
|
+
function placeholderAgentName() {
|
|
23041
|
+
return `preflight-quote-only-${Date.now()}`;
|
|
23042
|
+
}
|
|
23043
|
+
async function credentialPreflight(deps, input) {
|
|
23044
|
+
const blockers = [];
|
|
23045
|
+
const notChecked = [];
|
|
23046
|
+
const isVerified = input.credential === VERIFIED_AI_BIRTHCERT;
|
|
23047
|
+
const quoted = isVerified ? await deps.quoteVerified({ agentName: input.agentName?.trim() || placeholderAgentName(), dryRun: true }) : await deps.quoteTemplate(input.credential);
|
|
23048
|
+
const quote = quoted.quote;
|
|
23049
|
+
const schema = quoted.schema;
|
|
23050
|
+
if (!quote) {
|
|
23051
|
+
const why = String(quoted.error ?? quoted.reason ?? "the price could not be read");
|
|
23052
|
+
return {
|
|
23053
|
+
credential: input.credential,
|
|
23054
|
+
ready: false,
|
|
23055
|
+
balances: [],
|
|
23056
|
+
...schema ? { schema } : {},
|
|
23057
|
+
blockers: [`Could not price this credential: ${why}`],
|
|
23058
|
+
notChecked: uncheckable(isVerified)
|
|
23059
|
+
};
|
|
23060
|
+
}
|
|
23061
|
+
const asset = String(quote.asset ?? "");
|
|
23062
|
+
const requiredRaw = String(quote.maxAmountRequired ?? "0");
|
|
23063
|
+
const gasModel = quote.gasModel === "sponsored" || quote.gasModel === "self" ? quote.gasModel : void 0;
|
|
23064
|
+
const selfPaysGas = gasModel !== "sponsored";
|
|
23065
|
+
const wanted = asset === NATIVE || !selfPaysGas ? [asset] : [asset, NATIVE];
|
|
23066
|
+
const balances = await Promise.all(wanted.map((token) => deps.queryTokenBalance(token)));
|
|
23067
|
+
const isFree = quote.paymentRequired === false;
|
|
23068
|
+
const feeBalance = balances[0];
|
|
23069
|
+
if ("error" in feeBalance) {
|
|
23070
|
+
blockers.push(`Could not read the ${asset} balance (${feeBalance.error}) \u2014 retry before paying.`);
|
|
23071
|
+
} else if (!isFree && BigInt(feeBalance.balance) < BigInt(requiredRaw)) {
|
|
23072
|
+
blockers.push(`Not enough ${feeBalance.token}: the fee is ${renderAmount(requiredRaw, feeBalance)}, the balance is ${feeBalance.display}.`);
|
|
23073
|
+
}
|
|
23074
|
+
if (selfPaysGas && asset !== NATIVE) {
|
|
23075
|
+
const gas = balances[1];
|
|
23076
|
+
if (gas && "error" in gas) {
|
|
23077
|
+
blockers.push(`Could not read the ZTX balance (${gas.error}) \u2014 ZTX pays network gas and is separate from the fee.`);
|
|
23078
|
+
} else if (gas && BigInt(gas.balance) === 0n) {
|
|
23079
|
+
blockers.push(`No ZTX for network gas. ZTX is separate from the ${feeBalance.token ?? asset} fee and is needed for every transaction.`);
|
|
23080
|
+
}
|
|
23081
|
+
}
|
|
23082
|
+
const cap = describePaymentCap(asset, requiredRaw, deps.caps);
|
|
23083
|
+
if (!cap.wouldPass && !isFree) blockers.push(renderCapBlocker(cap, requiredRaw));
|
|
23084
|
+
return {
|
|
23085
|
+
credential: input.credential,
|
|
23086
|
+
ready: blockers.length === 0,
|
|
23087
|
+
fee: {
|
|
23088
|
+
asset,
|
|
23089
|
+
maxAmountRequired: requiredRaw,
|
|
23090
|
+
...quote.payTo ? { payTo: String(quote.payTo) } : {},
|
|
23091
|
+
...gasModel ? { gasModel } : {},
|
|
23092
|
+
..."error" in feeBalance ? {} : { display: renderAmount(requiredRaw, feeBalance) },
|
|
23093
|
+
...quote.paymentRequired === void 0 ? {} : { paymentRequired: quote.paymentRequired }
|
|
23094
|
+
},
|
|
23095
|
+
balances,
|
|
23096
|
+
cap,
|
|
23097
|
+
...schema ? { schema } : {},
|
|
23098
|
+
blockers,
|
|
23099
|
+
notChecked: uncheckable(isVerified, isFree)
|
|
23100
|
+
};
|
|
23101
|
+
}
|
|
23102
|
+
function renderAmount(raw, balance) {
|
|
23103
|
+
if ("error" in balance || balance.decimals === null) return raw;
|
|
23104
|
+
const d = BigInt(10) ** BigInt(balance.decimals);
|
|
23105
|
+
const whole = BigInt(raw) / d;
|
|
23106
|
+
const frac = (BigInt(raw) % d).toString().padStart(balance.decimals, "0").replace(/0+$/, "");
|
|
23107
|
+
return `${whole}${frac ? `.${frac}` : ""} ${balance.token}`;
|
|
23108
|
+
}
|
|
23109
|
+
function renderCapBlocker(cap, requiredRaw) {
|
|
23110
|
+
if (cap.capRaw === null) {
|
|
23111
|
+
return `No spending limit applies to ${cap.asset}, and limits are configured \u2014 so this payment would be refused. Set a limit keyed by "${cap.asset}".`;
|
|
23112
|
+
}
|
|
23113
|
+
const misKeyed = cap.matchedKey !== cap.asset;
|
|
23114
|
+
return misKeyed ? `The spending limit that applies is ${cap.capRaw} (from the "*" fallback \u2014 no limit is set for ${cap.asset}), and this needs ${requiredRaw}.` : `The spending limit for ${cap.asset} is ${cap.capRaw}, and this needs ${requiredRaw}.`;
|
|
23115
|
+
}
|
|
23116
|
+
function uncheckable(isVerified, isFree = false) {
|
|
23117
|
+
const items = [
|
|
23118
|
+
"Whether the wallet address is activated on chain \u2014 a never-funded address fails differently from a low balance."
|
|
23119
|
+
];
|
|
23120
|
+
if (isVerified) {
|
|
23121
|
+
items.unshift(
|
|
23122
|
+
"Whether the agent name is still free. myid checks uniqueness at issuance, not now, so a name in use still prices normally and is only refused after payment."
|
|
23123
|
+
);
|
|
23124
|
+
}
|
|
23125
|
+
if (isFree) {
|
|
23126
|
+
items.unshift(
|
|
23127
|
+
"Whether issuance is still free at apply time \u2014 `paymentRequired` is a service-wide MBI setting and can change between this quote and issuance."
|
|
23128
|
+
);
|
|
23129
|
+
}
|
|
23130
|
+
return items;
|
|
23131
|
+
}
|
|
23132
|
+
|
|
22877
23133
|
// src/mcp-tools.ts
|
|
22878
23134
|
async function loadValidCachedCredentials(cache) {
|
|
22879
23135
|
if (!cache) return [];
|
|
@@ -22882,10 +23138,15 @@ async function loadValidCachedCredentials(cache) {
|
|
|
22882
23138
|
}
|
|
22883
23139
|
var AI_BIRTHCERT_NOT_CONFIGURED_ERROR = "AI Birthcert verification is not configured on this wallet. On mainnet this is expected until SSIVC_BASE_URL is set explicitly (the mainnet host was never confirmed reachable \u2014 APP-M04); on testnet it means verifyAiBirthcert was not wired at all.";
|
|
22884
23140
|
function createTools(deps) {
|
|
22885
|
-
|
|
23141
|
+
const tools = {
|
|
22886
23142
|
async wallet_status(input = {}) {
|
|
22887
23143
|
const balances = deps.getBalances ? await deps.getBalances() : void 0;
|
|
22888
23144
|
const tokenBalance = input.token && deps.queryTokenBalance ? await deps.queryTokenBalance(input.token) : void 0;
|
|
23145
|
+
let tokenBalances;
|
|
23146
|
+
if (input.tokens?.length && deps.queryTokenBalance) {
|
|
23147
|
+
tokenBalances = [];
|
|
23148
|
+
for (const t of input.tokens) tokenBalances.push(await deps.queryTokenBalance(t));
|
|
23149
|
+
}
|
|
22889
23150
|
const credentials = input.heldCredentials ?? (await loadValidCachedCredentials(deps.cache)).map((entry) => entry.vc);
|
|
22890
23151
|
return {
|
|
22891
23152
|
holderDid: deps.config.holderDid,
|
|
@@ -22893,7 +23154,8 @@ function createTools(deps) {
|
|
|
22893
23154
|
network: deps.config.network,
|
|
22894
23155
|
credentials,
|
|
22895
23156
|
...balances !== void 0 ? { balances } : {},
|
|
22896
|
-
...tokenBalance !== void 0 ? { tokenBalance } : {}
|
|
23157
|
+
...tokenBalance !== void 0 ? { tokenBalance } : {},
|
|
23158
|
+
...tokenBalances !== void 0 ? { tokenBalances } : {}
|
|
22897
23159
|
};
|
|
22898
23160
|
},
|
|
22899
23161
|
async prove_identity(input) {
|
|
@@ -22990,6 +23252,38 @@ function createTools(deps) {
|
|
|
22990
23252
|
}
|
|
22991
23253
|
return deps.verifyAiBirthcert.request(input);
|
|
22992
23254
|
},
|
|
23255
|
+
/**
|
|
23256
|
+
* One read-only answer to "can this wallet buy this, and what does it cost?", before any
|
|
23257
|
+
* application field is collected. Composes the free pieces that already exist — a phase-1 quote,
|
|
23258
|
+
* balances, the spending cap — into a single verdict, so the balance and the cap stop being two
|
|
23259
|
+
* sequential dead ends.
|
|
23260
|
+
*/
|
|
23261
|
+
async credential_preflight(input) {
|
|
23262
|
+
if (input.credential === VERIFIED_AI_BIRTHCERT && !deps.verifyAiBirthcert) {
|
|
23263
|
+
return { credential: input.credential, ready: false, balances: [], blockers: [AI_BIRTHCERT_NOT_CONFIGURED_ERROR], notChecked: [] };
|
|
23264
|
+
}
|
|
23265
|
+
if (!deps.queryTokenBalance) {
|
|
23266
|
+
return {
|
|
23267
|
+
credential: input.credential,
|
|
23268
|
+
ready: false,
|
|
23269
|
+
balances: [],
|
|
23270
|
+
blockers: ["This wallet cannot read balances, so readiness cannot be confirmed."],
|
|
23271
|
+
notChecked: []
|
|
23272
|
+
};
|
|
23273
|
+
}
|
|
23274
|
+
return credentialPreflight(
|
|
23275
|
+
{
|
|
23276
|
+
quoteVerified: (q) => deps.verifyAiBirthcert.request(q),
|
|
23277
|
+
quoteTemplate: async (templateId) => {
|
|
23278
|
+
const resolved = resolveTemplateAlias(templateId, deps.config.network) ?? templateId;
|
|
23279
|
+
return priceTemplate(deps, tools, resolved);
|
|
23280
|
+
},
|
|
23281
|
+
queryTokenBalance: deps.queryTokenBalance,
|
|
23282
|
+
caps: deps.paymentCaps
|
|
23283
|
+
},
|
|
23284
|
+
input
|
|
23285
|
+
);
|
|
23286
|
+
},
|
|
22993
23287
|
check_ai_birthcert_verification() {
|
|
22994
23288
|
if (!deps.verifyAiBirthcert) {
|
|
22995
23289
|
return { error: AI_BIRTHCERT_NOT_CONFIGURED_ERROR };
|
|
@@ -22997,6 +23291,42 @@ function createTools(deps) {
|
|
|
22997
23291
|
return deps.verifyAiBirthcert.check();
|
|
22998
23292
|
}
|
|
22999
23293
|
};
|
|
23294
|
+
return tools;
|
|
23295
|
+
}
|
|
23296
|
+
async function priceTemplate(deps, tools, templateId) {
|
|
23297
|
+
const schemaResult = await tools.get_template_schema({ templateId });
|
|
23298
|
+
const schema = schemaResult.schema;
|
|
23299
|
+
if (!schema && schemaResult.error) {
|
|
23300
|
+
return { reason: schemaResult.error };
|
|
23301
|
+
}
|
|
23302
|
+
const quoteFn = deps.subscribeDeps.mbi.quote;
|
|
23303
|
+
if (!quoteFn) {
|
|
23304
|
+
return { reason: "this wallet cannot price template credentials (MBI quote unavailable)", ...schema ? { schema } : {} };
|
|
23305
|
+
}
|
|
23306
|
+
let accepts;
|
|
23307
|
+
let paymentRequired;
|
|
23308
|
+
try {
|
|
23309
|
+
;
|
|
23310
|
+
({ accepts, paymentRequired } = await quoteFn.call(deps.subscribeDeps.mbi, templateId, { preflight: "quote-only" }));
|
|
23311
|
+
} catch (err) {
|
|
23312
|
+
return { reason: err.message, ...schema ? { schema } : {} };
|
|
23313
|
+
}
|
|
23314
|
+
const accept = accepts?.[0];
|
|
23315
|
+
if (!accept) return { reason: "MBI quoted no payment options for this template", ...schema ? { schema } : {} };
|
|
23316
|
+
return {
|
|
23317
|
+
quote: {
|
|
23318
|
+
asset: accept.asset,
|
|
23319
|
+
maxAmountRequired: accept.maxAmountRequired,
|
|
23320
|
+
payTo: accept.payTo,
|
|
23321
|
+
// Stated either way: preflight reads a missing gasModel as self-pay, so silence here would be
|
|
23322
|
+
// indistinguishable from a deliberate "you pay the gas".
|
|
23323
|
+
gasModel: isSponsored(accept) ? "sponsored" : "self",
|
|
23324
|
+
// Carried through: this is the only thing in the response that
|
|
23325
|
+
// says whether `maxAmountRequired` will actually be charged, and preflight gates on it.
|
|
23326
|
+
...paymentRequired === void 0 ? {} : { paymentRequired }
|
|
23327
|
+
},
|
|
23328
|
+
...schema ? { schema } : {}
|
|
23329
|
+
};
|
|
23000
23330
|
}
|
|
23001
23331
|
|
|
23002
23332
|
// src/orchestrator/resolve-holder.ts
|
|
@@ -23338,43 +23668,6 @@ function canonicalizeJson(value) {
|
|
|
23338
23668
|
return JSON.stringify(value);
|
|
23339
23669
|
}
|
|
23340
23670
|
|
|
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
|
-
|
|
23378
23671
|
// src/orchestrator/verify-ai-birthcert.ts
|
|
23379
23672
|
function subjectMatches(vc, holderDid) {
|
|
23380
23673
|
if (typeof vc !== "object" || vc === null) return false;
|
|
@@ -23581,29 +23874,48 @@ async function requestAiBirthcertVerification(deps, input) {
|
|
|
23581
23874
|
throw new Error("requestAiBirthcertVerification: agentName is required");
|
|
23582
23875
|
}
|
|
23583
23876
|
const agentName = input.agentName.trim();
|
|
23877
|
+
if (input.dryRun) return quoteVerification(deps, agentName, input);
|
|
23584
23878
|
return withRequestLock(() => requestAiBirthcertVerificationLocked(deps, agentName, input));
|
|
23585
23879
|
}
|
|
23880
|
+
async function buildSessionBody(deps, agentName, input) {
|
|
23881
|
+
const fields = {
|
|
23882
|
+
publicKey: deps.publicKeyHex,
|
|
23883
|
+
address: deps.address,
|
|
23884
|
+
timestamp: isoSeconds(deps.now()),
|
|
23885
|
+
agentName,
|
|
23886
|
+
id: agentName,
|
|
23887
|
+
ownerReference: deps.holderDid
|
|
23888
|
+
};
|
|
23889
|
+
if (input.agentPurpose) fields.agentPurpose = input.agentPurpose;
|
|
23890
|
+
if (input.evidenceAssuranceLevel) fields.evidenceAssuranceLevel = input.evidenceAssuranceLevel;
|
|
23891
|
+
if (input.ownerType) fields.ownerType = input.ownerType;
|
|
23892
|
+
if (input.ownerVerified) fields.ownerVerified = input.ownerVerified;
|
|
23893
|
+
const digestHex = (0, import_node_crypto4.createHash)("sha256").update(canonicalizeJson(fields), "utf8").digest("hex");
|
|
23894
|
+
const { signBlob: signedData } = await deps.signHexBlob(digestHex);
|
|
23895
|
+
return { ...fields, signedData };
|
|
23896
|
+
}
|
|
23897
|
+
async function quoteVerification(deps, agentName, input) {
|
|
23898
|
+
const requestedGasPayer = input.gasPayer === "self" || input.gasPayer === "sponsored" ? input.gasPayer : void 0;
|
|
23899
|
+
const body = await buildSessionBody(deps, agentName, input);
|
|
23900
|
+
const challenge = await deps.ssivc.createSessionChallenge(body);
|
|
23901
|
+
const chosen = orderAccepts(challenge.accepts, requestedGasPayer ?? deps.gasPreference ?? "sponsored")[0];
|
|
23902
|
+
if (!chosen) return { error: "SSIVC 402 returned no usable payment options" };
|
|
23903
|
+
return {
|
|
23904
|
+
quote: {
|
|
23905
|
+
asset: String(chosen.asset ?? ""),
|
|
23906
|
+
maxAmountRequired: String(chosen.maxAmountRequired ?? ""),
|
|
23907
|
+
...chosen.payTo ? { payTo: String(chosen.payTo) } : {},
|
|
23908
|
+
// Report the option that would actually be paid, not merely what was offered — the ranking
|
|
23909
|
+
// already discards a sponsored quote we could not act on.
|
|
23910
|
+
gasModel: isSponsored(chosen) ? "sponsored" : "self"
|
|
23911
|
+
}
|
|
23912
|
+
};
|
|
23913
|
+
}
|
|
23586
23914
|
async function requestAiBirthcertVerificationLocked(deps, agentName, input) {
|
|
23587
23915
|
const decision = await decidePriorSession(deps, agentName);
|
|
23588
23916
|
if (decision.kind === "still_pending") return decision.result;
|
|
23589
23917
|
if (decision.kind === "blocked") return { error: decision.message };
|
|
23590
|
-
const buildBody =
|
|
23591
|
-
const fields = {
|
|
23592
|
-
publicKey: deps.publicKeyHex,
|
|
23593
|
-
address: deps.address,
|
|
23594
|
-
timestamp: isoSeconds(deps.now()),
|
|
23595
|
-
agentName,
|
|
23596
|
-
id: agentName,
|
|
23597
|
-
ownerReference: deps.holderDid
|
|
23598
|
-
};
|
|
23599
|
-
if (input.agentPurpose) fields.agentPurpose = input.agentPurpose;
|
|
23600
|
-
if (input.evidenceAssuranceLevel) fields.evidenceAssuranceLevel = input.evidenceAssuranceLevel;
|
|
23601
|
-
if (input.ownerType) fields.ownerType = input.ownerType;
|
|
23602
|
-
if (input.ownerVerified) fields.ownerVerified = input.ownerVerified;
|
|
23603
|
-
const digestHex = (0, import_node_crypto4.createHash)("sha256").update(canonicalizeJson(fields), "utf8").digest("hex");
|
|
23604
|
-
const { signBlob: signedData } = await deps.signHexBlob(digestHex);
|
|
23605
|
-
return { ...fields, signedData };
|
|
23606
|
-
};
|
|
23918
|
+
const buildBody = () => buildSessionBody(deps, agentName, input);
|
|
23607
23919
|
const persistQueuedReceipt = async (receipt) => {
|
|
23608
23920
|
try {
|
|
23609
23921
|
await deps.sessionStore.set({
|
|
@@ -23639,8 +23951,8 @@ async function requestAiBirthcertVerificationLocked(deps, agentName, input) {
|
|
|
23639
23951
|
}
|
|
23640
23952
|
} catch (rawErr) {
|
|
23641
23953
|
const err = rawErr instanceof PrepareStageError ? rawErr.cause : rawErr;
|
|
23642
|
-
if (err instanceof PaymentReadinessError) return { error: `insufficient funds: ${err.message}
|
|
23643
|
-
if (err instanceof PaymentCapError) return { error: err.message };
|
|
23954
|
+
if (err instanceof PaymentReadinessError) return { error: `insufficient funds: ${err.message}`, insufficientFunds: err.shortfall };
|
|
23955
|
+
if (err instanceof PaymentCapError) return { error: err.message, ...err.detail ? { paymentCap: err.detail } : {} };
|
|
23644
23956
|
if (err instanceof NoPaymentOptionsError) return { error: err.message };
|
|
23645
23957
|
if (err instanceof FacilitatorInsufficientFundsError) return { error: err.message };
|
|
23646
23958
|
if (err instanceof SettlementStillQueuedError) {
|
|
@@ -23693,6 +24005,9 @@ async function checkAiBirthcertVerification(deps) {
|
|
|
23693
24005
|
};
|
|
23694
24006
|
}
|
|
23695
24007
|
const status = await deps.ssivc.getSession(stored.sessionId);
|
|
24008
|
+
if (status.status !== "issued" && stored.verificationUrl) {
|
|
24009
|
+
return { ...status, verificationUrl: stored.verificationUrl };
|
|
24010
|
+
}
|
|
23696
24011
|
if (status.status !== "issued" || !status.vcId) return status;
|
|
23697
24012
|
if (deps.verifiedTemplateId && deps.cache) {
|
|
23698
24013
|
const cached2 = await deps.cache.get(deps.verifiedTemplateId);
|
|
@@ -23762,7 +24077,8 @@ function buildToolList() {
|
|
|
23762
24077
|
type: "object",
|
|
23763
24078
|
properties: {
|
|
23764
24079
|
heldCredentials: { type: "array", items: { type: "object" }, description: "VCs the client holds. Omit to report whatever the wallet has cached locally from prior subscribe_and_issue calls instead." },
|
|
23765
|
-
token: { type: "string", description:
|
|
24080
|
+
token: { type: "string", description: 'Optional token symbol (e.g. "ZTX", "JMYR") OR a ZTP20 contract address, to check its balance for the active network alongside the usual status fields. Returns { balance, decimals, display } \u2014 `balance` is in the asset\'s raw base units and `display` is the same amount in whole tokens with its symbol (e.g. balance "473999900", decimals 6, display "473.9999 JMYR"). Quote a `display` value to the user, never a bare `balance`. A failed lookup reports { error: "query_failed" } rather than a zero balance; an unrecognised name reports { error: "unknown_token" }.' },
|
|
24081
|
+
tokens: { type: "array", items: { type: "string" }, description: 'Several tokens (symbols and/or ZTP20 contract addresses) in one call, returned as `tokenBalances` in the order asked. Prefer this over repeated single-token calls when checking affordability: a credential fee and the native ZTX needed for gas are separate balances, and asking one at a time is how "you hold the token but no gas" is discovered only after the first shortfall was already fixed. Each entry carries its own result or error, so one failure does not hide the rest.' }
|
|
23766
24082
|
}
|
|
23767
24083
|
}
|
|
23768
24084
|
},
|
|
@@ -23845,7 +24161,7 @@ function buildToolList() {
|
|
|
23845
24161
|
expirationDate: { type: "string" },
|
|
23846
24162
|
dryRun: {
|
|
23847
24163
|
type: "boolean",
|
|
23848
|
-
description: "
|
|
24164
|
+
description: "Price the credential without paying, signing, or issuing anything. Returns { quote: { asset, maxAmountRequired, payTo, gasModel, paymentRequired? }, schema: { required, optional } }. Priced via MBI's /quote endpoint, which cannot issue \u2014 so this is safe even on a template that issues for free (such a template mints synchronously on the real path, which is why pricing never goes through it). Never writes the VC cache, so it cannot displace a credential you already hold. `quote.paymentRequired` is the authoritative free-vs-paid answer: false means issuance is currently free and `maxAmountRequired` will not be charged. When the field is ABSENT the MBI predates it and the amount is unconfirmed \u2014 report it as a possible charge, never as certainly free. Still validates required attributes locally first \u2014 a missing one blocks before any MBI call."
|
|
23849
24165
|
},
|
|
23850
24166
|
forceReissue: {
|
|
23851
24167
|
type: "boolean",
|
|
@@ -23857,7 +24173,7 @@ function buildToolList() {
|
|
|
23857
24173
|
},
|
|
23858
24174
|
{
|
|
23859
24175
|
name: "request_ai_birthcert_verification",
|
|
23860
|
-
description: `Start a Verified AI Birthcert issuance session with myid (MyDigital ID owner verification). Returns { sessionId, verificationUrl, expiresAt } \u2014 show verificationUrl to the human owner and ask them to open it and complete MyDigital ID verification (typically finishes in seconds). Once they confirm they are done, call check_ai_birthcert_verification to see whether the credential was issued. IMPORTANT: agentName must be unique \u2014 if this exact name has already been used to request a Verified AI Birthcert, issuance will fail. Before calling, ask the human owner whether they want to supply any of the optional fields \u2014 agentPurpose, evidenceAssuranceLevel, ownerType, ownerVerified \u2014 do not silently omit them; they only need to say no. Calling this again with the SAME agentName while a prior session is still pending returns that same session unchanged \u2014 no new session is started and nothing is paid again. This tool spends real funds: it self-pays an x402 challenge, subject to the
|
|
24176
|
+
description: `Start a Verified AI Birthcert issuance session with myid (MyDigital ID owner verification). Returns { sessionId, verificationUrl, expiresAt } \u2014 show verificationUrl to the human owner and ask them to open it and complete MyDigital ID verification (typically finishes in seconds). Once they confirm they are done, call check_ai_birthcert_verification to see whether the credential was issued. IMPORTANT: agentName must be unique \u2014 if this exact name has already been used to request a Verified AI Birthcert, issuance will fail. Before calling, ask the human owner whether they want to supply any of the optional fields \u2014 agentPurpose, evidenceAssuranceLevel, ownerType, ownerVerified \u2014 do not silently omit them; they only need to say no. Calling this again with the SAME agentName while a prior session is still pending returns that same session unchanged \u2014 no new session is started and nothing is paid again. This tool spends real funds: it self-pays an x402 challenge, subject to the same credential-issuance payment cap as subscribe_and_issue \u2014 a separate, narrower cap than pay_and_fetch's, which defaults to refusing everything on mainnet. Set MAX_PAYMENT_AMOUNT to override either. It can return { error: "..." } instead of a session if that payment fails (insufficient funds, or the payment cap blocked it) \u2014 nothing is created in that case. If the user did NOT ask for a "verified" credential specifically, they most likely want the self-declared, non-verified Basic AI Birthcert instead \u2014 use subscribe_and_issue for that.`,
|
|
23861
24177
|
inputSchema: {
|
|
23862
24178
|
type: "object",
|
|
23863
24179
|
properties: {
|
|
@@ -23873,14 +24189,36 @@ function buildToolList() {
|
|
|
23873
24189
|
type: "string",
|
|
23874
24190
|
enum: ["sponsored", "self"],
|
|
23875
24191
|
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.`
|
|
24192
|
+
},
|
|
24193
|
+
dryRun: {
|
|
24194
|
+
type: "boolean",
|
|
24195
|
+
description: "Ask the price WITHOUT paying. Returns { quote: { asset, maxAmountRequired, payTo, gasModel } } and spends nothing, creates no session, and starts no verification \u2014 so you can tell the user the cost before collecting anything. `maxAmountRequired` is in the asset's RAW base units; resolve decimals (wallet_status returns `display`) before quoting a figure to a human. A quote does NOT reserve the name and does NOT check whether it is already taken \u2014 myid checks uniqueness only at issuance, so a name already in use still quotes cleanly. `agentName` is still required because the server rejects a request without one, but the fee does not depend on it."
|
|
23876
24196
|
}
|
|
23877
24197
|
},
|
|
23878
24198
|
required: ["agentName"]
|
|
23879
24199
|
}
|
|
23880
24200
|
},
|
|
24201
|
+
{
|
|
24202
|
+
name: "credential_preflight",
|
|
24203
|
+
description: "FREE readiness check \u2014 call this FIRST, before collecting ANY application detail from the user, whenever they ask for a credential. Spends nothing, signs no transaction, creates no session. Returns { ready, fee, balances, cap, schema?, blockers, notChecked }: the live fee and which side pays gas, the balances that matter, whether the spending limit permits it, and \u2014 for a template credential \u2014 the attributes it requires. `blockers` lists EVERY reason it is not ready at once (a low balance and a too-low spending limit are different problems and both appear together), so one round of fixes is enough rather than discovering them one failed payment at a time. ALWAYS relay `notChecked` too: a clean result is not a guarantee. In particular it does NOT check whether an agent name is free \u2014 myid decides that at issuance, after payment. Report `fee.display` and each balance's `display` to the user, never the raw base-unit numbers. `fee.paymentRequired: false` means issuance is currently FREE \u2014 `fee.display` is then only what it WOULD cost if payment were switched back on, so do not ask the user to fund it and do not present that amount as a charge. Gas is separate and can still block a free credential. When the field is ABSENT the cost is unknown (an older MBI), and it is treated as chargeable \u2014 never report absent as free.",
|
|
24204
|
+
inputSchema: {
|
|
24205
|
+
type: "object",
|
|
24206
|
+
properties: {
|
|
24207
|
+
credential: {
|
|
24208
|
+
type: "string",
|
|
24209
|
+
description: 'Which credential to price: "verified_ai_birthcert" for the MyDigital-ID-verified AI Birthcert, or a template id (did:zid:...) / known template name (e.g. "AI Birthcert") for a template-issued one.'
|
|
24210
|
+
},
|
|
24211
|
+
agentName: {
|
|
24212
|
+
type: "string",
|
|
24213
|
+
description: 'Optional, and only used for "verified_ai_birthcert". The fee does not depend on it, so omit it when pricing before the user has chosen a name \u2014 the wallet substitutes a placeholder purely to satisfy the server. Never present that placeholder as the name that will be used.'
|
|
24214
|
+
}
|
|
24215
|
+
},
|
|
24216
|
+
required: ["credential"]
|
|
24217
|
+
}
|
|
24218
|
+
},
|
|
23881
24219
|
{
|
|
23882
24220
|
name: "check_ai_birthcert_verification",
|
|
23883
|
-
description: 'Check the status of the most recently requested Verified AI Birthcert session (see request_ai_birthcert_verification). Returns { status: "pending" } while the owner has not yet completed MyDigital ID verification, or { status: "issued", vcId } once myid has minted the credential \u2014 myid returns vcId ONLY when status is "issued", never otherwise. On { status: "issued" }, the wallet also fetches the credential from MBI, verifies it, and caches it locally, returning it as `vc` \u2014 it is then also visible via wallet_status and usable by prove_identity without any further call. If `cacheError` is present instead of `vc`, the credential WAS issued successfully but could not be fetched/verified/cached yet (e.g. a transient MBI error) \u2014 this is NOT the same as issuance failing, so do not retry request_ai_birthcert_verification; call check_ai_birthcert_verification again instead. Returns { status: "no_session" } if request_ai_birthcert_verification has never been called.',
|
|
24221
|
+
description: 'Check the status of the most recently requested Verified AI Birthcert session (see request_ai_birthcert_verification). FREE \u2014 spends nothing and starts nothing. Use this, not request_ai_birthcert_verification, whenever the user asks where their verification link is, what happened to their session, or whether their credential is ready. While the session is still open the result carries `verificationUrl` (the same link issued at creation) and `expiresAt` \u2014 give the user both, so they know how long it is good for. Returns { status: "pending" } while the owner has not yet completed MyDigital ID verification, or { status: "issued", vcId } once myid has minted the credential \u2014 myid returns vcId ONLY when status is "issued", never otherwise. On { status: "issued" }, the wallet also fetches the credential from MBI, verifies it, and caches it locally, returning it as `vc` \u2014 it is then also visible via wallet_status and usable by prove_identity without any further call. If `cacheError` is present instead of `vc`, the credential WAS issued successfully but could not be fetched/verified/cached yet (e.g. a transient MBI error) \u2014 this is NOT the same as issuance failing, so do not retry request_ai_birthcert_verification; call check_ai_birthcert_verification again instead. Returns { status: "no_session" } if request_ai_birthcert_verification has never been called.',
|
|
23884
24222
|
inputSchema: { type: "object", properties: {} }
|
|
23885
24223
|
},
|
|
23886
24224
|
{
|
|
@@ -23900,6 +24238,13 @@ function buildToolList() {
|
|
|
23900
24238
|
}
|
|
23901
24239
|
];
|
|
23902
24240
|
}
|
|
24241
|
+
function buildPayers(config2, makePay) {
|
|
24242
|
+
return {
|
|
24243
|
+
pay: makePay(config2.maxPaymentAmount),
|
|
24244
|
+
payForCredential: makePay(config2.credentialIssuanceCaps),
|
|
24245
|
+
preflightCaps: config2.credentialIssuanceCaps
|
|
24246
|
+
};
|
|
24247
|
+
}
|
|
23903
24248
|
function asPayRequest(accept) {
|
|
23904
24249
|
const extra = accept.extra ?? {};
|
|
23905
24250
|
const prepareEndpoint = extra.prepareEndpoint;
|
|
@@ -23992,15 +24337,19 @@ async function main() {
|
|
|
23992
24337
|
const human = formatHumanAmount(raw, decimals);
|
|
23993
24338
|
return human === raw ? `${raw} ${label}` : `${raw} (${human} ${label})`;
|
|
23994
24339
|
};
|
|
23995
|
-
const
|
|
24340
|
+
const makePay = (caps) => async (accept) => {
|
|
23996
24341
|
const rawAsset = String(accept.asset ?? "");
|
|
23997
24342
|
try {
|
|
23998
|
-
assertWithinPaymentCap(accept,
|
|
24343
|
+
assertWithinPaymentCap(accept, caps);
|
|
23999
24344
|
} catch (err) {
|
|
24000
24345
|
if (err instanceof PaymentCapError && err.detail) {
|
|
24001
24346
|
const { asset, requiredRaw, capRaw } = err.detail;
|
|
24002
24347
|
throw new PaymentCapError(
|
|
24003
|
-
|
|
24348
|
+
formatCapRefusal(
|
|
24349
|
+
err.detail,
|
|
24350
|
+
await formatAssetAmount(asset, requiredRaw),
|
|
24351
|
+
await formatAssetAmount(asset, capRaw)
|
|
24352
|
+
),
|
|
24004
24353
|
err.detail
|
|
24005
24354
|
);
|
|
24006
24355
|
}
|
|
@@ -24034,6 +24383,7 @@ async function main() {
|
|
|
24034
24383
|
throw err;
|
|
24035
24384
|
}
|
|
24036
24385
|
};
|
|
24386
|
+
const { pay, payForCredential, preflightCaps } = buildPayers(config2, makePay);
|
|
24037
24387
|
const verifyAiBirthcert = config2.ssivcBaseUrl ? (() => {
|
|
24038
24388
|
const ssivcSessionStore = createFsSsivcSessionStore((0, import_node_path6.join)(config2.stateDir, "ssivc-session.json"));
|
|
24039
24389
|
const downloadQuarantine = createFsDownloadQuarantineStore((0, import_node_path6.join)(config2.stateDir, "ssivc-download-quarantine"));
|
|
@@ -24043,7 +24393,7 @@ async function main() {
|
|
|
24043
24393
|
signHexBlob: walletBeSignerFn,
|
|
24044
24394
|
messageSigner,
|
|
24045
24395
|
mbi,
|
|
24046
|
-
pay,
|
|
24396
|
+
pay: payForCredential,
|
|
24047
24397
|
publicKeyHex,
|
|
24048
24398
|
address: zetrixAddress,
|
|
24049
24399
|
holderDid,
|
|
@@ -24105,9 +24455,13 @@ async function main() {
|
|
|
24105
24455
|
config: { holderDid, zetrixAddress, network: config2.network },
|
|
24106
24456
|
makeWallet,
|
|
24107
24457
|
payer,
|
|
24108
|
-
subscribeDeps: { mbi, sign: subscribeSign, pay, resolveSymbol, holderDid, resolveTemplateFields, cache: vcCache },
|
|
24458
|
+
subscribeDeps: { mbi, sign: subscribeSign, pay: payForCredential, resolveSymbol, holderDid, resolveTemplateFields, cache: vcCache },
|
|
24109
24459
|
queryContract: (input) => queryContract(input, contractQuery),
|
|
24110
24460
|
queryTokenBalance: queryTokenBalance2,
|
|
24461
|
+
// Read-only, for credential_preflight's cap headroom. Preflight only ever prices credentials,
|
|
24462
|
+
// so it must report against the issuance cap — the same map payForCredential enforces, or
|
|
24463
|
+
// preflight and the guard would disagree about what is permitted.
|
|
24464
|
+
paymentCaps: preflightCaps,
|
|
24111
24465
|
// The session password is bound here, in the wiring, so it never crosses into the tool
|
|
24112
24466
|
// layer — create_holder_account has no password parameter for a model to be asked for.
|
|
24113
24467
|
createAccount: (label, purpose) => be.createAccount(hsmPassword, label, purpose),
|
|
@@ -24150,5 +24504,6 @@ if (process.env.NODE_ENV !== "test") {
|
|
|
24150
24504
|
// Annotate the CommonJS export names for ESM import in node:
|
|
24151
24505
|
0 && (module.exports = {
|
|
24152
24506
|
asPayRequest,
|
|
24507
|
+
buildPayers,
|
|
24153
24508
|
buildToolList
|
|
24154
24509
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentic-wallet-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Agent-facing MCP wallet for Zetrix — orchestrates x401 identity proof, x402 payment, and MBI VC issuance",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
45
45
|
"x401-zetrix-client": "^0.2.1",
|
|
46
|
-
"x402-zetrix-client": "^0.2.
|
|
46
|
+
"x402-zetrix-client": "^0.2.4",
|
|
47
47
|
"zetrix-sdk-nodejs": "^1.0.3"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|