@zeroxyz/cli 0.0.24 → 0.0.25
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/dist/index.js +689 -419
- package/package.json +1 -1
- package/skills/zero/SKILL.md +2 -0
package/dist/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/app.ts
|
|
4
|
-
import { Command as
|
|
4
|
+
import { Command as Command11 } from "commander";
|
|
5
5
|
|
|
6
6
|
// package.json
|
|
7
7
|
var package_default = {
|
|
8
8
|
name: "@zeroxyz/cli",
|
|
9
|
-
version: "0.0.
|
|
9
|
+
version: "0.0.25",
|
|
10
10
|
type: "module",
|
|
11
11
|
bin: {
|
|
12
12
|
zero: "dist/index.js",
|
|
@@ -279,10 +279,14 @@ var ApiService = class {
|
|
|
279
279
|
const json = await this.request("POST", "/v1/bug-reports", data);
|
|
280
280
|
return createBugReportResponseSchema.parse(json);
|
|
281
281
|
};
|
|
282
|
-
getFundingUrl = async (amount) => {
|
|
282
|
+
getFundingUrl = async (amount, provider = "coinbase") => {
|
|
283
283
|
try {
|
|
284
|
-
const
|
|
285
|
-
|
|
284
|
+
const params = new URLSearchParams({ provider });
|
|
285
|
+
if (amount) params.set("amount", amount);
|
|
286
|
+
const json = await this.request(
|
|
287
|
+
"GET",
|
|
288
|
+
`/v1/wallet/fund-url?${params.toString()}`
|
|
289
|
+
);
|
|
286
290
|
const parsed = z.object({ url: z.string() }).parse(json);
|
|
287
291
|
return parsed.url;
|
|
288
292
|
} catch {
|
|
@@ -567,125 +571,634 @@ var configCommand = (_appContext) => new Command2("config").description("View or
|
|
|
567
571
|
|
|
568
572
|
// src/commands/fetch-command.ts
|
|
569
573
|
import { Command as Command3 } from "commander";
|
|
574
|
+
import { formatUnits as formatUnits2 } from "viem";
|
|
570
575
|
|
|
571
|
-
// src/
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
576
|
+
// src/services/payment-service.ts
|
|
577
|
+
import {
|
|
578
|
+
adaptViemWallet,
|
|
579
|
+
convertViemChainToRelayChain,
|
|
580
|
+
createClient as createRelayClient,
|
|
581
|
+
getClient as getRelayClient,
|
|
582
|
+
MAINNET_RELAY_API
|
|
583
|
+
} from "@relayprotocol/relay-sdk";
|
|
584
|
+
import { x402Client as X402Client } from "@x402/core/client";
|
|
585
|
+
import { decodePaymentResponseHeader, x402HTTPClient } from "@x402/core/http";
|
|
586
|
+
import { ExactEvmScheme } from "@x402/evm/exact/client";
|
|
587
|
+
import { createSIWxClientHook } from "@x402/extensions/sign-in-with-x";
|
|
588
|
+
import { wrapFetchWithPayment } from "@x402/fetch";
|
|
589
|
+
import { Challenge, Receipt } from "mppx";
|
|
590
|
+
import { Mppx, tempo } from "mppx/client";
|
|
591
|
+
import {
|
|
592
|
+
createPublicClient,
|
|
593
|
+
createWalletClient,
|
|
594
|
+
formatUnits,
|
|
595
|
+
http
|
|
596
|
+
} from "viem";
|
|
597
|
+
import { base, baseSepolia } from "viem/chains";
|
|
598
|
+
var SessionCloseFailedError = class extends Error {
|
|
599
|
+
session;
|
|
600
|
+
response;
|
|
601
|
+
capturedAmount;
|
|
602
|
+
constructor(params) {
|
|
603
|
+
super(params.message);
|
|
604
|
+
this.name = "SessionCloseFailedError";
|
|
605
|
+
this.session = params.session;
|
|
606
|
+
this.response = params.response;
|
|
607
|
+
this.capturedAmount = params.capturedAmount;
|
|
581
608
|
}
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
609
|
+
};
|
|
610
|
+
var USDC_BASE = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913";
|
|
611
|
+
var USDC_BASE_SEPOLIA = "0x036CbD53842c5426634e7929541eC2318f3dCF7e";
|
|
612
|
+
var USDC_TEMPO = "0x20c000000000000000000000b9537d11c60e8b50";
|
|
613
|
+
var PATHUSD_TEMPO = "0x20c0000000000000000000000000000000000000";
|
|
614
|
+
var BASE_CHAIN_ID = 8453;
|
|
615
|
+
var TEMPO_CHAIN_ID = 4217;
|
|
616
|
+
var TEMPO_TESTNET_CHAIN_ID = 42431;
|
|
617
|
+
var DEFAULT_MAX_DEPOSIT = "100";
|
|
618
|
+
var KNOWN_EIP712_DOMAINS = {
|
|
619
|
+
// USDC on Base
|
|
620
|
+
[USDC_BASE.toLowerCase()]: { name: "USDC", version: "2" },
|
|
621
|
+
// USDC on Base Sepolia
|
|
622
|
+
[USDC_BASE_SEPOLIA.toLowerCase()]: { name: "USDC", version: "2" }
|
|
623
|
+
};
|
|
624
|
+
var buildRelayClientOptions = () => ({
|
|
625
|
+
baseApiUrl: MAINNET_RELAY_API,
|
|
626
|
+
source: "zero-cli",
|
|
627
|
+
chains: [convertViemChainToRelayChain(base)]
|
|
628
|
+
});
|
|
629
|
+
var calculateBuffer = (baseBalance) => {
|
|
630
|
+
const twentyFivePercent = baseBalance / 4n;
|
|
631
|
+
const twoDollars = 2000000n;
|
|
632
|
+
return twentyFivePercent < twoDollars ? twentyFivePercent : twoDollars;
|
|
633
|
+
};
|
|
634
|
+
var tempoChain = {
|
|
635
|
+
id: TEMPO_CHAIN_ID,
|
|
636
|
+
name: "Tempo",
|
|
637
|
+
nativeCurrency: { name: "USD", symbol: "USD", decimals: 18 },
|
|
638
|
+
rpcUrls: {
|
|
639
|
+
default: { http: ["https://rpc.tempo.xyz"] }
|
|
591
640
|
}
|
|
592
|
-
return { type: typeOf(value) };
|
|
593
641
|
};
|
|
594
|
-
var
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
642
|
+
var tempoTestnetChain = {
|
|
643
|
+
id: TEMPO_TESTNET_CHAIN_ID,
|
|
644
|
+
name: "Tempo Testnet",
|
|
645
|
+
nativeCurrency: { name: "USD", symbol: "USD", decimals: 18 },
|
|
646
|
+
rpcUrls: {
|
|
647
|
+
default: { http: ["https://rpc.moderato.tempo.xyz"] }
|
|
648
|
+
}
|
|
598
649
|
};
|
|
599
|
-
var
|
|
650
|
+
var ERC20_BALANCE_ABI = [
|
|
651
|
+
{
|
|
652
|
+
inputs: [{ name: "account", type: "address" }],
|
|
653
|
+
name: "balanceOf",
|
|
654
|
+
outputs: [{ name: "", type: "uint256" }],
|
|
655
|
+
stateMutability: "view",
|
|
656
|
+
type: "function"
|
|
657
|
+
}
|
|
658
|
+
];
|
|
659
|
+
var decodeSessionReceiptHeader = (header) => {
|
|
660
|
+
if (!header) return null;
|
|
600
661
|
try {
|
|
601
|
-
|
|
662
|
+
const padLen = (4 - header.length % 4) % 4;
|
|
663
|
+
const padded = header.replace(/-/g, "+").replace(/_/g, "/") + "=".repeat(padLen);
|
|
664
|
+
const json = Buffer.from(padded, "base64").toString("utf8");
|
|
665
|
+
const parsed = JSON.parse(json);
|
|
666
|
+
if (typeof parsed.channelId !== "string" || typeof parsed.acceptedCumulative !== "string" || typeof parsed.spent !== "string" || typeof parsed.challengeId !== "string") {
|
|
667
|
+
return null;
|
|
668
|
+
}
|
|
669
|
+
return parsed;
|
|
602
670
|
} catch {
|
|
603
671
|
return null;
|
|
604
672
|
}
|
|
605
673
|
};
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
674
|
+
var readSessionReceipt = (response) => decodeSessionReceiptHeader(
|
|
675
|
+
response.headers.get("Payment-Receipt") ?? response.headers.get("payment-receipt")
|
|
676
|
+
);
|
|
677
|
+
var pickSessionCloseAmount = (receipt, openTimeCumulative) => {
|
|
678
|
+
if (!receipt) return openTimeCumulative;
|
|
679
|
+
const accepted = BigInt(receipt.acceptedCumulative);
|
|
680
|
+
const spent = BigInt(receipt.spent);
|
|
681
|
+
const fromReceipt = accepted > spent ? accepted : spent;
|
|
682
|
+
return fromReceipt > openTimeCumulative ? fromReceipt : openTimeCumulative;
|
|
683
|
+
};
|
|
684
|
+
var PaymentService = class {
|
|
685
|
+
constructor(account, config, deps = {}) {
|
|
686
|
+
this.account = account;
|
|
687
|
+
this.config = config;
|
|
688
|
+
this.fetchOverride = deps.fetchImpl;
|
|
689
|
+
}
|
|
690
|
+
relayInitialized = false;
|
|
691
|
+
fetchOverride;
|
|
692
|
+
/**
|
|
693
|
+
* Resolve the fetch implementation lazily so tests can `vi.stubGlobal`
|
|
694
|
+
* the global `fetch` after the service is constructed.
|
|
695
|
+
*/
|
|
696
|
+
get fetchImpl() {
|
|
697
|
+
return this.fetchOverride ?? globalThis.fetch;
|
|
698
|
+
}
|
|
699
|
+
getAccount = () => this.account;
|
|
700
|
+
ensureRelayClient = () => {
|
|
701
|
+
if (!this.relayInitialized) {
|
|
702
|
+
createRelayClient(buildRelayClientOptions());
|
|
703
|
+
this.relayInitialized = true;
|
|
704
|
+
}
|
|
705
|
+
};
|
|
706
|
+
bridgeToTempo = async (requiredAmount, onProgress) => {
|
|
707
|
+
if (!this.account) throw new Error("No wallet configured");
|
|
708
|
+
this.ensureRelayClient();
|
|
709
|
+
const baseBalance = await this.getBalanceRaw("base");
|
|
710
|
+
const buffer = calculateBuffer(baseBalance);
|
|
711
|
+
const bridgeAmount = requiredAmount + buffer;
|
|
712
|
+
if (baseBalance < bridgeAmount) {
|
|
713
|
+
throw new Error(
|
|
714
|
+
`Insufficient Base USDC to bridge: have ${formatUnits(baseBalance, 6)}, need ${formatUnits(bridgeAmount, 6)} (${formatUnits(requiredAmount, 6)} + ${formatUnits(buffer, 6)} buffer)`
|
|
615
715
|
);
|
|
616
|
-
return { protocol: "x402", raw: decoded };
|
|
617
|
-
} catch {
|
|
618
|
-
return { protocol: "x402", raw: { encoded: x402Header } };
|
|
619
716
|
}
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
if (options.header) {
|
|
649
|
-
for (const h of options.header) {
|
|
650
|
-
const colonIdx = h.indexOf(":");
|
|
651
|
-
if (colonIdx > 0) {
|
|
652
|
-
headers[h.slice(0, colonIdx).trim()] = h.slice(colonIdx + 1).trim();
|
|
653
|
-
}
|
|
717
|
+
onProgress?.(
|
|
718
|
+
`Bridging ${formatUnits(bridgeAmount, 6)} USDC from Base to Tempo...`
|
|
719
|
+
);
|
|
720
|
+
const walletClient = createWalletClient({
|
|
721
|
+
account: this.account,
|
|
722
|
+
chain: base,
|
|
723
|
+
transport: http()
|
|
724
|
+
});
|
|
725
|
+
const quote = await getRelayClient().actions.getQuote({
|
|
726
|
+
chainId: BASE_CHAIN_ID,
|
|
727
|
+
toChainId: TEMPO_CHAIN_ID,
|
|
728
|
+
currency: USDC_BASE,
|
|
729
|
+
toCurrency: USDC_TEMPO,
|
|
730
|
+
amount: bridgeAmount.toString(),
|
|
731
|
+
tradeType: "EXACT_INPUT",
|
|
732
|
+
user: this.account.address,
|
|
733
|
+
recipient: this.account.address,
|
|
734
|
+
options: {
|
|
735
|
+
usePermit: true
|
|
736
|
+
}
|
|
737
|
+
});
|
|
738
|
+
let bridgeTxHash = null;
|
|
739
|
+
await getRelayClient().actions.execute({
|
|
740
|
+
quote,
|
|
741
|
+
wallet: adaptViemWallet(walletClient),
|
|
742
|
+
onProgress: ({ txHashes }) => {
|
|
743
|
+
if (txHashes?.length && !bridgeTxHash) {
|
|
744
|
+
bridgeTxHash = txHashes[0]?.txHash ?? null;
|
|
654
745
|
}
|
|
655
746
|
}
|
|
656
|
-
|
|
657
|
-
|
|
747
|
+
});
|
|
748
|
+
return bridgeTxHash;
|
|
749
|
+
};
|
|
750
|
+
handlePayment = async (url, request, paymentRequirement, maxPay, onProgress) => {
|
|
751
|
+
if (!this.account) {
|
|
752
|
+
throw new Error(
|
|
753
|
+
"No wallet configured \u2014 run `zero init` or set ZERO_PRIVATE_KEY"
|
|
658
754
|
);
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
755
|
+
}
|
|
756
|
+
if (paymentRequirement.protocol === "x402") {
|
|
757
|
+
onProgress?.("Paying via x402 on Base...");
|
|
758
|
+
return this.payX402(url, request, paymentRequirement.raw, maxPay);
|
|
759
|
+
}
|
|
760
|
+
if (paymentRequirement.protocol === "mpp") {
|
|
761
|
+
onProgress?.("Paying via MPP on Tempo...");
|
|
762
|
+
return this.payMpp(
|
|
763
|
+
url,
|
|
764
|
+
request,
|
|
765
|
+
paymentRequirement.raw,
|
|
766
|
+
maxPay,
|
|
767
|
+
onProgress
|
|
672
768
|
);
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
769
|
+
}
|
|
770
|
+
throw new Error("Unrecognized 402 payment protocol");
|
|
771
|
+
};
|
|
772
|
+
payX402 = async (url, request, _raw, maxPay) => {
|
|
773
|
+
if (!this.account) throw new Error("No wallet configured");
|
|
774
|
+
let capturedAmount = "0";
|
|
775
|
+
const client = new X402Client().register(
|
|
776
|
+
"eip155:*",
|
|
777
|
+
new ExactEvmScheme(this.account)
|
|
778
|
+
);
|
|
779
|
+
client.onBeforePaymentCreation(async (context) => {
|
|
780
|
+
const selected = context.selectedRequirements;
|
|
781
|
+
if (selected && (!selected.extra?.name || !selected.extra?.version)) {
|
|
782
|
+
const known = KNOWN_EIP712_DOMAINS[selected.asset?.toLowerCase() ?? ""];
|
|
783
|
+
if (known) {
|
|
784
|
+
selected.extra = { ...selected.extra, ...known };
|
|
785
|
+
}
|
|
680
786
|
}
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
787
|
+
const requirement = context.paymentRequired.accepts[0];
|
|
788
|
+
if (!requirement) return;
|
|
789
|
+
capturedAmount = formatUnits(BigInt(requirement.amount), 6);
|
|
790
|
+
if (maxPay && Number.parseFloat(capturedAmount) > Number.parseFloat(maxPay)) {
|
|
791
|
+
return {
|
|
792
|
+
abort: true,
|
|
793
|
+
reason: `Payment of ${capturedAmount} USDC exceeds --max-pay ${maxPay}`
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
});
|
|
797
|
+
const httpClient = new x402HTTPClient(client).onPaymentRequired(
|
|
798
|
+
createSIWxClientHook(this.account)
|
|
799
|
+
);
|
|
800
|
+
const wrappedFetch = wrapFetchWithPayment(fetch, httpClient);
|
|
801
|
+
const response = await wrappedFetch(url, {
|
|
802
|
+
method: request.method,
|
|
803
|
+
headers: request.headers,
|
|
804
|
+
body: request.body
|
|
805
|
+
});
|
|
806
|
+
let txHash = null;
|
|
807
|
+
const paymentResponseHeader = response.headers.get("payment-response") ?? response.headers.get("x-payment-response");
|
|
808
|
+
if (paymentResponseHeader) {
|
|
809
|
+
try {
|
|
810
|
+
const settlement = decodePaymentResponseHeader(paymentResponseHeader);
|
|
811
|
+
txHash = settlement.transaction ?? null;
|
|
812
|
+
} catch {
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
return {
|
|
816
|
+
response,
|
|
817
|
+
protocol: "x402",
|
|
818
|
+
chain: "base",
|
|
819
|
+
txHash,
|
|
820
|
+
amount: capturedAmount,
|
|
821
|
+
asset: "USDC"
|
|
822
|
+
};
|
|
823
|
+
};
|
|
824
|
+
/**
|
|
825
|
+
* Shared pre-payment work: compute the Tempo amount we need, enforce
|
|
826
|
+
* --max-pay, and bridge from Base USDC if the Tempo balance is short.
|
|
827
|
+
* Invoked from mppx's `onChallenge` — i.e. AFTER the server's 402 and
|
|
828
|
+
* BEFORE mppx signs a credential — so balance/bridge logic runs in-band
|
|
829
|
+
* without adding a pre-probe round-trip.
|
|
830
|
+
*/
|
|
831
|
+
prepareTempoFunds = async (challenge, maxPay, onProgress) => {
|
|
832
|
+
const challengeRequest = challenge.request;
|
|
833
|
+
let requiredRaw;
|
|
834
|
+
if (challenge.intent === "session") {
|
|
835
|
+
const suggestedDeposit = challengeRequest.suggestedDeposit;
|
|
836
|
+
requiredRaw = suggestedDeposit ? BigInt(suggestedDeposit) : BigInt(
|
|
837
|
+
Math.floor(
|
|
838
|
+
Number.parseFloat(maxPay ?? DEFAULT_MAX_DEPOSIT) * 1e6
|
|
839
|
+
)
|
|
840
|
+
);
|
|
841
|
+
} else {
|
|
842
|
+
requiredRaw = BigInt(challengeRequest.amount);
|
|
843
|
+
}
|
|
844
|
+
const capturedAmount = formatUnits(requiredRaw, 6);
|
|
845
|
+
if (maxPay && Number.parseFloat(capturedAmount) > Number.parseFloat(maxPay)) {
|
|
846
|
+
throw new Error(
|
|
847
|
+
`Payment of ${capturedAmount} USDC exceeds --max-pay ${maxPay}`
|
|
848
|
+
);
|
|
849
|
+
}
|
|
850
|
+
const methodDetails = challengeRequest.methodDetails;
|
|
851
|
+
const challengeChainId = challengeRequest.chainId ?? methodDetails?.chainId;
|
|
852
|
+
const isTestnet = challengeChainId === TEMPO_TESTNET_CHAIN_ID;
|
|
853
|
+
onProgress?.(`Checking Tempo balance...`);
|
|
854
|
+
const tempoBalance = await this.getBalanceRaw(
|
|
855
|
+
isTestnet ? "tempo-testnet" : "tempo"
|
|
856
|
+
);
|
|
857
|
+
if (tempoBalance < requiredRaw) {
|
|
858
|
+
if (isTestnet) {
|
|
859
|
+
throw new Error(
|
|
860
|
+
`Insufficient pathUSD on Tempo testnet: have ${formatUnits(tempoBalance, 6)}, need ${capturedAmount}. Fund your wallet with the Tempo testnet faucet: https://docs.tempo.xyz/quickstart/faucet`
|
|
861
|
+
);
|
|
862
|
+
}
|
|
863
|
+
await this.bridgeToTempo(requiredRaw, onProgress);
|
|
864
|
+
}
|
|
865
|
+
return capturedAmount;
|
|
866
|
+
};
|
|
867
|
+
/**
|
|
868
|
+
* MPP payment entrypoint. Runs a SINGLE `mppx.fetch` and uses mppx's
|
|
869
|
+
* `onChallenge` callback to run balance/bridge/max-pay logic in-band
|
|
870
|
+
* with the server's 402. After the 200 comes back, we branch on the
|
|
871
|
+
* captured challenge intent + whether the tempo method reported the
|
|
872
|
+
* channel as opened (only session lifecycle fires `onChannelUpdate`).
|
|
873
|
+
*
|
|
874
|
+
* Why no pre-probe: a pre-probe adds an extra unauthenticated POST
|
|
875
|
+
* before mppx's own 402 dance (2 → 3 server-side requests). Session-
|
|
876
|
+
* intent facilitators that re-run the LLM per request can't reconcile
|
|
877
|
+
* the extra state and return `410 "channel not found"` on the close.
|
|
878
|
+
* This mirrors the working production v0.0.21 single-fetch flow.
|
|
879
|
+
*/
|
|
880
|
+
payMpp = async (url, request, _raw, maxPay, onProgress) => {
|
|
881
|
+
const account = this.account;
|
|
882
|
+
if (!account) throw new Error("No wallet configured");
|
|
883
|
+
let capturedAmount = "0";
|
|
884
|
+
let capturedChallenge;
|
|
885
|
+
let channelEntry;
|
|
886
|
+
const mppx = Mppx.create({
|
|
887
|
+
polyfill: false,
|
|
888
|
+
fetch: this.fetchImpl,
|
|
889
|
+
methods: [
|
|
890
|
+
tempo({
|
|
891
|
+
account,
|
|
892
|
+
maxDeposit: maxPay ?? DEFAULT_MAX_DEPOSIT,
|
|
893
|
+
onChannelUpdate: (entry) => {
|
|
894
|
+
channelEntry = {
|
|
895
|
+
channelId: entry.channelId,
|
|
896
|
+
escrowContract: entry.escrowContract,
|
|
897
|
+
cumulativeAmount: entry.cumulativeAmount,
|
|
898
|
+
opened: entry.opened
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
})
|
|
902
|
+
],
|
|
903
|
+
onChallenge: async (challenge) => {
|
|
904
|
+
capturedChallenge = challenge;
|
|
905
|
+
capturedAmount = await this.prepareTempoFunds(
|
|
906
|
+
challenge,
|
|
907
|
+
maxPay,
|
|
908
|
+
onProgress
|
|
909
|
+
);
|
|
910
|
+
return void 0;
|
|
911
|
+
}
|
|
912
|
+
});
|
|
913
|
+
const response = await mppx.fetch(url, {
|
|
914
|
+
method: request.method,
|
|
915
|
+
headers: request.headers,
|
|
916
|
+
body: request.body
|
|
917
|
+
});
|
|
918
|
+
if (capturedChallenge?.intent === "session" && channelEntry?.opened) {
|
|
919
|
+
return this.completeMppSession({
|
|
920
|
+
url,
|
|
921
|
+
request,
|
|
922
|
+
challenge: capturedChallenge,
|
|
923
|
+
channelEntry,
|
|
924
|
+
response,
|
|
925
|
+
capturedAmount,
|
|
926
|
+
mppx
|
|
927
|
+
});
|
|
928
|
+
}
|
|
929
|
+
let txHash = null;
|
|
930
|
+
try {
|
|
931
|
+
const receipt = Receipt.fromResponse(response);
|
|
932
|
+
txHash = receipt.reference ?? null;
|
|
933
|
+
} catch {
|
|
934
|
+
}
|
|
935
|
+
return {
|
|
936
|
+
response,
|
|
937
|
+
protocol: "mpp",
|
|
938
|
+
chain: "tempo",
|
|
939
|
+
txHash,
|
|
940
|
+
amount: capturedAmount,
|
|
941
|
+
asset: "USDC"
|
|
942
|
+
};
|
|
943
|
+
};
|
|
944
|
+
/**
|
|
945
|
+
* Close an opened MPP session channel after the seller returns a 200.
|
|
946
|
+
*
|
|
947
|
+
* Decodes the server's `Payment-Receipt` to get the settled
|
|
948
|
+
* `acceptedCumulative`/`spent`, signs a close voucher at that amount
|
|
949
|
+
* (never below on-chain settled), and POSTs it back to the seller.
|
|
950
|
+
* Needed for dynamic-pricing sellers whose open-time `amount` is `0` —
|
|
951
|
+
* signing close with `cumulativeAmount=0` would be rejected with
|
|
952
|
+
* `voucher cumulativeAmount is below on-chain settled amount`.
|
|
953
|
+
*
|
|
954
|
+
* When the seller's facilitator rejects the close (e.g. strict matching
|
|
955
|
+
* of fresh challenge IDs per request), `SessionCloseFailedError` is
|
|
956
|
+
* thrown carrying the session metadata so the caller can record the run
|
|
957
|
+
* against the orphaned channel.
|
|
958
|
+
*/
|
|
959
|
+
completeMppSession = async (params) => {
|
|
960
|
+
const {
|
|
961
|
+
url,
|
|
962
|
+
request,
|
|
963
|
+
challenge,
|
|
964
|
+
channelEntry,
|
|
965
|
+
response,
|
|
966
|
+
capturedAmount,
|
|
967
|
+
mppx
|
|
968
|
+
} = params;
|
|
969
|
+
const challengeRequest = challenge.request;
|
|
970
|
+
const recipient = challengeRequest.recipient;
|
|
971
|
+
const methodDetails = challengeRequest.methodDetails;
|
|
972
|
+
const challengeEscrow = methodDetails?.escrowContract;
|
|
973
|
+
const challengeChainId = challengeRequest.chainId ?? methodDetails?.chainId;
|
|
974
|
+
if (!recipient || !challengeEscrow) {
|
|
975
|
+
throw new Error("session challenge missing recipient/escrowContract");
|
|
976
|
+
}
|
|
977
|
+
if (!challengeChainId) {
|
|
978
|
+
throw new Error("session challenge missing chainId");
|
|
979
|
+
}
|
|
980
|
+
const {
|
|
981
|
+
channelId,
|
|
982
|
+
escrowContract,
|
|
983
|
+
cumulativeAmount: openTimeCumulative
|
|
984
|
+
} = channelEntry;
|
|
985
|
+
const receipt = readSessionReceipt(response);
|
|
986
|
+
const closeAmount = pickSessionCloseAmount(receipt, openTimeCumulative);
|
|
987
|
+
const orphanedSession = {
|
|
988
|
+
channelId,
|
|
989
|
+
escrowContract,
|
|
990
|
+
chainId: challengeChainId,
|
|
991
|
+
recipient,
|
|
992
|
+
cumulativeAmount: closeAmount.toString()
|
|
993
|
+
};
|
|
994
|
+
const closeFailed = (message) => new SessionCloseFailedError({
|
|
995
|
+
message,
|
|
996
|
+
session: orphanedSession,
|
|
997
|
+
response,
|
|
998
|
+
capturedAmount
|
|
999
|
+
});
|
|
1000
|
+
const closeChallengeResponse = new Response("", {
|
|
1001
|
+
status: 402,
|
|
1002
|
+
headers: { "WWW-Authenticate": Challenge.serialize(challenge) }
|
|
1003
|
+
});
|
|
1004
|
+
let closeCredential;
|
|
1005
|
+
try {
|
|
1006
|
+
closeCredential = await mppx.createCredential(closeChallengeResponse, {
|
|
1007
|
+
action: "close",
|
|
1008
|
+
channelId,
|
|
1009
|
+
cumulativeAmountRaw: closeAmount.toString()
|
|
1010
|
+
});
|
|
1011
|
+
} catch (err) {
|
|
1012
|
+
throw closeFailed(
|
|
1013
|
+
`Session close credential build failed for channel ${channelId}: ${err instanceof Error ? err.message : String(err)}`
|
|
1014
|
+
);
|
|
1015
|
+
}
|
|
1016
|
+
const closeMethod = request.method.toUpperCase();
|
|
1017
|
+
const methodCarriesBody = closeMethod === "POST" || closeMethod === "PUT" || closeMethod === "PATCH";
|
|
1018
|
+
const closeResponse = await this.fetchImpl(url, {
|
|
1019
|
+
method: closeMethod,
|
|
1020
|
+
headers: {
|
|
1021
|
+
...request.headers,
|
|
1022
|
+
// biome-ignore lint/style/useNamingConvention: HTTP header name
|
|
1023
|
+
Authorization: closeCredential
|
|
1024
|
+
},
|
|
1025
|
+
...methodCarriesBody && request.body ? { body: request.body } : {}
|
|
1026
|
+
});
|
|
1027
|
+
if (!closeResponse.ok) {
|
|
1028
|
+
const body = await closeResponse.text().catch(() => "");
|
|
1029
|
+
throw closeFailed(
|
|
1030
|
+
`Seller rejected close credential (status ${closeResponse.status}${body ? `: ${body.slice(0, 200)}` : ""}) for channel ${channelId}`
|
|
1031
|
+
);
|
|
1032
|
+
}
|
|
1033
|
+
const closeReceipt = readSessionReceipt(closeResponse);
|
|
1034
|
+
return {
|
|
1035
|
+
response,
|
|
1036
|
+
protocol: "mpp",
|
|
1037
|
+
chain: "tempo",
|
|
1038
|
+
txHash: null,
|
|
1039
|
+
amount: capturedAmount,
|
|
1040
|
+
asset: "USDC",
|
|
1041
|
+
session: {
|
|
1042
|
+
channelId,
|
|
1043
|
+
escrowContract,
|
|
1044
|
+
chainId: challengeChainId,
|
|
1045
|
+
recipient,
|
|
1046
|
+
cumulativeAmount: closeReceipt?.acceptedCumulative ?? closeAmount.toString()
|
|
1047
|
+
}
|
|
1048
|
+
};
|
|
1049
|
+
};
|
|
1050
|
+
resolveChainConfig = (chain) => {
|
|
1051
|
+
switch (chain) {
|
|
1052
|
+
case "base":
|
|
1053
|
+
return { viemChain: base, token: USDC_BASE };
|
|
1054
|
+
case "base-sepolia":
|
|
1055
|
+
return { viemChain: baseSepolia, token: USDC_BASE_SEPOLIA };
|
|
1056
|
+
case "tempo":
|
|
1057
|
+
return { viemChain: tempoChain, token: USDC_TEMPO };
|
|
1058
|
+
case "tempo-testnet":
|
|
1059
|
+
return { viemChain: tempoTestnetChain, token: PATHUSD_TEMPO };
|
|
1060
|
+
}
|
|
1061
|
+
};
|
|
1062
|
+
getBalanceRaw = async (chain) => {
|
|
1063
|
+
if (!this.account) return 0n;
|
|
1064
|
+
const { viemChain, token } = this.resolveChainConfig(chain);
|
|
1065
|
+
const client = createPublicClient({
|
|
1066
|
+
chain: viemChain,
|
|
1067
|
+
transport: http()
|
|
1068
|
+
});
|
|
1069
|
+
const balance = await client.readContract({
|
|
1070
|
+
address: token,
|
|
1071
|
+
abi: ERC20_BALANCE_ABI,
|
|
1072
|
+
functionName: "balanceOf",
|
|
1073
|
+
args: [this.account.address]
|
|
1074
|
+
});
|
|
1075
|
+
return balance;
|
|
1076
|
+
};
|
|
1077
|
+
getBalance = async (chain) => {
|
|
1078
|
+
const raw = await this.getBalanceRaw(chain);
|
|
1079
|
+
return { amount: formatUnits(raw, 6), asset: "USDC" };
|
|
1080
|
+
};
|
|
1081
|
+
};
|
|
1082
|
+
|
|
1083
|
+
// src/util/infer-schema.ts
|
|
1084
|
+
var inferSchema = (value, depth = 0) => {
|
|
1085
|
+
if (depth > 6) return { type: typeOf(value) };
|
|
1086
|
+
if (value === null) return { type: "null" };
|
|
1087
|
+
if (Array.isArray(value)) {
|
|
1088
|
+
const itemSchemas = value.slice(0, 3).map((v) => inferSchema(v, depth + 1));
|
|
1089
|
+
return {
|
|
1090
|
+
type: "array",
|
|
1091
|
+
items: itemSchemas[0] ?? { type: "unknown" }
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
if (typeof value === "object") {
|
|
1095
|
+
const obj = value;
|
|
1096
|
+
const properties = {};
|
|
1097
|
+
const required = [];
|
|
1098
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
1099
|
+
properties[k] = inferSchema(v, depth + 1);
|
|
1100
|
+
required.push(k);
|
|
1101
|
+
}
|
|
1102
|
+
return { type: "object", properties, required };
|
|
1103
|
+
}
|
|
1104
|
+
return { type: typeOf(value) };
|
|
1105
|
+
};
|
|
1106
|
+
var typeOf = (v) => {
|
|
1107
|
+
if (v === null) return "null";
|
|
1108
|
+
if (Array.isArray(v)) return "array";
|
|
1109
|
+
return typeof v;
|
|
1110
|
+
};
|
|
1111
|
+
var tryParseJson = (text) => {
|
|
1112
|
+
try {
|
|
1113
|
+
return JSON.parse(text);
|
|
1114
|
+
} catch {
|
|
1115
|
+
return null;
|
|
1116
|
+
}
|
|
1117
|
+
};
|
|
1118
|
+
|
|
1119
|
+
// src/commands/fetch-command.ts
|
|
1120
|
+
var detectPaymentRequirement = (headers, status) => {
|
|
1121
|
+
if (status !== 402) return null;
|
|
1122
|
+
const x402Header = headers.get("payment-required") ?? headers.get("x-payment-required");
|
|
1123
|
+
if (x402Header) {
|
|
1124
|
+
try {
|
|
1125
|
+
const decoded = JSON.parse(
|
|
1126
|
+
Buffer.from(x402Header, "base64").toString("utf8")
|
|
1127
|
+
);
|
|
1128
|
+
return { protocol: "x402", raw: decoded };
|
|
1129
|
+
} catch {
|
|
1130
|
+
return { protocol: "x402", raw: { encoded: x402Header } };
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
const wwwAuth = headers.get("www-authenticate");
|
|
1134
|
+
if (wwwAuth?.toLowerCase().includes("payment")) {
|
|
1135
|
+
return { protocol: "mpp", raw: { "www-authenticate": wwwAuth } };
|
|
1136
|
+
}
|
|
1137
|
+
return { protocol: "unknown", raw: {} };
|
|
1138
|
+
};
|
|
1139
|
+
var fetchCommand = (appContext) => new Command3("fetch").description("Fetch a capability URL with automatic payment handling").argument("<url>", "URL to fetch").option(
|
|
1140
|
+
"-X, --method <method>",
|
|
1141
|
+
"HTTP method (GET, POST, PUT, PATCH, DELETE). Defaults to POST when -d is set, otherwise GET"
|
|
1142
|
+
).option("-d, --data <body>", "Request body (JSON string)").option("-H, --header <header...>", "Headers in Key:Value format").option("--max-pay <amount>", "Maximum amount willing to pay (USDC)").option(
|
|
1143
|
+
"--capability <id>",
|
|
1144
|
+
"Bind this fetch to a capability (uid or slug) so a reviewable run is recorded even without a prior `zero search`"
|
|
1145
|
+
).option(
|
|
1146
|
+
"--json",
|
|
1147
|
+
"Emit {runId, status, latencyMs, payment, body} as JSON on stdout (for batch/non-TTY use)"
|
|
1148
|
+
).action(
|
|
1149
|
+
async (url, options) => {
|
|
1150
|
+
try {
|
|
1151
|
+
const {
|
|
1152
|
+
analyticsService,
|
|
1153
|
+
apiService,
|
|
1154
|
+
paymentService,
|
|
1155
|
+
stateService,
|
|
1156
|
+
walletService
|
|
1157
|
+
} = appContext.services;
|
|
1158
|
+
const startTime = Date.now();
|
|
1159
|
+
const headers = {};
|
|
1160
|
+
if (options.header) {
|
|
1161
|
+
for (const h of options.header) {
|
|
1162
|
+
const colonIdx = h.indexOf(":");
|
|
1163
|
+
if (colonIdx > 0) {
|
|
1164
|
+
headers[h.slice(0, colonIdx).trim()] = h.slice(colonIdx + 1).trim();
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
const hasContentType = Object.keys(headers).some(
|
|
1169
|
+
(k) => k.toLowerCase() === "content-type"
|
|
1170
|
+
);
|
|
1171
|
+
if (options.data && !hasContentType) {
|
|
1172
|
+
headers["content-type"] = "application/json";
|
|
1173
|
+
}
|
|
1174
|
+
const log = (msg) => console.error(` ${msg}`);
|
|
1175
|
+
const method = options.method ? options.method.toUpperCase() : options.data ? "POST" : "GET";
|
|
1176
|
+
const requestInit = {
|
|
1177
|
+
method,
|
|
1178
|
+
headers,
|
|
1179
|
+
body: options.data
|
|
1180
|
+
};
|
|
1181
|
+
const lastSearch = stateService.loadLastSearch();
|
|
1182
|
+
const matchedCapability = lastSearch?.capabilities.find(
|
|
1183
|
+
(c) => url.startsWith(c.url)
|
|
1184
|
+
);
|
|
1185
|
+
const capabilityId = options.capability ?? matchedCapability?.id ?? null;
|
|
1186
|
+
const searchId = matchedCapability ? lastSearch?.searchId : void 0;
|
|
1187
|
+
const skipReasons = [];
|
|
1188
|
+
if (!apiService.walletAddress) {
|
|
1189
|
+
skipReasons.push(
|
|
1190
|
+
"no wallet configured (run `zero wallet import` / set ZERO_PRIVATE_KEY)"
|
|
1191
|
+
);
|
|
1192
|
+
}
|
|
1193
|
+
if (!capabilityId) {
|
|
1194
|
+
skipReasons.push(
|
|
1195
|
+
"no capability resolved \u2014 pass --capability <uid|slug> or run `zero search` first so the URL can be matched"
|
|
684
1196
|
);
|
|
685
1197
|
}
|
|
686
1198
|
let finalResponse;
|
|
687
1199
|
let body = "";
|
|
688
1200
|
let paymentMeta;
|
|
1201
|
+
let sessionMeta;
|
|
689
1202
|
let fetchError;
|
|
690
1203
|
try {
|
|
691
1204
|
log(`Calling ${url}...`);
|
|
@@ -711,17 +1224,50 @@ var fetchCommand = (appContext) => new Command3("fetch").description("Fetch a ca
|
|
|
711
1224
|
chain: result.chain,
|
|
712
1225
|
txHash: result.txHash,
|
|
713
1226
|
amount: result.amount,
|
|
714
|
-
asset: result.asset
|
|
1227
|
+
asset: result.asset,
|
|
1228
|
+
...result.session && { session: result.session }
|
|
715
1229
|
};
|
|
716
1230
|
log(
|
|
717
1231
|
`Paid ${result.amount} ${result.asset} via ${result.protocol} on ${result.chain}`
|
|
718
1232
|
);
|
|
1233
|
+
if (result.session) {
|
|
1234
|
+
sessionMeta = result.session;
|
|
1235
|
+
log(
|
|
1236
|
+
`MPP session closed \u2014 channel ${result.session.channelId.slice(0, 10)}... \u2192 ${result.session.recipient.slice(0, 8)}... (${formatUnits2(BigInt(result.session.cumulativeAmount), 6)} USDC settled)`
|
|
1237
|
+
);
|
|
1238
|
+
}
|
|
719
1239
|
} else {
|
|
720
1240
|
finalResponse = response;
|
|
721
1241
|
}
|
|
722
1242
|
body = await finalResponse.text();
|
|
723
1243
|
} catch (err) {
|
|
724
|
-
|
|
1244
|
+
if (err instanceof SessionCloseFailedError) {
|
|
1245
|
+
finalResponse = err.response;
|
|
1246
|
+
body = await err.response.text().catch(() => "");
|
|
1247
|
+
paymentMeta = {
|
|
1248
|
+
protocol: "mpp",
|
|
1249
|
+
chain: "tempo",
|
|
1250
|
+
txHash: null,
|
|
1251
|
+
amount: err.capturedAmount,
|
|
1252
|
+
asset: "USDC",
|
|
1253
|
+
session: err.session
|
|
1254
|
+
};
|
|
1255
|
+
sessionMeta = err.session;
|
|
1256
|
+
fetchError = err;
|
|
1257
|
+
console.error(
|
|
1258
|
+
[
|
|
1259
|
+
"",
|
|
1260
|
+
" WARNING: failed to auto-close MPP session.",
|
|
1261
|
+
` ${err.message}`,
|
|
1262
|
+
` Channel ${err.session.channelId} is still open on chain \u2014 see payment.session in --json output.`,
|
|
1263
|
+
" Contact the seller to settle the channel from their facilitator, or use",
|
|
1264
|
+
" your wallet to call escrow.close/requestClose directly.",
|
|
1265
|
+
""
|
|
1266
|
+
].join("\n")
|
|
1267
|
+
);
|
|
1268
|
+
} else {
|
|
1269
|
+
fetchError = err instanceof Error ? err : new Error(String(err));
|
|
1270
|
+
}
|
|
725
1271
|
}
|
|
726
1272
|
const latencyMs = Date.now() - startTime;
|
|
727
1273
|
if (finalResponse && !options.json) {
|
|
@@ -782,7 +1328,7 @@ var fetchCommand = (appContext) => new Command3("fetch").description("Fetch a ca
|
|
|
782
1328
|
paymentProtocol: paymentMeta.protocol,
|
|
783
1329
|
paymentChain: paymentMeta.chain,
|
|
784
1330
|
paymentTxHash: paymentMeta.txHash ?? void 0,
|
|
785
|
-
paymentMode: "charge"
|
|
1331
|
+
paymentMode: sessionMeta ? "session" : "charge"
|
|
786
1332
|
}
|
|
787
1333
|
});
|
|
788
1334
|
runId = runResult.runId;
|
|
@@ -1247,7 +1793,7 @@ To remove them, run: zero init cleanup`
|
|
|
1247
1793
|
);
|
|
1248
1794
|
}
|
|
1249
1795
|
console.error(
|
|
1250
|
-
'Zero is ready! Run `zero search` to find capabilities.\n\nTry:\n zero search "translate text to Spanish"\n zero search "generate an image"\n zero search "weather forecast"'
|
|
1796
|
+
'Zero is ready! Run `zero search` to find capabilities.\n\nBy using Zero, you agree to our Terms of Service:\n https://zero.xyz/terms-of-service\n\nRun `zero terms` to view the full terms.\n\nTry:\n zero search "translate text to Spanish"\n zero search "generate an image"\n zero search "weather forecast"'
|
|
1251
1797
|
);
|
|
1252
1798
|
appContext.services.analyticsService.capture("wallet_initialized", {
|
|
1253
1799
|
// biome-ignore lint/style/useNamingConvention: snake_case for analytics
|
|
@@ -1606,14 +2152,34 @@ var searchCommand = (appContext) => new Command8("search").description("Search f
|
|
|
1606
2152
|
}
|
|
1607
2153
|
);
|
|
1608
2154
|
|
|
2155
|
+
// src/commands/terms-command.ts
|
|
2156
|
+
import { Command as Command9 } from "commander";
|
|
2157
|
+
var TERMS_URL = "https://zero.xyz/terms-of-service";
|
|
2158
|
+
var termsCommand = (_appContext) => new Command9("terms").description("View the ZeroClick Terms of Service").action(async () => {
|
|
2159
|
+
console.log(
|
|
2160
|
+
`ZeroClick Agentic Capability Search \u2014 Terms of Service
|
|
2161
|
+
|
|
2162
|
+
By using Zero, you agree to our Terms of Service.
|
|
2163
|
+
|
|
2164
|
+
Read the full terms at: ${TERMS_URL}
|
|
2165
|
+
`
|
|
2166
|
+
);
|
|
2167
|
+
try {
|
|
2168
|
+
const { exec } = await import("child_process");
|
|
2169
|
+
const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
2170
|
+
exec(`${openCmd} ${TERMS_URL}`);
|
|
2171
|
+
} catch {
|
|
2172
|
+
}
|
|
2173
|
+
});
|
|
2174
|
+
|
|
1609
2175
|
// src/commands/wallet-command.ts
|
|
1610
2176
|
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
|
|
1611
2177
|
import { homedir as homedir3 } from "os";
|
|
1612
2178
|
import { join as join3 } from "path";
|
|
1613
|
-
import { Command as
|
|
2179
|
+
import { Command as Command10 } from "commander";
|
|
1614
2180
|
import open from "open";
|
|
1615
2181
|
import { privateKeyToAccount as privateKeyToAccount2 } from "viem/accounts";
|
|
1616
|
-
var walletBalanceCommand = (appContext) => new
|
|
2182
|
+
var walletBalanceCommand = (appContext) => new Command10("balance").description("Show wallet balance").action(async () => {
|
|
1617
2183
|
const { walletService } = appContext.services;
|
|
1618
2184
|
const balance = await walletService.getBalance();
|
|
1619
2185
|
if (balance === null) {
|
|
@@ -1628,7 +2194,11 @@ var walletBalanceCommand = (appContext) => new Command9("balance").description("
|
|
|
1628
2194
|
}
|
|
1629
2195
|
console.log(`${balance.amount} ${balance.asset}`);
|
|
1630
2196
|
});
|
|
1631
|
-
var walletFundCommand = (appContext) => new
|
|
2197
|
+
var walletFundCommand = (appContext) => new Command10("fund").description("Fund your wallet").argument("[amount]", "Amount to fund in USDC").option("--manual", "Show wallet address for manual transfer").option(
|
|
2198
|
+
"--use <provider>",
|
|
2199
|
+
"Onramp provider: coinbase or stripe",
|
|
2200
|
+
"coinbase"
|
|
2201
|
+
).action(
|
|
1632
2202
|
async (amount, options) => {
|
|
1633
2203
|
const { analyticsService, walletService } = appContext.services;
|
|
1634
2204
|
const address = walletService.getAddress();
|
|
@@ -1646,7 +2216,11 @@ ${address}`);
|
|
|
1646
2216
|
});
|
|
1647
2217
|
return;
|
|
1648
2218
|
}
|
|
1649
|
-
const
|
|
2219
|
+
const provider = options.use === "stripe" ? "stripe" : "coinbase";
|
|
2220
|
+
const url = await appContext.services.apiService.getFundingUrl(
|
|
2221
|
+
amount,
|
|
2222
|
+
provider
|
|
2223
|
+
);
|
|
1650
2224
|
if (url) {
|
|
1651
2225
|
await open(url);
|
|
1652
2226
|
console.log("Opened funding page in your browser.");
|
|
@@ -1665,7 +2239,7 @@ ${address}`);
|
|
|
1665
2239
|
}
|
|
1666
2240
|
}
|
|
1667
2241
|
);
|
|
1668
|
-
var walletAddressCommand = (appContext) => new
|
|
2242
|
+
var walletAddressCommand = (appContext) => new Command10("address").description("Show wallet address").action(() => {
|
|
1669
2243
|
const { walletService } = appContext.services;
|
|
1670
2244
|
const address = walletService.getAddress();
|
|
1671
2245
|
if (!address) {
|
|
@@ -1675,7 +2249,7 @@ var walletAddressCommand = (appContext) => new Command9("address").description("
|
|
|
1675
2249
|
}
|
|
1676
2250
|
console.log(address);
|
|
1677
2251
|
});
|
|
1678
|
-
var walletSetCommand = (appContext) => new
|
|
2252
|
+
var walletSetCommand = (appContext) => new Command10("set").description("Set wallet from an existing private key").argument("<privateKey>", "Hex-encoded private key (0x-prefixed)").option("--force", "Overwrite existing wallet without prompting").action(async (privateKey, options) => {
|
|
1679
2253
|
const { analyticsService } = appContext.services;
|
|
1680
2254
|
if (!privateKey.startsWith("0x")) {
|
|
1681
2255
|
console.error("Private key must be 0x-prefixed hex string.");
|
|
@@ -1727,7 +2301,7 @@ var walletSetCommand = (appContext) => new Command9("set").description("Set wall
|
|
|
1727
2301
|
});
|
|
1728
2302
|
});
|
|
1729
2303
|
var walletCommand = (appContext) => {
|
|
1730
|
-
const cmd = new
|
|
2304
|
+
const cmd = new Command10("wallet").description("Manage your wallet");
|
|
1731
2305
|
cmd.addCommand(walletBalanceCommand(appContext));
|
|
1732
2306
|
cmd.addCommand(walletFundCommand(appContext));
|
|
1733
2307
|
cmd.addCommand(walletAddressCommand(appContext));
|
|
@@ -1738,7 +2312,7 @@ var walletCommand = (appContext) => {
|
|
|
1738
2312
|
// src/app.ts
|
|
1739
2313
|
var createApp = (appContext) => {
|
|
1740
2314
|
const { analyticsService } = appContext.services;
|
|
1741
|
-
const program = new
|
|
2315
|
+
const program = new Command11().name("zero").description("Zero CLI \u2014 Search engine and payment platform for AI agents").version(package_default.version, "-v, --version").exitOverride().hook("preAction", async (_thisCommand, actionCommand) => {
|
|
1742
2316
|
analyticsService.capture("command_executed", {
|
|
1743
2317
|
command: actionCommand.name()
|
|
1744
2318
|
});
|
|
@@ -1752,6 +2326,7 @@ var createApp = (appContext) => {
|
|
|
1752
2326
|
program.addCommand(bugReportCommand(appContext));
|
|
1753
2327
|
program.addCommand(walletCommand(appContext));
|
|
1754
2328
|
program.addCommand(configCommand(appContext));
|
|
2329
|
+
program.addCommand(termsCommand(appContext));
|
|
1755
2330
|
return program;
|
|
1756
2331
|
};
|
|
1757
2332
|
|
|
@@ -1865,311 +2440,6 @@ var AnalyticsService = class {
|
|
|
1865
2440
|
}
|
|
1866
2441
|
};
|
|
1867
2442
|
|
|
1868
|
-
// src/services/payment-service.ts
|
|
1869
|
-
import {
|
|
1870
|
-
adaptViemWallet,
|
|
1871
|
-
convertViemChainToRelayChain,
|
|
1872
|
-
createClient as createRelayClient,
|
|
1873
|
-
getClient as getRelayClient,
|
|
1874
|
-
MAINNET_RELAY_API
|
|
1875
|
-
} from "@relayprotocol/relay-sdk";
|
|
1876
|
-
import { x402Client as X402Client } from "@x402/core/client";
|
|
1877
|
-
import { decodePaymentResponseHeader, x402HTTPClient } from "@x402/core/http";
|
|
1878
|
-
import { ExactEvmScheme } from "@x402/evm/exact/client";
|
|
1879
|
-
import { createSIWxClientHook } from "@x402/extensions/sign-in-with-x";
|
|
1880
|
-
import { wrapFetchWithPayment } from "@x402/fetch";
|
|
1881
|
-
import { Receipt } from "mppx";
|
|
1882
|
-
import { Mppx, tempo } from "mppx/client";
|
|
1883
|
-
import {
|
|
1884
|
-
createPublicClient,
|
|
1885
|
-
createWalletClient,
|
|
1886
|
-
formatUnits,
|
|
1887
|
-
http
|
|
1888
|
-
} from "viem";
|
|
1889
|
-
import { base, baseSepolia } from "viem/chains";
|
|
1890
|
-
var USDC_BASE = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913";
|
|
1891
|
-
var USDC_BASE_SEPOLIA = "0x036CbD53842c5426634e7929541eC2318f3dCF7e";
|
|
1892
|
-
var USDC_TEMPO = "0x20c000000000000000000000b9537d11c60e8b50";
|
|
1893
|
-
var PATHUSD_TEMPO = "0x20c0000000000000000000000000000000000000";
|
|
1894
|
-
var BASE_CHAIN_ID = 8453;
|
|
1895
|
-
var TEMPO_CHAIN_ID = 4217;
|
|
1896
|
-
var TEMPO_TESTNET_CHAIN_ID = 42431;
|
|
1897
|
-
var DEFAULT_MAX_DEPOSIT = "100";
|
|
1898
|
-
var KNOWN_EIP712_DOMAINS = {
|
|
1899
|
-
// USDC on Base
|
|
1900
|
-
[USDC_BASE.toLowerCase()]: { name: "USDC", version: "2" },
|
|
1901
|
-
// USDC on Base Sepolia
|
|
1902
|
-
[USDC_BASE_SEPOLIA.toLowerCase()]: { name: "USDC", version: "2" }
|
|
1903
|
-
};
|
|
1904
|
-
var buildRelayClientOptions = () => ({
|
|
1905
|
-
baseApiUrl: MAINNET_RELAY_API,
|
|
1906
|
-
source: "zero-cli",
|
|
1907
|
-
chains: [convertViemChainToRelayChain(base)]
|
|
1908
|
-
});
|
|
1909
|
-
var calculateBuffer = (baseBalance) => {
|
|
1910
|
-
const twentyFivePercent = baseBalance / 4n;
|
|
1911
|
-
const twoDollars = 2000000n;
|
|
1912
|
-
return twentyFivePercent < twoDollars ? twentyFivePercent : twoDollars;
|
|
1913
|
-
};
|
|
1914
|
-
var tempoChain = {
|
|
1915
|
-
id: TEMPO_CHAIN_ID,
|
|
1916
|
-
name: "Tempo",
|
|
1917
|
-
nativeCurrency: { name: "USD", symbol: "USD", decimals: 18 },
|
|
1918
|
-
rpcUrls: {
|
|
1919
|
-
default: { http: ["https://rpc.tempo.xyz"] }
|
|
1920
|
-
}
|
|
1921
|
-
};
|
|
1922
|
-
var tempoTestnetChain = {
|
|
1923
|
-
id: TEMPO_TESTNET_CHAIN_ID,
|
|
1924
|
-
name: "Tempo Testnet",
|
|
1925
|
-
nativeCurrency: { name: "USD", symbol: "USD", decimals: 18 },
|
|
1926
|
-
rpcUrls: {
|
|
1927
|
-
default: { http: ["https://rpc.moderato.tempo.xyz"] }
|
|
1928
|
-
}
|
|
1929
|
-
};
|
|
1930
|
-
var ERC20_BALANCE_ABI = [
|
|
1931
|
-
{
|
|
1932
|
-
inputs: [{ name: "account", type: "address" }],
|
|
1933
|
-
name: "balanceOf",
|
|
1934
|
-
outputs: [{ name: "", type: "uint256" }],
|
|
1935
|
-
stateMutability: "view",
|
|
1936
|
-
type: "function"
|
|
1937
|
-
}
|
|
1938
|
-
];
|
|
1939
|
-
var PaymentService = class {
|
|
1940
|
-
constructor(account, config) {
|
|
1941
|
-
this.account = account;
|
|
1942
|
-
this.config = config;
|
|
1943
|
-
}
|
|
1944
|
-
relayInitialized = false;
|
|
1945
|
-
ensureRelayClient = () => {
|
|
1946
|
-
if (!this.relayInitialized) {
|
|
1947
|
-
createRelayClient(buildRelayClientOptions());
|
|
1948
|
-
this.relayInitialized = true;
|
|
1949
|
-
}
|
|
1950
|
-
};
|
|
1951
|
-
bridgeToTempo = async (requiredAmount, onProgress) => {
|
|
1952
|
-
if (!this.account) throw new Error("No wallet configured");
|
|
1953
|
-
this.ensureRelayClient();
|
|
1954
|
-
const baseBalance = await this.getBalanceRaw("base");
|
|
1955
|
-
const buffer = calculateBuffer(baseBalance);
|
|
1956
|
-
const bridgeAmount = requiredAmount + buffer;
|
|
1957
|
-
if (baseBalance < bridgeAmount) {
|
|
1958
|
-
throw new Error(
|
|
1959
|
-
`Insufficient Base USDC to bridge: have ${formatUnits(baseBalance, 6)}, need ${formatUnits(bridgeAmount, 6)} (${formatUnits(requiredAmount, 6)} + ${formatUnits(buffer, 6)} buffer)`
|
|
1960
|
-
);
|
|
1961
|
-
}
|
|
1962
|
-
onProgress?.(
|
|
1963
|
-
`Bridging ${formatUnits(bridgeAmount, 6)} USDC from Base to Tempo...`
|
|
1964
|
-
);
|
|
1965
|
-
const walletClient = createWalletClient({
|
|
1966
|
-
account: this.account,
|
|
1967
|
-
chain: base,
|
|
1968
|
-
transport: http()
|
|
1969
|
-
});
|
|
1970
|
-
const quote = await getRelayClient().actions.getQuote({
|
|
1971
|
-
chainId: BASE_CHAIN_ID,
|
|
1972
|
-
toChainId: TEMPO_CHAIN_ID,
|
|
1973
|
-
currency: USDC_BASE,
|
|
1974
|
-
toCurrency: USDC_TEMPO,
|
|
1975
|
-
amount: bridgeAmount.toString(),
|
|
1976
|
-
tradeType: "EXACT_INPUT",
|
|
1977
|
-
user: this.account.address,
|
|
1978
|
-
recipient: this.account.address,
|
|
1979
|
-
options: {
|
|
1980
|
-
usePermit: true
|
|
1981
|
-
}
|
|
1982
|
-
});
|
|
1983
|
-
let bridgeTxHash = null;
|
|
1984
|
-
await getRelayClient().actions.execute({
|
|
1985
|
-
quote,
|
|
1986
|
-
wallet: adaptViemWallet(walletClient),
|
|
1987
|
-
onProgress: ({ txHashes }) => {
|
|
1988
|
-
if (txHashes?.length && !bridgeTxHash) {
|
|
1989
|
-
bridgeTxHash = txHashes[0]?.txHash ?? null;
|
|
1990
|
-
}
|
|
1991
|
-
}
|
|
1992
|
-
});
|
|
1993
|
-
return bridgeTxHash;
|
|
1994
|
-
};
|
|
1995
|
-
handlePayment = async (url, request, paymentRequirement, maxPay, onProgress) => {
|
|
1996
|
-
if (!this.account) {
|
|
1997
|
-
throw new Error(
|
|
1998
|
-
"No wallet configured \u2014 run `zero init` or set ZERO_PRIVATE_KEY"
|
|
1999
|
-
);
|
|
2000
|
-
}
|
|
2001
|
-
if (paymentRequirement.protocol === "x402") {
|
|
2002
|
-
onProgress?.("Paying via x402 on Base...");
|
|
2003
|
-
return this.payX402(url, request, paymentRequirement.raw, maxPay);
|
|
2004
|
-
}
|
|
2005
|
-
if (paymentRequirement.protocol === "mpp") {
|
|
2006
|
-
onProgress?.("Paying via MPP on Tempo...");
|
|
2007
|
-
return this.payMpp(
|
|
2008
|
-
url,
|
|
2009
|
-
request,
|
|
2010
|
-
paymentRequirement.raw,
|
|
2011
|
-
maxPay,
|
|
2012
|
-
onProgress
|
|
2013
|
-
);
|
|
2014
|
-
}
|
|
2015
|
-
throw new Error("Unrecognized 402 payment protocol");
|
|
2016
|
-
};
|
|
2017
|
-
payX402 = async (url, request, _raw, maxPay) => {
|
|
2018
|
-
if (!this.account) throw new Error("No wallet configured");
|
|
2019
|
-
let capturedAmount = "0";
|
|
2020
|
-
const client = new X402Client().register(
|
|
2021
|
-
"eip155:*",
|
|
2022
|
-
new ExactEvmScheme(this.account)
|
|
2023
|
-
);
|
|
2024
|
-
client.onBeforePaymentCreation(async (context) => {
|
|
2025
|
-
const selected = context.selectedRequirements;
|
|
2026
|
-
if (selected && (!selected.extra?.name || !selected.extra?.version)) {
|
|
2027
|
-
const known = KNOWN_EIP712_DOMAINS[selected.asset?.toLowerCase() ?? ""];
|
|
2028
|
-
if (known) {
|
|
2029
|
-
selected.extra = { ...selected.extra, ...known };
|
|
2030
|
-
}
|
|
2031
|
-
}
|
|
2032
|
-
const requirement = context.paymentRequired.accepts[0];
|
|
2033
|
-
if (!requirement) return;
|
|
2034
|
-
capturedAmount = formatUnits(BigInt(requirement.amount), 6);
|
|
2035
|
-
if (maxPay && Number.parseFloat(capturedAmount) > Number.parseFloat(maxPay)) {
|
|
2036
|
-
return {
|
|
2037
|
-
abort: true,
|
|
2038
|
-
reason: `Payment of ${capturedAmount} USDC exceeds --max-pay ${maxPay}`
|
|
2039
|
-
};
|
|
2040
|
-
}
|
|
2041
|
-
});
|
|
2042
|
-
const httpClient = new x402HTTPClient(client).onPaymentRequired(
|
|
2043
|
-
createSIWxClientHook(this.account)
|
|
2044
|
-
);
|
|
2045
|
-
const wrappedFetch = wrapFetchWithPayment(fetch, httpClient);
|
|
2046
|
-
const response = await wrappedFetch(url, {
|
|
2047
|
-
method: request.method,
|
|
2048
|
-
headers: request.headers,
|
|
2049
|
-
body: request.body
|
|
2050
|
-
});
|
|
2051
|
-
let txHash = null;
|
|
2052
|
-
const paymentResponseHeader = response.headers.get("payment-response") ?? response.headers.get("x-payment-response");
|
|
2053
|
-
if (paymentResponseHeader) {
|
|
2054
|
-
try {
|
|
2055
|
-
const settlement = decodePaymentResponseHeader(paymentResponseHeader);
|
|
2056
|
-
txHash = settlement.transaction ?? null;
|
|
2057
|
-
} catch {
|
|
2058
|
-
}
|
|
2059
|
-
}
|
|
2060
|
-
return {
|
|
2061
|
-
response,
|
|
2062
|
-
protocol: "x402",
|
|
2063
|
-
chain: "base",
|
|
2064
|
-
txHash,
|
|
2065
|
-
amount: capturedAmount,
|
|
2066
|
-
asset: "USDC"
|
|
2067
|
-
};
|
|
2068
|
-
};
|
|
2069
|
-
payMpp = async (url, request, _raw, maxPay, onProgress) => {
|
|
2070
|
-
if (!this.account) throw new Error("No wallet configured");
|
|
2071
|
-
let capturedAmount = "0";
|
|
2072
|
-
const mppx = Mppx.create({
|
|
2073
|
-
polyfill: false,
|
|
2074
|
-
methods: [
|
|
2075
|
-
tempo({
|
|
2076
|
-
account: this.account,
|
|
2077
|
-
maxDeposit: maxPay ?? DEFAULT_MAX_DEPOSIT
|
|
2078
|
-
})
|
|
2079
|
-
],
|
|
2080
|
-
onChallenge: async (challenge) => {
|
|
2081
|
-
const challengeRequest = challenge.request;
|
|
2082
|
-
const intent = challenge.intent;
|
|
2083
|
-
let requiredRaw;
|
|
2084
|
-
if (intent === "session") {
|
|
2085
|
-
const suggestedDeposit = challengeRequest.suggestedDeposit;
|
|
2086
|
-
if (suggestedDeposit) {
|
|
2087
|
-
requiredRaw = BigInt(suggestedDeposit);
|
|
2088
|
-
} else {
|
|
2089
|
-
const depositStr = maxPay ?? DEFAULT_MAX_DEPOSIT;
|
|
2090
|
-
requiredRaw = BigInt(
|
|
2091
|
-
Math.floor(Number.parseFloat(depositStr) * 1e6)
|
|
2092
|
-
);
|
|
2093
|
-
}
|
|
2094
|
-
} else {
|
|
2095
|
-
requiredRaw = BigInt(challengeRequest.amount);
|
|
2096
|
-
}
|
|
2097
|
-
capturedAmount = formatUnits(requiredRaw, 6);
|
|
2098
|
-
if (maxPay && Number.parseFloat(capturedAmount) > Number.parseFloat(maxPay)) {
|
|
2099
|
-
throw new Error(
|
|
2100
|
-
`Payment of ${capturedAmount} USDC exceeds --max-pay ${maxPay}`
|
|
2101
|
-
);
|
|
2102
|
-
}
|
|
2103
|
-
const methodDetails = challengeRequest.methodDetails;
|
|
2104
|
-
const challengeChainId = challengeRequest.chainId ?? methodDetails?.chainId;
|
|
2105
|
-
const isTestnet = challengeChainId === TEMPO_TESTNET_CHAIN_ID;
|
|
2106
|
-
const balanceChain = isTestnet ? "tempo-testnet" : "tempo";
|
|
2107
|
-
onProgress?.(`Checking Tempo balance...`);
|
|
2108
|
-
const tempoBalance = await this.getBalanceRaw(balanceChain);
|
|
2109
|
-
if (tempoBalance < requiredRaw) {
|
|
2110
|
-
if (isTestnet) {
|
|
2111
|
-
throw new Error(
|
|
2112
|
-
`Insufficient pathUSD on Tempo testnet: have ${formatUnits(tempoBalance, 6)}, need ${capturedAmount}. Fund your wallet with the Tempo testnet faucet: https://docs.tempo.xyz/quickstart/faucet`
|
|
2113
|
-
);
|
|
2114
|
-
}
|
|
2115
|
-
await this.bridgeToTempo(requiredRaw, onProgress);
|
|
2116
|
-
}
|
|
2117
|
-
return void 0;
|
|
2118
|
-
}
|
|
2119
|
-
});
|
|
2120
|
-
const response = await mppx.fetch(url, {
|
|
2121
|
-
method: request.method,
|
|
2122
|
-
headers: request.headers,
|
|
2123
|
-
body: request.body
|
|
2124
|
-
});
|
|
2125
|
-
let txHash = null;
|
|
2126
|
-
try {
|
|
2127
|
-
const receipt = Receipt.fromResponse(response);
|
|
2128
|
-
txHash = receipt.reference ?? null;
|
|
2129
|
-
} catch {
|
|
2130
|
-
}
|
|
2131
|
-
return {
|
|
2132
|
-
response,
|
|
2133
|
-
protocol: "mpp",
|
|
2134
|
-
chain: "tempo",
|
|
2135
|
-
txHash,
|
|
2136
|
-
amount: capturedAmount,
|
|
2137
|
-
asset: "USDC"
|
|
2138
|
-
};
|
|
2139
|
-
};
|
|
2140
|
-
resolveChainConfig = (chain) => {
|
|
2141
|
-
switch (chain) {
|
|
2142
|
-
case "base":
|
|
2143
|
-
return { viemChain: base, token: USDC_BASE };
|
|
2144
|
-
case "base-sepolia":
|
|
2145
|
-
return { viemChain: baseSepolia, token: USDC_BASE_SEPOLIA };
|
|
2146
|
-
case "tempo":
|
|
2147
|
-
return { viemChain: tempoChain, token: USDC_TEMPO };
|
|
2148
|
-
case "tempo-testnet":
|
|
2149
|
-
return { viemChain: tempoTestnetChain, token: PATHUSD_TEMPO };
|
|
2150
|
-
}
|
|
2151
|
-
};
|
|
2152
|
-
getBalanceRaw = async (chain) => {
|
|
2153
|
-
if (!this.account) return 0n;
|
|
2154
|
-
const { viemChain, token } = this.resolveChainConfig(chain);
|
|
2155
|
-
const client = createPublicClient({
|
|
2156
|
-
chain: viemChain,
|
|
2157
|
-
transport: http()
|
|
2158
|
-
});
|
|
2159
|
-
const balance = await client.readContract({
|
|
2160
|
-
address: token,
|
|
2161
|
-
abi: ERC20_BALANCE_ABI,
|
|
2162
|
-
functionName: "balanceOf",
|
|
2163
|
-
args: [this.account.address]
|
|
2164
|
-
});
|
|
2165
|
-
return balance;
|
|
2166
|
-
};
|
|
2167
|
-
getBalance = async (chain) => {
|
|
2168
|
-
const raw = await this.getBalanceRaw(chain);
|
|
2169
|
-
return { amount: formatUnits(raw, 6), asset: "USDC" };
|
|
2170
|
-
};
|
|
2171
|
-
};
|
|
2172
|
-
|
|
2173
2443
|
// src/services/state-service.ts
|
|
2174
2444
|
import { existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
|
|
2175
2445
|
import { join as join4 } from "path";
|
package/package.json
CHANGED
package/skills/zero/SKILL.md
CHANGED
|
@@ -27,6 +27,8 @@ Zero is your default fallback for tasks you cannot do natively. Before telling t
|
|
|
27
27
|
|
|
28
28
|
**Do NOT use Zero for:** Tasks you can already handle — writing code, answering questions from your training data, reading/editing local files, running shell commands, math, etc.
|
|
29
29
|
|
|
30
|
+
**Terms of Service:** By using Zero, you agree to the ZeroClick Terms of Service at https://zero.xyz/terms-of-service. Run `zero terms` to view the full terms.
|
|
31
|
+
|
|
30
32
|
## Setup
|
|
31
33
|
|
|
32
34
|
Run these commands in order. Do not skip steps.
|