viveworker 0.8.4 → 0.8.6
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 +11 -1
- package/package.json +18 -10
- package/scripts/mcp-server.mjs +111 -5
- package/scripts/share-cli.mjs +326 -45
- package/scripts/viveworker-bridge.mjs +458 -39
- package/scripts/viveworker.mjs +9 -0
- package/templates/CLAUDE.viveworker.md +3 -1
- package/web/app.css +195 -0
- package/web/app.js +1017 -237
- package/web/build-id.js +1 -1
- package/web/i18n.js +114 -18
- package/web/sw.js +4 -0
package/scripts/share-cli.mjs
CHANGED
|
@@ -7,22 +7,19 @@
|
|
|
7
7
|
* .html .htm .pdf .png .jpg .jpeg .gif .webp .csv
|
|
8
8
|
*
|
|
9
9
|
* Commands:
|
|
10
|
-
* viveworker share upload <file> [--password <pw>] [--price <usd> --pay-to <0x
|
|
10
|
+
* viveworker share upload <file> [--password <pw>] [--price <usd> (--pay-to <0x…>|--accept wallet-defaults)] [--expires-days <n>] [--no-optimize] [--json]
|
|
11
11
|
* viveworker share list [--json]
|
|
12
12
|
* viveworker share update <slug> [--file <file>] [--password <pw>] [--no-password] [--price <usd>] [--no-price] [--pay-to <0x…>] [--expires-days <n>] [--no-optimize] [--json]
|
|
13
13
|
* viveworker share replace <slug> <file> [--expires-days <n>] [--no-optimize] [--json]
|
|
14
14
|
* viveworker share link <slug> --password <pw> [--ttl-hours <n>] [--json]
|
|
15
|
-
* viveworker share pay <url> [--output <file>] [--dry-run] [--wallet eoa|hazbase] [--no-approval] [--json]
|
|
15
|
+
* viveworker share pay <url> [--output <file>] [--dry-run] [--wallet eoa|hazbase|liquid] [--no-approval] [--json]
|
|
16
16
|
* viveworker share delete <slug>
|
|
17
17
|
*
|
|
18
|
-
* Paid shares: `--price 0.10 --
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* signing, non-dry-run EOA payments are sent to the paired viveworker device for
|
|
24
|
-
* human approval; --wallet hazbase asks the paired device to reauth with passkey
|
|
25
|
-
* and pay from the configured hazBase Smart Wallet.
|
|
18
|
+
* Paid shares: `--price 0.10 --accept wallet-defaults` attaches x402 payment
|
|
19
|
+
* options from Wallet settings. EVM payments use @hazbase/auth, while Liquid
|
|
20
|
+
* PSET payments use @hazbase/simplicity from the Node CLI. Before signing,
|
|
21
|
+
* non-dry-run buyer payments are sent to the paired viveworker device for human
|
|
22
|
+
* approval.
|
|
26
23
|
*
|
|
27
24
|
* Environment overrides:
|
|
28
25
|
* VIVEWORKER_SHARE_URL — share worker base URL (default: https://share.viveworker.com)
|
|
@@ -50,6 +47,21 @@ const DEFAULT_SHARE_URL = "https://share.viveworker.com";
|
|
|
50
47
|
const MAX_FILE_SIZE = 5 * 1024 * 1024; // mirror worker
|
|
51
48
|
const PAYMENT_APPROVAL_TIMEOUT_MS = 10 * 60 * 1000;
|
|
52
49
|
const SUPPORTED_BUYER_NETWORKS = SUPPORTED_X402_BUYER_NETWORKS;
|
|
50
|
+
const LIQUID_X402_SCHEME = "exact-liquid-pset";
|
|
51
|
+
const PAYMENT_NETWORKS = {
|
|
52
|
+
base: { family: "evm", chainId: 8453, asset: "usdc", assets: ["usdc"], scheme: "exact", label: "Base", releaseStatus: "comingSoon" },
|
|
53
|
+
"base-sepolia": { family: "evm", chainId: 84532, asset: "usdc", assets: ["usdc"], scheme: "exact", label: "Base Sepolia" },
|
|
54
|
+
polygon: { family: "evm", chainId: 137, asset: "usdc", assets: ["usdc", "jpyc"], scheme: "exact", label: "Polygon", releaseStatus: "comingSoon" },
|
|
55
|
+
"polygon-amoy": { family: "evm", chainId: 80002, asset: "usdc", assets: ["usdc", "jpyc"], scheme: "exact", label: "Polygon Amoy" },
|
|
56
|
+
liquidtestnet: { family: "liquid", chainId: null, asset: "usdt", assets: ["usdt", "lbtc"], scheme: LIQUID_X402_SCHEME, label: "Liquid Testnet" },
|
|
57
|
+
liquidv1: { family: "liquid", chainId: null, asset: "usdt", assets: ["usdt", "lbtc"], scheme: LIQUID_X402_SCHEME, label: "Liquid", releaseStatus: "comingSoon" },
|
|
58
|
+
};
|
|
59
|
+
const PAYMENT_ASSETS = {
|
|
60
|
+
usdc: { decimals: 6, label: "USDC" },
|
|
61
|
+
jpyc: { decimals: 18, label: "JPYC" },
|
|
62
|
+
usdt: { decimals: 8, label: "USDt" },
|
|
63
|
+
lbtc: { decimals: 8, label: "L-BTC" },
|
|
64
|
+
};
|
|
53
65
|
|
|
54
66
|
// Mirror share-worker/worker.js SHARE_TYPES. Keep in sync by inspection —
|
|
55
67
|
// scripts/ and share-worker/ don't share a module. Adding a new type here
|
|
@@ -100,20 +112,22 @@ export async function runShareCli(args) {
|
|
|
100
112
|
|
|
101
113
|
function printHelp() {
|
|
102
114
|
console.log("Commands:");
|
|
103
|
-
console.log(" viveworker share upload <file> [--password <pw>] [--price <
|
|
115
|
+
console.log(" viveworker share upload <file> [--password <pw>] [--price <amount> (--pay-to <0x…>|--accept wallet-defaults|--accept net:asset,...)] [--expires-days <n>] [--no-optimize] [--json]");
|
|
104
116
|
console.log(" viveworker share list [--metrics] [--json]");
|
|
105
|
-
console.log(" viveworker share update <slug> [--file <file>] [--password <pw>] [--no-password] [--price <usd>] [--no-price] [--pay-to <0x
|
|
117
|
+
console.log(" viveworker share update <slug> [--file <file>] [--password <pw>] [--no-password] [--price <usd>] [--no-price] [--pay-to <0x…>|--accept wallet-defaults] [--expires-days <n>] [--no-optimize] [--json]");
|
|
106
118
|
console.log(" viveworker share replace <slug> <file> [--expires-days <n>] [--no-optimize] [--json]");
|
|
107
119
|
console.log(" viveworker share link <slug> --password <pw> [--ttl-hours <n>] [--json]");
|
|
108
|
-
console.log(" viveworker share pay <url> [--output <file>] [--dry-run] [--no-approval] [--json]");
|
|
120
|
+
console.log(" viveworker share pay <url> [--output <file>] [--dry-run] [--wallet eoa|hazbase|liquid] [--no-approval] [--json]");
|
|
109
121
|
console.log(" viveworker share delete <slug>");
|
|
110
122
|
console.log("");
|
|
111
123
|
console.log(`Accepted file types: ${ALLOWED_EXTENSIONS.join(" / ")}`);
|
|
112
124
|
console.log("CSV files are rendered as an HTML table on view; append ?raw=1 for bytes.");
|
|
113
125
|
console.log("HTML uploads are optimized by default when possible (use --no-optimize to disable).");
|
|
114
126
|
console.log("");
|
|
115
|
-
console.log("Paid shares (x402
|
|
116
|
-
console.log("
|
|
127
|
+
console.log("Paid shares (x402 — CLOSED BETA): --price 0.10 --pay-to 0x… or --price 0.10 --accept wallet-defaults");
|
|
128
|
+
console.log(" Explicit multi-network accepts: --accept base-sepolia:usdc,polygon-amoy:usdc,polygon-amoy:jpyc,liquidtestnet:usdt");
|
|
129
|
+
console.log(" EVM buyers can use: VIVEWORKER_BUYER_PRIVATE_KEY=0x… viveworker share pay <url>");
|
|
130
|
+
console.log(" Liquid buyers can use: viveworker share pay <url> --wallet liquid with VIVEWORKER_LIQUID_* RPC env.");
|
|
117
131
|
console.log(" Non-dry-run payments require paired-device approval before signing.");
|
|
118
132
|
console.log(" Use --no-approval / --yes only for trusted test automation.");
|
|
119
133
|
console.log(" To use a hazbase wallet as payTo, resolve it first via the local /api/hazbase/payout-address endpoint.");
|
|
@@ -123,7 +137,7 @@ function printHelp() {
|
|
|
123
137
|
console.log("Credentials are read from ~/.viveworker/a2a.env (same as `viveworker a2a`).");
|
|
124
138
|
}
|
|
125
139
|
|
|
126
|
-
const PRICE_REGEX = /^\d+(\.\d{1,
|
|
140
|
+
const PRICE_REGEX = /^\d+(\.\d{1,8})?$/;
|
|
127
141
|
const ETH_ADDR_REGEX = /^0x[0-9a-fA-F]{40}$/;
|
|
128
142
|
|
|
129
143
|
// ---------------------------------------------------------------------------
|
|
@@ -141,6 +155,7 @@ async function handleUpload(args) {
|
|
|
141
155
|
const expiresDays = flags["expires-days"] || flags["expiresDays"] || "";
|
|
142
156
|
const price = flags["price"] || "";
|
|
143
157
|
const payTo = flags["pay-to"] || flags["payTo"] || "";
|
|
158
|
+
const accept = flags.accept || flags.accepts || "";
|
|
144
159
|
|
|
145
160
|
if (password && password.length > 256) {
|
|
146
161
|
throw new Error("Password too long (max 256 chars)");
|
|
@@ -152,17 +167,25 @@ async function handleUpload(args) {
|
|
|
152
167
|
}
|
|
153
168
|
}
|
|
154
169
|
|
|
155
|
-
// --price
|
|
170
|
+
// --price must be paired with either legacy --pay-to or capability-based
|
|
171
|
+
// --accept. The latter lets a share advertise multiple x402 payment rails.
|
|
156
172
|
const anyPrice = Boolean(price);
|
|
157
173
|
const anyPayTo = Boolean(payTo);
|
|
158
|
-
|
|
159
|
-
|
|
174
|
+
const anyAccept = Boolean(accept);
|
|
175
|
+
if (anyPayTo && anyAccept) {
|
|
176
|
+
throw new Error("Use either --pay-to or --accept, not both");
|
|
177
|
+
}
|
|
178
|
+
if (anyPrice && !anyPayTo && !anyAccept) {
|
|
179
|
+
throw new Error("--price requires --pay-to or --accept");
|
|
180
|
+
}
|
|
181
|
+
if (!anyPrice && (anyPayTo || anyAccept)) {
|
|
182
|
+
throw new Error("--pay-to / --accept require --price");
|
|
160
183
|
}
|
|
161
184
|
if (anyPrice) {
|
|
162
185
|
if (!PRICE_REGEX.test(price)) {
|
|
163
|
-
throw new Error("--price must be a decimal with ≤
|
|
186
|
+
throw new Error("--price must be a decimal with ≤8 fractional digits (e.g. `0.10`)");
|
|
164
187
|
}
|
|
165
|
-
if (!ETH_ADDR_REGEX.test(payTo)) {
|
|
188
|
+
if (anyPayTo && !ETH_ADDR_REGEX.test(payTo)) {
|
|
166
189
|
throw new Error("--pay-to must be a 0x-prefixed 40-hex-char EVM address");
|
|
167
190
|
}
|
|
168
191
|
if (password) {
|
|
@@ -181,7 +204,12 @@ async function handleUpload(args) {
|
|
|
181
204
|
if (expiresDays) form.set("expiresDays", String(expiresDays));
|
|
182
205
|
if (anyPrice) {
|
|
183
206
|
form.set("price", price);
|
|
184
|
-
|
|
207
|
+
if (anyAccept) {
|
|
208
|
+
const paymentOptions = await resolveAcceptedPaymentOptions(accept);
|
|
209
|
+
form.set("paymentOptions", JSON.stringify(paymentOptions));
|
|
210
|
+
} else {
|
|
211
|
+
form.set("payTo", payTo);
|
|
212
|
+
}
|
|
185
213
|
}
|
|
186
214
|
|
|
187
215
|
const res = await fetchWithTimeout(`${shareUrl}/api/upload`, {
|
|
@@ -219,7 +247,7 @@ async function handleUpload(args) {
|
|
|
219
247
|
console.log("");
|
|
220
248
|
if (body.hasPassword) console.log(` 🔒 Password-protected`);
|
|
221
249
|
if (body.price && body.payTo) {
|
|
222
|
-
|
|
250
|
+
printPaidShareLine(body, " ");
|
|
223
251
|
}
|
|
224
252
|
if (body.expiresAtMs) console.log(` ⏱ Expires ${new Date(body.expiresAtMs).toISOString()}`);
|
|
225
253
|
if (body.hasPassword || body.price || body.expiresAtMs) console.log("");
|
|
@@ -446,7 +474,7 @@ async function handleList(args) {
|
|
|
446
474
|
console.log(` ${item.url}`);
|
|
447
475
|
if (item.originalName) console.log(` ${item.originalName}`);
|
|
448
476
|
if (item.price && item.payTo) {
|
|
449
|
-
|
|
477
|
+
printPaidShareLine(item, " ");
|
|
450
478
|
}
|
|
451
479
|
console.log("");
|
|
452
480
|
}
|
|
@@ -553,6 +581,8 @@ async function handleUpdateWithFlags(flags, mode = "update") {
|
|
|
553
581
|
Object.prototype.hasOwnProperty.call(flags, "noPrice");
|
|
554
582
|
const hasPayTo = Object.prototype.hasOwnProperty.call(flags, "pay-to") ||
|
|
555
583
|
Object.prototype.hasOwnProperty.call(flags, "payTo");
|
|
584
|
+
const hasAccept = Object.prototype.hasOwnProperty.call(flags, "accept") ||
|
|
585
|
+
Object.prototype.hasOwnProperty.call(flags, "accepts");
|
|
556
586
|
const hasFile = Object.prototype.hasOwnProperty.call(flags, "file") ||
|
|
557
587
|
Object.prototype.hasOwnProperty.call(flags, "f");
|
|
558
588
|
const filePath = flags.file || flags.f || "";
|
|
@@ -569,9 +599,15 @@ async function handleUpdateWithFlags(flags, mode = "update") {
|
|
|
569
599
|
if (hasFile && (typeof filePath !== "string" || filePath.length === 0)) {
|
|
570
600
|
throw new Error("--file requires a path");
|
|
571
601
|
}
|
|
572
|
-
if (
|
|
602
|
+
if (hasAccept && hasPayTo) {
|
|
603
|
+
throw new Error("Use either --pay-to or --accept, not both");
|
|
604
|
+
}
|
|
605
|
+
if (hasAccept && !hasPrice) {
|
|
606
|
+
throw new Error("--accept currently requires --price so all accepted assets can be repriced together");
|
|
607
|
+
}
|
|
608
|
+
if (!hasPassword && !hasNoPassword && !hasExpires && !hasPrice && !hasNoPrice && !hasPayTo && !hasAccept && !hasFile) {
|
|
573
609
|
throw new Error(
|
|
574
|
-
"Nothing to update — specify at least one of --file <file>, --password <pw>, --no-password, --price <usd>, --no-price, --pay-to <0x…>, --expires-days <n>"
|
|
610
|
+
"Nothing to update — specify at least one of --file <file>, --password <pw>, --no-password, --price <usd>, --no-price, --pay-to <0x…>, --accept <list>, --expires-days <n>"
|
|
575
611
|
);
|
|
576
612
|
}
|
|
577
613
|
|
|
@@ -596,9 +632,13 @@ async function handleUpdateWithFlags(flags, mode = "update") {
|
|
|
596
632
|
throw new Error("--price requires a value (use --no-price to clear)");
|
|
597
633
|
}
|
|
598
634
|
if (!PRICE_REGEX.test(pv)) {
|
|
599
|
-
throw new Error("--price must be a decimal with ≤
|
|
635
|
+
throw new Error("--price must be a decimal with ≤8 fractional digits (e.g. `0.10`)");
|
|
600
636
|
}
|
|
601
637
|
body.price = pv;
|
|
638
|
+
if (hasAccept) {
|
|
639
|
+
const accept = flags.accept || flags.accepts || "";
|
|
640
|
+
body.paymentOptions = JSON.stringify(await resolveAcceptedPaymentOptions(accept));
|
|
641
|
+
}
|
|
602
642
|
// --pay-to is only required on first set; the worker rejects with
|
|
603
643
|
// `payTo-required-on-first-price` if the share had no payTo before, so
|
|
604
644
|
// let it do the policy enforcement. But if the user passed --pay-to
|
|
@@ -702,9 +742,7 @@ async function handleUpdateWithFlags(flags, mode = "update") {
|
|
|
702
742
|
}
|
|
703
743
|
if (respBody.price && respBody.payTo) {
|
|
704
744
|
const rotated = hasPrice || respBody.paymentSessionsInvalidated ? " (paid sessions invalidated)" : "";
|
|
705
|
-
|
|
706
|
-
` 💰 Paid — ${formatUsdc(respBody.price)} USDC on ${respBody.network || "?"} → ${respBody.payTo}${rotated}`
|
|
707
|
-
);
|
|
745
|
+
printPaidShareLine(respBody, " ", rotated);
|
|
708
746
|
} else if (hasNoPrice) {
|
|
709
747
|
console.log(` 💸 Payment gate removed`);
|
|
710
748
|
}
|
|
@@ -719,7 +757,7 @@ async function handleUpdateWithFlags(flags, mode = "update") {
|
|
|
719
757
|
// password-protected share to another agent without disclosing the password.
|
|
720
758
|
//
|
|
721
759
|
// The owner keeps the password on their side; the receiver only needs to GET
|
|
722
|
-
// the returned URL. Tokens default to 24h, capped at
|
|
760
|
+
// the returned URL. Tokens default to 24h, capped at 720h (30d) and capped by
|
|
723
761
|
// the share's own `expiresAtMs`. Rotating the password via `share update
|
|
724
762
|
// --password ...` invalidates every outstanding token for the slug.
|
|
725
763
|
// ---------------------------------------------------------------------------
|
|
@@ -748,8 +786,8 @@ async function handleLink(args) {
|
|
|
748
786
|
if (hasTtl) {
|
|
749
787
|
const raw = flags["ttl-hours"] || flags["ttlHours"];
|
|
750
788
|
const n = Number(raw);
|
|
751
|
-
if (!Number.isFinite(n) || n <= 0 || n >
|
|
752
|
-
throw new Error("--ttl-hours must be a number between 1 and
|
|
789
|
+
if (!Number.isFinite(n) || n <= 0 || n > 720) {
|
|
790
|
+
throw new Error("--ttl-hours must be a number between 1 and 720");
|
|
753
791
|
}
|
|
754
792
|
ttlHours = n;
|
|
755
793
|
}
|
|
@@ -834,7 +872,7 @@ async function handlePay(args) {
|
|
|
834
872
|
}
|
|
835
873
|
|
|
836
874
|
const x402 = parseX402ResponseBody(initialText);
|
|
837
|
-
const requirement =
|
|
875
|
+
const requirement = selectSupportedPaymentRequirement(x402, flags);
|
|
838
876
|
const paymentSummary = summarizeRequirement(requirement);
|
|
839
877
|
if (flags["dry-run"] || flags.dryRun) {
|
|
840
878
|
const dryRun = {
|
|
@@ -853,7 +891,9 @@ async function handlePay(args) {
|
|
|
853
891
|
}
|
|
854
892
|
|
|
855
893
|
const walletMode = resolveBuyerWalletMode(flags);
|
|
856
|
-
const payment =
|
|
894
|
+
const payment = String(requirement.scheme || "") === LIQUID_X402_SCHEME
|
|
895
|
+
? await requestLiquidPayment({ url, requirement, paymentSummary, flags })
|
|
896
|
+
: walletMode === "hazbase"
|
|
857
897
|
? await requestHazbaseWalletPayment({ url, x402, requirement, paymentSummary, flags })
|
|
858
898
|
: await requestEoaPayment({ url, requirement, paymentSummary, flags });
|
|
859
899
|
const paid = await fetchWithTimeout(url, {
|
|
@@ -946,24 +986,50 @@ async function handleDelete(args) {
|
|
|
946
986
|
console.log(`✅ Deleted ${slug}`);
|
|
947
987
|
}
|
|
948
988
|
|
|
989
|
+
function selectSupportedPaymentRequirement(x402, flags = {}) {
|
|
990
|
+
const accepts = Array.isArray(x402?.accepts) ? x402.accepts : [];
|
|
991
|
+
const preferredNetwork = normalizePaymentNetworkKey(flags.network || flags["payment-network"] || flags.paymentNetwork || "");
|
|
992
|
+
if (preferredNetwork) {
|
|
993
|
+
const match = accepts.find((item) => String(item?.network || "") === preferredNetwork);
|
|
994
|
+
if (match) return match;
|
|
995
|
+
}
|
|
996
|
+
const liquid = accepts.find((item) => String(item?.scheme || "") === LIQUID_X402_SCHEME);
|
|
997
|
+
if (liquid && resolveBuyerWalletMode(flags) === "liquid") return liquid;
|
|
998
|
+
const evm = accepts.find((item) => (
|
|
999
|
+
String(item?.scheme || "exact") === "exact" &&
|
|
1000
|
+
SUPPORTED_BUYER_NETWORKS[String(item?.network || "")]
|
|
1001
|
+
));
|
|
1002
|
+
if (evm) return selectX402PaymentRequirement({ ...x402, accepts: [evm] });
|
|
1003
|
+
if (liquid) return liquid;
|
|
1004
|
+
const offered = accepts.map((item) => `${item?.scheme || "?"}:${item?.network || "?"}`).join(", ") || "none";
|
|
1005
|
+
throw new Error(`No supported x402 payment option found (offered: ${offered})`);
|
|
1006
|
+
}
|
|
1007
|
+
|
|
949
1008
|
function summarizeRequirement(requirement) {
|
|
950
1009
|
const network = SUPPORTED_BUYER_NETWORKS[String(requirement.network)];
|
|
1010
|
+
const assetKey = String(requirement.extra?.asset || "").toLowerCase();
|
|
1011
|
+
const decimals = Number(requirement.extra?.decimals ?? (String(requirement.scheme || "") === LIQUID_X402_SCHEME ? 8 : 6));
|
|
1012
|
+
const assetLabel = PAYMENT_ASSETS[assetKey]?.label || (String(requirement.scheme || "") === LIQUID_X402_SCHEME ? "USDt" : "USDC");
|
|
951
1013
|
return {
|
|
952
1014
|
network: String(requirement.network),
|
|
953
|
-
chainId: network
|
|
1015
|
+
chainId: network?.chainId ?? null,
|
|
1016
|
+
scheme: String(requirement.scheme || "exact"),
|
|
954
1017
|
amountAtomic: String(requirement.maxAmountRequired),
|
|
955
|
-
|
|
1018
|
+
amount: formatAtomicAmount(requirement.maxAmountRequired, decimals),
|
|
1019
|
+
amountUsdc: formatAtomicAmount(requirement.maxAmountRequired, decimals),
|
|
1020
|
+
assetLabel,
|
|
956
1021
|
payTo: String(requirement.payTo),
|
|
957
1022
|
asset: String(requirement.asset),
|
|
1023
|
+
assetKey,
|
|
958
1024
|
resource: String(requirement.resource || ""),
|
|
959
1025
|
description: String(requirement.description || ""),
|
|
960
1026
|
};
|
|
961
1027
|
}
|
|
962
1028
|
|
|
963
1029
|
function printPaymentSummary(summary) {
|
|
964
|
-
const network = SUPPORTED_BUYER_NETWORKS[summary.network];
|
|
1030
|
+
const network = SUPPORTED_BUYER_NETWORKS[summary.network] || PAYMENT_NETWORKS[summary.network];
|
|
965
1031
|
console.log("");
|
|
966
|
-
console.log(`${summary.paid ? "Paid" : "Payment required"} — ${summary.
|
|
1032
|
+
console.log(`${summary.paid ? "Paid" : "Payment required"} — ${summary.amount} ${summary.assetLabel} on ${network?.label || summary.network}`);
|
|
967
1033
|
console.log(` to: ${summary.payTo}`);
|
|
968
1034
|
if (summary.payer) console.log(` from: ${summary.payer}`);
|
|
969
1035
|
if (summary.resource) console.log(` resource: ${summary.resource}`);
|
|
@@ -975,7 +1041,8 @@ function resolveBuyerWalletMode(flags) {
|
|
|
975
1041
|
const raw = String(flags.wallet || flags["buyer-wallet"] || flags.buyerWallet || flags["payment-wallet"] || "eoa").trim().toLowerCase();
|
|
976
1042
|
if (!raw || raw === "eoa" || raw === "private-key" || raw === "private_key") return "eoa";
|
|
977
1043
|
if (raw === "hazbase" || raw === "hazbase-wallet" || raw === "smart-wallet" || raw === "smart_wallet") return "hazbase";
|
|
978
|
-
|
|
1044
|
+
if (raw === "liquid" || raw === "elements" || raw === "liquid-wallet" || raw === "liquid_wallet") return "liquid";
|
|
1045
|
+
throw new Error("--wallet must be eoa, hazbase, or liquid");
|
|
979
1046
|
}
|
|
980
1047
|
|
|
981
1048
|
async function requestEoaPayment({ url, requirement, paymentSummary, flags }) {
|
|
@@ -1028,6 +1095,55 @@ async function requestHazbaseWalletPayment({ url, x402, requirement, paymentSumm
|
|
|
1028
1095
|
};
|
|
1029
1096
|
}
|
|
1030
1097
|
|
|
1098
|
+
async function requestLiquidPayment({ url, requirement, paymentSummary, flags }) {
|
|
1099
|
+
await requirePaymentApproval({ url, paymentSummary, flags });
|
|
1100
|
+
const configText = await readOptionalEnvFile(CONFIG_ENV_FILE);
|
|
1101
|
+
const rpcUrl = String(process.env.VIVEWORKER_LIQUID_RPC_URL || envValue(configText, "VIVEWORKER_LIQUID_RPC_URL") || "").trim();
|
|
1102
|
+
const username = String(process.env.VIVEWORKER_LIQUID_RPC_USER || envValue(configText, "VIVEWORKER_LIQUID_RPC_USER") || "").trim();
|
|
1103
|
+
const password = String(process.env.VIVEWORKER_LIQUID_RPC_PASSWORD || envValue(configText, "VIVEWORKER_LIQUID_RPC_PASSWORD") || "").trim();
|
|
1104
|
+
const wallet = String(
|
|
1105
|
+
flags["liquid-wallet"] ||
|
|
1106
|
+
flags.liquidWallet ||
|
|
1107
|
+
process.env.VIVEWORKER_LIQUID_WALLET ||
|
|
1108
|
+
envValue(configText, "VIVEWORKER_LIQUID_WALLET") ||
|
|
1109
|
+
"",
|
|
1110
|
+
).trim();
|
|
1111
|
+
const payer = String(process.env.VIVEWORKER_LIQUID_PAYER || envValue(configText, "VIVEWORKER_LIQUID_PAYER") || "").trim();
|
|
1112
|
+
if (!rpcUrl || !username || !password || !wallet) {
|
|
1113
|
+
throw new Error(
|
|
1114
|
+
"Liquid x402 payment requires VIVEWORKER_LIQUID_RPC_URL, VIVEWORKER_LIQUID_RPC_USER, " +
|
|
1115
|
+
"VIVEWORKER_LIQUID_RPC_PASSWORD, and VIVEWORKER_LIQUID_WALLET."
|
|
1116
|
+
);
|
|
1117
|
+
}
|
|
1118
|
+
const simplicity = await loadSimplicitySdk();
|
|
1119
|
+
const rpc = new simplicity.ElementsRpcClient({ url: rpcUrl, username, password, wallet });
|
|
1120
|
+
const prepared = await simplicity.prepareLiquidX402PsetPayment({
|
|
1121
|
+
call: (method, params = [], selectedWallet) => rpc.call(method, params, selectedWallet),
|
|
1122
|
+
}, {
|
|
1123
|
+
requirements: requirement,
|
|
1124
|
+
wallet,
|
|
1125
|
+
payer: payer || undefined,
|
|
1126
|
+
});
|
|
1127
|
+
return {
|
|
1128
|
+
header: prepared.xPayment,
|
|
1129
|
+
payer: prepared.paymentPayload?.payer || payer || "",
|
|
1130
|
+
payload: prepared.paymentPayload,
|
|
1131
|
+
walletMode: "liquid",
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
async function loadSimplicitySdk() {
|
|
1136
|
+
try {
|
|
1137
|
+
return await import("@hazbase/simplicity");
|
|
1138
|
+
} catch (error) {
|
|
1139
|
+
throw new Error(
|
|
1140
|
+
"@hazbase/simplicity is required for Liquid x402 payments. " +
|
|
1141
|
+
"For local testing, symlink node_modules/@hazbase/simplicity to the local simplicity-sdk build. " +
|
|
1142
|
+
`Import failed: ${error?.message || error}`
|
|
1143
|
+
);
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1031
1147
|
async function requirePaymentApproval({ url, paymentSummary, flags }) {
|
|
1032
1148
|
const config = await resolveApprovalBridgeConfig();
|
|
1033
1149
|
if (shouldSkipPaymentApproval(flags, config.envText)) {
|
|
@@ -1211,7 +1327,7 @@ function resolveBuyerPrivateKey() {
|
|
|
1211
1327
|
if (!raw) {
|
|
1212
1328
|
throw new Error(
|
|
1213
1329
|
"Missing buyer wallet private key. Set VIVEWORKER_BUYER_PRIVATE_KEY=0x... " +
|
|
1214
|
-
"or BUYER_PK=0x... for
|
|
1330
|
+
"or BUYER_PK=0x... for EVM x402 payments."
|
|
1215
1331
|
);
|
|
1216
1332
|
}
|
|
1217
1333
|
return raw.startsWith("0x") ? raw : `0x${raw}`;
|
|
@@ -1237,6 +1353,145 @@ function previewPaidBody(contentType, bytes) {
|
|
|
1237
1353
|
console.log(` received ${formatSize(bytes.length)} (${contentType || "unknown content type"}). Pass --output <file> to save bytes.`);
|
|
1238
1354
|
}
|
|
1239
1355
|
|
|
1356
|
+
function normalizePaymentNetworkKey(raw) {
|
|
1357
|
+
const value = String(raw || "").trim().toLowerCase();
|
|
1358
|
+
if (value === "basesepolia") return "base-sepolia";
|
|
1359
|
+
if (value === "polygonamoy" || value === "amoy") return "polygon-amoy";
|
|
1360
|
+
if (value === "matic") return "polygon";
|
|
1361
|
+
if (value === "liquid" || value === "liquid-mainnet") return "liquidv1";
|
|
1362
|
+
return PAYMENT_NETWORKS[value] ? value : "";
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
function isPaymentNetworkAvailable(network) {
|
|
1366
|
+
return PAYMENT_NETWORKS[network]?.releaseStatus !== "comingSoon";
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
function normalizePaymentAsset(raw, network) {
|
|
1370
|
+
const networkInfo = PAYMENT_NETWORKS[network] || null;
|
|
1371
|
+
const value = String(raw || PAYMENT_NETWORKS[network]?.asset || "").trim().toLowerCase();
|
|
1372
|
+
if (value === "usd-t" || value === "tether") return "usdt";
|
|
1373
|
+
if (value === "jpy" || value === "jpy-coin") return "jpyc";
|
|
1374
|
+
if (value === "bitcoin" || value === "btc") return "lbtc";
|
|
1375
|
+
const allowed = Array.isArray(networkInfo?.assets) ? networkInfo.assets : [networkInfo?.asset].filter(Boolean);
|
|
1376
|
+
return PAYMENT_ASSETS[value] && allowed.includes(value) ? value : "";
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
async function resolveAcceptedPaymentOptions(rawAccept) {
|
|
1380
|
+
const accept = String(rawAccept || "").trim();
|
|
1381
|
+
if (!accept) throw new Error("--accept requires wallet-defaults or a comma-separated network:asset list");
|
|
1382
|
+
const status = await fetchHazbaseWalletStatusForCli();
|
|
1383
|
+
const capabilities = Array.isArray(status.paymentCapabilities) ? status.paymentCapabilities : [];
|
|
1384
|
+
const defaults = status.agentPaymentDefaults && typeof status.agentPaymentDefaults === "object"
|
|
1385
|
+
? status.agentPaymentDefaults
|
|
1386
|
+
: { mode: "configured", accepts: [] };
|
|
1387
|
+
const requested = accept === "wallet-defaults"
|
|
1388
|
+
? String(defaults.mode || "") === "custom"
|
|
1389
|
+
? (Array.isArray(defaults.effectiveAccepts) ? defaults.effectiveAccepts : defaults.accepts || [])
|
|
1390
|
+
.filter((entry) => isPaymentNetworkAvailable(normalizePaymentNetworkKey(entry?.network)))
|
|
1391
|
+
: capabilities
|
|
1392
|
+
.filter((entry) => entry?.configured && entry?.enabled !== false && isPaymentNetworkAvailable(normalizePaymentNetworkKey(entry.network)))
|
|
1393
|
+
.map((entry) => ({ network: entry.network, asset: entry.asset || PAYMENT_NETWORKS[entry.network]?.asset }))
|
|
1394
|
+
: accept.split(",").map((part) => {
|
|
1395
|
+
const [networkRaw, assetRaw = ""] = part.trim().split(":");
|
|
1396
|
+
const network = normalizePaymentNetworkKey(networkRaw);
|
|
1397
|
+
const asset = normalizePaymentAsset(assetRaw, network);
|
|
1398
|
+
if (!network || !asset) throw new Error(`Invalid --accept entry: ${part}`);
|
|
1399
|
+
if (!isPaymentNetworkAvailable(network)) {
|
|
1400
|
+
throw new Error(`${PAYMENT_NETWORKS[network].label} payment capabilities are coming soon and cannot be used before the formal release.`);
|
|
1401
|
+
}
|
|
1402
|
+
return { network, asset };
|
|
1403
|
+
});
|
|
1404
|
+
const resolved = [];
|
|
1405
|
+
for (const item of requested) {
|
|
1406
|
+
const network = normalizePaymentNetworkKey(item.network);
|
|
1407
|
+
const asset = normalizePaymentAsset(item.asset, network);
|
|
1408
|
+
const capability = capabilities.find((entry) => (
|
|
1409
|
+
normalizePaymentNetworkKey(entry.network) === network &&
|
|
1410
|
+
normalizePaymentAsset(entry.asset, network) === asset &&
|
|
1411
|
+
entry.configured
|
|
1412
|
+
));
|
|
1413
|
+
if (!capability?.payTo && !capability?.payoutAddress) {
|
|
1414
|
+
throw new Error(`No configured payout capability for ${network}:${asset}. Open Wallet settings and issue/register it first.`);
|
|
1415
|
+
}
|
|
1416
|
+
resolved.push({
|
|
1417
|
+
network,
|
|
1418
|
+
asset,
|
|
1419
|
+
scheme: capability.scheme || PAYMENT_NETWORKS[network]?.scheme || "exact",
|
|
1420
|
+
payTo: capability.payTo || capability.payoutAddress,
|
|
1421
|
+
payoutMethod: capability.payoutMethod || (PAYMENT_NETWORKS[network]?.family === "liquid" ? "external_liquid" : "hazbase_wallet"),
|
|
1422
|
+
...(capability.assetId ? { assetId: capability.assetId } : {}),
|
|
1423
|
+
});
|
|
1424
|
+
}
|
|
1425
|
+
if (resolved.length === 0) {
|
|
1426
|
+
throw new Error("No configured payment capabilities found. Open Wallet settings and issue/register a payout option first.");
|
|
1427
|
+
}
|
|
1428
|
+
return resolved;
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
async function fetchHazbaseWalletStatusForCli() {
|
|
1432
|
+
const config = await resolveApprovalBridgeConfig();
|
|
1433
|
+
if (!config.baseUrl || !config.sessionSecret) {
|
|
1434
|
+
throw new Error(`--accept requires the local viveworker bridge. Start viveworker or check ${CONFIG_ENV_FILE}.`);
|
|
1435
|
+
}
|
|
1436
|
+
const response = await getBridgeJson(config, "/api/hazbase/status", 10_000);
|
|
1437
|
+
if (!response.ok) {
|
|
1438
|
+
throw new Error(`Failed to read Wallet settings from bridge (${response.body?.error || `http-${response.status}`}).`);
|
|
1439
|
+
}
|
|
1440
|
+
return response.body || {};
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
function getBridgeJson(config, pathname, timeoutMs) {
|
|
1444
|
+
const endpoint = `${config.baseUrl}${pathname}`;
|
|
1445
|
+
return new Promise((resolve) => {
|
|
1446
|
+
let resolved = false;
|
|
1447
|
+
const done = (value) => {
|
|
1448
|
+
if (resolved) return;
|
|
1449
|
+
resolved = true;
|
|
1450
|
+
resolve(value);
|
|
1451
|
+
};
|
|
1452
|
+
const timer = setTimeout(() => done({ ok: false, status: 0, body: { error: "bridge-request-timeout" } }), timeoutMs);
|
|
1453
|
+
let parsedUrl;
|
|
1454
|
+
try {
|
|
1455
|
+
parsedUrl = new URL(endpoint);
|
|
1456
|
+
} catch {
|
|
1457
|
+
clearTimeout(timer);
|
|
1458
|
+
done({ ok: false, status: 0, body: { error: "invalid-approval-server-url" } });
|
|
1459
|
+
return;
|
|
1460
|
+
}
|
|
1461
|
+
const isHttps = parsedUrl.protocol === "https:";
|
|
1462
|
+
const port = parsedUrl.port ? Number(parsedUrl.port) : isHttps ? 443 : 80;
|
|
1463
|
+
const req = (isHttps ? https : http).request({
|
|
1464
|
+
hostname: parsedUrl.hostname,
|
|
1465
|
+
port,
|
|
1466
|
+
path: parsedUrl.pathname + parsedUrl.search,
|
|
1467
|
+
method: "GET",
|
|
1468
|
+
headers: {
|
|
1469
|
+
accept: "application/json",
|
|
1470
|
+
"x-viveworker-hook-secret": config.sessionSecret,
|
|
1471
|
+
},
|
|
1472
|
+
rejectUnauthorized: false,
|
|
1473
|
+
}, (res) => {
|
|
1474
|
+
let text = "";
|
|
1475
|
+
res.on("data", (chunk) => { text += chunk; });
|
|
1476
|
+
res.on("end", () => {
|
|
1477
|
+
clearTimeout(timer);
|
|
1478
|
+
let body = {};
|
|
1479
|
+
try {
|
|
1480
|
+
body = text ? JSON.parse(text) : {};
|
|
1481
|
+
} catch {
|
|
1482
|
+
body = { error: text.slice(0, 200) };
|
|
1483
|
+
}
|
|
1484
|
+
done({ ok: res.statusCode >= 200 && res.statusCode < 300, status: res.statusCode || 0, body });
|
|
1485
|
+
});
|
|
1486
|
+
});
|
|
1487
|
+
req.on("error", (error) => {
|
|
1488
|
+
clearTimeout(timer);
|
|
1489
|
+
done({ ok: false, status: 0, body: { error: error.message || "bridge-request-failed" } });
|
|
1490
|
+
});
|
|
1491
|
+
req.end();
|
|
1492
|
+
});
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1240
1495
|
function formatApiError(op, status, body) {
|
|
1241
1496
|
const code = body?.error || "";
|
|
1242
1497
|
switch (code) {
|
|
@@ -1269,7 +1524,7 @@ function formatApiError(op, status, body) {
|
|
|
1269
1524
|
case "not-password-protected":
|
|
1270
1525
|
return `${op} failed (${status}): share has no password — no link token needed, just share the URL directly`;
|
|
1271
1526
|
case "invalid-ttlHours":
|
|
1272
|
-
return `${op} failed (${status}): --ttl-hours must be between 1 and ${body.maxHours ||
|
|
1527
|
+
return `${op} failed (${status}): --ttl-hours must be between 1 and ${body.maxHours || 720}`;
|
|
1273
1528
|
// x402 / paid-share error codes — thrown by the worker on upload,
|
|
1274
1529
|
// PATCH, or view. Keep the messages actionable; agents read these
|
|
1275
1530
|
// back to the user verbatim.
|
|
@@ -1288,7 +1543,7 @@ function formatApiError(op, status, body) {
|
|
|
1288
1543
|
case "payTo-cannot-be-cleared-alone":
|
|
1289
1544
|
return `${op} failed (${status}): --no-price clears both price and payTo; clearing payTo alone is not supported`;
|
|
1290
1545
|
case "payment-network-not-configured":
|
|
1291
|
-
return `${op} failed (${status}): the worker's X402_NETWORK var is not set to a supported chain (base / base-sepolia)`;
|
|
1546
|
+
return `${op} failed (${status}): the worker's X402_NETWORK var is not set to a supported chain (base / base-sepolia / polygon / polygon-amoy)`;
|
|
1292
1547
|
case "payment-required":
|
|
1293
1548
|
return `${op} failed (${status}): this share requires payment (x402). Use an x402-compatible client (e.g. \`x402-fetch\` on npm) to pay.`;
|
|
1294
1549
|
case "payment-verification-failed":
|
|
@@ -1408,6 +1663,10 @@ function formatSize(bytes) {
|
|
|
1408
1663
|
// back to "0.00" on anything unparseable so we never crash the CLI output
|
|
1409
1664
|
// on a malformed server response.
|
|
1410
1665
|
function formatUsdc(atomic) {
|
|
1666
|
+
return formatAtomicAmount(atomic, 6);
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
function formatAtomicAmount(atomic, decimals = 6) {
|
|
1411
1670
|
let n;
|
|
1412
1671
|
try {
|
|
1413
1672
|
n = BigInt(String(atomic ?? "0"));
|
|
@@ -1415,12 +1674,34 @@ function formatUsdc(atomic) {
|
|
|
1415
1674
|
return "0.00";
|
|
1416
1675
|
}
|
|
1417
1676
|
if (n < 0n) n = -n;
|
|
1418
|
-
const
|
|
1419
|
-
const
|
|
1677
|
+
const places = Math.max(0, Math.min(18, Number(decimals) || 0));
|
|
1678
|
+
const scale = 10n ** BigInt(places);
|
|
1679
|
+
const whole = scale > 0n ? n / scale : n;
|
|
1680
|
+
const frac = scale > 0n ? (n % scale).toString().padStart(places, "0").replace(/0+$/u, "") : "";
|
|
1420
1681
|
if (!frac) return `${whole}.00`;
|
|
1421
1682
|
return `${whole}.${frac.padEnd(2, "0")}`;
|
|
1422
1683
|
}
|
|
1423
1684
|
|
|
1685
|
+
function printPaidShareLine(item, indent = "", suffix = "") {
|
|
1686
|
+
const options = Array.isArray(item.paymentOptions) && item.paymentOptions.length > 0
|
|
1687
|
+
? item.paymentOptions
|
|
1688
|
+
: [{
|
|
1689
|
+
network: item.network || item.paymentNetwork || "?",
|
|
1690
|
+
asset: "usdc",
|
|
1691
|
+
priceAtomic: item.price,
|
|
1692
|
+
payTo: item.payTo,
|
|
1693
|
+
}];
|
|
1694
|
+
const rendered = options.map((option) => {
|
|
1695
|
+
const network = option.network || item.network || item.paymentNetwork || "?";
|
|
1696
|
+
const asset = normalizePaymentAsset(option.asset || "usdc", network) || "usdc";
|
|
1697
|
+
const label = PAYMENT_ASSETS[asset]?.label || asset.toUpperCase();
|
|
1698
|
+
const decimals = Number(option.decimals || PAYMENT_ASSETS[asset]?.decimals || 6);
|
|
1699
|
+
const amount = formatAtomicAmount(option.priceAtomic || item.price, decimals);
|
|
1700
|
+
return `${amount} ${label} on ${network} → ${option.payTo || item.payTo || "?"}`;
|
|
1701
|
+
});
|
|
1702
|
+
console.log(`${indent}💰 Paid — ${rendered.join(" / ")}${suffix}`);
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1424
1705
|
function formatRelative(ms) {
|
|
1425
1706
|
const sec = Math.floor(ms / 1000);
|
|
1426
1707
|
if (sec < 60) return `${sec}s ago`;
|