settld 0.2.0 → 0.2.2
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 +21 -0
- package/SETTLD_VERSION +1 -1
- package/bin/settld.js +18 -0
- package/docs/QUICKSTART_MCP_HOSTS.md +78 -4
- package/docs/gitbook/quickstart.md +47 -4
- package/docs/integrations/openclaw/PUBLIC_QUICKSTART.md +13 -0
- package/docs/ops/VERCEL_MONOREPO_DEPLOY.md +42 -0
- package/package.json +2 -1
- package/scripts/ci/run-mcp-host-smoke.mjs +6 -0
- package/scripts/ci/run-production-cutover-gate.mjs +6 -0
- package/scripts/demo/mcp-paid-exa.mjs +18 -1
- package/scripts/setup/login.mjs +299 -0
- package/scripts/setup/onboard.mjs +281 -26
- package/scripts/setup/session-store.mjs +65 -0
- package/scripts/vercel/build-mkdocs.sh +3 -3
- package/scripts/vercel/ignore-dashboard.sh +26 -0
- package/scripts/vercel/ignore-mkdocs.sh +2 -0
- package/scripts/vercel/install-mkdocs.sh +2 -3
- package/scripts/wallet/cli.mjs +871 -0
- package/src/core/wallet-funding-coinbase.js +197 -0
- package/src/core/wallet-funding-hosted.js +155 -0
- package/src/core/wallet-provider-bootstrap.js +95 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { generateJwt } from "@coinbase/cdp-sdk/auth";
|
|
2
|
+
|
|
3
|
+
function normalizeHttpUrl(value) {
|
|
4
|
+
const raw = String(value ?? "").trim();
|
|
5
|
+
if (!raw) return null;
|
|
6
|
+
try {
|
|
7
|
+
const u = new URL(raw);
|
|
8
|
+
if (u.protocol !== "http:" && u.protocol !== "https:") return null;
|
|
9
|
+
return u.toString();
|
|
10
|
+
} catch {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function safeTrim(value) {
|
|
16
|
+
return typeof value === "string" ? value.trim() : "";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function normalizeClientIp(value) {
|
|
20
|
+
const raw = safeTrim(value);
|
|
21
|
+
if (!raw) return null;
|
|
22
|
+
if (raw.startsWith("::ffff:")) return raw.slice(7);
|
|
23
|
+
return raw;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function mapBlockchainToCoinbaseNetwork(raw) {
|
|
27
|
+
const value = safeTrim(raw).toLowerCase();
|
|
28
|
+
if (!value) return null;
|
|
29
|
+
if (value === "base" || value === "base-mainnet" || value === "base_mainnet") return "base";
|
|
30
|
+
if (value === "ethereum" || value === "eth" || value === "ethereum-mainnet" || value === "ethereum_mainnet") return "ethereum";
|
|
31
|
+
if (value === "polygon" || value === "polygon-mainnet" || value === "polygon_mainnet") return "polygon";
|
|
32
|
+
if (value === "solana" || value === "sol") return "solana";
|
|
33
|
+
if (value === "arbitrum" || value === "arbitrum-mainnet" || value === "arbitrum_mainnet") return "arbitrum";
|
|
34
|
+
if (value === "optimism" || value === "optimism-mainnet" || value === "optimism_mainnet") return "optimism";
|
|
35
|
+
if (value === "bitcoin" || value === "btc") return "bitcoin";
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function buildHostedUrl({ payBaseUrl, sessionToken, defaultNetwork, defaultAsset, redirectUrl, fiatCurrency, paymentMethod }) {
|
|
40
|
+
const u = new URL(payBaseUrl);
|
|
41
|
+
u.searchParams.set("sessionToken", sessionToken);
|
|
42
|
+
if (defaultNetwork) u.searchParams.set("defaultNetwork", defaultNetwork);
|
|
43
|
+
if (defaultAsset) u.searchParams.set("defaultAsset", defaultAsset);
|
|
44
|
+
if (redirectUrl) u.searchParams.set("redirectUrl", redirectUrl);
|
|
45
|
+
if (fiatCurrency) u.searchParams.set("fiatCurrency", fiatCurrency);
|
|
46
|
+
if (paymentMethod) u.searchParams.set("defaultPaymentMethod", paymentMethod);
|
|
47
|
+
return u.toString();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function hostedMethodUrls({
|
|
51
|
+
requestedMethod,
|
|
52
|
+
payBaseUrl,
|
|
53
|
+
sessionToken,
|
|
54
|
+
defaultNetwork,
|
|
55
|
+
defaultAsset,
|
|
56
|
+
redirectUrl,
|
|
57
|
+
fiatCurrency,
|
|
58
|
+
cardPaymentMethod,
|
|
59
|
+
bankPaymentMethod
|
|
60
|
+
}) {
|
|
61
|
+
const cardUrl = buildHostedUrl({
|
|
62
|
+
payBaseUrl,
|
|
63
|
+
sessionToken,
|
|
64
|
+
defaultNetwork,
|
|
65
|
+
defaultAsset,
|
|
66
|
+
redirectUrl,
|
|
67
|
+
fiatCurrency,
|
|
68
|
+
paymentMethod: safeTrim(cardPaymentMethod) || null
|
|
69
|
+
});
|
|
70
|
+
const bankUrl = buildHostedUrl({
|
|
71
|
+
payBaseUrl,
|
|
72
|
+
sessionToken,
|
|
73
|
+
defaultNetwork,
|
|
74
|
+
defaultAsset,
|
|
75
|
+
redirectUrl,
|
|
76
|
+
fiatCurrency,
|
|
77
|
+
paymentMethod: safeTrim(bankPaymentMethod) || null
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
if (requestedMethod === "card") return { card: cardUrl, bank: null, preferredMethod: "card" };
|
|
81
|
+
if (requestedMethod === "bank") return { card: null, bank: bankUrl, preferredMethod: "bank" };
|
|
82
|
+
return { card: cardUrl, bank: bankUrl, preferredMethod: "card" };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function readJsonSafe(raw) {
|
|
86
|
+
try {
|
|
87
|
+
return JSON.parse(String(raw ?? ""));
|
|
88
|
+
} catch {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export async function buildCoinbaseHostedUrls({
|
|
94
|
+
requestedMethod = null,
|
|
95
|
+
walletAddress = null,
|
|
96
|
+
blockchain = null,
|
|
97
|
+
clientIp = null,
|
|
98
|
+
config = {},
|
|
99
|
+
fetchImpl = fetch,
|
|
100
|
+
generateJwtImpl = generateJwt
|
|
101
|
+
} = {}) {
|
|
102
|
+
const apiKeyId = safeTrim(config.apiKeyId);
|
|
103
|
+
const apiKeySecret = safeTrim(config.apiKeySecret);
|
|
104
|
+
if (!apiKeyId || !apiKeySecret) {
|
|
105
|
+
return { card: null, bank: null, preferredMethod: null, provider: "coinbase", unavailableReason: "MISSING_API_KEYS" };
|
|
106
|
+
}
|
|
107
|
+
const normalizedApiKeySecret = apiKeySecret.includes("\\n") ? apiKeySecret.replace(/\\n/g, "\n") : apiKeySecret;
|
|
108
|
+
|
|
109
|
+
const tokenUrl = normalizeHttpUrl(config.tokenUrl ?? "https://api.developer.coinbase.com/onramp/v1/token");
|
|
110
|
+
const payBaseUrl = normalizeHttpUrl(config.payBaseUrl ?? "https://pay.coinbase.com/buy/select-asset");
|
|
111
|
+
if (!tokenUrl || !payBaseUrl) {
|
|
112
|
+
return { card: null, bank: null, preferredMethod: null, provider: "coinbase", unavailableReason: "INVALID_URLS" };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const parsedTokenUrl = new URL(tokenUrl);
|
|
116
|
+
const networkOverride = safeTrim(config.destinationNetwork);
|
|
117
|
+
const destinationNetwork = networkOverride || mapBlockchainToCoinbaseNetwork(blockchain);
|
|
118
|
+
if (!destinationNetwork) {
|
|
119
|
+
return { card: null, bank: null, preferredMethod: null, provider: "coinbase", unavailableReason: "UNSUPPORTED_NETWORK" };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const destinationAddress = safeTrim(walletAddress);
|
|
123
|
+
if (!destinationAddress) {
|
|
124
|
+
return { card: null, bank: null, preferredMethod: null, provider: "coinbase", unavailableReason: "MISSING_WALLET_ADDRESS" };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const purchaseAsset = safeTrim(config.purchaseAsset).toUpperCase() || "USDC";
|
|
128
|
+
const fiatCurrency = safeTrim(config.fiatCurrency).toUpperCase() || "USD";
|
|
129
|
+
const redirectUrl = normalizeHttpUrl(config.redirectUrl);
|
|
130
|
+
const partnerUserRef = safeTrim(config.partnerUserRef) || null;
|
|
131
|
+
const resolvedClientIp = normalizeClientIp(config.clientIp) || normalizeClientIp(clientIp) || null;
|
|
132
|
+
|
|
133
|
+
const jwt = await generateJwtImpl({
|
|
134
|
+
apiKeyId,
|
|
135
|
+
apiKeySecret: normalizedApiKeySecret,
|
|
136
|
+
requestMethod: "POST",
|
|
137
|
+
requestHost: parsedTokenUrl.host,
|
|
138
|
+
requestPath: parsedTokenUrl.pathname,
|
|
139
|
+
expiresIn: 120
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const requestBody = {
|
|
143
|
+
addresses: [{
|
|
144
|
+
address: destinationAddress,
|
|
145
|
+
blockchains: [destinationNetwork]
|
|
146
|
+
}],
|
|
147
|
+
assets: [purchaseAsset]
|
|
148
|
+
};
|
|
149
|
+
if (resolvedClientIp) requestBody.clientIp = resolvedClientIp;
|
|
150
|
+
if (partnerUserRef) requestBody.partnerUserRef = partnerUserRef;
|
|
151
|
+
|
|
152
|
+
const res = await fetchImpl(tokenUrl, {
|
|
153
|
+
method: "POST",
|
|
154
|
+
headers: {
|
|
155
|
+
authorization: `Bearer ${jwt}`,
|
|
156
|
+
"content-type": "application/json",
|
|
157
|
+
accept: "application/json"
|
|
158
|
+
},
|
|
159
|
+
body: JSON.stringify(requestBody)
|
|
160
|
+
});
|
|
161
|
+
const raw = await res.text();
|
|
162
|
+
const json = readJsonSafe(raw);
|
|
163
|
+
if (!res.ok) {
|
|
164
|
+
const detail = json && typeof json === "object" ? json : { message: raw || `HTTP ${res.status}` };
|
|
165
|
+
const err = new Error(`coinbase session token request failed (${res.status})`);
|
|
166
|
+
err.detail = detail;
|
|
167
|
+
throw err;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const sessionToken = safeTrim(json?.token || json?.sessionToken || json?.session?.token);
|
|
171
|
+
if (!sessionToken) {
|
|
172
|
+
const err = new Error("coinbase session token response missing token");
|
|
173
|
+
err.detail = json;
|
|
174
|
+
throw err;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const urls = hostedMethodUrls({
|
|
178
|
+
requestedMethod,
|
|
179
|
+
payBaseUrl,
|
|
180
|
+
sessionToken,
|
|
181
|
+
defaultNetwork: destinationNetwork,
|
|
182
|
+
defaultAsset: purchaseAsset,
|
|
183
|
+
redirectUrl,
|
|
184
|
+
fiatCurrency,
|
|
185
|
+
cardPaymentMethod: config.cardPaymentMethod,
|
|
186
|
+
bankPaymentMethod: config.bankPaymentMethod
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
...urls,
|
|
191
|
+
provider: "coinbase",
|
|
192
|
+
sessionToken,
|
|
193
|
+
destinationNetwork,
|
|
194
|
+
purchaseAsset,
|
|
195
|
+
fiatCurrency
|
|
196
|
+
};
|
|
197
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
|
|
3
|
+
function normalizeHttpUrl(value) {
|
|
4
|
+
const raw = String(value ?? "").trim();
|
|
5
|
+
if (!raw) return null;
|
|
6
|
+
try {
|
|
7
|
+
const u = new URL(raw);
|
|
8
|
+
if (u.protocol !== "http:" && u.protocol !== "https:") return null;
|
|
9
|
+
return u.toString();
|
|
10
|
+
} catch {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function parseCsvList(raw) {
|
|
16
|
+
const input = String(raw ?? "").trim();
|
|
17
|
+
if (!input) return [];
|
|
18
|
+
return [...new Set(input.split(",").map((x) => String(x ?? "").trim()).filter(Boolean))];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function normalizeOnramperNetworkId(raw) {
|
|
22
|
+
const value = String(raw ?? "").trim();
|
|
23
|
+
if (!value) return null;
|
|
24
|
+
const key = value.toLowerCase();
|
|
25
|
+
if (key === "base-sepolia") return "base_sepolia";
|
|
26
|
+
if (key === "base") return "base";
|
|
27
|
+
if (key === "ethereum" || key === "eth") return "ethereum";
|
|
28
|
+
if (key === "ethereum-sepolia" || key === "sepolia") return "ethereum_sepolia";
|
|
29
|
+
if (key === "polygon" || key === "matic") return "polygon";
|
|
30
|
+
if (key === "solana" || key === "sol") return "solana";
|
|
31
|
+
return key.replace(/[^a-z0-9_-]+/g, "_");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function normalizedList(raw) {
|
|
35
|
+
return parseCsvList(raw)
|
|
36
|
+
.map((x) => String(x).trim().toLowerCase())
|
|
37
|
+
.filter(Boolean);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function addIfPresent(searchParams, key, value) {
|
|
41
|
+
const raw = String(value ?? "").trim();
|
|
42
|
+
if (!raw) return;
|
|
43
|
+
searchParams.set(key, raw);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function buildSensitiveSignature({ signingSecret, sensitivePairs }) {
|
|
47
|
+
const secret = String(signingSecret ?? "").trim();
|
|
48
|
+
if (!secret || !Array.isArray(sensitivePairs) || !sensitivePairs.length) return null;
|
|
49
|
+
const rows = sensitivePairs
|
|
50
|
+
.map((entry) => {
|
|
51
|
+
const k = String(entry?.key ?? "").trim();
|
|
52
|
+
const v = String(entry?.value ?? "").trim();
|
|
53
|
+
if (!k || !v) return null;
|
|
54
|
+
return { key: k, value: v };
|
|
55
|
+
})
|
|
56
|
+
.filter(Boolean)
|
|
57
|
+
.sort((a, b) => a.key.localeCompare(b.key));
|
|
58
|
+
if (!rows.length) return null;
|
|
59
|
+
const signContent = rows.map(({ key, value }) => `${key}=${value}`).join("&");
|
|
60
|
+
return crypto.createHmac("sha256", secret).update(signContent, "utf8").digest("hex");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function buildMethodUrl({
|
|
64
|
+
baseUrl,
|
|
65
|
+
apiKey,
|
|
66
|
+
method,
|
|
67
|
+
defaultFiat,
|
|
68
|
+
defaultCrypto,
|
|
69
|
+
onlyCryptos,
|
|
70
|
+
onlyCryptoNetworks,
|
|
71
|
+
walletAddress,
|
|
72
|
+
networkId,
|
|
73
|
+
signingSecret,
|
|
74
|
+
successRedirectUrl,
|
|
75
|
+
failureRedirectUrl
|
|
76
|
+
}) {
|
|
77
|
+
const u = new URL(baseUrl);
|
|
78
|
+
const sp = u.searchParams;
|
|
79
|
+
sp.set("apiKey", apiKey);
|
|
80
|
+
sp.set("mode", "buy");
|
|
81
|
+
|
|
82
|
+
addIfPresent(sp, "defaultFiat", defaultFiat);
|
|
83
|
+
addIfPresent(sp, "defaultCrypto", defaultCrypto);
|
|
84
|
+
|
|
85
|
+
if (onlyCryptos.length) sp.set("onlyCryptos", onlyCryptos.join(","));
|
|
86
|
+
if (onlyCryptoNetworks.length) sp.set("onlyCryptoNetworks", onlyCryptoNetworks.join(","));
|
|
87
|
+
|
|
88
|
+
if (method === "card") sp.set("defaultPaymentMethod", "creditcard");
|
|
89
|
+
if (method === "bank") sp.set("defaultPaymentMethod", "banktransfer");
|
|
90
|
+
|
|
91
|
+
addIfPresent(sp, "successRedirectUrl", successRedirectUrl);
|
|
92
|
+
addIfPresent(sp, "failureRedirectUrl", failureRedirectUrl);
|
|
93
|
+
|
|
94
|
+
const sensitivePairs = [];
|
|
95
|
+
if (walletAddress && networkId && signingSecret) {
|
|
96
|
+
const networkWallets = `${networkId}:${walletAddress}`;
|
|
97
|
+
sp.set("networkWallets", networkWallets);
|
|
98
|
+
sensitivePairs.push({ key: "networkWallets", value: networkWallets });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const signature = buildSensitiveSignature({ signingSecret, sensitivePairs });
|
|
102
|
+
if (signature) sp.set("signature", signature);
|
|
103
|
+
|
|
104
|
+
return u.toString();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function buildOnramperHostedUrls({
|
|
108
|
+
requestedMethod = null,
|
|
109
|
+
walletAddress = null,
|
|
110
|
+
blockchain = null,
|
|
111
|
+
config = {}
|
|
112
|
+
} = {}) {
|
|
113
|
+
const apiKey = String(config.apiKey ?? "").trim();
|
|
114
|
+
if (!apiKey) return { card: null, bank: null, preferredMethod: null, provider: "onramper" };
|
|
115
|
+
|
|
116
|
+
const baseUrl = normalizeHttpUrl(config.baseUrl ?? "https://buy.onramper.com");
|
|
117
|
+
if (!baseUrl) return { card: null, bank: null, preferredMethod: null, provider: "onramper" };
|
|
118
|
+
|
|
119
|
+
const onlyCryptos = normalizedList(config.onlyCryptos);
|
|
120
|
+
const onlyCryptoNetworks = normalizedList(config.onlyCryptoNetworks);
|
|
121
|
+
const defaultCrypto = String(config.defaultCrypto ?? "usdc").trim().toLowerCase();
|
|
122
|
+
const defaultFiat = String(config.defaultFiat ?? "usd").trim().toLowerCase();
|
|
123
|
+
|
|
124
|
+
const configuredNetworkId = normalizeOnramperNetworkId(config.networkId);
|
|
125
|
+
const inferredNetworkId = normalizeOnramperNetworkId(blockchain);
|
|
126
|
+
const networkId = configuredNetworkId || inferredNetworkId;
|
|
127
|
+
const signingSecret = String(config.signingSecret ?? "").trim();
|
|
128
|
+
const successRedirectUrl = normalizeHttpUrl(config.successRedirectUrl);
|
|
129
|
+
const failureRedirectUrl = normalizeHttpUrl(config.failureRedirectUrl);
|
|
130
|
+
|
|
131
|
+
const make = (method) =>
|
|
132
|
+
buildMethodUrl({
|
|
133
|
+
baseUrl,
|
|
134
|
+
apiKey,
|
|
135
|
+
method,
|
|
136
|
+
defaultFiat,
|
|
137
|
+
defaultCrypto,
|
|
138
|
+
onlyCryptos,
|
|
139
|
+
onlyCryptoNetworks,
|
|
140
|
+
walletAddress: String(walletAddress ?? "").trim() || null,
|
|
141
|
+
networkId,
|
|
142
|
+
signingSecret,
|
|
143
|
+
successRedirectUrl,
|
|
144
|
+
failureRedirectUrl
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
let card = make("card");
|
|
148
|
+
let bank = make("bank");
|
|
149
|
+
|
|
150
|
+
if (requestedMethod === "card") bank = null;
|
|
151
|
+
if (requestedMethod === "bank") card = null;
|
|
152
|
+
|
|
153
|
+
const preferredMethod = card ? "card" : bank ? "bank" : null;
|
|
154
|
+
return { card, bank, preferredMethod, provider: "onramper" };
|
|
155
|
+
}
|
|
@@ -86,6 +86,44 @@ function pickUsdcTokenId(payload) {
|
|
|
86
86
|
return null;
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
function pickUsdcBalance(payload, { tokenIdHint = null } = {}) {
|
|
90
|
+
const root = payload && typeof payload === "object" ? payload : {};
|
|
91
|
+
const balances =
|
|
92
|
+
(Array.isArray(root?.data?.tokenBalances) && root.data.tokenBalances) ||
|
|
93
|
+
(Array.isArray(root?.tokenBalances) && root.tokenBalances) ||
|
|
94
|
+
[];
|
|
95
|
+
const normalizedHint = String(tokenIdHint ?? "").trim();
|
|
96
|
+
const ranked = [];
|
|
97
|
+
for (const row of balances) {
|
|
98
|
+
if (!row || typeof row !== "object") continue;
|
|
99
|
+
const token = row.token && typeof row.token === "object" ? row.token : null;
|
|
100
|
+
const tokenId = String(token?.id ?? row.tokenId ?? row.id ?? "").trim();
|
|
101
|
+
const symbol = String(token?.symbol ?? row.symbol ?? "").trim().toUpperCase();
|
|
102
|
+
const amountRaw = row.amount ?? row.amountUsdc ?? row.balance ?? null;
|
|
103
|
+
const amountText = amountRaw === null || amountRaw === undefined ? "" : String(amountRaw).trim();
|
|
104
|
+
if (!amountText) continue;
|
|
105
|
+
const amount = Number(amountText);
|
|
106
|
+
if (!Number.isFinite(amount) || amount < 0) continue;
|
|
107
|
+
const score =
|
|
108
|
+
normalizedHint && tokenId === normalizedHint
|
|
109
|
+
? 3
|
|
110
|
+
: symbol === "USDC"
|
|
111
|
+
? 2
|
|
112
|
+
: tokenId && normalizedHint && tokenId.toLowerCase().includes("usdc")
|
|
113
|
+
? 1
|
|
114
|
+
: 0;
|
|
115
|
+
ranked.push({
|
|
116
|
+
score,
|
|
117
|
+
tokenId: tokenId || null,
|
|
118
|
+
symbol: symbol || null,
|
|
119
|
+
amount,
|
|
120
|
+
amountText
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
ranked.sort((a, b) => b.score - a.score || b.amount - a.amount);
|
|
124
|
+
return ranked[0] ?? null;
|
|
125
|
+
}
|
|
126
|
+
|
|
89
127
|
function inferModeFromBaseUrl(baseUrl) {
|
|
90
128
|
const u = normalizeHttpUrl(baseUrl);
|
|
91
129
|
if (!u) return null;
|
|
@@ -198,6 +236,27 @@ async function resolveUsdcTokenId({ baseUrl, apiKey, walletIds, fetchImpl = fetc
|
|
|
198
236
|
return null;
|
|
199
237
|
}
|
|
200
238
|
|
|
239
|
+
async function resolveWalletUsdcBalance({ baseUrl, apiKey, walletId, tokenIdHint = null, fetchImpl = fetch }) {
|
|
240
|
+
const out = await callCircle({
|
|
241
|
+
baseUrl,
|
|
242
|
+
apiKey,
|
|
243
|
+
method: "GET",
|
|
244
|
+
endpoint: `/v1/w3s/wallets/${encodeURIComponent(walletId)}/balances`,
|
|
245
|
+
fetchImpl
|
|
246
|
+
});
|
|
247
|
+
if (out.status < 200 || out.status >= 300) {
|
|
248
|
+
throw new Error(`wallet balance lookup failed for ${walletId} (HTTP ${out.status})`);
|
|
249
|
+
}
|
|
250
|
+
const picked = pickUsdcBalance(out.json, { tokenIdHint });
|
|
251
|
+
return {
|
|
252
|
+
walletId,
|
|
253
|
+
usdcAmount: picked ? picked.amount : null,
|
|
254
|
+
usdcAmountText: picked ? picked.amountText : null,
|
|
255
|
+
tokenId: picked?.tokenId ?? null,
|
|
256
|
+
symbol: picked?.symbol ?? null
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
201
260
|
async function requestFaucet({ baseUrl, apiKey, address, blockchain, native, usdc, fetchImpl = fetch }) {
|
|
202
261
|
const out = await callCircle({
|
|
203
262
|
baseUrl,
|
|
@@ -229,6 +288,7 @@ export async function bootstrapCircleProvider({
|
|
|
229
288
|
escrowWalletId = null,
|
|
230
289
|
tokenIdUsdc = null,
|
|
231
290
|
faucet = null,
|
|
291
|
+
includeBalances = false,
|
|
232
292
|
includeApiKey = false,
|
|
233
293
|
entitySecretHex = null,
|
|
234
294
|
fetchImpl = fetch
|
|
@@ -284,6 +344,40 @@ export async function bootstrapCircleProvider({
|
|
|
284
344
|
throw new Error("could not discover USDC token id; pass tokenIdUsdc explicitly");
|
|
285
345
|
}
|
|
286
346
|
|
|
347
|
+
let balances = null;
|
|
348
|
+
if (includeBalances) {
|
|
349
|
+
try {
|
|
350
|
+
const [spendBalance, escrowBalance] = await Promise.all([
|
|
351
|
+
resolveWalletUsdcBalance({
|
|
352
|
+
baseUrl: detected.baseUrl,
|
|
353
|
+
apiKey: circleApiKey,
|
|
354
|
+
walletId: chosen.spendWalletId,
|
|
355
|
+
tokenIdHint: resolvedTokenIdUsdc,
|
|
356
|
+
fetchImpl
|
|
357
|
+
}),
|
|
358
|
+
resolveWalletUsdcBalance({
|
|
359
|
+
baseUrl: detected.baseUrl,
|
|
360
|
+
apiKey: circleApiKey,
|
|
361
|
+
walletId: chosen.escrowWalletId,
|
|
362
|
+
tokenIdHint: resolvedTokenIdUsdc,
|
|
363
|
+
fetchImpl
|
|
364
|
+
})
|
|
365
|
+
]);
|
|
366
|
+
balances = {
|
|
367
|
+
asOf: new Date().toISOString(),
|
|
368
|
+
spend: spendBalance,
|
|
369
|
+
escrow: escrowBalance
|
|
370
|
+
};
|
|
371
|
+
} catch (err) {
|
|
372
|
+
balances = {
|
|
373
|
+
asOf: new Date().toISOString(),
|
|
374
|
+
error: err?.message ?? "balance_lookup_failed",
|
|
375
|
+
spend: null,
|
|
376
|
+
escrow: null
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
287
381
|
const faucetEnabled =
|
|
288
382
|
typeof faucet === "boolean"
|
|
289
383
|
? faucet
|
|
@@ -342,6 +436,7 @@ export async function bootstrapCircleProvider({
|
|
|
342
436
|
escrow: escrowMeta
|
|
343
437
|
},
|
|
344
438
|
tokenIdUsdc: resolvedTokenIdUsdc,
|
|
439
|
+
balances,
|
|
345
440
|
entitySecretHex: resolvedEntitySecretHex,
|
|
346
441
|
faucetEnabled,
|
|
347
442
|
faucetResults,
|