agentic-wallet-mcp 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +154 -0
- package/README.md +97 -16
- package/dist/server-bundle.cjs +205 -38
- package/package.json +11 -2
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `agentic-wallet-mcp` are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
> Entries for 0.5.0 and earlier were reconstructed from commit history when this file was
|
|
9
|
+
> introduced in 0.6.0, so they summarise each release rather than being exhaustive.
|
|
10
|
+
|
|
11
|
+
## [0.6.1] — 2026-07-29
|
|
12
|
+
|
|
13
|
+
**Upgrade if you use testnet.** The testnet Wallet BE and MBI endpoints have moved to the Zetrix
|
|
14
|
+
sandbox hosts. No API, tool or config surface changed — this is an endpoint migration only.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- **Testnet default endpoints moved to the Zetrix sandbox hosts.** `ZETRIX_NETWORK=zetrix:testnet`
|
|
19
|
+
now derives `https://wallet-api-sandbox.zetrix.com/server` (was
|
|
20
|
+
`https://wallet-api.myegdev.com/server`) and `https://mbi-vc-sandbox.zetrix.com` (was
|
|
21
|
+
`https://mbi-vc.myegdev.com`). Anyone on testnet relying on the built-in defaults should upgrade
|
|
22
|
+
to follow the platform; mainnet defaults are unchanged, and an explicit `WALLET_BE_URL` /
|
|
23
|
+
`MBI_BASE_URL` still wins over the network default, so anyone pinning those is unaffected.
|
|
24
|
+
Note the Wallet BE base keeps its `/server` path suffix — verified against the new host, which
|
|
25
|
+
returns nginx `404` without it.
|
|
26
|
+
|
|
27
|
+
## [0.6.0] — 2026-07-28
|
|
28
|
+
|
|
29
|
+
This release is about the wallet never misstating what a call cost. Four reporting defects were
|
|
30
|
+
found during live use, in each case the wallet told the caller something untrue about money.
|
|
31
|
+
|
|
32
|
+
### Changed — BREAKING
|
|
33
|
+
|
|
34
|
+
- **`amountPaid` is now `undefined` on a cache hit.** Previously a cache hit replayed the original
|
|
35
|
+
issuance's `txHash`/`paidAsset`/`amountPaid` at the top level, so a **free** call was
|
|
36
|
+
indistinguishable from a fresh charge and anything summing `amountPaid` across calls
|
|
37
|
+
double-counted. Those values now appear under `originalPayment: { txHash, asset, amount }`
|
|
38
|
+
instead, and are omitted entirely when the cached credential was issued free. Any consumer
|
|
39
|
+
reading top-level `amountPaid` to track spend needs updating.
|
|
40
|
+
|
|
41
|
+
### Added
|
|
42
|
+
|
|
43
|
+
- **`get_template_schema` tool** — a free read of a credential template's declared attribute
|
|
44
|
+
schema (`{ required, optional }`), taking a `did:zid:...` id or a known template name. No
|
|
45
|
+
payment, no signing, no issuer call. Previously the only way to ask what a template required was
|
|
46
|
+
`subscribe_and_issue` with `dryRun` — a tool whose name reads as "this charges money" — so an
|
|
47
|
+
agent had no obvious reason to reach for it and would discover a newly-required attribute by
|
|
48
|
+
failing an issuance first. A template that cannot be read returns `{ error }` rather than an
|
|
49
|
+
empty schema, so "needs nothing" is never confused with "could not look it up".
|
|
50
|
+
- **`staleAttributes` on a cache hit** — `{ missing, dropped }` when a held credential no longer
|
|
51
|
+
matches the template it came from. Validity checks only ever asked whether a credential had
|
|
52
|
+
expired, never whether its fields still fit the template, so an issuer changing a template left
|
|
53
|
+
holders with a credential that looked valid and wasn't. The cached credential is still returned;
|
|
54
|
+
this reports, it does not re-issue or charge.
|
|
55
|
+
- **`decimals` on `wallet_status({ token })`** — the raw base-unit balance stays canonical (it is
|
|
56
|
+
the unit x402 quotes `maxAmountRequired` in, so cap checks and comparisons remain integer-only),
|
|
57
|
+
but callers no longer need a second contract call to know whether `"473999900"` means 474 or
|
|
58
|
+
474 million.
|
|
59
|
+
- **`paymentAttempted: { asset, amount, paymentId }`** on any failure occurring after the x402
|
|
60
|
+
payment has settled on chain. The issuer's error body carries neither the amount nor a
|
|
61
|
+
transaction reference, so such a debit was previously invisible in the response and discoverable
|
|
62
|
+
only by comparing `balanceOf` before and after. `paymentId` is the handle the issuer's idempotent
|
|
63
|
+
recovery endpoint takes.
|
|
64
|
+
- **Indeterminate-settlement recovery.** The issuer distinguishes a definitive facilitator
|
|
65
|
+
rejection from an outcome that is *unknown* — where the payment may well have landed and the
|
|
66
|
+
record is deliberately left recoverable. On the indeterminate code the wallet now polls the
|
|
67
|
+
issuer's status endpoint (bounded) and reports `recovery: { status, txHash?, vcId?, polls }`,
|
|
68
|
+
rather than discarding a credential already paid for. `status: "ISSUED"` means it exists after
|
|
69
|
+
all; note the status endpoint returns only its id, not the credential body.
|
|
70
|
+
- **`MbiError.mbiStatus`** — the issuer's own numeric status code, parsed from the error body.
|
|
71
|
+
Previously only the HTTP status and an opaque message string were available, so the two
|
|
72
|
+
post-payment failures could be told apart only by substring-matching or by trusting an HTTP 502
|
|
73
|
+
that any gateway can emit.
|
|
74
|
+
- **Explicit HTTP deadline** on the issuer client (90s, overridable), above the issuer's own 60s
|
|
75
|
+
facilitator timeout. Previously the runtime default applied, which happened to be longer but was
|
|
76
|
+
not a deliberate choice — a deadline at or below the issuer's would abort a settlement still
|
|
77
|
+
legitimately in progress.
|
|
78
|
+
|
|
79
|
+
### Fixed
|
|
80
|
+
|
|
81
|
+
- **Failed balance lookups no longer report a fabricated zero.** Any failure — non-zero
|
|
82
|
+
`errorCode`, missing field, malformed payload — previously collapsed to `{ balance: '0' }`,
|
|
83
|
+
making an unreachable node indistinguishable from an empty wallet. Worse, because the
|
|
84
|
+
underlying helpers returned *normally*, a caller's `try`/`catch` never fired. Now surfaced as
|
|
85
|
+
`{ error: 'query_failed' }`. Both the ZTP20 and the native ZTX path had the same defect; both
|
|
86
|
+
are fixed.
|
|
87
|
+
- `originalPayment` is omitted for a cached credential that was issued free, rather than reported
|
|
88
|
+
as `{ asset: 'none', amount: '0' }` — matching the documented behaviour.
|
|
89
|
+
|
|
90
|
+
### Documentation
|
|
91
|
+
|
|
92
|
+
- `query_contract` documented for the first time. It shipped in 0.5.0 but was never added to the
|
|
93
|
+
tool list. The documentation now also states plainly that it is a pass-through with no ABI or
|
|
94
|
+
method list — the contract decides what it understands, and an unknown method returns the same
|
|
95
|
+
shape as a typo.
|
|
96
|
+
- The `subscribe_and_issue` description previously claimed `schema` is returned on *every*
|
|
97
|
+
response; the cache path returns before the chain lookup, so it never did. Corrected, and the
|
|
98
|
+
cache path now performs the lookup so the claim holds.
|
|
99
|
+
|
|
100
|
+
## [0.5.0] — 2026-07-27
|
|
101
|
+
|
|
102
|
+
### Added
|
|
103
|
+
|
|
104
|
+
- Wallet BE account-activation checking: `activated`/`activationTxHash` fields,
|
|
105
|
+
`checkActivationStatus`, a bounded `waitForActivation` polling helper, and polling wired into
|
|
106
|
+
both first-run account creation and `create_holder_account`.
|
|
107
|
+
- x402 payment readiness — `pay_and_fetch` and `subscribe_and_issue` surface an insufficient-funds
|
|
108
|
+
shortfall as a structured result instead of throwing.
|
|
109
|
+
- Per-network JMYR token registry (`resolveTokenAddress`) and token-balance lookup on
|
|
110
|
+
`wallet_status`.
|
|
111
|
+
- `query_contract` — general-purpose read-only contract/account query, exposed as an agent tool.
|
|
112
|
+
- Template attribute validation and derivation, with the full declared schema surfaced.
|
|
113
|
+
|
|
114
|
+
### Fixed
|
|
115
|
+
|
|
116
|
+
- A `resolveHolder` polling failure degrades instead of crashing startup.
|
|
117
|
+
- The `hsmPassword` is persisted alongside address/DID on account override, not dropped.
|
|
118
|
+
|
|
119
|
+
## [0.4.0] — 2026-07-24
|
|
120
|
+
|
|
121
|
+
### Added
|
|
122
|
+
|
|
123
|
+
- Local cache of issued credentials, keyed by template, so `subscribe_and_issue` does not pay and
|
|
124
|
+
re-issue for a credential already held.
|
|
125
|
+
- Named-template alias resolution (e.g. `"AI Birthcert"`), and an `agentDid` auto-fill gated on the
|
|
126
|
+
template's declared schema.
|
|
127
|
+
|
|
128
|
+
### Fixed
|
|
129
|
+
|
|
130
|
+
- `revealAttributes` ordering to match the credential's signed field order, which was breaking BBS+
|
|
131
|
+
presentation verification.
|
|
132
|
+
- Free-template synchronous issuance handled in the issuer's phase 1.
|
|
133
|
+
|
|
134
|
+
## [0.3.0] — 0.3.1
|
|
135
|
+
|
|
136
|
+
### Added
|
|
137
|
+
|
|
138
|
+
- Live x401 proof integration: OID4VP submit authentication, DCQL reveal mapping, and an issuer-key
|
|
139
|
+
override for when the resolver is unreachable.
|
|
140
|
+
- Integration guide, presentation-submission fix, and the switch to the published
|
|
141
|
+
`x401-zetrix-client` package.
|
|
142
|
+
|
|
143
|
+
## [0.2.0]
|
|
144
|
+
|
|
145
|
+
### Added
|
|
146
|
+
|
|
147
|
+
- Optional `ZETRIX_ADDRESS`/`HOLDER_DID` onboarding, with `HSM_PASSWORD` guaranteed present.
|
|
148
|
+
- x402 asset symbol resolved from a ZTP20 contract's `contractInfo`.
|
|
149
|
+
|
|
150
|
+
## [0.1.0]
|
|
151
|
+
|
|
152
|
+
Initial release — the five agent-facing tools (`wallet_status`, `prove_identity`, `pay_and_fetch`,
|
|
153
|
+
`subscribe_and_issue`, `create_holder_account`) over x401, x402, and issuer-side credential
|
|
154
|
+
issuance, with all signing through Wallet BE's HSM.
|
package/README.md
CHANGED
|
@@ -13,18 +13,20 @@ through Wallet BE's HSM.
|
|
|
13
13
|
- **holder key custody + signing** → Wallet BE softHSM (`/wallet/hsm/*`)
|
|
14
14
|
- **VC issuance** → MBI RS (`/v1/vc/pay/*`)
|
|
15
15
|
|
|
16
|
-
[`docs/USAGE_FLOW.md`](docs/USAGE_FLOW.md) has a full end-to-end prompt script (onboarding →
|
|
16
|
+
[`docs/USAGE_FLOW.md`](https://github.com/Zetrix-Chain/zetrix-agentic-wallet/blob/main/docs/USAGE_FLOW.md) has a full end-to-end prompt script (onboarding →
|
|
17
17
|
VC issuance → identity proof → pay-per-use).
|
|
18
18
|
|
|
19
19
|
## Tools
|
|
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) | `{ heldCredentials? }` | `{ holderDid, zetrixAddress, network, credentials }` |
|
|
23
|
+
| `wallet_status` | Report holder DID/address/network + held VCs (client-supplied, or the local cache); optionally a token balance | `{ heldCredentials?, token? }` | `{ holderDid, zetrixAddress, network, credentials, tokenBalance? }` |
|
|
24
24
|
| `prove_identity` | Answer an x401 `PROOF-REQUEST` → return the `PROOF-RESPONSE` header to replay | `{ proofRequest, vc?, revealAttribute?, issuerKeys? }` | `{ proofResponseHeader, verified, presentationId }` |
|
|
25
25
|
| `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
|
-
| `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? }` |
|
|
26
|
+
| `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
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
|
+
| `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
|
+
| `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 }` |
|
|
28
30
|
|
|
29
31
|
> **VCs are cached locally**, keyed by `templateId`, under `~/.agentic-wallet-mcp/vc-cache/`
|
|
30
32
|
> (scoped per network + holder — different identities or networks never share a cache).
|
|
@@ -39,14 +41,39 @@ VC issuance → identity proof → pay-per-use).
|
|
|
39
41
|
> All Ed25519 signing still goes through Wallet BE HSM; no plaintext private keys.
|
|
40
42
|
|
|
41
43
|
> `subscribe_and_issue` also returns `schema: { required, optional }` — the template's full
|
|
42
|
-
> declared attribute list, read from chain — on every outcome
|
|
43
|
-
> missing-attribute error), so you always see the complete field list rather
|
|
44
|
-
> wrong
|
|
44
|
+
> declared attribute list, read from chain — on every outcome that reaches the chain (issued,
|
|
45
|
+
> dry-run quote, or a missing-attribute error), so you always see the complete field list rather
|
|
46
|
+
> than only what went wrong — **including on a cache hit**, so a caller holding a credential can
|
|
47
|
+
> still see what the template currently asks for.
|
|
48
|
+
> Attributes the wallet auto-fills for you (e.g. `agentDid`, or a template-declared derived
|
|
45
49
|
> key like the `AI Birthcert` template's `id` ← `agentUsername`) are omitted from `schema` since
|
|
46
50
|
> you never need to supply them. Some templates also declare format validators for optional
|
|
47
51
|
> attributes (e.g. `AI Birthcert`'s `dob` must be `YYYY-MM-DD`, `countryOfOrigin` must be a valid
|
|
48
52
|
> ISO 3166 code or name) — an invalid value is rejected locally before any payment or MBI call.
|
|
49
53
|
|
|
54
|
+
> **A held credential can go stale without expiring.** `isVcValid` only asks whether a VC is past
|
|
55
|
+
> its `validUntil` — a VC whose *fields* no longer match the template it came from still reads as
|
|
56
|
+
> valid. When an issuer changes a template (adds a required attribute, drops one), a cache hit now
|
|
57
|
+
> reports the delta:
|
|
58
|
+
>
|
|
59
|
+
> ```jsonc
|
|
60
|
+
> { "fromCache": true, "staleAttributes": { "missing": ["agentUsername"], "dropped": ["agentName"] } }
|
|
61
|
+
> ```
|
|
62
|
+
>
|
|
63
|
+
> `missing` is what a reissue would need you to supply; `dropped` is what the held VC carries that
|
|
64
|
+
> the template no longer declares. Absent when the held VC still satisfies the template. **The
|
|
65
|
+
> cached VC is still returned** — this reports, it does not re-issue or charge. Reported from live
|
|
66
|
+
> testing: without it, an agent reusing the attributes from a previously-issued credential only
|
|
67
|
+
> discovered a newly-required field by attempting an issuance and being rejected.
|
|
68
|
+
|
|
69
|
+
> **`get_template_schema` is the free way to ask what a template needs**, and is what an agent
|
|
70
|
+
> should reach for before building an issuance request. It takes a `did:zid:...` id *or* a known
|
|
71
|
+
> template name (e.g. `"AI Birthcert"`), performs no payment/signing/MBI call, and hides the
|
|
72
|
+
> attributes the wallet fills in itself. A template it cannot read returns `{ error }` rather than
|
|
73
|
+
> an empty schema, so *"needs nothing"* is never confused with *"couldn't look it up"*.
|
|
74
|
+
> (`subscribe_and_issue` with `dryRun: true` also returns the schema, alongside the price — use
|
|
75
|
+
> that when you want both.)
|
|
76
|
+
|
|
50
77
|
> `create_holder_account` mints a **brand-new** keypair — Wallet BE's `/account/create` has no
|
|
51
78
|
> way to provision a pre-chosen address. It always checks first whether an account is already
|
|
52
79
|
> active for this session; if so, it returns `{ alreadyExists: true, existing }` and creates
|
|
@@ -58,6 +85,49 @@ VC issuance → identity proof → pay-per-use).
|
|
|
58
85
|
> in your MCP config always overrides the saved account (see Environment below); the tool never
|
|
59
86
|
> writes the MCP host's own config file or restarts the server for you.
|
|
60
87
|
|
|
88
|
+
> **What a call cost is reported precisely.** `paidAsset`/`amountPaid` are set **only when this
|
|
89
|
+
> call paid**. A cache hit reports the earlier charge under `originalPayment: { txHash, asset,
|
|
90
|
+
> amount }` — never as `amountPaid` — so summing spend across calls cannot double-count a free
|
|
91
|
+
> hit (omitted entirely when the cached VC was issued free). If issuance fails **after** the x402
|
|
92
|
+
> payment has already settled on chain (MBI phase 2), the response carries
|
|
93
|
+
> `paymentAttempted: { asset, amount, paymentId }`. MBI's own error body reports none of these, so
|
|
94
|
+
> this is the only in-band signal that you were charged — and `paymentId` is the handle MBI's
|
|
95
|
+
> recovery endpoint takes. It is never set for a phase-1 failure, which happens before anything is
|
|
96
|
+
> paid. **Never retry a post-payment failure: the funds may already be gone, and each attempt costs
|
|
97
|
+
> the full amount again — look the `paymentId` up instead.**
|
|
98
|
+
|
|
99
|
+
> **Two post-payment failures exist and they mean different things:**
|
|
100
|
+
> `4006` is a *definitive* facilitator rejection, while `4012` (HTTP 502) means the settle outcome
|
|
101
|
+
> is **indeterminate** — MBI stopped listening but the payment may well have landed, so it
|
|
102
|
+
> deliberately leaves the record recoverable rather than marking it failed. On `4012` the wallet
|
|
103
|
+
> polls `GET /v1/vc/pay/status/{paymentId}` for you and reports
|
|
104
|
+
> `recovery: { status, txHash?, vcId?, polls }`:
|
|
105
|
+
>
|
|
106
|
+
> | `recovery.status` | Means |
|
|
107
|
+
> |---|---|
|
|
108
|
+
> | `ISSUED` | The payment landed and the credential exists after all. `/status` returns only its `vcId`, **not** the VC body — so `vc` stays unset and you fetch it separately. |
|
|
109
|
+
> | `FAILED` | MBI's terminal verdict on the payment. |
|
|
110
|
+
> | `REQUIRED` / `SETTLED` | Still unresolved when the poll budget ran out — check again later with the `paymentId`. |
|
|
111
|
+
> | `UNKNOWN` | `/status` itself was unreachable. The charge still stands; retry the lookup, not the payment. |
|
|
112
|
+
|
|
113
|
+
> `wallet_status({ token })` returns `tokenBalance: { token, balance, decimals }` for `ZTX` or any
|
|
114
|
+
> registered ZTP20 symbol (e.g. `JMYR`). `balance` is in the asset's **raw base units** — the same
|
|
115
|
+
> unit x402 quotes `maxAmountRequired` in, so cap checks and quote comparisons stay integer-only.
|
|
116
|
+
> Divide by `10^decimals` to display it: `"473999900"` with `decimals: 6` is `473.9999 JMYR`.
|
|
117
|
+
> `decimals` is `null` if the token's `contractInfo` can't be read (the balance is still returned).
|
|
118
|
+
> A failed lookup reports `{ token, error: "query_failed" }` and an unregistered symbol
|
|
119
|
+
> `{ token, error: "unknown_token" }` — **never a zero balance**, so "0" always means you really
|
|
120
|
+
> hold nothing rather than that the node was unreachable.
|
|
121
|
+
|
|
122
|
+
> `query_contract` is a **pass-through**: whatever you put in `method` is sent to the contract
|
|
123
|
+
> as-is. It performs no validation and holds no ABI or method list — the contract decides what it
|
|
124
|
+
> understands, and an unknown method comes back as `{ ok: false, error: "query_contract: no result
|
|
125
|
+
> value returned" }`, the same shape as a typo. To discover what a token supports, start with
|
|
126
|
+
> `contractInfo` (returns `symbol`, `decimals`, `protocol`, `supply`, …); a `protocol: "ztp20"`
|
|
127
|
+
> contract implements at least `balanceOf({ address })`, `totalSupply()` and
|
|
128
|
+
> `allowance({ owner, spender })`. Read-only only (`optType: 2`) — state-changing methods like
|
|
129
|
+
> `transfer`/`approve` need signing and are not reachable through this tool.
|
|
130
|
+
|
|
61
131
|
> `revealAttribute` on `prove_identity` is optional and usually should stay that way. Omitted,
|
|
62
132
|
> it's derived automatically from the challenge's DCQL `credential_requirements` — each claim path
|
|
63
133
|
> is resolved against the presented VC's `credentialSubject` (e.g. a DCQL leaf name `agentName`
|
|
@@ -146,11 +216,11 @@ network:
|
|
|
146
216
|
|
|
147
217
|
| Network | `WALLET_BE_URL` default | `MBI_BASE_URL` default | `OID4VP_BASE_URL` default | `ZID_RESOLVER_BASE_URL` default |
|
|
148
218
|
|---|---|---|---|---|
|
|
149
|
-
| `zetrix:testnet` | `https://wallet-api.
|
|
219
|
+
| `zetrix:testnet` | `https://wallet-api-sandbox.zetrix.com/server` | `https://mbi-vc-sandbox.zetrix.com` | `https://zid-oid4vp-sandbox.zetrix.com/api` | `https://zid-resolver-sandbox.zetrix.com` |
|
|
150
220
|
| `zetrix:mainnet` | `https://wallet-api.zetrix.com/server` | `https://mbi-vc.zetrix.com` | `https://zid-oid4vp.zetrix.com/api` | `https://zid-resolver.zetrix.com` |
|
|
151
221
|
|
|
152
222
|
Only set any of the four explicitly if you run your own instance of that service instead of the
|
|
153
|
-
default one — an explicit value always wins over the network default. The
|
|
223
|
+
default one — an explicit value always wins over the network default. The `*-sandbox.zetrix.com`
|
|
154
224
|
(testnet) hosts are public endpoints — no VPN needed; if you're on a corporate VPN and one of
|
|
155
225
|
them times out, disconnecting it is more likely to fix that than connecting it. See "Network
|
|
156
226
|
reachability & troubleshooting" below.
|
|
@@ -205,16 +275,19 @@ the `<...>` placeholders (don't commit a filled copy; `mcp.local.json` is gitign
|
|
|
205
275
|
## Example prompts
|
|
206
276
|
|
|
207
277
|
- *"Check my wallet status."* → `wallet_status`
|
|
278
|
+
- *"What's my JMYR balance?"* → `wallet_status({ token: "JMYR" })` (divide `balance` by `10^decimals` to display it)
|
|
208
279
|
- *"I got a 401 with this PROOF-REQUEST header — prove my identity and give me the PROOF-RESPONSE to replay."* → `prove_identity`
|
|
209
280
|
- *"Fetch `https://api.example/data` and pay automatically if it asks."* → `pay_and_fetch`
|
|
281
|
+
- *"What fields does the AI Birthcert template need?"* → `get_template_schema` (free — do this before issuing)
|
|
210
282
|
- *"Apply for the agent-identity credential with these attributes and pay for it."* → `subscribe_and_issue`
|
|
211
283
|
- *"My wallet_status call is failing — I don't have a holder account yet. Set one up."* → `create_holder_account` (asks you for a password; if an account already exists it reports that instead of creating — confirm with the user, then re-call with `confirmNew: true` to replace it)
|
|
284
|
+
- *"What's the total supply of this token contract?"* → `query_contract({ contractAddress, method: "totalSupply" })`
|
|
212
285
|
|
|
213
|
-
For the full ordered script (onboarding → check → issue → prove → pay), see [`docs/USAGE_FLOW.md`](docs/USAGE_FLOW.md).
|
|
286
|
+
For the full ordered script (onboarding → check → issue → prove → pay), see [`docs/USAGE_FLOW.md`](https://github.com/Zetrix-Chain/zetrix-agentic-wallet/blob/main/docs/USAGE_FLOW.md).
|
|
214
287
|
|
|
215
288
|
## End-to-end usage flow
|
|
216
289
|
|
|
217
|
-
Full narrative version with example prompts: [`docs/USAGE_FLOW.md`](docs/USAGE_FLOW.md).
|
|
290
|
+
Full narrative version with example prompts: [`docs/USAGE_FLOW.md`](https://github.com/Zetrix-Chain/zetrix-agentic-wallet/blob/main/docs/USAGE_FLOW.md).
|
|
218
291
|
|
|
219
292
|
**Step 0 — onboarding (once).** Only if `ZETRIX_ADDRESS` isn't set yet: the MCP creates an HSM
|
|
220
293
|
account automatically at startup from `HSM_PASSWORD` alone (see "Onboarding" under Environment
|
|
@@ -241,7 +314,7 @@ via MBI (`/vp/ext/create` + `/vp/ext/submit`, `includeVp: true`) → resolve the
|
|
|
241
314
|
verification keys via the ZID resolver → submit to the verifier with wallet-auth headers →
|
|
242
315
|
package the signed result as a `PROOF-RESPONSE`. Replaying that header back to the original
|
|
243
316
|
resource server happens outside this MCP, in whatever drove the conversation. See "The
|
|
244
|
-
`PROOF-REQUEST` header" section in [`docs/USAGE_FLOW.md`](docs/USAGE_FLOW.md) for the exact wire
|
|
317
|
+
`PROOF-REQUEST` header" section in [`docs/USAGE_FLOW.md`](https://github.com/Zetrix-Chain/zetrix-agentic-wallet/blob/main/docs/USAGE_FLOW.md) for the exact wire
|
|
245
318
|
structure and field semantics.
|
|
246
319
|
|
|
247
320
|
**Phase 4 — `pay_and_fetch` — pay-per-use.**
|
|
@@ -250,17 +323,25 @@ Independent of Phases 2/3 — no VC or identity proof involved, just a fresh pay
|
|
|
250
323
|
|
|
251
324
|
## Network reachability & troubleshooting
|
|
252
325
|
|
|
253
|
-
The
|
|
254
|
-
|
|
255
|
-
|
|
326
|
+
The `*-sandbox.zetrix.com` testnet endpoints are public — no VPN needed to reach them. Reachability
|
|
327
|
+
differs per host, and this is the first thing to check when something that worked before suddenly
|
|
328
|
+
times out or 403s, before assuming a code regression.
|
|
329
|
+
|
|
330
|
+
- **`wallet-api-sandbox.zetrix.com` / `mbi-vc-sandbox.zetrix.com`** — direct origin IPs
|
|
331
|
+
(`124.243.148.237` / `111.119.237.222` as of 2026-07-29), **not** CDN-fronted, unlike the ZID
|
|
332
|
+
hosts and the mainnet endpoints. Being off-CDN is why the **corporate VPN blocks them
|
|
333
|
+
specifically** while `test-node.zetrix.com` and the mainnet hosts keep working over the same
|
|
334
|
+
VPN — so a failure here looks like a total outage while everything else looks healthy.
|
|
335
|
+
Disconnect the VPN. Verified reachable over a plain internet path on 2026-07-29.
|
|
336
|
+
- **ZID resolver** sits behind a CDN edge with a managed challenge — see its row below.
|
|
256
337
|
|
|
257
338
|
| Symptom | Cause | Fix / status |
|
|
258
339
|
|---|---|---|
|
|
259
|
-
| `Wallet BE /wallet/hsm/sign-blob request failed <- fetch failed <- UND_ERR_CONNECT_TIMEOUT` (or same for `mbi-vc.
|
|
340
|
+
| `Wallet BE /wallet/hsm/sign-blob request failed <- fetch failed <- UND_ERR_CONNECT_TIMEOUT` (or same for `mbi-vc-sandbox.zetrix.com`) | Corporate VPN routing away from the public internet — it blocks these two direct-origin hosts specifically (observed 2026-07-29), or a transient network issue | **Disconnect the VPN and retry.** Verify reachability directly: `curl -sS -o /dev/null -w '%{http_code}\n' "https://wallet-api-sandbox.zetrix.com/server/wallet/hsm/account/activate/status?address=<yourAddress>"` — `200` (with an `errorCode: 0` body) is healthy; `000` means the TCP connect never completed, so it's a network path problem, not a code problem |
|
|
260
341
|
| `VP derivation failed <- ZID resolver HTTP 403 ... cf-mitigated: challenge` | ZID resolver (`zid-resolver-sandbox.zetrix.com`) sitting behind a Cloudflare **managed challenge** that blocks plain server-to-server `fetch` | Server-to-server access to the resolver must be allowlisted so it returns `200` directly. If the challenge is active, `prove_identity`'s `issuerKeys` input is the fallback — fetch the DID document via a real browser (it clears the JS challenge) and pass its `verificationMethod` entries' `publicKeyMultibase` (BBS+) / `publicKeyHex` (Ed25519) directly. |
|
|
261
342
|
| `OID4VP backend returned a malformed presentation definition` | Historical SDK bug: the live sandbox's `GET /v1/presentation/{id}` response has no `expires_at` field, but the SDK guard required one | **Fixed** in `x401-zetrix-client` — `expiresAt` is now optional on `PresentationDefinition`. If you see this, you're on a stale cached `npx` install — clear it (`npx clear-npx-cache` or bump the version) to pick up the current published `agentic-wallet-mcp`. |
|
|
262
343
|
| `SUBMIT_FAILED: OID4VP backend returned 401 ... Missing X-Wallet-Public-Key header` | Historical SDK gap: `POST /v1/presentation/submit` requires wallet-auth headers (`X-Wallet-Public-Key` / `X-Wallet-Signed-Data`, holder signs their own address) that the SDK didn't send | **Fixed** — `X401Wallet` now accepts an injected `submitAuth` provider and the wallet wires it automatically; nothing to configure. |
|
|
263
|
-
| `MBI /vp/ext/submit did not return vp` | MBI's `includeVp` opt-in not deployed on the target instance | Deployed on the sandbox (`mbi-vc.
|
|
344
|
+
| `MBI /vp/ext/submit did not return vp` | MBI's `includeVp` opt-in not deployed on the target instance | Deployed on the sandbox (`mbi-vc-sandbox.zetrix.com`). If you see this against a different MBI instance, that instance needs the same rollout. |
|
|
264
345
|
|
|
265
346
|
## Security notes
|
|
266
347
|
|
package/dist/server-bundle.cjs
CHANGED
|
@@ -12547,9 +12547,17 @@ var import_node_path3 = require("node:path");
|
|
|
12547
12547
|
// package.json
|
|
12548
12548
|
var package_default = {
|
|
12549
12549
|
name: "agentic-wallet-mcp",
|
|
12550
|
-
version: "0.
|
|
12550
|
+
version: "0.6.1",
|
|
12551
12551
|
description: "Agent-facing MCP wallet for Zetrix \u2014 orchestrates x401 identity proof, x402 payment, and MBI VC issuance",
|
|
12552
|
-
keywords: [
|
|
12552
|
+
keywords: [
|
|
12553
|
+
"mcp",
|
|
12554
|
+
"model-context-protocol",
|
|
12555
|
+
"zetrix",
|
|
12556
|
+
"wallet",
|
|
12557
|
+
"x401",
|
|
12558
|
+
"x402",
|
|
12559
|
+
"blockchain"
|
|
12560
|
+
],
|
|
12553
12561
|
license: "MIT",
|
|
12554
12562
|
author: "MyEG Services Berhad",
|
|
12555
12563
|
homepage: "https://github.com/Zetrix-Chain/zetrix-agentic-wallet",
|
|
@@ -12570,6 +12578,7 @@ var package_default = {
|
|
|
12570
12578
|
files: [
|
|
12571
12579
|
"dist/server-bundle.cjs",
|
|
12572
12580
|
"README.md",
|
|
12581
|
+
"CHANGELOG.md",
|
|
12573
12582
|
"LICENSE"
|
|
12574
12583
|
],
|
|
12575
12584
|
scripts: {
|
|
@@ -21539,10 +21548,10 @@ function deriveZidResolverBaseUrl(network) {
|
|
|
21539
21548
|
return network.includes("testnet") ? "https://zid-resolver-sandbox.zetrix.com" : "https://zid-resolver.zetrix.com";
|
|
21540
21549
|
}
|
|
21541
21550
|
function deriveWalletBeUrl(network) {
|
|
21542
|
-
return network.includes("testnet") ? "https://wallet-api.
|
|
21551
|
+
return network.includes("testnet") ? "https://wallet-api-sandbox.zetrix.com/server" : "https://wallet-api.zetrix.com/server";
|
|
21543
21552
|
}
|
|
21544
21553
|
function deriveMbiBaseUrl(network) {
|
|
21545
|
-
return network.includes("testnet") ? "https://mbi-vc.
|
|
21554
|
+
return network.includes("testnet") ? "https://mbi-vc-sandbox.zetrix.com" : "https://mbi-vc.zetrix.com";
|
|
21546
21555
|
}
|
|
21547
21556
|
function deriveTemplateRegistryAddress(network) {
|
|
21548
21557
|
return network.includes("testnet") ? "ZTX3JszqPgRUx743SAp7q7zURfjvkWuH2FMEz" : "ZTX3GqJM1U6ifMPonwD4fGvrgoTKJua7b2cKX";
|
|
@@ -21643,6 +21652,49 @@ async function queryContract(input, query) {
|
|
|
21643
21652
|
}
|
|
21644
21653
|
}
|
|
21645
21654
|
|
|
21655
|
+
// src/clients/token-balance-client.ts
|
|
21656
|
+
var ZTX_DECIMALS = 6;
|
|
21657
|
+
async function fetchZTP20BalanceStrict(contractAddress, address, query) {
|
|
21658
|
+
const result = await query({
|
|
21659
|
+
contractAddress,
|
|
21660
|
+
input: JSON.stringify({ method: "balanceOf", params: { address } }),
|
|
21661
|
+
optType: 2
|
|
21662
|
+
});
|
|
21663
|
+
if (result?.errorCode !== 0) {
|
|
21664
|
+
throw new Error(`balanceOf failed with errorCode ${result?.errorCode}`);
|
|
21665
|
+
}
|
|
21666
|
+
const raw = result.result?.query_rets?.[0]?.result?.value;
|
|
21667
|
+
if (raw === void 0 || raw === null || raw === "") {
|
|
21668
|
+
throw new Error("balanceOf returned no value");
|
|
21669
|
+
}
|
|
21670
|
+
const parsed = JSON.parse(raw);
|
|
21671
|
+
const balance = parsed?.balance;
|
|
21672
|
+
if (typeof balance !== "string") {
|
|
21673
|
+
throw new Error("balanceOf returned no balance field");
|
|
21674
|
+
}
|
|
21675
|
+
return balance;
|
|
21676
|
+
}
|
|
21677
|
+
async function queryTokenBalance(deps, token) {
|
|
21678
|
+
const symbol = token.toUpperCase();
|
|
21679
|
+
if (symbol === "ZTX") {
|
|
21680
|
+
try {
|
|
21681
|
+
return { token: symbol, balance: await deps.fetchNativeBalance(deps.address), decimals: ZTX_DECIMALS };
|
|
21682
|
+
} catch {
|
|
21683
|
+
return { token: symbol, error: "query_failed" };
|
|
21684
|
+
}
|
|
21685
|
+
}
|
|
21686
|
+
const contractAddress = deps.resolveTokenAddress(symbol);
|
|
21687
|
+
if (!contractAddress) return { token: symbol, error: "unknown_token" };
|
|
21688
|
+
let balance;
|
|
21689
|
+
try {
|
|
21690
|
+
balance = await fetchZTP20BalanceStrict(contractAddress, deps.address, deps.query);
|
|
21691
|
+
} catch {
|
|
21692
|
+
return { token: symbol, error: "query_failed" };
|
|
21693
|
+
}
|
|
21694
|
+
const info = await fetchTokenInfo(contractAddress, deps.query);
|
|
21695
|
+
return { token: symbol, balance, decimals: info?.decimals ?? null };
|
|
21696
|
+
}
|
|
21697
|
+
|
|
21646
21698
|
// src/clients/template-info-client.ts
|
|
21647
21699
|
async function fetchTemplateFields(templateId, registryAddress, nodeBaseUrl, query) {
|
|
21648
21700
|
const key = `template__${templateId}`;
|
|
@@ -21860,16 +21912,35 @@ var MbiVpAdapter = class {
|
|
|
21860
21912
|
// src/clients/mbi-client.ts
|
|
21861
21913
|
var MbiError = class extends Error {
|
|
21862
21914
|
httpStatus;
|
|
21863
|
-
|
|
21915
|
+
/**
|
|
21916
|
+
* MBI's own numeric code from the ResponseWrapper body — finer-grained than the HTTP status
|
|
21917
|
+
* and the only reliable way to tell a *definitive* post-payment failure from an indeterminate
|
|
21918
|
+
* one. Undefined when the body carries no numeric `status`.
|
|
21919
|
+
*/
|
|
21920
|
+
mbiStatus;
|
|
21921
|
+
constructor(message, httpStatus, mbiStatus) {
|
|
21864
21922
|
super(message);
|
|
21865
21923
|
this.name = "MbiError";
|
|
21866
21924
|
this.httpStatus = httpStatus;
|
|
21925
|
+
this.mbiStatus = mbiStatus;
|
|
21867
21926
|
}
|
|
21868
21927
|
};
|
|
21928
|
+
var MBI_SETTLEMENT_INDETERMINATE = 4012;
|
|
21869
21929
|
var MbiClient = class _MbiClient {
|
|
21870
21930
|
baseUrl;
|
|
21871
|
-
|
|
21931
|
+
/** Per-request deadline in ms. See {@link MbiClient.DEFAULT_TIMEOUT_MS}. */
|
|
21932
|
+
timeoutMs;
|
|
21933
|
+
/**
|
|
21934
|
+
* MBI's own facilitator read timeout is 60s, and `/settle` submits
|
|
21935
|
+
* a transaction on chain. A client deadline at or below that would abort a settle MBI is
|
|
21936
|
+
* still legitimately waiting on — manufacturing exactly the indeterminate outcome that MR
|
|
21937
|
+
* exists to prevent, except on our side where nothing records it. Sits above 60s so MBI
|
|
21938
|
+
* always gets to answer first; raise it in step if MBI's own timeout is ever raised.
|
|
21939
|
+
*/
|
|
21940
|
+
static DEFAULT_TIMEOUT_MS = 9e4;
|
|
21941
|
+
constructor(baseUrl, opts) {
|
|
21872
21942
|
this.baseUrl = baseUrl.replace(/\/+$/, "");
|
|
21943
|
+
this.timeoutMs = opts?.timeoutMs ?? _MbiClient.DEFAULT_TIMEOUT_MS;
|
|
21873
21944
|
}
|
|
21874
21945
|
/**
|
|
21875
21946
|
* Phase 1 — POST /v1/vc/pay/apply without X-PAYMENT; expects the 402 challenge.
|
|
@@ -21919,6 +21990,7 @@ var MbiClient = class _MbiClient {
|
|
|
21919
21990
|
return fetch(`${this.baseUrl}${path}`, {
|
|
21920
21991
|
method,
|
|
21921
21992
|
headers,
|
|
21993
|
+
signal: AbortSignal.timeout(this.timeoutMs),
|
|
21922
21994
|
...body !== void 0 ? { body: JSON.stringify(body) } : {}
|
|
21923
21995
|
}).catch((e) => {
|
|
21924
21996
|
throw new MbiError(`MBI ${path} request failed: ${e.message}`);
|
|
@@ -21933,13 +22005,15 @@ var MbiClient = class _MbiClient {
|
|
|
21933
22005
|
async error(res, context) {
|
|
21934
22006
|
const text = await res.text().catch(() => "");
|
|
21935
22007
|
let msg = text;
|
|
22008
|
+
let mbiStatus;
|
|
21936
22009
|
try {
|
|
21937
22010
|
const j = JSON.parse(text);
|
|
22011
|
+
if (typeof j.status === "number") mbiStatus = j.status;
|
|
21938
22012
|
const truncated = text.length > _MbiClient.ERROR_BODY_MAX_LEN ? `${text.slice(0, _MbiClient.ERROR_BODY_MAX_LEN)}\u2026 (truncated, ${text.length} bytes total)` : text;
|
|
21939
22013
|
msg = `${j.message ?? j.error ?? text} | full body: ${truncated}`;
|
|
21940
22014
|
} catch {
|
|
21941
22015
|
}
|
|
21942
|
-
return new MbiError(`MBI ${context} \u2014 HTTP ${res.status}: ${msg}`, res.status);
|
|
22016
|
+
return new MbiError(`MBI ${context} \u2014 HTTP ${res.status}: ${msg}`, res.status, mbiStatus);
|
|
21943
22017
|
}
|
|
21944
22018
|
};
|
|
21945
22019
|
|
|
@@ -22084,6 +22158,19 @@ function isVcValid(entry, now = /* @__PURE__ */ new Date()) {
|
|
|
22084
22158
|
if (Number.isNaN(expiry.getTime())) return false;
|
|
22085
22159
|
return expiry.getTime() > now.getTime();
|
|
22086
22160
|
}
|
|
22161
|
+
function extractAttributeKeys(vc) {
|
|
22162
|
+
if (typeof vc !== "object" || vc === null) return [];
|
|
22163
|
+
const subject = vc.credentialSubject;
|
|
22164
|
+
if (typeof subject !== "object" || subject === null) return [];
|
|
22165
|
+
const keys = /* @__PURE__ */ new Set();
|
|
22166
|
+
for (const [key, value] of Object.entries(subject)) {
|
|
22167
|
+
if (key === "id") continue;
|
|
22168
|
+
if (typeof value === "object" && value !== null) {
|
|
22169
|
+
for (const inner of Object.keys(value)) keys.add(inner);
|
|
22170
|
+
}
|
|
22171
|
+
}
|
|
22172
|
+
return [...keys];
|
|
22173
|
+
}
|
|
22087
22174
|
function extractValidUntil(vc, fallback) {
|
|
22088
22175
|
if (typeof vc === "object" && vc !== null && "validUntil" in vc) {
|
|
22089
22176
|
const v = vc.validUntil;
|
|
@@ -22123,29 +22210,60 @@ async function payWithReadinessCheck(requestedAsset, rawPay) {
|
|
|
22123
22210
|
}
|
|
22124
22211
|
|
|
22125
22212
|
// src/orchestrator/subscribe.ts
|
|
22213
|
+
function diffAgainstTemplate(vc, fields) {
|
|
22214
|
+
const held = new Set(extractAttributeKeys(vc));
|
|
22215
|
+
const missing = fields.required.filter((k) => !held.has(k));
|
|
22216
|
+
const dropped = [...held].filter((k) => !fields.allKeys.includes(k));
|
|
22217
|
+
return missing.length > 0 || dropped.length > 0 ? { missing, dropped } : void 0;
|
|
22218
|
+
}
|
|
22219
|
+
var TERMINAL_SETTLEMENT_STATUSES = /* @__PURE__ */ new Set(["ISSUED", "FAILED"]);
|
|
22220
|
+
var RECOVERY_MAX_POLLS = 5;
|
|
22221
|
+
var RECOVERY_POLL_DELAY_MS = 2e3;
|
|
22222
|
+
async function pollSettlementOutcome(deps, paymentId) {
|
|
22223
|
+
if (!deps.mbi.getStatus) return void 0;
|
|
22224
|
+
const sleep = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
22225
|
+
let last;
|
|
22226
|
+
for (let polls = 1; polls <= RECOVERY_MAX_POLLS; polls++) {
|
|
22227
|
+
let status;
|
|
22228
|
+
try {
|
|
22229
|
+
status = await deps.mbi.getStatus(paymentId);
|
|
22230
|
+
} catch {
|
|
22231
|
+
return { status: "UNKNOWN", ...last ? { txHash: last.txHash, vcId: last.vcId } : {}, polls };
|
|
22232
|
+
}
|
|
22233
|
+
last = { status: status.status, txHash: status.txHash, vcId: status.vcId };
|
|
22234
|
+
if (TERMINAL_SETTLEMENT_STATUSES.has(status.status)) {
|
|
22235
|
+
return { status: status.status, txHash: status.txHash, vcId: status.vcId, polls };
|
|
22236
|
+
}
|
|
22237
|
+
if (polls < RECOVERY_MAX_POLLS) await sleep(RECOVERY_POLL_DELAY_MS);
|
|
22238
|
+
}
|
|
22239
|
+
return { ...last, polls: RECOVERY_MAX_POLLS };
|
|
22240
|
+
}
|
|
22126
22241
|
async function subscribeAndIssue(deps, opts) {
|
|
22127
22242
|
if (!/^did:zid:/.test(opts.templateId)) {
|
|
22128
22243
|
return { issued: false, reason: `templateId must be a did:zid:... credential-definition id, got "${opts.templateId}"` };
|
|
22129
22244
|
}
|
|
22245
|
+
const fields = deps.resolveTemplateFields ? await deps.resolveTemplateFields(opts.templateId) : null;
|
|
22246
|
+
const schema = fields ? { required: fields.required, optional: fields.allKeys.filter((k) => !fields.required.includes(k)) } : void 0;
|
|
22130
22247
|
if (deps.cache && !opts.forceReissue && !opts.dryRun) {
|
|
22131
22248
|
const cached2 = await deps.cache.get(opts.templateId);
|
|
22132
22249
|
if (cached2 && isVcValid(cached2)) {
|
|
22250
|
+
const originalPayment = { txHash: cached2.txHash, asset: cached2.paidAsset, amount: cached2.amountPaid };
|
|
22251
|
+
const paidOriginally = cached2.amountPaid !== void 0 && cached2.amountPaid !== "0";
|
|
22252
|
+
const staleAttributes = fields ? diffAgainstTemplate(cached2.vc, fields) : void 0;
|
|
22133
22253
|
return {
|
|
22134
22254
|
issued: true,
|
|
22135
22255
|
vcId: cached2.vcId,
|
|
22136
22256
|
vc: cached2.vc,
|
|
22137
|
-
|
|
22138
|
-
|
|
22139
|
-
|
|
22140
|
-
|
|
22257
|
+
fromCache: true,
|
|
22258
|
+
...paidOriginally ? { originalPayment } : {},
|
|
22259
|
+
...schema ? { schema } : {},
|
|
22260
|
+
...staleAttributes ? { staleAttributes } : {}
|
|
22141
22261
|
};
|
|
22142
22262
|
}
|
|
22143
22263
|
}
|
|
22144
|
-
const fields = deps.resolveTemplateFields ? await deps.resolveTemplateFields(opts.templateId) : null;
|
|
22145
22264
|
const { agentDid, ...rest } = opts.attributes ?? {};
|
|
22146
22265
|
const shouldAutoFillAgentDid = !agentDid && fields !== null && fields.allKeys.includes("agentDid");
|
|
22147
22266
|
const attributes = shouldAutoFillAgentDid ? { agentDid: deps.holderDid, ...rest } : opts.attributes ?? {};
|
|
22148
|
-
const schema = fields ? { required: fields.required, optional: fields.allKeys.filter((k) => !fields.required.includes(k)) } : void 0;
|
|
22149
22267
|
if (fields) {
|
|
22150
22268
|
const attrs = attributes;
|
|
22151
22269
|
const missing = fields.required.filter((k) => attrs[k] === void 0 || attrs[k] === null || attrs[k] === "");
|
|
@@ -22221,18 +22339,27 @@ async function subscribeAndIssue(deps, opts) {
|
|
|
22221
22339
|
}
|
|
22222
22340
|
throw err;
|
|
22223
22341
|
}
|
|
22342
|
+
const rawAsset = String(accept.asset ?? "");
|
|
22343
|
+
const paidAsset = deps.resolveSymbol ? await deps.resolveSymbol(rawAsset) : rawAsset;
|
|
22344
|
+
const amountPaid = String(accept.maxAmountRequired ?? "");
|
|
22224
22345
|
let issued;
|
|
22225
22346
|
try {
|
|
22226
22347
|
issued = await deps.mbi.applySettle({ ...body, paymentId: challenge.paymentId }, xPayment);
|
|
22227
22348
|
} catch (err) {
|
|
22228
22349
|
if (err instanceof MbiError) {
|
|
22229
|
-
|
|
22350
|
+
const paymentAttempted = { asset: paidAsset, amount: amountPaid, paymentId: challenge.paymentId };
|
|
22351
|
+
const recovery = err.mbiStatus === MBI_SETTLEMENT_INDETERMINATE && challenge.paymentId ? await pollSettlementOutcome(deps, challenge.paymentId) : void 0;
|
|
22352
|
+
return {
|
|
22353
|
+
issued: false,
|
|
22354
|
+
reason: err.message,
|
|
22355
|
+
httpStatus: err.httpStatus,
|
|
22356
|
+
...schema ? { schema } : {},
|
|
22357
|
+
paymentAttempted,
|
|
22358
|
+
...recovery ? { recovery } : {}
|
|
22359
|
+
};
|
|
22230
22360
|
}
|
|
22231
22361
|
throw err;
|
|
22232
22362
|
}
|
|
22233
|
-
const rawAsset = String(accept.asset ?? "");
|
|
22234
|
-
const paidAsset = deps.resolveSymbol ? await deps.resolveSymbol(rawAsset) : rawAsset;
|
|
22235
|
-
const amountPaid = String(accept.maxAmountRequired ?? "");
|
|
22236
22363
|
if (deps.cache) {
|
|
22237
22364
|
await deps.cache.set(opts.templateId, {
|
|
22238
22365
|
templateId: opts.templateId,
|
|
@@ -22726,6 +22853,37 @@ function createTools(deps) {
|
|
|
22726
22853
|
}
|
|
22727
22854
|
};
|
|
22728
22855
|
},
|
|
22856
|
+
/**
|
|
22857
|
+
* Free schema lookup. Previously the only way to ask "what does this template need?" was
|
|
22858
|
+
* subscribe_and_issue({ dryRun: true }) — a tool whose name reads as "this charges money", so
|
|
22859
|
+
* an agent reasoning about required fields had no obvious reason to reach for it. Reported
|
|
22860
|
+
* live: an agent discovered a newly-required attribute only by failing an issuance first.
|
|
22861
|
+
*/
|
|
22862
|
+
async get_template_schema(input) {
|
|
22863
|
+
const resolved = resolveTemplateAlias(input.templateId, deps.config.network);
|
|
22864
|
+
const templateId = resolved ?? input.templateId;
|
|
22865
|
+
if (!/^did:zid:/.test(templateId)) {
|
|
22866
|
+
return {
|
|
22867
|
+
templateId,
|
|
22868
|
+
error: `templateId must be a did:zid:... credential-definition id or a known template name, got "${input.templateId}"`
|
|
22869
|
+
};
|
|
22870
|
+
}
|
|
22871
|
+
const fields = deps.subscribeDeps.resolveTemplateFields ? await deps.subscribeDeps.resolveTemplateFields(templateId) : null;
|
|
22872
|
+
if (!fields) {
|
|
22873
|
+
return {
|
|
22874
|
+
templateId,
|
|
22875
|
+
error: `template schema could not be read from chain for ${templateId} (unknown/inactive template, or node unavailable)`
|
|
22876
|
+
};
|
|
22877
|
+
}
|
|
22878
|
+
const hidden = /* @__PURE__ */ new Set(["agentDid", ...derivedAttributeKeys(templateId, deps.config.network)]);
|
|
22879
|
+
return {
|
|
22880
|
+
templateId,
|
|
22881
|
+
schema: {
|
|
22882
|
+
required: fields.required.filter((k) => !hidden.has(k)),
|
|
22883
|
+
optional: fields.allKeys.filter((k) => !fields.required.includes(k) && !hidden.has(k))
|
|
22884
|
+
}
|
|
22885
|
+
};
|
|
22886
|
+
},
|
|
22729
22887
|
query_contract(input) {
|
|
22730
22888
|
return deps.queryContract(input);
|
|
22731
22889
|
},
|
|
@@ -22799,7 +22957,7 @@ function buildToolList() {
|
|
|
22799
22957
|
type: "object",
|
|
22800
22958
|
properties: {
|
|
22801
22959
|
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." },
|
|
22802
|
-
token: { type: "string", description:
|
|
22960
|
+
token: { type: "string", description: `Optional token symbol (e.g. "ZTX", "JMYR") to check its balance for the active network, alongside the usual status fields. Returns { balance, decimals } where balance is in the asset's raw base units \u2014 divide by 10^decimals for the human amount (e.g. balance "473999900" with decimals 6 is 473.9999 JMYR). A failed lookup reports { error: "query_failed" } rather than a zero balance.` }
|
|
22803
22961
|
}
|
|
22804
22962
|
}
|
|
22805
22963
|
},
|
|
@@ -22838,6 +22996,20 @@ function buildToolList() {
|
|
|
22838
22996
|
required: ["url"]
|
|
22839
22997
|
}
|
|
22840
22998
|
},
|
|
22999
|
+
{
|
|
23000
|
+
name: "get_template_schema",
|
|
23001
|
+
description: `Read a VC template's declared attribute schema from chain \u2014 FREE, no payment, no signing, no MBI issuance. Call this BEFORE subscribe_and_issue to find out which attributes a template requires, rather than discovering a missing one by attempting an issuance and being rejected. Accepts a did:zid:... credential-definition id or a known template name (e.g. "AI Birthcert"). Returns { templateId, schema: { required, optional } }; attributes the wallet fills in itself (agentDid, alias-derived keys) are omitted since you never supply them. A template that cannot be read reports { error } rather than an empty schema, so "needs nothing" is never confused with "could not look it up".`,
|
|
23002
|
+
inputSchema: {
|
|
23003
|
+
type: "object",
|
|
23004
|
+
properties: {
|
|
23005
|
+
templateId: {
|
|
23006
|
+
type: "string",
|
|
23007
|
+
description: 'The MBI credential-definition id (did:zid:...) or a known template name, e.g. "AI Birthcert".'
|
|
23008
|
+
}
|
|
23009
|
+
},
|
|
23010
|
+
required: ["templateId"]
|
|
23011
|
+
}
|
|
23012
|
+
},
|
|
22841
23013
|
{
|
|
22842
23014
|
name: "query_contract",
|
|
22843
23015
|
description: 'Read-only query against a Zetrix contract or account \u2014 call an arbitrary contract method (e.g. "balanceOf", "contractInfo") and return its raw result. No signing, no state change.',
|
|
@@ -22853,7 +23025,7 @@ function buildToolList() {
|
|
|
22853
23025
|
},
|
|
22854
23026
|
{
|
|
22855
23027
|
name: "subscribe_and_issue",
|
|
22856
|
-
description: "Obtain a VC from MBI: build the signed payload, pay x402, and return the issued credential. If a still-valid credential for this templateId is already cached locally, it is returned directly with no payment (fromCache: true) \u2014 pass forceReissue:true to pay and issue fresh regardless. Payment is asset-agnostic \u2014 MBI's 402 challenge may quote the native ZETRIX token or a ZTP20 token (e.g. JMYR); pass dryRun:true first to see the quoted asset/amount for free before committing to pay.
|
|
23028
|
+
description: "Obtain a VC from MBI: build the signed payload, pay x402, and return the issued credential. If a still-valid credential for this templateId is already cached locally, it is returned directly with no payment (fromCache: true) \u2014 pass forceReissue:true to pay and issue fresh regardless. Payment is asset-agnostic \u2014 MBI's 402 challenge may quote the native ZETRIX token or a ZTP20 token (e.g. JMYR); pass dryRun:true first to see the quoted asset/amount for free before committing to pay. What a call actually cost is reported precisely: paidAsset/amountPaid are set ONLY when this call paid, a cache hit reports the earlier charge under originalPayment instead (never as amountPaid, so summing spend cannot double-count), and any failure after the payment has settled on chain reports paymentAttempted: { asset, amount, paymentId }. Two such failures exist and mean different things: MBI 4006 is a definitive facilitator rejection, while 4012 (HTTP 502) means the outcome is INDETERMINATE \u2014 the payment may well have landed. On 4012 the wallet automatically polls MBI's recovery endpoint and reports recovery: { status, txHash?, vcId?, polls }, where status is ISSUED (the credential exists after all \u2014 fetch it by vcId, since recovery returns no VC body), FAILED, or REQUIRED/SETTLED (still unresolved) / UNKNOWN (recovery itself unreachable). NEVER retry a payment after either failure: the funds may already be gone, and a retry charges the full amount again \u2014 look the paymentId up instead. Every response except a cache hit also includes { schema: { required, optional } } \u2014 the template's full declared attribute schema read from chain \u2014 so you see the complete field list, not just what went wrong; a cache hit skips the chain lookup and omits it.",
|
|
22857
23029
|
inputSchema: {
|
|
22858
23030
|
type: "object",
|
|
22859
23031
|
properties: {
|
|
@@ -22952,25 +23124,20 @@ async function main() {
|
|
|
22952
23124
|
const sdk = new import_zetrix_sdk_nodejs.default({ host: config2.nodeHost, port: config2.nodePort });
|
|
22953
23125
|
const contractQuery = (a) => sdk.contract.call(a);
|
|
22954
23126
|
const resolveSymbol = (asset) => resolveAssetSymbol(asset, contractQuery);
|
|
22955
|
-
const
|
|
22956
|
-
const
|
|
22957
|
-
if (
|
|
22958
|
-
|
|
22959
|
-
|
|
22960
|
-
|
|
22961
|
-
|
|
22962
|
-
|
|
22963
|
-
|
|
22964
|
-
|
|
22965
|
-
|
|
22966
|
-
|
|
22967
|
-
try {
|
|
22968
|
-
const { balance } = await import_x402_zetrix_client2.PaymentEngine.fetchZTP20Balance(contractAddress, zetrixAddress, node);
|
|
22969
|
-
return { token: symbol, balance };
|
|
22970
|
-
} catch {
|
|
22971
|
-
return { token: symbol, error: "query_failed" };
|
|
22972
|
-
}
|
|
23127
|
+
const fetchNativeBalance = async (address) => {
|
|
23128
|
+
const res = await sdk.account.getInfo(address);
|
|
23129
|
+
if (res.errorCode !== 0) throw new Error(`getInfo failed with errorCode ${res.errorCode}`);
|
|
23130
|
+
const balance = res.result?.balance;
|
|
23131
|
+
if (typeof balance !== "string") throw new Error("getInfo returned no balance");
|
|
23132
|
+
return balance;
|
|
23133
|
+
};
|
|
23134
|
+
const tokenBalanceDeps = {
|
|
23135
|
+
address: zetrixAddress,
|
|
23136
|
+
fetchNativeBalance,
|
|
23137
|
+
resolveTokenAddress: (symbol) => resolveTokenAddress(symbol, config2.network) ?? null,
|
|
23138
|
+
query: contractQuery
|
|
22973
23139
|
};
|
|
23140
|
+
const queryTokenBalance2 = (token) => queryTokenBalance(tokenBalanceDeps, token);
|
|
22974
23141
|
const nodeBaseUrl = `https://${config2.nodeHost}${config2.nodePort ? `:${config2.nodePort}` : ""}`;
|
|
22975
23142
|
const nodeMetaQuery = (url) => fetch(url, { headers: { Accept: "application/json" } }).then((r) => r.json());
|
|
22976
23143
|
const resolveTemplateFields = (templateId) => fetchTemplateFields(templateId, config2.templateRegistryAddress, nodeBaseUrl, nodeMetaQuery);
|
|
@@ -23031,7 +23198,7 @@ async function main() {
|
|
|
23031
23198
|
payer,
|
|
23032
23199
|
subscribeDeps: { mbi, sign: subscribeSign, pay, resolveSymbol, holderDid, resolveTemplateFields, cache: vcCache },
|
|
23033
23200
|
queryContract: (input) => queryContract(input, contractQuery),
|
|
23034
|
-
queryTokenBalance,
|
|
23201
|
+
queryTokenBalance: queryTokenBalance2,
|
|
23035
23202
|
createAccount: (password, label, purpose) => be.createAccount(password, label, purpose),
|
|
23036
23203
|
saveAccount: (account) => accountStore.set({ ...account, createdAt: (/* @__PURE__ */ new Date()).toISOString() }),
|
|
23037
23204
|
checkActivationStatus: (address) => be.checkActivationStatus(address),
|
package/package.json
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentic-wallet-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "Agent-facing MCP wallet for Zetrix — orchestrates x401 identity proof, x402 payment, and MBI VC issuance",
|
|
5
|
-
"keywords": [
|
|
5
|
+
"keywords": [
|
|
6
|
+
"mcp",
|
|
7
|
+
"model-context-protocol",
|
|
8
|
+
"zetrix",
|
|
9
|
+
"wallet",
|
|
10
|
+
"x401",
|
|
11
|
+
"x402",
|
|
12
|
+
"blockchain"
|
|
13
|
+
],
|
|
6
14
|
"license": "MIT",
|
|
7
15
|
"author": "MyEG Services Berhad",
|
|
8
16
|
"homepage": "https://github.com/Zetrix-Chain/zetrix-agentic-wallet",
|
|
@@ -23,6 +31,7 @@
|
|
|
23
31
|
"files": [
|
|
24
32
|
"dist/server-bundle.cjs",
|
|
25
33
|
"README.md",
|
|
34
|
+
"CHANGELOG.md",
|
|
26
35
|
"LICENSE"
|
|
27
36
|
],
|
|
28
37
|
"scripts": {
|