warpmetal 0.6.0 → 0.7.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/README.md +25 -14
- package/package.json +1 -1
- package/skills/warpmetal/SKILL.md +9 -4
- package/skills/warpmetal/references/cli-reference.md +13 -7
- package/skills/warpmetal/references/payments.md +12 -6
- package/skills/warpmetal/references/renewals.md +17 -10
- package/src/api.js +8 -0
- package/src/cli.js +179 -33
- package/src/payment.js +24 -2
- package/src/state.js +4 -0
package/README.md
CHANGED
|
@@ -32,7 +32,7 @@ published wallet CLI with the exact version WarpMetal reports:
|
|
|
32
32
|
npm install --global warpmetal
|
|
33
33
|
warpmetal --help
|
|
34
34
|
|
|
35
|
-
npm install --global @x402api/agent-wallet-cli@0.2.
|
|
35
|
+
npm install --global @x402api/agent-wallet-cli@0.2.2
|
|
36
36
|
x402api help --json
|
|
37
37
|
```
|
|
38
38
|
|
|
@@ -85,10 +85,13 @@ After preparing an order, request its live payment challenge:
|
|
|
85
85
|
warpmetal checkout challenge --task <taskId> --json
|
|
86
86
|
```
|
|
87
87
|
|
|
88
|
-
On HTTP 402 the CLI returns exact `paymentTerms`, the
|
|
88
|
+
On HTTP 402 the CLI returns exact `paymentTerms`, the opaque `challengeHandle`
|
|
89
|
+
that WarpMetal uses for merchant-side reconciliation, the pinned
|
|
89
90
|
`@x402api/agent-wallet-cli` package contract, and argv arrays under
|
|
90
91
|
`paymentWorkflow`. It also writes an owner-only request envelope that contains
|
|
91
|
-
the exact checkout URL and body but no WarpMetal credential
|
|
92
|
+
the exact checkout URL and body but no WarpMetal credential or challenge
|
|
93
|
+
handle. The handle is not a buyer payment identifier and is never a wallet
|
|
94
|
+
signing input. The published
|
|
92
95
|
launch wallet accepts sponsored Base USDC and sponsored Solana USDC/USDT only;
|
|
93
96
|
the returned terms identify compatible alternatives and confirm that the buyer
|
|
94
97
|
does not need ETH or SOL. Payment authority depends on execution context:
|
|
@@ -141,11 +144,13 @@ after authorization.
|
|
|
141
144
|
|
|
142
145
|
If funding is short in an interactive conversation, tell the human the exact
|
|
143
146
|
top-up in normal and atomic units, the network, stablecoin and contract/mint,
|
|
144
|
-
and the payer wallet's public receiving address. The
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
147
|
+
and the payer wallet's public receiving address. The returned
|
|
148
|
+
`paymentWorkflow.fundingWorkflow` provides safe address and balance argv plus a
|
|
149
|
+
presentation contract: render that public address as both a QR code and
|
|
150
|
+
copyable text. The human sends the token to that wallet address, never to the
|
|
151
|
+
token contract/mint or WarpMetal's payment recipient, and never sends ETH or
|
|
152
|
+
SOL for a sponsored payment. In an unattended run, use a preconfigured refill
|
|
153
|
+
or escalation mechanism or stop with `funding_required`.
|
|
149
154
|
|
|
150
155
|
## Autonomous renewal and refill
|
|
151
156
|
|
|
@@ -168,7 +173,11 @@ warpmetal renewal configure \
|
|
|
168
173
|
```
|
|
169
174
|
|
|
170
175
|
The recipient must follow the one-time verification link before lifecycle or
|
|
171
|
-
refill mail is sent.
|
|
176
|
+
refill mail is sent. If no verified notification email exists, `renewal
|
|
177
|
+
configure` returns `email_required` before changing policy. Supply `--email`,
|
|
178
|
+
or deliberately continue with `--without-email-notifications`; the latter does
|
|
179
|
+
not enable signed refill-email workflows. A recurring unattended agent can then
|
|
180
|
+
run:
|
|
172
181
|
|
|
173
182
|
```sh
|
|
174
183
|
warpmetal renewal due --all --json
|
|
@@ -176,14 +185,16 @@ warpmetal renewal run --all-due --json
|
|
|
176
185
|
```
|
|
177
186
|
|
|
178
187
|
Inside policy, the CLI returns the exact Agent Wallet authorization and submit
|
|
179
|
-
argv. If balance is insufficient,
|
|
180
|
-
|
|
188
|
+
argv. If balance is insufficient, `refillWorkflow` is returned only when the
|
|
189
|
+
server has an active verified notification subscription. Run its argv with the
|
|
190
|
+
returned `X402API_NOTIFICATION_URL` environment value. `x402api wallet
|
|
181
191
|
notify-refill` signs an opaque subscription reference and wallet-produced
|
|
182
192
|
balance fields; it cannot choose an email address. WarpMetal verifies the
|
|
183
193
|
wallet signature and current on-chain balance before emailing the verified
|
|
184
|
-
human the network, stablecoin, public wallet address,
|
|
185
|
-
|
|
186
|
-
|
|
194
|
+
human the network, stablecoin, public wallet address, required minimum top-up,
|
|
195
|
+
and a locally generated QR encoding only that wallet address. The address is
|
|
196
|
+
also repeated as copyable text. The human may transfer more than that minimum;
|
|
197
|
+
the renewal policy—not the refill target—remains the spending authority.
|
|
187
198
|
|
|
188
199
|
The agent never sends a partial x402 payment. If no verified refill path
|
|
189
200
|
exists, it reports `funding_required`. If a previous payment is pending or
|
package/package.json
CHANGED
|
@@ -111,7 +111,9 @@ task or operator limit.
|
|
|
111
111
|
After interactive confirmation or autonomous policy validation, invoke the
|
|
112
112
|
returned authorize argv once. The separate
|
|
113
113
|
`x402api` executable owns the wallet, validates the envelope, and writes the
|
|
114
|
-
payment artifact.
|
|
114
|
+
payment artifact. The returned `challengeHandle` is opaque merchant
|
|
115
|
+
reconciliation metadata; never copy it into the request envelope or treat it as
|
|
116
|
+
the buyer payment identifier. Then invoke the returned submit argv, equivalent to:
|
|
115
117
|
|
|
116
118
|
```sh
|
|
117
119
|
warpmetal checkout submit \
|
|
@@ -152,9 +154,12 @@ price, asset, network, count, horizon, or total-budget mismatch.
|
|
|
152
154
|
Run `warpmetal renewal prepare --server <serverId> --json`, then invoke only
|
|
153
155
|
the returned `paymentWorkflow.authorize.argv` and
|
|
154
156
|
`paymentWorkflow.submit.argv`. If authorization reports insufficient balance,
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
157
|
+
use `paymentWorkflow.fundingWorkflow` to obtain the public address and balance,
|
|
158
|
+
then show the address as both a QR code and copyable text. Only when
|
|
159
|
+
`refillNotification.available` is true, set the returned
|
|
160
|
+
`refillWorkflow.environment`, invoke its exact argv once, and stop until
|
|
161
|
+
funding arrives. The signed refill intent resolves to a verified human contact;
|
|
162
|
+
never add an email address to it. Never make a partial payment.
|
|
158
163
|
|
|
159
164
|
After funding, prepare again, authorize exactly once, submit with WarpMetal,
|
|
160
165
|
and confirm the returned `termEndsAt`. On `reconcile_pending` or
|
|
@@ -53,14 +53,16 @@ warpmetal order status \
|
|
|
53
53
|
```
|
|
54
54
|
|
|
55
55
|
On HTTP 402, `checkout challenge` validates and displays exact payment terms,
|
|
56
|
-
writes
|
|
56
|
+
writes the opaque x402api `challengeHandle` to private WarpMetal state for
|
|
57
|
+
merchant reconciliation, and writes a credential-free x402api V1 request envelope with owner-only
|
|
57
58
|
permissions, and returns the exact pinned wallet package, V1 contract probe,
|
|
58
59
|
matching wallet-skill install, authorization, and WarpMetal submission argv
|
|
59
60
|
arrays. The default envelope and suggested artifact paths live under the
|
|
60
61
|
private WarpMetal state directory. An explicit output path must not already
|
|
61
|
-
contain different content.
|
|
62
|
+
contain different content. `challengeHandle` is intentionally absent from the
|
|
63
|
+
wallet envelope: it is neither a buyer payment identifier nor signing input.
|
|
62
64
|
|
|
63
|
-
The current integration targets `@x402api/agent-wallet-cli@0.2.
|
|
65
|
+
The current integration targets `@x402api/agent-wallet-cli@0.2.2`. A compatible
|
|
64
66
|
live term is marked `agentWalletSupported: true` and must use the sponsored
|
|
65
67
|
Base USDC or Solana USDC/USDT launch profile with buyer native fees disabled.
|
|
66
68
|
WarpMetal rejects a challenge with no compatible sponsored term. Because the
|
|
@@ -101,7 +103,8 @@ warpmetal renewal configure \
|
|
|
101
103
|
(--maximum-renewals <n> | --renew-through <UTC>) \
|
|
102
104
|
[--maximum-total-spend-atomic <amount>] \
|
|
103
105
|
--allowed-network <network> --allowed-asset <asset> \
|
|
104
|
-
--wallet <name> [--refill-target-atomic <amount>]
|
|
106
|
+
--wallet <name> [--refill-target-atomic <amount>] \
|
|
107
|
+
[--email <address> | --without-email-notifications] --json
|
|
105
108
|
warpmetal renewal status --server <serverId> --json
|
|
106
109
|
warpmetal renewal due (--server <serverId> | --all) --json
|
|
107
110
|
warpmetal renewal prepare --server <serverId> --json
|
|
@@ -113,9 +116,12 @@ warpmetal notifications configure --server <serverId> --email <address> \
|
|
|
113
116
|
warpmetal notifications status --server <serverId> --json
|
|
114
117
|
```
|
|
115
118
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
+
Without a verified notification email, renewal configuration asks for an email
|
|
120
|
+
before mutating policy unless `--without-email-notifications` explicitly opts
|
|
121
|
+
out. `renewal prepare` always returns safe funding address/balance argv and
|
|
122
|
+
returns signed refill-email argv only for a verified active subscription.
|
|
123
|
+
`renewal run` is an agent-facing state machine, not a wallet-signing daemon.
|
|
124
|
+
See [renewals.md](renewals.md).
|
|
119
125
|
|
|
120
126
|
## Server management
|
|
121
127
|
|
|
@@ -19,7 +19,7 @@ files or pass a WarpMetal credential to `x402api`.
|
|
|
19
19
|
WarpMetal runs on Node.js 20 or 22. The x402api Agent Wallet currently requires
|
|
20
20
|
Node.js 22. Use the exact published package reported by
|
|
21
21
|
`paymentWorkflow.signerPackage.spec`; the current contract is
|
|
22
|
-
`@x402api/agent-wallet-cli@0.2.
|
|
22
|
+
`@x402api/agent-wallet-cli@0.2.2`. Do not add it as a WarpMetal dependency,
|
|
23
23
|
install executable wallet code from an unpinned repository URL, or substitute
|
|
24
24
|
a similarly named package.
|
|
25
25
|
|
|
@@ -66,10 +66,12 @@ skill automatically.
|
|
|
66
66
|
|
|
67
67
|
6. If funding is short in an interactive conversation, show the exact deficit
|
|
68
68
|
in normal token units and atomic units, the exact network, token symbol and
|
|
69
|
-
contract/mint, and the payer wallet's public receiving address.
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
69
|
+
contract/mint, and the payer wallet's public receiving address. Render only
|
|
70
|
+
that public address as a QR code and repeat the full address as copyable
|
|
71
|
+
text. Tell the human to transfer the token to the payer wallet address,
|
|
72
|
+
never to the token contract/mint or `paymentTerms.recipient`. Sponsored
|
|
73
|
+
launch payments never ask the buyer to fund ETH or SOL. Recheck the balance
|
|
74
|
+
after the transfer.
|
|
73
75
|
7. If funding is short in an unattended run, use only a preconfigured refill
|
|
74
76
|
mechanism returned by WarpMetal. For a configured renewal, set the returned
|
|
75
77
|
`refillWorkflow.environment`, invoke `refillWorkflow.argv` once, and stop
|
|
@@ -114,7 +116,11 @@ paymentWorkflow.submit.argv
|
|
|
114
116
|
Replace only `<wallet-name>` after selecting the wallet for the advertised
|
|
115
117
|
network. Do not parse, rewrite, copy, or display the request envelope or payment
|
|
116
118
|
artifact. The envelope excludes the WarpMetal owner token; the artifact holds
|
|
117
|
-
the complete payment signature and remains owner-only.
|
|
119
|
+
the complete payment signature and remains owner-only. The safe challenge JSON
|
|
120
|
+
also contains `challengeHandle`, an opaque value retained by WarpMetal for
|
|
121
|
+
merchant reconciliation. It is deliberately excluded from the x402api V1
|
|
122
|
+
request envelope and must not be confused with the wallet-created buyer payment
|
|
123
|
+
identifier.
|
|
118
124
|
|
|
119
125
|
Choose only a term marked `agentWalletSupported: true`,
|
|
120
126
|
`sponsoredNetworkFee: true`, and `buyerNativeFeeRequired: false`. The supported
|
|
@@ -23,15 +23,18 @@ warpmetal renewal configure \
|
|
|
23
23
|
--allowed-asset <exact-contract-or-mint> \
|
|
24
24
|
--wallet <agent-wallet-name> \
|
|
25
25
|
[--refill-target-atomic <target>] \
|
|
26
|
-
[--email <human-address>] \
|
|
26
|
+
[--email <human-address> | --without-email-notifications] \
|
|
27
27
|
--json
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
Without an active verified notification email, configuration asks for one
|
|
31
|
+
before changing policy. `--email` queues verification; refill and expanded
|
|
32
|
+
lifecycle notices remain disabled until the human opens the verification link.
|
|
33
|
+
Use `--without-email-notifications` only as an explicit opt-out; no signed
|
|
34
|
+
refill-email workflow is returned in that state. A refill target is a funding
|
|
35
|
+
target, not spend permission, and cannot exceed the per-renewal cap. The human
|
|
36
|
+
may transfer more than the displayed deficit. The agent may spend only the
|
|
37
|
+
exact renewal amount allowed by policy.
|
|
35
38
|
|
|
36
39
|
The WarpMetal CLI stores only the local wallet name. The backend stores
|
|
37
40
|
non-secret limits, counters, payment rail, time horizon, and an opaque
|
|
@@ -70,8 +73,10 @@ one policy-compatible rail, and returns:
|
|
|
70
73
|
```text
|
|
71
74
|
paymentWorkflow.authorize.argv
|
|
72
75
|
paymentWorkflow.submit.argv
|
|
73
|
-
|
|
74
|
-
|
|
76
|
+
paymentWorkflow.fundingWorkflow
|
|
77
|
+
refillNotification
|
|
78
|
+
refillWorkflow.environment (verified email only)
|
|
79
|
+
refillWorkflow.argv (verified email only)
|
|
75
80
|
```
|
|
76
81
|
|
|
77
82
|
Invoke the authorize argv once. It writes an owner-only artifact. Then invoke
|
|
@@ -112,10 +117,12 @@ authoritative balance before queuing email. The intent cannot name a recipient
|
|
|
112
117
|
email, tenant, or product.
|
|
113
118
|
|
|
114
119
|
In a human conversation, also display the exact deficit, network, token symbol,
|
|
115
|
-
contract or mint, and payer wallet address.
|
|
120
|
+
contract or mint, and payer wallet address. Render only that public address as
|
|
121
|
+
a QR code and repeat it as copyable text. Tell the human to fund the payer
|
|
116
122
|
wallet address—not the token contract/mint and not WarpMetal's merchant
|
|
117
123
|
recipient. Do not ask for ETH or SOL because supported network fees are
|
|
118
|
-
sponsored.
|
|
124
|
+
sponsored. A refill email repeats the address as text and includes a locally
|
|
125
|
+
generated QR encoding only that address.
|
|
119
126
|
|
|
120
127
|
## Stop and recovery states
|
|
121
128
|
|
package/src/api.js
CHANGED
|
@@ -216,6 +216,14 @@ export class WarpMetalClient {
|
|
|
216
216
|
);
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
+
deleteNotifications(serverId, token) {
|
|
220
|
+
return this.request(
|
|
221
|
+
"DELETE",
|
|
222
|
+
`/servers/${encodeURIComponent(serverId)}/notifications`,
|
|
223
|
+
{ token },
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
219
227
|
renewalCheckout(planId, { bodyText, token, paymentSignature }) {
|
|
220
228
|
return this.checkout(`/checkout/${encodeURIComponent(planId)}/renew`, {
|
|
221
229
|
bodyText,
|
package/src/cli.js
CHANGED
|
@@ -84,6 +84,7 @@ Usage:
|
|
|
84
84
|
warpmetal renewal configure --server <serverId> --renew-before-days <n>
|
|
85
85
|
--maximum-payment-atomic <amount> (--maximum-renewals <n> | --renew-through <UTC>)
|
|
86
86
|
--allowed-network <CAIP-2> --allowed-asset <asset> --wallet <name>
|
|
87
|
+
[--email <address> | --without-email-notifications]
|
|
87
88
|
warpmetal renewal status|prepare|submit|run ...
|
|
88
89
|
warpmetal renewal due (--server <serverId> | --all)
|
|
89
90
|
warpmetal notifications configure --server <serverId> --email <address>
|
|
@@ -364,9 +365,15 @@ async function pollGrant(
|
|
|
364
365
|
|
|
365
366
|
function challengeResult(taskId, checkoutBody, response) {
|
|
366
367
|
const paymentRequired = response.headers["payment-required"];
|
|
368
|
+
const challengeHandle = response.headers["x-x402api-challenge-handle"];
|
|
367
369
|
if (response.status === 402 && !paymentRequired) {
|
|
368
370
|
throw new CliError("WarpMetal returned HTTP 402 without PAYMENT-REQUIRED.");
|
|
369
371
|
}
|
|
372
|
+
if (response.status === 402 && !challengeHandle) {
|
|
373
|
+
throw new CliError(
|
|
374
|
+
"WarpMetal returned HTTP 402 without X-X402API-Challenge-Handle.",
|
|
375
|
+
);
|
|
376
|
+
}
|
|
370
377
|
return {
|
|
371
378
|
status: response.data?.status,
|
|
372
379
|
taskId,
|
|
@@ -374,6 +381,7 @@ function challengeResult(taskId, checkoutBody, response) {
|
|
|
374
381
|
response.data?.paymentAttemptId ||
|
|
375
382
|
response.headers["x-warpmetal-payment-attempt"],
|
|
376
383
|
paymentRequired,
|
|
384
|
+
challengeHandle,
|
|
377
385
|
checkoutBodySha256: createHash("sha256").update(checkoutBody).digest("hex"),
|
|
378
386
|
};
|
|
379
387
|
}
|
|
@@ -645,7 +653,7 @@ async function handleCheckoutChallenge(client, store, options, context) {
|
|
|
645
653
|
stringOption(options, "request-envelope-out"),
|
|
646
654
|
);
|
|
647
655
|
const paymentInstructions = safe.paymentWorkflow
|
|
648
|
-
? `\nWallet package: ${safe.paymentWorkflow.signerPackage.spec} (Node ${safe.paymentWorkflow.signerNodeRequirement})\nInstall: ${shellCommand(safe.paymentWorkflow.signerPackage.install.argv)}\nVerify: ${shellCommand(safe.paymentWorkflow.signerContract.probe.argv)}\nRequest envelope: ${safe.paymentWorkflow.requestEnvelopePath}\nAuthorize: ${shellCommand(safe.paymentWorkflow.authorize.argv)}\nSubmit with WarpMetal: ${shellCommand(safe.paymentWorkflow.submit.argv)}`
|
|
656
|
+
? `\nWallet package: ${safe.paymentWorkflow.signerPackage.spec} (Node ${safe.paymentWorkflow.signerNodeRequirement})\nInstall: ${shellCommand(safe.paymentWorkflow.signerPackage.install.argv)}\nVerify: ${shellCommand(safe.paymentWorkflow.signerContract.probe.argv)}\nRequest envelope: ${safe.paymentWorkflow.requestEnvelopePath}\nAuthorize: ${shellCommand(safe.paymentWorkflow.authorize.argv)}\nIf funding is short, run ${shellCommand(safe.paymentWorkflow.fundingWorkflow.address.argv)} and ${shellCommand(safe.paymentWorkflow.fundingWorkflow.balance.argv)}, then show the payer address as both a QR code and copyable text.\nSubmit with WarpMetal: ${shellCommand(safe.paymentWorkflow.submit.argv)}`
|
|
649
657
|
: "";
|
|
650
658
|
emit(
|
|
651
659
|
context.stdout,
|
|
@@ -775,6 +783,44 @@ export function refillRenewBy(termEndsAt, currentTime = Date.now()) {
|
|
|
775
783
|
return new Date(Number.isFinite(parsed) && parsed > minimum ? parsed : minimum).toISOString();
|
|
776
784
|
}
|
|
777
785
|
|
|
786
|
+
function activeNotificationSubscription(notifications) {
|
|
787
|
+
const subscription = notifications?.subscription;
|
|
788
|
+
return Boolean(
|
|
789
|
+
notifications?.configured &&
|
|
790
|
+
subscription?.verified &&
|
|
791
|
+
!subscription?.disabled,
|
|
792
|
+
);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function refillNotificationState(notifications, policy) {
|
|
796
|
+
const subscription = notifications?.subscription;
|
|
797
|
+
if (!notifications?.configured || !subscription || subscription.disabled) {
|
|
798
|
+
return {
|
|
799
|
+
available: false,
|
|
800
|
+
reason: "verified_email_required",
|
|
801
|
+
subscription: subscription || null,
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
if (!subscription.verified) {
|
|
805
|
+
return {
|
|
806
|
+
available: false,
|
|
807
|
+
reason: "email_verification_required",
|
|
808
|
+
subscription,
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
if (
|
|
812
|
+
policy?.notificationReference &&
|
|
813
|
+
subscription.reference !== policy.notificationReference
|
|
814
|
+
) {
|
|
815
|
+
return {
|
|
816
|
+
available: false,
|
|
817
|
+
reason: "notification_reference_mismatch",
|
|
818
|
+
subscription,
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
return { available: true, reason: null, subscription };
|
|
822
|
+
}
|
|
823
|
+
|
|
778
824
|
async function handleRenewalConfigure(client, store, options, context) {
|
|
779
825
|
const serverId = stringOption(options, "server", { required: true });
|
|
780
826
|
const wallet = stringOption(options, "wallet", { required: true });
|
|
@@ -818,7 +864,72 @@ async function handleRenewalConfigure(client, store, options, context) {
|
|
|
818
864
|
for (const [key, value] of Object.entries(body)) {
|
|
819
865
|
if (value === undefined) delete body[key];
|
|
820
866
|
}
|
|
867
|
+
const email = stringOption(options, "email");
|
|
868
|
+
const withoutEmailNotifications = booleanOption(
|
|
869
|
+
options,
|
|
870
|
+
"without-email-notifications",
|
|
871
|
+
);
|
|
872
|
+
if (email && withoutEmailNotifications) {
|
|
873
|
+
throw new CliError(
|
|
874
|
+
"Use either --email or --without-email-notifications, not both.",
|
|
875
|
+
{ exitCode: 2 },
|
|
876
|
+
);
|
|
877
|
+
}
|
|
821
878
|
const token = await requireServerToken(store, serverId, options, context.env);
|
|
879
|
+
let notifications = (await client.getNotifications(serverId, token)).data;
|
|
880
|
+
if (
|
|
881
|
+
body.enabled &&
|
|
882
|
+
!email &&
|
|
883
|
+
!withoutEmailNotifications &&
|
|
884
|
+
!activeNotificationSubscription(notifications)
|
|
885
|
+
) {
|
|
886
|
+
const verificationPending = Boolean(
|
|
887
|
+
notifications.configured &&
|
|
888
|
+
notifications.subscription &&
|
|
889
|
+
!notifications.subscription.disabled,
|
|
890
|
+
);
|
|
891
|
+
const action = verificationPending
|
|
892
|
+
? "email_verification_required"
|
|
893
|
+
: "email_required";
|
|
894
|
+
const output = {
|
|
895
|
+
action,
|
|
896
|
+
serverId,
|
|
897
|
+
reason: verificationPending
|
|
898
|
+
? "Verify the existing notification email or resend verification with --email."
|
|
899
|
+
: "A verified email enables renewal and wallet-refill notifications.",
|
|
900
|
+
notifications,
|
|
901
|
+
next: {
|
|
902
|
+
configureEmail: `warpmetal renewal configure ... --email <address>`,
|
|
903
|
+
continueWithoutEmail:
|
|
904
|
+
"warpmetal renewal configure ... --without-email-notifications",
|
|
905
|
+
},
|
|
906
|
+
};
|
|
907
|
+
emit(
|
|
908
|
+
context.stdout,
|
|
909
|
+
output,
|
|
910
|
+
context.json,
|
|
911
|
+
verificationPending
|
|
912
|
+
? `Verify the notification email for ${serverId}, or rerun with --email to resend verification. To opt out explicitly, rerun with --without-email-notifications.`
|
|
913
|
+
: `An email is needed for renewal and wallet-refill notifications. Rerun with --email <address>, or opt out explicitly with --without-email-notifications.`,
|
|
914
|
+
);
|
|
915
|
+
return 6;
|
|
916
|
+
}
|
|
917
|
+
if (email) {
|
|
918
|
+
notifications = (
|
|
919
|
+
await client.putNotifications(serverId, { email }, token)
|
|
920
|
+
).data;
|
|
921
|
+
} else if (
|
|
922
|
+
withoutEmailNotifications &&
|
|
923
|
+
notifications.configured &&
|
|
924
|
+
notifications.subscription &&
|
|
925
|
+
!notifications.subscription.disabled
|
|
926
|
+
) {
|
|
927
|
+
await client.deleteNotifications(serverId, token);
|
|
928
|
+
notifications = {
|
|
929
|
+
...notifications,
|
|
930
|
+
subscription: { ...notifications.subscription, disabled: true },
|
|
931
|
+
};
|
|
932
|
+
}
|
|
822
933
|
const result = await client.putRenewalPolicy(serverId, body, token);
|
|
823
934
|
await store.saveRenewalPolicy(
|
|
824
935
|
serverId,
|
|
@@ -826,19 +937,27 @@ async function handleRenewalConfigure(client, store, options, context) {
|
|
|
826
937
|
wallet,
|
|
827
938
|
refillTargetAtomic,
|
|
828
939
|
);
|
|
829
|
-
const
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
940
|
+
const notificationState = {
|
|
941
|
+
optedOut: withoutEmailNotifications,
|
|
942
|
+
refillAvailable: activeNotificationSubscription(notifications),
|
|
943
|
+
status: withoutEmailNotifications
|
|
944
|
+
? "opted_out"
|
|
945
|
+
: activeNotificationSubscription(notifications)
|
|
946
|
+
? "verified"
|
|
947
|
+
: "verification_required",
|
|
948
|
+
};
|
|
949
|
+
const output = {
|
|
950
|
+
...result.data,
|
|
951
|
+
wallet,
|
|
952
|
+
refillTargetAtomic,
|
|
953
|
+
notifications,
|
|
954
|
+
notificationState,
|
|
955
|
+
};
|
|
837
956
|
emit(
|
|
838
957
|
context.stdout,
|
|
839
958
|
output,
|
|
840
959
|
context.json,
|
|
841
|
-
`Configured bounded renewal for ${serverId} with wallet ${wallet}.${email ? " Check the email verification link." : ""}`,
|
|
960
|
+
`Configured bounded renewal for ${serverId} with wallet ${wallet}.${email && !notificationState.refillAvailable ? " Check the email verification link." : withoutEmailNotifications ? " Email and wallet-refill notifications were explicitly skipped." : ""}`,
|
|
842
961
|
);
|
|
843
962
|
return 0;
|
|
844
963
|
}
|
|
@@ -866,6 +985,7 @@ async function attachRenewalPaymentWorkflow(
|
|
|
866
985
|
serverId,
|
|
867
986
|
server,
|
|
868
987
|
renewal,
|
|
988
|
+
notifications,
|
|
869
989
|
response,
|
|
870
990
|
requestedEnvelopePath,
|
|
871
991
|
) {
|
|
@@ -924,6 +1044,7 @@ async function attachRenewalPaymentWorkflow(
|
|
|
924
1044
|
taskId: server.id,
|
|
925
1045
|
serverId,
|
|
926
1046
|
kind: "renewal",
|
|
1047
|
+
wallet: renewal.wallet,
|
|
927
1048
|
requestEnvelopePath,
|
|
928
1049
|
paymentArtifactPath: defaults.paymentArtifactPath,
|
|
929
1050
|
});
|
|
@@ -932,27 +1053,39 @@ async function attachRenewalPaymentWorkflow(
|
|
|
932
1053
|
BigInt(renewal.refillTargetAtomic) >= BigInt(term.amountAtomic)
|
|
933
1054
|
? renewal.refillTargetAtomic
|
|
934
1055
|
: term.amountAtomic;
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
1056
|
+
Object.assign(workflow.fundingWorkflow, {
|
|
1057
|
+
network: term.network,
|
|
1058
|
+
asset: term.asset,
|
|
1059
|
+
requiredPaymentAtomic: term.amountAtomic,
|
|
1060
|
+
targetBalanceAtomic: refillTarget,
|
|
1061
|
+
});
|
|
1062
|
+
const refillNotification = refillNotificationState(
|
|
1063
|
+
notifications,
|
|
1064
|
+
renewal.policy,
|
|
1065
|
+
);
|
|
1066
|
+
const refillWorkflow = refillNotification.available
|
|
1067
|
+
? {
|
|
1068
|
+
environment: {
|
|
1069
|
+
X402API_NOTIFICATION_URL: `${client.baseUrl}/notifications/x402api/refill`,
|
|
1070
|
+
},
|
|
1071
|
+
argv: [
|
|
1072
|
+
"x402api",
|
|
1073
|
+
"wallet",
|
|
1074
|
+
"notify-refill",
|
|
1075
|
+
"--wallet",
|
|
1076
|
+
renewal.wallet,
|
|
1077
|
+
"--subscription-reference",
|
|
1078
|
+
refillNotification.subscription.reference,
|
|
1079
|
+
"--renew-by",
|
|
1080
|
+
refillRenewBy(server.termEndsAt),
|
|
1081
|
+
"--target-balance-atomic",
|
|
1082
|
+
refillTarget,
|
|
1083
|
+
"--reason",
|
|
1084
|
+
"renewal",
|
|
1085
|
+
"--json",
|
|
1086
|
+
],
|
|
1087
|
+
}
|
|
1088
|
+
: undefined;
|
|
956
1089
|
Object.assign(safe, {
|
|
957
1090
|
serverId,
|
|
958
1091
|
paymentTerms: request.terms,
|
|
@@ -960,6 +1093,7 @@ async function attachRenewalPaymentWorkflow(
|
|
|
960
1093
|
paymentRequestDigest: request.requestDigest,
|
|
961
1094
|
paymentChallengeDigest: request.challengeDigest,
|
|
962
1095
|
paymentWorkflow: workflow,
|
|
1096
|
+
refillNotification,
|
|
963
1097
|
refillWorkflow,
|
|
964
1098
|
});
|
|
965
1099
|
await store.saveRenewalChallenge(serverId, {
|
|
@@ -968,6 +1102,7 @@ async function attachRenewalPaymentWorkflow(
|
|
|
968
1102
|
termEndsAt: server.termEndsAt,
|
|
969
1103
|
paymentRequired: safe.paymentRequired,
|
|
970
1104
|
paymentAttemptId: safe.paymentAttemptId,
|
|
1105
|
+
challengeHandle: safe.challengeHandle,
|
|
971
1106
|
paymentRequestDigest: safe.paymentRequestDigest,
|
|
972
1107
|
paymentChallengeDigest: safe.paymentChallengeDigest,
|
|
973
1108
|
paymentRequestEnvelopePath: requestEnvelopePath,
|
|
@@ -988,9 +1123,10 @@ function renewalActionExitCode(action) {
|
|
|
988
1123
|
|
|
989
1124
|
async function prepareRenewal(client, store, serverId, options, context) {
|
|
990
1125
|
const token = await requireServerToken(store, serverId, options, context.env);
|
|
991
|
-
const [serverResult, policyResult] = await Promise.all([
|
|
1126
|
+
const [serverResult, policyResult, notificationsResult] = await Promise.all([
|
|
992
1127
|
client.getServer(serverId, token),
|
|
993
1128
|
client.getRenewalPolicy(serverId, token),
|
|
1129
|
+
client.getNotifications(serverId, token),
|
|
994
1130
|
]);
|
|
995
1131
|
if (!policyResult.data.configured) {
|
|
996
1132
|
return {
|
|
@@ -1032,6 +1168,7 @@ async function prepareRenewal(client, store, serverId, options, context) {
|
|
|
1032
1168
|
serverId,
|
|
1033
1169
|
serverResult.data.task,
|
|
1034
1170
|
local,
|
|
1171
|
+
notificationsResult.data,
|
|
1035
1172
|
response,
|
|
1036
1173
|
stringOption(options, "request-envelope-out"),
|
|
1037
1174
|
);
|
|
@@ -1053,8 +1190,16 @@ async function prepareRenewal(client, store, serverId, options, context) {
|
|
|
1053
1190
|
async function handleRenewalPrepare(client, store, options, context) {
|
|
1054
1191
|
const serverId = stringOption(options, "server", { required: true });
|
|
1055
1192
|
const safe = await prepareRenewal(client, store, serverId, options, context);
|
|
1193
|
+
const fundingInstructions = safe.paymentWorkflow
|
|
1194
|
+
? `\nIf funding is short, run ${shellCommand(safe.paymentWorkflow.fundingWorkflow.address.argv)} and ${shellCommand(safe.paymentWorkflow.fundingWorkflow.balance.argv)}. Show the returned payer address as both a QR code and copyable text, with the exact network, asset, and deficit.`
|
|
1195
|
+
: "";
|
|
1196
|
+
const refillInstructions = safe.refillWorkflow
|
|
1197
|
+
? `\nFor verified email refill, set ${Object.entries(safe.refillWorkflow.environment).map(([key, value]) => `${key}=${value}`).join(" ")} and run: ${shellCommand(safe.refillWorkflow.argv)}`
|
|
1198
|
+
: safe.paymentWorkflow
|
|
1199
|
+
? `\nVerified email refill is unavailable (${safe.refillNotification.reason}). Configure and verify notifications, or fund the displayed payer address directly.`
|
|
1200
|
+
: "";
|
|
1056
1201
|
const instructions = safe.paymentWorkflow
|
|
1057
|
-
? `\nAuthorize: ${shellCommand(safe.paymentWorkflow.authorize.argv)}
|
|
1202
|
+
? `\nAuthorize: ${shellCommand(safe.paymentWorkflow.authorize.argv)}${fundingInstructions}${refillInstructions}\nSubmit: ${shellCommand(safe.paymentWorkflow.submit.argv)}`
|
|
1058
1203
|
: "";
|
|
1059
1204
|
emit(
|
|
1060
1205
|
context.stdout,
|
|
@@ -2325,6 +2470,7 @@ async function dispatch(positionals, options, passthrough, context) {
|
|
|
2325
2470
|
"allowed-asset",
|
|
2326
2471
|
"refill-target-atomic",
|
|
2327
2472
|
"email",
|
|
2473
|
+
"without-email-notifications",
|
|
2328
2474
|
"disabled",
|
|
2329
2475
|
]);
|
|
2330
2476
|
return handleRenewalConfigure(client, store, options, context);
|
package/src/payment.js
CHANGED
|
@@ -15,7 +15,7 @@ const MAX_ARTIFACT_BYTES = 1024 * 1024;
|
|
|
15
15
|
const MAX_SIGNATURE_BYTES = 512 * 1024;
|
|
16
16
|
|
|
17
17
|
export const AGENT_WALLET_PACKAGE = "@x402api/agent-wallet-cli";
|
|
18
|
-
export const AGENT_WALLET_VERSION = "0.2.
|
|
18
|
+
export const AGENT_WALLET_VERSION = "0.2.2";
|
|
19
19
|
const AGENT_WALLET_SPEC = `${AGENT_WALLET_PACKAGE}@${AGENT_WALLET_VERSION}`;
|
|
20
20
|
const GAS_SPONSORSHIP_EXTENSION = "com.x402api.gas-sponsorship";
|
|
21
21
|
const BASE_NETWORK = "eip155:8453";
|
|
@@ -696,6 +696,7 @@ export function paymentWorkflow({
|
|
|
696
696
|
taskId,
|
|
697
697
|
serverId,
|
|
698
698
|
kind = "checkout",
|
|
699
|
+
wallet = "<wallet-name>",
|
|
699
700
|
requestEnvelopePath,
|
|
700
701
|
paymentArtifactPath,
|
|
701
702
|
}) {
|
|
@@ -739,7 +740,7 @@ export function paymentWorkflow({
|
|
|
739
740
|
"payment",
|
|
740
741
|
"authorize",
|
|
741
742
|
"--wallet",
|
|
742
|
-
|
|
743
|
+
wallet,
|
|
743
744
|
"--request-envelope",
|
|
744
745
|
requestEnvelopePath,
|
|
745
746
|
"--artifact-out",
|
|
@@ -747,6 +748,27 @@ export function paymentWorkflow({
|
|
|
747
748
|
"--json",
|
|
748
749
|
],
|
|
749
750
|
},
|
|
751
|
+
fundingWorkflow: {
|
|
752
|
+
action: "fund_wallet",
|
|
753
|
+
trigger: "insufficient_asset_balance",
|
|
754
|
+
wallet,
|
|
755
|
+
address: {
|
|
756
|
+
argv: ["x402api", "wallet", "address", "--wallet", wallet, "--json"],
|
|
757
|
+
},
|
|
758
|
+
balance: {
|
|
759
|
+
argv: ["x402api", "wallet", "balance", "--wallet", wallet, "--json"],
|
|
760
|
+
},
|
|
761
|
+
presentation: {
|
|
762
|
+
qrPayloadField: "address",
|
|
763
|
+
textPayloadField: "address",
|
|
764
|
+
showQr: true,
|
|
765
|
+
showAddressString: true,
|
|
766
|
+
showExactDeficit: true,
|
|
767
|
+
showNetworkAndAsset: true,
|
|
768
|
+
},
|
|
769
|
+
safety:
|
|
770
|
+
"Encode and fund only the payer wallet public address. Never fund the token contract, mint, or WarpMetal merchant recipient; sponsored payments do not require ETH or SOL.",
|
|
771
|
+
},
|
|
750
772
|
submit: {
|
|
751
773
|
argv:
|
|
752
774
|
kind === "renewal"
|
package/src/state.js
CHANGED
|
@@ -280,6 +280,7 @@ export class StateStore {
|
|
|
280
280
|
{
|
|
281
281
|
paymentRequired,
|
|
282
282
|
paymentAttemptId,
|
|
283
|
+
challengeHandle,
|
|
283
284
|
paymentRequestDigest,
|
|
284
285
|
paymentChallengeDigest,
|
|
285
286
|
paymentWorkflow,
|
|
@@ -293,6 +294,7 @@ export class StateStore {
|
|
|
293
294
|
});
|
|
294
295
|
order.paymentRequired = paymentRequired;
|
|
295
296
|
order.paymentAttemptId = paymentAttemptId;
|
|
297
|
+
order.challengeHandle = challengeHandle;
|
|
296
298
|
order.paymentRequestDigest = paymentRequestDigest;
|
|
297
299
|
order.paymentChallengeDigest = paymentChallengeDigest;
|
|
298
300
|
order.paymentRequestEnvelopePath = paymentWorkflow?.requestEnvelopePath;
|
|
@@ -465,6 +467,7 @@ export class StateStore {
|
|
|
465
467
|
serverId: order.serverId,
|
|
466
468
|
planId: order.planId,
|
|
467
469
|
paymentAttemptId: order.paymentAttemptId,
|
|
470
|
+
challengeHandle: order.challengeHandle,
|
|
468
471
|
walletPaymentAttemptId: order.walletPaymentAttemptId,
|
|
469
472
|
walletName: order.walletName,
|
|
470
473
|
payerAddress: order.walletPayerAddress,
|
|
@@ -499,6 +502,7 @@ export class StateStore {
|
|
|
499
502
|
refillTargetAtomic: renewal.refillTargetAtomic,
|
|
500
503
|
policy: renewal.policy,
|
|
501
504
|
paymentAttemptId: renewal.paymentAttemptId,
|
|
505
|
+
challengeHandle: renewal.challengeHandle,
|
|
502
506
|
paymentArtifactExpiresAt: renewal.paymentArtifactExpiresAt,
|
|
503
507
|
})),
|
|
504
508
|
};
|