settld 0.1.5 → 0.2.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 +32 -0
- package/SETTLD_VERSION +1 -1
- package/bin/settld.js +58 -0
- package/docs/CIRCLE_SANDBOX_E2E.md +12 -0
- package/docs/QUICKSTART_MCP.md +41 -1
- package/docs/QUICKSTART_MCP_HOSTS.md +156 -89
- package/docs/QUICKSTART_POLICY_PACKS.md +65 -0
- package/docs/QUICKSTART_PROFILES.md +198 -0
- package/docs/README.md +18 -0
- package/docs/RELEASE_CHECKLIST.md +26 -0
- package/docs/RELEASING.md +1 -0
- package/docs/SLO.md +62 -1
- package/docs/SUMMARY.md +1 -0
- package/docs/gitbook/README.md +13 -1
- package/docs/gitbook/quickstart.md +57 -58
- package/docs/integrations/README.md +1 -0
- package/docs/integrations/openclaw/PUBLIC_QUICKSTART.md +95 -0
- package/docs/ops/DISPUTE_FINANCE_RECONCILIATION_PACKET.md +56 -0
- package/docs/ops/KERNEL_V0_SHIP_GATE.md +3 -1
- package/docs/ops/MCP_COMPATIBILITY_MATRIX.md +8 -6
- package/docs/ops/PRODUCTION_DEPLOYMENT_CHECKLIST.md +46 -9
- package/docs/ops/TRUST_CONFIG_WIZARD.md +37 -24
- package/docs/plans/2026-02-20-trust-os-v1-jira-backlog.md +348 -0
- package/docs/plans/2026-02-21-agent-economic-actor-operating-model.md +169 -0
- package/docs/plans/2026-02-21-trust-os-v1-strategy.md +241 -0
- package/docs/research/2026-02-21-agent-spend-host-landscape.md +57 -0
- package/docs/spec/ArbitrationOutcomeMapping.v1.md +62 -0
- package/docs/spec/DisputeCaseLifecycle.v1.md +51 -0
- package/docs/spec/OperatorAction.v1.md +90 -0
- package/docs/spec/PolicyDecision.v1.md +83 -0
- package/docs/spec/README.md +5 -0
- package/docs/spec/SettlementDecisionRecord.v2.md +2 -0
- package/docs/spec/schemas/OperatorAction.v1.schema.json +113 -0
- package/docs/spec/schemas/PolicyDecision.v1.schema.json +74 -0
- package/docs/spec/schemas/SettlementDecisionRecord.v2.schema.json +1 -0
- package/docs/spec/x402-error-codes.v1.txt +14 -0
- package/package.json +14 -1
- package/scripts/ci/build-launch-cutover-packet.mjs +177 -21
- package/scripts/ci/run-10x-throughput-drill.mjs +76 -4
- package/scripts/ci/run-10x-throughput-incident-rehearsal.mjs +49 -6
- package/scripts/ci/run-mcp-host-cert-matrix.mjs +201 -0
- package/scripts/ci/run-mcp-host-smoke.mjs +203 -5
- package/scripts/ci/run-offline-verification-parity-gate.mjs +762 -0
- package/scripts/ci/run-onboarding-host-success-gate.mjs +516 -0
- package/scripts/ci/run-onboarding-policy-slo-gate.mjs +537 -0
- package/scripts/ci/run-production-cutover-gate.mjs +540 -0
- package/scripts/ci/run-public-openclaw-npx-smoke.mjs +148 -0
- package/scripts/ci/run-release-promotion-guard.mjs +756 -0
- package/scripts/doctor/mcp-host.mjs +120 -0
- package/scripts/mcp/settld-mcp-server.mjs +330 -20
- package/scripts/ops/dispute-finance-reconciliation-packet.mjs +313 -0
- package/scripts/ops/hosted-baseline-evidence.mjs +286 -77
- package/scripts/ops/run-x402-hitl-smoke.mjs +607 -0
- package/scripts/policy/cli.mjs +600 -0
- package/scripts/profile/cli.mjs +1324 -0
- package/scripts/register-entity-secret.mjs +102 -0
- package/scripts/setup/circle-bootstrap.mjs +310 -0
- package/scripts/setup/host-config.mjs +617 -0
- package/scripts/setup/onboard.mjs +1337 -0
- package/scripts/setup/openclaw-onboard.mjs +423 -0
- package/scripts/setup/wizard.mjs +986 -0
- package/scripts/slo/check.mjs +123 -62
- package/scripts/spec/generate-protocol-vectors.mjs +88 -0
- package/scripts/test/run.sh +23 -9
- package/services/x402-gateway/src/server.js +147 -36
- package/src/api/app.js +2345 -267
- package/src/api/middleware/trust-kernel.js +114 -0
- package/src/api/openapi.js +598 -3
- package/src/api/persistence.js +184 -0
- package/src/api/store.js +277 -0
- package/src/core/agent-wallets.js +134 -0
- package/src/core/event-policy.js +21 -2
- package/src/core/operator-action.js +303 -0
- package/src/core/policy-decision.js +322 -0
- package/src/core/policy-packs.js +207 -0
- package/src/core/profile-fingerprint.js +27 -0
- package/src/core/profile-simulation-reasons.js +84 -0
- package/src/core/profile-templates.js +242 -0
- package/src/core/settlement-kernel.js +27 -1
- package/src/core/wallet-assignment-resolver.js +129 -0
- package/src/core/wallet-provider-bootstrap.js +365 -0
- package/src/db/store-pg.js +631 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { canonicalJsonStringify, normalizeForCanonicalJson } from "./canonical-json.js";
|
|
2
|
+
import { sha256Hex } from "./crypto.js";
|
|
3
|
+
|
|
4
|
+
const RISK_CLASS_VALUES = new Set(["read", "compute", "action", "financial"]);
|
|
5
|
+
|
|
6
|
+
function normalizeTenantId(value) {
|
|
7
|
+
if (typeof value !== "string" || value.trim() === "") throw new TypeError("tenantId is required");
|
|
8
|
+
const out = value.trim();
|
|
9
|
+
if (out.length > 200) throw new TypeError("tenantId must be <= 200 chars");
|
|
10
|
+
return out;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function normalizeOptionalRef(value, name, { max = 200 } = {}) {
|
|
14
|
+
if (value === null || value === undefined || String(value).trim() === "") return null;
|
|
15
|
+
const out = String(value).trim();
|
|
16
|
+
if (out.length > max) throw new TypeError(`${name} must be <= ${max} chars`);
|
|
17
|
+
if (!/^[A-Za-z0-9:_-]+$/.test(out)) throw new TypeError(`${name} must match ^[A-Za-z0-9:_-]+$`);
|
|
18
|
+
return out;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function normalizeOptionalRiskClass(value) {
|
|
22
|
+
if (value === null || value === undefined || String(value).trim() === "") return null;
|
|
23
|
+
const out = String(value).trim().toLowerCase();
|
|
24
|
+
if (!RISK_CLASS_VALUES.has(out)) throw new TypeError("riskClass must be read|compute|action|financial");
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function normalizeOptionalDelegationDepth(value) {
|
|
29
|
+
if (value === null || value === undefined || value === "") return null;
|
|
30
|
+
const out = Number(value);
|
|
31
|
+
if (!Number.isSafeInteger(out) || out < 0) throw new TypeError("delegationDepth must be a non-negative safe integer");
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function normalizePolicyCandidate(policy, index) {
|
|
36
|
+
if (!policy || typeof policy !== "object" || Array.isArray(policy)) throw new TypeError(`policies[${index}] must be an object`);
|
|
37
|
+
const sponsorWalletRef = normalizeOptionalRef(policy.sponsorWalletRef, `policies[${index}].sponsorWalletRef`, { max: 200 });
|
|
38
|
+
const policyRef = normalizeOptionalRef(policy.policyRef, `policies[${index}].policyRef`, { max: 200 });
|
|
39
|
+
const policyVersion = Number(policy.policyVersion);
|
|
40
|
+
if (!sponsorWalletRef || !policyRef || !Number.isSafeInteger(policyVersion) || policyVersion <= 0) {
|
|
41
|
+
throw new TypeError(`policies[${index}] requires sponsorWalletRef + policyRef + policyVersion`);
|
|
42
|
+
}
|
|
43
|
+
const sponsorRef = normalizeOptionalRef(policy.sponsorRef ?? null, `policies[${index}].sponsorRef`, { max: 200 });
|
|
44
|
+
const status = typeof policy.status === "string" && policy.status.trim() !== "" ? policy.status.trim().toLowerCase() : "active";
|
|
45
|
+
const maxDelegationDepth = normalizeOptionalDelegationDepth(policy.maxDelegationDepth ?? null);
|
|
46
|
+
return normalizeForCanonicalJson(
|
|
47
|
+
{
|
|
48
|
+
sponsorRef,
|
|
49
|
+
sponsorWalletRef,
|
|
50
|
+
policyRef,
|
|
51
|
+
policyVersion,
|
|
52
|
+
status,
|
|
53
|
+
maxDelegationDepth
|
|
54
|
+
},
|
|
55
|
+
{ path: "$" }
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function resolveDeterministicWalletAssignment({
|
|
60
|
+
tenantId,
|
|
61
|
+
profileRef = null,
|
|
62
|
+
riskClass = null,
|
|
63
|
+
delegationRef = null,
|
|
64
|
+
delegationDepth = null,
|
|
65
|
+
policies = []
|
|
66
|
+
} = {}) {
|
|
67
|
+
const normalizedTenantId = normalizeTenantId(tenantId);
|
|
68
|
+
const normalizedProfileRef = normalizeOptionalRef(profileRef, "profileRef", { max: 200 });
|
|
69
|
+
const normalizedRiskClass = normalizeOptionalRiskClass(riskClass);
|
|
70
|
+
const normalizedDelegationRef = normalizeOptionalRef(delegationRef, "delegationRef", { max: 200 });
|
|
71
|
+
const normalizedDelegationDepth = normalizeOptionalDelegationDepth(delegationDepth);
|
|
72
|
+
if (!Array.isArray(policies)) throw new TypeError("policies must be an array");
|
|
73
|
+
const normalizedPolicies = policies.map((row, index) => normalizePolicyCandidate(row, index));
|
|
74
|
+
const activePolicies = normalizedPolicies.filter((row) => row.status === "active");
|
|
75
|
+
const eligible = activePolicies.filter((row) => {
|
|
76
|
+
if (normalizedDelegationDepth === null) return true;
|
|
77
|
+
if (row.maxDelegationDepth === null || row.maxDelegationDepth === undefined) return true;
|
|
78
|
+
return normalizedDelegationDepth <= row.maxDelegationDepth;
|
|
79
|
+
});
|
|
80
|
+
if (!eligible.length) return null;
|
|
81
|
+
|
|
82
|
+
const scored = eligible.map((row) => {
|
|
83
|
+
const sponsorRank =
|
|
84
|
+
normalizedProfileRef && row.sponsorRef && normalizedProfileRef === row.sponsorRef ? 2 : row.sponsorRef === null ? 1 : 0;
|
|
85
|
+
const delegationDistance =
|
|
86
|
+
normalizedDelegationDepth !== null && row.maxDelegationDepth !== null ? row.maxDelegationDepth - normalizedDelegationDepth : 1_000_000_000;
|
|
87
|
+
const tiebreakHash = sha256Hex(
|
|
88
|
+
canonicalJsonStringify(
|
|
89
|
+
normalizeForCanonicalJson(
|
|
90
|
+
{
|
|
91
|
+
schemaVersion: "X402WalletAssignmentResolverSeed.v1",
|
|
92
|
+
tenantId: normalizedTenantId,
|
|
93
|
+
profileRef: normalizedProfileRef,
|
|
94
|
+
riskClass: normalizedRiskClass,
|
|
95
|
+
delegationRef: normalizedDelegationRef,
|
|
96
|
+
delegationDepth: normalizedDelegationDepth,
|
|
97
|
+
policy: {
|
|
98
|
+
sponsorRef: row.sponsorRef,
|
|
99
|
+
sponsorWalletRef: row.sponsorWalletRef,
|
|
100
|
+
policyRef: row.policyRef,
|
|
101
|
+
policyVersion: row.policyVersion
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
{ path: "$" }
|
|
105
|
+
)
|
|
106
|
+
)
|
|
107
|
+
);
|
|
108
|
+
return { row, sponsorRank, delegationDistance, tiebreakHash };
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
scored.sort((left, right) => {
|
|
112
|
+
if (right.sponsorRank !== left.sponsorRank) return right.sponsorRank - left.sponsorRank;
|
|
113
|
+
if (left.delegationDistance !== right.delegationDistance) return left.delegationDistance - right.delegationDistance;
|
|
114
|
+
if (left.tiebreakHash < right.tiebreakHash) return -1;
|
|
115
|
+
if (left.tiebreakHash > right.tiebreakHash) return 1;
|
|
116
|
+
return 0;
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const selected = scored[0]?.row ?? null;
|
|
120
|
+
if (!selected) return null;
|
|
121
|
+
return normalizeForCanonicalJson(
|
|
122
|
+
{
|
|
123
|
+
sponsorWalletRef: selected.sponsorWalletRef,
|
|
124
|
+
policyRef: selected.policyRef,
|
|
125
|
+
policyVersion: selected.policyVersion
|
|
126
|
+
},
|
|
127
|
+
{ path: "$" }
|
|
128
|
+
);
|
|
129
|
+
}
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
|
|
3
|
+
const PROVIDERS = new Set(["circle"]);
|
|
4
|
+
const MODES = new Set(["auto", "sandbox", "production"]);
|
|
5
|
+
|
|
6
|
+
function mustString(value, name) {
|
|
7
|
+
if (typeof value !== "string" || value.trim() === "") throw new Error(`${name} is required`);
|
|
8
|
+
return value.trim();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function normalizeHttpUrl(value) {
|
|
12
|
+
const raw = String(value ?? "").trim();
|
|
13
|
+
if (!raw) return null;
|
|
14
|
+
let parsed;
|
|
15
|
+
try {
|
|
16
|
+
parsed = new URL(raw);
|
|
17
|
+
} catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
|
|
21
|
+
return parsed.toString().replace(/\/+$/, "");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function normalizeHex64(value) {
|
|
25
|
+
const raw = String(value ?? "").trim().toLowerCase();
|
|
26
|
+
if (!raw) return null;
|
|
27
|
+
if (!/^[0-9a-f]{64}$/.test(raw)) throw new Error("entity secret must be a 64-char hex string");
|
|
28
|
+
return raw;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function callCircle({ baseUrl, apiKey, method, endpoint, body = null, fetchImpl = fetch }) {
|
|
32
|
+
const response = await fetchImpl(`${baseUrl}${endpoint}`, {
|
|
33
|
+
method,
|
|
34
|
+
headers: {
|
|
35
|
+
authorization: `Bearer ${apiKey}`,
|
|
36
|
+
accept: "application/json",
|
|
37
|
+
...(body ? { "content-type": "application/json; charset=utf-8" } : {})
|
|
38
|
+
},
|
|
39
|
+
body: body ? JSON.stringify(body) : undefined
|
|
40
|
+
});
|
|
41
|
+
const text = await response.text();
|
|
42
|
+
let json = null;
|
|
43
|
+
try {
|
|
44
|
+
json = text ? JSON.parse(text) : null;
|
|
45
|
+
} catch {
|
|
46
|
+
json = null;
|
|
47
|
+
}
|
|
48
|
+
return { status: response.status, text, json };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function pickWalletRows(payload) {
|
|
52
|
+
const root = payload && typeof payload === "object" ? payload : {};
|
|
53
|
+
const wallets =
|
|
54
|
+
(Array.isArray(root?.data?.wallets) && root.data.wallets) ||
|
|
55
|
+
(Array.isArray(root?.wallets) && root.wallets) ||
|
|
56
|
+
[];
|
|
57
|
+
return wallets.filter((row) => row && typeof row === "object" && !Array.isArray(row));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function pickWalletAddress(payload) {
|
|
61
|
+
const root = payload && typeof payload === "object" ? payload : {};
|
|
62
|
+
const candidates = [root, root.wallet, root.data, root.data?.wallet];
|
|
63
|
+
if (Array.isArray(root?.data?.wallets)) candidates.push(...root.data.wallets);
|
|
64
|
+
for (const row of candidates) {
|
|
65
|
+
if (!row || typeof row !== "object" || Array.isArray(row)) continue;
|
|
66
|
+
if (typeof row.address === "string" && row.address.trim()) return row.address.trim();
|
|
67
|
+
if (typeof row.blockchainAddress === "string" && row.blockchainAddress.trim()) return row.blockchainAddress.trim();
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function pickUsdcTokenId(payload) {
|
|
73
|
+
const root = payload && typeof payload === "object" ? payload : {};
|
|
74
|
+
const balances =
|
|
75
|
+
(Array.isArray(root?.data?.tokenBalances) && root.data.tokenBalances) ||
|
|
76
|
+
(Array.isArray(root?.tokenBalances) && root.tokenBalances) ||
|
|
77
|
+
[];
|
|
78
|
+
for (const row of balances) {
|
|
79
|
+
if (!row || typeof row !== "object") continue;
|
|
80
|
+
const token = row.token && typeof row.token === "object" ? row.token : null;
|
|
81
|
+
const symbol = String(token?.symbol ?? row.symbol ?? "").trim().toUpperCase();
|
|
82
|
+
if (symbol !== "USDC") continue;
|
|
83
|
+
const id = String(token?.id ?? row.tokenId ?? row.id ?? "").trim();
|
|
84
|
+
if (id) return id;
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function inferModeFromBaseUrl(baseUrl) {
|
|
90
|
+
const u = normalizeHttpUrl(baseUrl);
|
|
91
|
+
if (!u) return null;
|
|
92
|
+
if (u.includes("api-sandbox.circle.com")) return "sandbox";
|
|
93
|
+
return "production";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function detectCircleBaseUrl({ apiKey, preferredMode = "auto", explicitBaseUrl = null, fetchImpl = fetch }) {
|
|
97
|
+
const explicit = normalizeHttpUrl(explicitBaseUrl);
|
|
98
|
+
if (explicit) {
|
|
99
|
+
const check = await callCircle({
|
|
100
|
+
baseUrl: explicit,
|
|
101
|
+
apiKey,
|
|
102
|
+
method: "GET",
|
|
103
|
+
endpoint: "/v1/w3s/wallets",
|
|
104
|
+
fetchImpl
|
|
105
|
+
});
|
|
106
|
+
if (check.status >= 200 && check.status < 300) {
|
|
107
|
+
const explicitMode = preferredMode === "auto" ? inferModeFromBaseUrl(explicit) ?? "production" : preferredMode;
|
|
108
|
+
return {
|
|
109
|
+
baseUrl: explicit,
|
|
110
|
+
mode: explicitMode,
|
|
111
|
+
probeStatus: check.status
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
throw new Error(`Circle API auth failed at ${explicit} (HTTP ${check.status})`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const targets =
|
|
118
|
+
preferredMode === "sandbox"
|
|
119
|
+
? [{ mode: "sandbox", baseUrl: "https://api-sandbox.circle.com" }]
|
|
120
|
+
: preferredMode === "production"
|
|
121
|
+
? [{ mode: "production", baseUrl: "https://api.circle.com" }]
|
|
122
|
+
: [
|
|
123
|
+
{ mode: "sandbox", baseUrl: "https://api-sandbox.circle.com" },
|
|
124
|
+
{ mode: "production", baseUrl: "https://api.circle.com" }
|
|
125
|
+
];
|
|
126
|
+
|
|
127
|
+
const failures = [];
|
|
128
|
+
for (const target of targets) {
|
|
129
|
+
const probe = await callCircle({
|
|
130
|
+
baseUrl: target.baseUrl,
|
|
131
|
+
apiKey,
|
|
132
|
+
method: "GET",
|
|
133
|
+
endpoint: "/v1/w3s/wallets",
|
|
134
|
+
fetchImpl
|
|
135
|
+
});
|
|
136
|
+
if (probe.status >= 200 && probe.status < 300) {
|
|
137
|
+
return { ...target, probeStatus: probe.status };
|
|
138
|
+
}
|
|
139
|
+
failures.push(`${target.baseUrl}:HTTP${probe.status}`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
throw new Error(`Circle API auth failed for all endpoints (${failures.join(", ")})`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function chooseWalletIds({ wallets, spendWalletId = null, escrowWalletId = null, blockchain = null }) {
|
|
146
|
+
const normalizedSpend = String(spendWalletId ?? "").trim() || null;
|
|
147
|
+
const normalizedEscrow = String(escrowWalletId ?? "").trim() || null;
|
|
148
|
+
if (normalizedSpend && normalizedEscrow) return { spendWalletId: normalizedSpend, escrowWalletId: normalizedEscrow };
|
|
149
|
+
|
|
150
|
+
const chain = String(blockchain ?? "").trim().toUpperCase();
|
|
151
|
+
const candidates = wallets.filter((row) => {
|
|
152
|
+
if (!row || typeof row !== "object") return false;
|
|
153
|
+
const id = typeof row.id === "string" ? row.id.trim() : "";
|
|
154
|
+
if (!id) return false;
|
|
155
|
+
if (!chain) return true;
|
|
156
|
+
const rowChain = String(row.blockchain ?? "").trim().toUpperCase();
|
|
157
|
+
return rowChain === chain;
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
if (candidates.length === 0) {
|
|
161
|
+
throw new Error(`no wallets found for blockchain=${chain || "(any)"}; create wallets in Circle Console first`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const spend = normalizedSpend || String(candidates[0].id).trim();
|
|
165
|
+
const escrow = normalizedEscrow || String((candidates[1] ?? candidates[0]).id).trim();
|
|
166
|
+
return { spendWalletId: spend, escrowWalletId: escrow };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function resolveWalletMeta({ baseUrl, apiKey, walletId, fetchImpl = fetch }) {
|
|
170
|
+
const out = await callCircle({
|
|
171
|
+
baseUrl,
|
|
172
|
+
apiKey,
|
|
173
|
+
method: "GET",
|
|
174
|
+
endpoint: `/v1/w3s/wallets/${encodeURIComponent(walletId)}`,
|
|
175
|
+
fetchImpl
|
|
176
|
+
});
|
|
177
|
+
if (out.status < 200 || out.status >= 300) {
|
|
178
|
+
throw new Error(`wallet lookup failed for ${walletId} (HTTP ${out.status})`);
|
|
179
|
+
}
|
|
180
|
+
const address = pickWalletAddress(out.json);
|
|
181
|
+
if (!address) throw new Error(`wallet lookup for ${walletId} returned no address`);
|
|
182
|
+
return { walletId, address };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function resolveUsdcTokenId({ baseUrl, apiKey, walletIds, fetchImpl = fetch }) {
|
|
186
|
+
for (const walletId of walletIds) {
|
|
187
|
+
const out = await callCircle({
|
|
188
|
+
baseUrl,
|
|
189
|
+
apiKey,
|
|
190
|
+
method: "GET",
|
|
191
|
+
endpoint: `/v1/w3s/wallets/${encodeURIComponent(walletId)}/balances`,
|
|
192
|
+
fetchImpl
|
|
193
|
+
});
|
|
194
|
+
if (out.status < 200 || out.status >= 300) continue;
|
|
195
|
+
const tokenId = pickUsdcTokenId(out.json);
|
|
196
|
+
if (tokenId) return tokenId;
|
|
197
|
+
}
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function requestFaucet({ baseUrl, apiKey, address, blockchain, native, usdc, fetchImpl = fetch }) {
|
|
202
|
+
const out = await callCircle({
|
|
203
|
+
baseUrl,
|
|
204
|
+
apiKey,
|
|
205
|
+
method: "POST",
|
|
206
|
+
endpoint: "/v1/faucet/drips",
|
|
207
|
+
body: {
|
|
208
|
+
address,
|
|
209
|
+
blockchain,
|
|
210
|
+
native: Boolean(native),
|
|
211
|
+
usdc: Boolean(usdc),
|
|
212
|
+
eurc: false
|
|
213
|
+
},
|
|
214
|
+
fetchImpl
|
|
215
|
+
});
|
|
216
|
+
return {
|
|
217
|
+
ok: out.status === 204 || out.status === 409 || out.status === 429 || out.status === 400,
|
|
218
|
+
status: out.status,
|
|
219
|
+
body: out.json ?? out.text ?? null
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export async function bootstrapCircleProvider({
|
|
224
|
+
apiKey,
|
|
225
|
+
mode = "auto",
|
|
226
|
+
baseUrl = null,
|
|
227
|
+
blockchain = null,
|
|
228
|
+
spendWalletId = null,
|
|
229
|
+
escrowWalletId = null,
|
|
230
|
+
tokenIdUsdc = null,
|
|
231
|
+
faucet = null,
|
|
232
|
+
includeApiKey = false,
|
|
233
|
+
entitySecretHex = null,
|
|
234
|
+
fetchImpl = fetch
|
|
235
|
+
} = {}) {
|
|
236
|
+
const normalizedMode = String(mode ?? "auto").trim().toLowerCase();
|
|
237
|
+
if (!MODES.has(normalizedMode)) throw new Error("mode must be auto|sandbox|production");
|
|
238
|
+
|
|
239
|
+
const circleApiKey = mustString(apiKey, "apiKey");
|
|
240
|
+
const detected = await detectCircleBaseUrl({
|
|
241
|
+
apiKey: circleApiKey,
|
|
242
|
+
preferredMode: normalizedMode,
|
|
243
|
+
explicitBaseUrl: baseUrl,
|
|
244
|
+
fetchImpl
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
const resolvedBlockchain =
|
|
248
|
+
String(blockchain ?? "").trim() ||
|
|
249
|
+
(detected.mode === "production" ? "BASE" : "BASE-SEPOLIA");
|
|
250
|
+
|
|
251
|
+
const walletsRes = await callCircle({
|
|
252
|
+
baseUrl: detected.baseUrl,
|
|
253
|
+
apiKey: circleApiKey,
|
|
254
|
+
method: "GET",
|
|
255
|
+
endpoint: "/v1/w3s/wallets",
|
|
256
|
+
fetchImpl
|
|
257
|
+
});
|
|
258
|
+
if (walletsRes.status < 200 || walletsRes.status >= 300) {
|
|
259
|
+
throw new Error(`wallet list failed (HTTP ${walletsRes.status})`);
|
|
260
|
+
}
|
|
261
|
+
const wallets = pickWalletRows(walletsRes.json);
|
|
262
|
+
const chosen = chooseWalletIds({
|
|
263
|
+
wallets,
|
|
264
|
+
spendWalletId,
|
|
265
|
+
escrowWalletId,
|
|
266
|
+
blockchain: resolvedBlockchain
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
const [spendMeta, escrowMeta] = await Promise.all([
|
|
270
|
+
resolveWalletMeta({ baseUrl: detected.baseUrl, apiKey: circleApiKey, walletId: chosen.spendWalletId, fetchImpl }),
|
|
271
|
+
resolveWalletMeta({ baseUrl: detected.baseUrl, apiKey: circleApiKey, walletId: chosen.escrowWalletId, fetchImpl })
|
|
272
|
+
]);
|
|
273
|
+
|
|
274
|
+
const resolvedTokenIdUsdc =
|
|
275
|
+
String(tokenIdUsdc ?? "").trim() ||
|
|
276
|
+
(await resolveUsdcTokenId({
|
|
277
|
+
baseUrl: detected.baseUrl,
|
|
278
|
+
apiKey: circleApiKey,
|
|
279
|
+
walletIds: [chosen.spendWalletId, chosen.escrowWalletId],
|
|
280
|
+
fetchImpl
|
|
281
|
+
}));
|
|
282
|
+
|
|
283
|
+
if (!resolvedTokenIdUsdc) {
|
|
284
|
+
throw new Error("could not discover USDC token id; pass tokenIdUsdc explicitly");
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const faucetEnabled =
|
|
288
|
+
typeof faucet === "boolean"
|
|
289
|
+
? faucet
|
|
290
|
+
: detected.mode === "sandbox";
|
|
291
|
+
const faucetResults = [];
|
|
292
|
+
if (faucetEnabled) {
|
|
293
|
+
faucetResults.push(
|
|
294
|
+
{
|
|
295
|
+
wallet: "spend",
|
|
296
|
+
...(await requestFaucet({
|
|
297
|
+
baseUrl: detected.baseUrl,
|
|
298
|
+
apiKey: circleApiKey,
|
|
299
|
+
address: spendMeta.address,
|
|
300
|
+
blockchain: resolvedBlockchain,
|
|
301
|
+
native: true,
|
|
302
|
+
usdc: true,
|
|
303
|
+
fetchImpl
|
|
304
|
+
}))
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
wallet: "escrow",
|
|
308
|
+
...(await requestFaucet({
|
|
309
|
+
baseUrl: detected.baseUrl,
|
|
310
|
+
apiKey: circleApiKey,
|
|
311
|
+
address: escrowMeta.address,
|
|
312
|
+
blockchain: resolvedBlockchain,
|
|
313
|
+
native: true,
|
|
314
|
+
usdc: true,
|
|
315
|
+
fetchImpl
|
|
316
|
+
}))
|
|
317
|
+
}
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const resolvedEntitySecretHex = normalizeHex64(entitySecretHex) || crypto.randomBytes(32).toString("hex");
|
|
322
|
+
|
|
323
|
+
const env = {
|
|
324
|
+
CIRCLE_BASE_URL: detected.baseUrl,
|
|
325
|
+
CIRCLE_BLOCKCHAIN: resolvedBlockchain,
|
|
326
|
+
CIRCLE_WALLET_ID_SPEND: chosen.spendWalletId,
|
|
327
|
+
CIRCLE_WALLET_ID_ESCROW: chosen.escrowWalletId,
|
|
328
|
+
CIRCLE_TOKEN_ID_USDC: resolvedTokenIdUsdc,
|
|
329
|
+
CIRCLE_ENTITY_SECRET_HEX: resolvedEntitySecretHex,
|
|
330
|
+
X402_CIRCLE_RESERVE_MODE: detected.mode,
|
|
331
|
+
X402_REQUIRE_EXTERNAL_RESERVE: "1"
|
|
332
|
+
};
|
|
333
|
+
if (includeApiKey) env.CIRCLE_API_KEY = circleApiKey;
|
|
334
|
+
|
|
335
|
+
return {
|
|
336
|
+
provider: "circle",
|
|
337
|
+
mode: detected.mode,
|
|
338
|
+
baseUrl: detected.baseUrl,
|
|
339
|
+
blockchain: resolvedBlockchain,
|
|
340
|
+
wallets: {
|
|
341
|
+
spend: spendMeta,
|
|
342
|
+
escrow: escrowMeta
|
|
343
|
+
},
|
|
344
|
+
tokenIdUsdc: resolvedTokenIdUsdc,
|
|
345
|
+
entitySecretHex: resolvedEntitySecretHex,
|
|
346
|
+
faucetEnabled,
|
|
347
|
+
faucetResults,
|
|
348
|
+
env
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export async function bootstrapWalletProvider({ provider = "circle", ...rest } = {}) {
|
|
353
|
+
const normalized = String(provider ?? "").trim().toLowerCase();
|
|
354
|
+
if (!PROVIDERS.has(normalized)) {
|
|
355
|
+
throw new Error(`unsupported provider: ${normalized || "(empty)"}`);
|
|
356
|
+
}
|
|
357
|
+
if (normalized === "circle") {
|
|
358
|
+
return await bootstrapCircleProvider(rest);
|
|
359
|
+
}
|
|
360
|
+
throw new Error(`unsupported provider: ${normalized}`);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
export function supportedWalletBootstrapProviders() {
|
|
364
|
+
return [...PROVIDERS];
|
|
365
|
+
}
|