chain-insights 0.71.4 → 0.73.1
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 +38 -3
- package/dist/buy-97M6dtEu.cjs +195 -0
- package/dist/buy-_mpdRieG.mjs +195 -0
- package/dist/buy-_mpdRieG.mjs.map +1 -0
- package/dist/{capabilities-DGavTSoW.cjs → capabilities-Ds3320iS.cjs} +1 -1
- package/dist/{capabilities-BBVDIECk.mjs → capabilities-DxojbX4D.mjs} +2 -2
- package/dist/{capabilities-BBVDIECk.mjs.map → capabilities-DxojbX4D.mjs.map} +1 -1
- package/dist/{claude-desktop-CTaBCUIm.mjs → claude-desktop-6jwX0Lux.mjs} +18 -3
- package/dist/{claude-desktop-CTaBCUIm.mjs.map → claude-desktop-6jwX0Lux.mjs.map} +1 -1
- package/dist/{claude-desktop-BI_mGkZ6.cjs → claude-desktop-Jmm4PlBZ.cjs} +17 -2
- package/dist/cli.cjs +163 -31
- package/dist/cli.mjs +163 -31
- package/dist/cli.mjs.map +1 -1
- package/dist/{client-gsLi528K.mjs → client-7qOfAdpH.mjs} +31 -12
- package/dist/client-7qOfAdpH.mjs.map +1 -0
- package/dist/{client-Ypw9BNbn.cjs → client-C9uwHnBS.cjs} +33 -14
- package/dist/{dex-registry-KOTJhAQf.cjs → dex-registry-Chs79Ukc.cjs} +11 -8
- package/dist/{dex-registry-05wAhSa0.mjs → dex-registry-rziydXYq.mjs} +12 -9
- package/dist/{dex-registry-05wAhSa0.mjs.map → dex-registry-rziydXYq.mjs.map} +1 -1
- package/dist/{dex-tool-B4T8Czhm.cjs → dex-tool-DLWnff1n.cjs} +6 -4
- package/dist/{dex-tool-B5Qwz1Zd.mjs → dex-tool-DsaAIa-a.mjs} +7 -5
- package/dist/{dex-tool-B5Qwz1Zd.mjs.map → dex-tool-DsaAIa-a.mjs.map} +1 -1
- package/dist/{graph-schema-BoUMa03H.cjs → graph-schema-BGbJumCY.cjs} +63 -25
- package/dist/{graph-schema-DqZUTnP0.mjs → graph-schema-ClqjzgrM.mjs} +59 -27
- package/dist/{graph-schema-DqZUTnP0.mjs.map → graph-schema-ClqjzgrM.mjs.map} +1 -1
- package/dist/index.cjs +5 -5
- package/dist/index.d.cts +7 -0
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +7 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +5 -5
- package/dist/known-keys-B87ZqOgE.mjs +78 -0
- package/dist/known-keys-B87ZqOgE.mjs.map +1 -0
- package/dist/known-keys-PKZF5GYd.cjs +99 -0
- package/dist/mcp-proxy.cjs +86 -39
- package/dist/mcp-proxy.d.cts.map +1 -1
- package/dist/mcp-proxy.d.mts.map +1 -1
- package/dist/mcp-proxy.mjs +86 -39
- package/dist/mcp-proxy.mjs.map +1 -1
- package/dist/{public-tools-Dj130EuJ.cjs → public-tools-Bl_l_rKK.cjs} +8 -5
- package/dist/{public-tools-B77ZT0jt.mjs → public-tools-CQjGt7_q.mjs} +9 -6
- package/dist/{public-tools-B77ZT0jt.mjs.map → public-tools-CQjGt7_q.mjs.map} +1 -1
- package/dist/quote-B9sYNOiv.cjs +70 -0
- package/dist/quote-DfTut0cx.mjs +68 -0
- package/dist/quote-DfTut0cx.mjs.map +1 -0
- package/dist/robinhood-CCTMagwM.mjs +226 -0
- package/dist/robinhood-CCTMagwM.mjs.map +1 -0
- package/dist/robinhood-ChiT69sc.cjs +313 -0
- package/dist/rules-BD-cNHMX.cjs +31 -0
- package/dist/rules-BWdbOtHi.mjs +10 -0
- package/dist/rules-BWdbOtHi.mjs.map +1 -0
- package/dist/{schema-cache-DwddPLcY.cjs → schema-cache-C7v9ml6S.cjs} +1 -1
- package/dist/{schema-cache-BT-kHy_r.mjs → schema-cache-DRObKUvS.mjs} +3 -3
- package/dist/{schema-cache-BT-kHy_r.mjs.map → schema-cache-DRObKUvS.mjs.map} +1 -1
- package/dist/{skill-refresh-FXhQpW94.cjs → skill-refresh-Be2D5FrX.cjs} +2 -2
- package/dist/{skill-refresh-I5MSULwP.mjs → skill-refresh-xYWZ_hua.mjs} +3 -3
- package/dist/{skill-refresh-I5MSULwP.mjs.map → skill-refresh-xYWZ_hua.mjs.map} +1 -1
- package/dist/subscription-status-9tMfD90J.cjs +168 -0
- package/dist/subscription-status-DIuH84fM.mjs +148 -0
- package/dist/subscription-status-DIuH84fM.mjs.map +1 -0
- package/dist/{tools-BHfphgy9.cjs → tools-Cmnq7yip.cjs} +21 -4
- package/dist/{tools-BnThuvE2.mjs → tools-D9ep9E7Q.mjs} +22 -5
- package/dist/tools-D9ep9E7Q.mjs.map +1 -0
- package/dist/topup-BE8WP69S.mjs +45 -0
- package/dist/topup-BE8WP69S.mjs.map +1 -0
- package/dist/topup-CWFRNQIx.cjs +43 -0
- package/dist/{topup-server-BrkPhqJG.mjs → topup-server-9rA3qLgf.mjs} +232 -24
- package/dist/topup-server-9rA3qLgf.mjs.map +1 -0
- package/dist/{topup-server-DzeM_YAt.mjs → topup-server-BsYYbgBA.mjs} +2 -2
- package/dist/{topup-server-DzeM_YAt.mjs.map → topup-server-BsYYbgBA.mjs.map} +1 -1
- package/dist/{topup-server-Bh7MlzRt.cjs → topup-server-COy0toY-.cjs} +242 -22
- package/dist/{topup-server-BIbSfvX5.cjs → topup-server-CcW4OZJv.cjs} +1 -1
- package/dist/{wallet-B3hAr70M.cjs → wallet-CgIAfdxL.cjs} +34 -0
- package/dist/{wallet-GNDl3FkR.mjs → wallet-QTo5YCFR.mjs} +35 -1
- package/dist/wallet-QTo5YCFR.mjs.map +1 -0
- package/docs/development.md +1 -1
- package/docs/graph-tools.md +2 -1
- package/docs/mcp-proxy.md +30 -4
- package/docs/stability.md +3 -2
- package/package.json +1 -1
- package/skills/chain-insights-cypher/SKILL.md +9 -0
- package/dist/client-gsLi528K.mjs.map +0 -1
- package/dist/subscription-status-DA9DCFrC.mjs +0 -32
- package/dist/subscription-status-DA9DCFrC.mjs.map +0 -1
- package/dist/subscription-status-ru99UCPZ.cjs +0 -40
- package/dist/tools-BnThuvE2.mjs.map +0 -1
- package/dist/topup-server-BrkPhqJG.mjs.map +0 -1
- package/dist/wallet-GNDl3FkR.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -108,11 +108,12 @@ Never touches:
|
|
|
108
108
|
| `meta_schema` | Read the live graph schema of a network: labels, link types, fields, indexes |
|
|
109
109
|
| `meta_dex_lookup` | Turn an exchange name (Aero, Uniswap, Virtuals) into its contracts, pools, reads |
|
|
110
110
|
| `meta_usage_status` | Check the caller's daily free-tier graph query allowance |
|
|
111
|
-
| `meta_subscription_status` | Check
|
|
111
|
+
| `meta_subscription_status` | Check your wallet's subscription: days left, today's allowance, payments pending |
|
|
112
112
|
| `meta_rules` | Read the answer rules when the `chain-insights-cypher` skill is not loaded |
|
|
113
113
|
| `meta_help` | Show Chain Insights tool and workflow guidance |
|
|
114
114
|
| `wallet_balance` | Show the local payment wallet amount |
|
|
115
115
|
| `wallet_topup` | Show the local payment wallet address and its QR code for a USDC top-up on Base |
|
|
116
|
+
| `subscription_topup` | Show the same wallet and its QR code for a $CIA and ETH top-up on Robinhood Chain |
|
|
116
117
|
|
|
117
118
|
## Dependencies
|
|
118
119
|
|
|
@@ -123,6 +124,9 @@ Upstream:
|
|
|
123
124
|
production endpoint `https://mcp.chain-insights.ai/`.
|
|
124
125
|
- **Base mainnet RPC** — wallet balance and payment only
|
|
125
126
|
(`BASE_RPC_URL` override). Not a graph-support claim.
|
|
127
|
+
- **Robinhood Chain RPC** — the $CIA subscription top-up and buy only
|
|
128
|
+
(`ROBINHOOD_RPC_URL` override).
|
|
129
|
+
- **CoinGecko** — the $CIA price of a subscription buy, and nothing else.
|
|
126
130
|
|
|
127
131
|
Downstream:
|
|
128
132
|
|
|
@@ -202,7 +206,7 @@ Chain Insights Graph bills by **billable units**.
|
|
|
202
206
|
| --- | --- | --- |
|
|
203
207
|
| Free tier | Free | 5,000 units a day per address, 30 requests a minute, resets at midnight UTC |
|
|
204
208
|
| Pay as you go (USDC per call, and the ACP offering) | One cent per 100 units, rounded up to the next cent, one cent minimum per call. A batch is one call. | No daily limit, no rate limit for paying agents |
|
|
205
|
-
| Subscription in $CIA (not yet live) |
|
|
209
|
+
| Subscription in $CIA (not yet live) | 4 USD a day, paid in $CIA ([how](#subscription-in-cia)) | 100,000 units (10 USD) a day, 60 requests a minute |
|
|
206
210
|
|
|
207
211
|
**Check your own count.** `src/lib/recount-units.ts` mirrors the server's
|
|
208
212
|
counting logic. Use it client-side to recount units in a response and
|
|
@@ -215,18 +219,49 @@ cut off. When you see `truncated: true`:
|
|
|
215
219
|
- Page through results with `SKIP` to fetch the next batch.
|
|
216
220
|
- Add a tighter `WHERE` filter before raising the limit.
|
|
217
221
|
|
|
222
|
+
## Subscription in $CIA
|
|
223
|
+
|
|
224
|
+
The subscription is bought in a terminal, by you. An agent cannot buy it:
|
|
225
|
+
no MCP tool can pay.
|
|
226
|
+
|
|
227
|
+
1. Fund your wallet: `cia subscription topup`. It prints your own wallet
|
|
228
|
+
address and its QR code, and opens a local page. Send $CIA and a little
|
|
229
|
+
ETH, for gas, on Robinhood Chain.
|
|
230
|
+
2. Buy days: `cia subscription buy --days N`. A wallet holds at most 90 days: one buy pays at most 90 minus the days left, so a wallet holding 90 days buys again after a day has passed.
|
|
231
|
+
3. Check it: `cia subscription status`.
|
|
232
|
+
|
|
233
|
+
What a buy does:
|
|
234
|
+
|
|
235
|
+
- A day costs 4 USD (120 USD a month of 30 days), paid in $CIA at the
|
|
236
|
+
CoinGecko price. The price must be at most 10 minutes old.
|
|
237
|
+
- A subscriber may spend 10 USD of queries a day, 100,000 units.
|
|
238
|
+
- It shows the days, the USD, the $CIA amount and the price, and waits for
|
|
239
|
+
your typed `yes`. `--yes` skips the prompt for a run with no terminal.
|
|
240
|
+
- It signs up to two transactions on Robinhood Chain: an approval of the
|
|
241
|
+
exact amount, only when needed, then the payment.
|
|
242
|
+
- It waits up to 2 minutes for the server to credit the payment. A payment
|
|
243
|
+
not credited by then shows in `cia subscription status`. Do not pay again:
|
|
244
|
+
`buy` refuses while a payment is pending.
|
|
245
|
+
- Never send $CIA by hand to any other address. It is not credited.
|
|
246
|
+
|
|
247
|
+
A wallet whose private key is public, such as the key `1` or a key of a
|
|
248
|
+
public test mnemonic, is refused: bots take anything sent to it within
|
|
249
|
+
seconds. Replace it with `cia wallet create --replace`.
|
|
250
|
+
|
|
218
251
|
## Prerequisites And Environment Setup
|
|
219
252
|
|
|
220
253
|
- **Linux** is the reference platform; shell snippets use bash (macOS works
|
|
221
254
|
the same; on Windows use WSL).
|
|
222
255
|
- **Node.js 22 or newer** (`package.json` engines) and npm.
|
|
223
256
|
|
|
224
|
-
`.env.example` documents the
|
|
257
|
+
`.env.example` documents the supported overrides:
|
|
225
258
|
|
|
226
259
|
| Variable | Purpose |
|
|
227
260
|
| ----------------------------------- | ----------------------------------------------------------------------------------------------------- |
|
|
228
261
|
| `BASE_RPC_URL` | Base RPC override for wallet balance and the local top-up page |
|
|
229
262
|
| `CHAIN_INSIGHTS_GRAPH_MCP_ENDPOINT` | Chain Insights Graph endpoint override; local HTTP loopback allowed, remote hosts must use `https://` |
|
|
263
|
+
| `ROBINHOOD_RPC_URL` | Robinhood Chain RPC override for the $CIA top-up page, balances and `cia subscription buy` |
|
|
264
|
+
| `CHAIN_INSIGHTS_COINGECKO_API_KEY` | Optional CoinGecko demo API key for the $CIA price of `cia subscription buy` |
|
|
230
265
|
|
|
231
266
|
## Run
|
|
232
267
|
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
const require_known_keys = require("./known-keys-PKZF5GYd.cjs");
|
|
2
|
+
const require_robinhood = require("./robinhood-ChiT69sc.cjs");
|
|
3
|
+
const require_quote = require("./quote-B9sYNOiv.cjs");
|
|
4
|
+
//#region src/subscription/buy.ts
|
|
5
|
+
const GRANT_WAIT_MS = 12e4;
|
|
6
|
+
const GRANT_POLL_MS = 5e3;
|
|
7
|
+
const STATUS_COMMAND = "cia subscription status";
|
|
8
|
+
const TOPUP_COMMAND = "cia subscription topup";
|
|
9
|
+
/** Decides the buy from what was read. Pure: no I/O. */
|
|
10
|
+
function planBuy(state) {
|
|
11
|
+
const { quote } = state;
|
|
12
|
+
if (state.wallet.keyIsPublic === true) return {
|
|
13
|
+
ok: false,
|
|
14
|
+
reason: require_known_keys.PUBLIC_KEY_WALLET_TEXT
|
|
15
|
+
};
|
|
16
|
+
if (!state.status) return {
|
|
17
|
+
ok: false,
|
|
18
|
+
reason: `The subscription status could not be read, so a payment cannot be checked. Nothing was sent. Run \`${STATUS_COMMAND}\` and try again.`
|
|
19
|
+
};
|
|
20
|
+
const pending = state.status.pending_payments[0];
|
|
21
|
+
if (pending) return {
|
|
22
|
+
ok: false,
|
|
23
|
+
reason: `A payment is still being credited: ${require_robinhood.explorerTxUrl(pending.tx_hash)}. Nothing was sent. Wait a few minutes and run \`${STATUS_COMMAND}\`.`
|
|
24
|
+
};
|
|
25
|
+
const held = state.status.active ? Math.max(0, state.status.days_left) : 0;
|
|
26
|
+
const room = Math.floor(require_quote.MAX_DAYS_HELD - held);
|
|
27
|
+
if (quote.days > room) return {
|
|
28
|
+
ok: false,
|
|
29
|
+
reason: room < 1 ? `The wallet already holds ${held.toFixed(1)} days, and a wallet may hold at most ${require_quote.MAX_DAYS_HELD}. Nothing was sent. Buy again after a day has passed.` : `The wallet already holds ${held.toFixed(1)} days, and a wallet may hold at most ${require_quote.MAX_DAYS_HELD}. Buy at most ${room} ${room === 1 ? "day" : "days"} now. Nothing was sent.`
|
|
30
|
+
};
|
|
31
|
+
if (state.ciaBalance === null || state.ethBalance === null) return {
|
|
32
|
+
ok: false,
|
|
33
|
+
reason: "The wallet balance on Robinhood Chain could not be read. Nothing was sent. Try again, or set ROBINHOOD_RPC_URL to a working Robinhood Chain RPC."
|
|
34
|
+
};
|
|
35
|
+
if (state.ciaBalance < quote.amount) {
|
|
36
|
+
const short = quote.amount - state.ciaBalance;
|
|
37
|
+
return {
|
|
38
|
+
ok: false,
|
|
39
|
+
reason: `Not enough $CIA: the buy needs ${require_robinhood.formatCia(quote.amount)} $CIA and the wallet holds ${require_robinhood.formatCia(state.ciaBalance)}, ${require_robinhood.formatCia(short)} short. Run \`${TOPUP_COMMAND}\` to fund it.`
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
if (state.ethBalance === 0n) return {
|
|
43
|
+
ok: false,
|
|
44
|
+
reason: `The wallet holds no ETH on Robinhood Chain to pay the gas. Run \`${TOPUP_COMMAND}\` and send a little ETH.`
|
|
45
|
+
};
|
|
46
|
+
return {
|
|
47
|
+
ok: true,
|
|
48
|
+
approve: (state.allowance ?? 0n) < quote.amount,
|
|
49
|
+
quote
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function utcMinute(date) {
|
|
53
|
+
return `${date.toISOString().slice(0, 16).replace("T", " ")} UTC`;
|
|
54
|
+
}
|
|
55
|
+
/** The text the user says yes to. It never names the sink address. */
|
|
56
|
+
function confirmText(quote, approve) {
|
|
57
|
+
const transactions = approve ? `It signs up to two transactions on Robinhood Chain: approve exactly ${require_robinhood.formatCia(quote.amount)} $CIA, then pay.` : "It signs up to two transactions on Robinhood Chain; this time one is enough: the payment.";
|
|
58
|
+
return [
|
|
59
|
+
`Buy about ${quote.days} ${quote.days === 1 ? "day" : "days"} of the Chain Insights subscription.`,
|
|
60
|
+
`Price: ${quote.days} x ${require_quote.SUBSCRIPTION_DAY_USD} USD = ${quote.usd} USD.`,
|
|
61
|
+
`Pay: ${require_robinhood.formatCia(quote.amount)} $CIA at ${quote.price.usd} USD per $CIA (CoinGecko, ${utcMinute(quote.price.updatedAt)}).`,
|
|
62
|
+
"The server prices the payment again when it credits it, so the days can differ by a few minutes.",
|
|
63
|
+
transactions
|
|
64
|
+
].join("\n");
|
|
65
|
+
}
|
|
66
|
+
/** A buy needs a typed yes on a terminal, or --yes. */
|
|
67
|
+
function assertConfirmable(options) {
|
|
68
|
+
if (options.yes || options.stdinIsTTY) return;
|
|
69
|
+
throw new Error("`cia subscription buy` needs a typed yes, and stdin is not a terminal. Run it in a terminal, or pass --yes.");
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Polls the status every 5 s for at most 120 s. granted: the window moved
|
|
73
|
+
* and the payment is no longer pending. pending: the server saw the payment
|
|
74
|
+
* and has not priced it yet. timeout: the server has not seen it. Never resends.
|
|
75
|
+
*/
|
|
76
|
+
async function waitForGrant(options) {
|
|
77
|
+
const now = options.now ?? (() => Date.now());
|
|
78
|
+
const sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
79
|
+
const timeoutMs = options.timeoutMs ?? GRANT_WAIT_MS;
|
|
80
|
+
const intervalMs = options.intervalMs ?? GRANT_POLL_MS;
|
|
81
|
+
const tx = options.txHash.toLowerCase();
|
|
82
|
+
const before = options.before;
|
|
83
|
+
const deadline = now() + timeoutMs;
|
|
84
|
+
let seen = false;
|
|
85
|
+
while (now() < deadline) {
|
|
86
|
+
await sleep(Math.min(intervalMs, deadline - now()));
|
|
87
|
+
let status = null;
|
|
88
|
+
try {
|
|
89
|
+
status = await options.readStatus();
|
|
90
|
+
} catch {
|
|
91
|
+
status = null;
|
|
92
|
+
}
|
|
93
|
+
if (!status) continue;
|
|
94
|
+
if (status.pending_payments.some((payment) => payment.tx_hash.toLowerCase() === tx)) {
|
|
95
|
+
seen = true;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
const moved = !before || !before.active || status.window_end !== before.window_end;
|
|
99
|
+
if (status.active && moved) return {
|
|
100
|
+
outcome: "granted",
|
|
101
|
+
tx_hash: options.txHash,
|
|
102
|
+
window_end: status.window_end,
|
|
103
|
+
days_left: status.days_left
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
const next = `Run \`${STATUS_COMMAND}\` in a few minutes. Do not pay again.`;
|
|
107
|
+
return seen ? {
|
|
108
|
+
outcome: "pending",
|
|
109
|
+
tx_hash: options.txHash,
|
|
110
|
+
next
|
|
111
|
+
} : {
|
|
112
|
+
outcome: "timeout",
|
|
113
|
+
tx_hash: options.txHash,
|
|
114
|
+
next
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/** The Robinhood Chain, through ROBINHOOD_RPC_URL or the public endpoints. */
|
|
118
|
+
function robinhoodBuyChain(env = process.env) {
|
|
119
|
+
return {
|
|
120
|
+
readCiaBalance: (address) => require_robinhood.readCiaBalance(address, env),
|
|
121
|
+
readEthBalance: (address) => require_robinhood.readRobinhoodEthBalance(address, env),
|
|
122
|
+
readAllowance: (owner) => require_robinhood.readCiaAllowance(owner, env),
|
|
123
|
+
approve: (key, amount) => require_robinhood.approveCiaForSubscription(key, amount, env),
|
|
124
|
+
deposit: (key, amount) => require_robinhood.depositCiaForSubscription(key, amount, env)
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
/** Runs one buy. Throws, with nothing sent, on every refusal before the yes. */
|
|
128
|
+
async function executeBuy(deps) {
|
|
129
|
+
const chain = deps.chain ?? robinhoodBuyChain();
|
|
130
|
+
const log = deps.log ?? (() => {});
|
|
131
|
+
const { account } = deps;
|
|
132
|
+
if (account.keyIsPublic === true) throw new Error(require_known_keys.PUBLIC_KEY_WALLET_TEXT);
|
|
133
|
+
const status = await deps.readStatus();
|
|
134
|
+
const price = await deps.fetchPrice();
|
|
135
|
+
const quote = require_quote.quoteForDays(deps.days, price);
|
|
136
|
+
const [ciaBalance, ethBalance, allowance] = await Promise.all([
|
|
137
|
+
chain.readCiaBalance(account.address),
|
|
138
|
+
chain.readEthBalance(account.address),
|
|
139
|
+
chain.readAllowance(account.address)
|
|
140
|
+
]);
|
|
141
|
+
const plan = planBuy({
|
|
142
|
+
wallet: account,
|
|
143
|
+
status,
|
|
144
|
+
quote,
|
|
145
|
+
ciaBalance,
|
|
146
|
+
ethBalance,
|
|
147
|
+
allowance
|
|
148
|
+
});
|
|
149
|
+
if (!plan.ok) throw new Error(plan.reason);
|
|
150
|
+
if (!await deps.confirm(confirmText(quote, plan.approve))) throw new Error("Buy cancelled. Nothing was sent.");
|
|
151
|
+
let approveTx = null;
|
|
152
|
+
if (plan.approve) {
|
|
153
|
+
const approval = await chain.approve(account.privateKey, quote.amount);
|
|
154
|
+
approveTx = approval.txHash;
|
|
155
|
+
log(`Approve: ${require_robinhood.explorerTxUrl(approval.txHash)}`);
|
|
156
|
+
if (approval.status !== "success") throw new Error(`The approval failed on chain: ${require_robinhood.explorerTxUrl(approval.txHash)}. No $CIA was paid.`);
|
|
157
|
+
}
|
|
158
|
+
const deposit = await chain.deposit(account.privateKey, quote.amount);
|
|
159
|
+
log(`Payment: ${require_robinhood.explorerTxUrl(deposit.txHash)}`);
|
|
160
|
+
if (deposit.status !== "success") throw new Error(`The payment failed on chain: ${require_robinhood.explorerTxUrl(deposit.txHash)}. No $CIA was paid.`);
|
|
161
|
+
const received = deposit.received;
|
|
162
|
+
if (!received || received.from.toLowerCase() !== account.address.toLowerCase() || received.token.toLowerCase() !== require_robinhood.CIA_TOKEN_ADDRESS || received.amount !== quote.amount) throw new Error(`The payment landed, but its receipt does not show ${require_robinhood.formatCia(quote.amount)} $CIA from this wallet: ${require_robinhood.explorerTxUrl(deposit.txHash)}. Do not pay again. Run \`${STATUS_COMMAND}\`.`);
|
|
163
|
+
const grant = await waitForGrant({
|
|
164
|
+
txHash: deposit.txHash,
|
|
165
|
+
before: status,
|
|
166
|
+
readStatus: deps.readStatus,
|
|
167
|
+
now: deps.now,
|
|
168
|
+
sleep: deps.sleep,
|
|
169
|
+
timeoutMs: deps.timeoutMs
|
|
170
|
+
});
|
|
171
|
+
return {
|
|
172
|
+
days: quote.days,
|
|
173
|
+
usd: quote.usd,
|
|
174
|
+
amount_cia: require_robinhood.formatCia(quote.amount),
|
|
175
|
+
price_usd: price.usd,
|
|
176
|
+
price_time: price.updatedAt.toISOString(),
|
|
177
|
+
approve_tx: approveTx,
|
|
178
|
+
deposit_tx: deposit.txHash,
|
|
179
|
+
deposit_url: require_robinhood.explorerTxUrl(deposit.txHash),
|
|
180
|
+
grant
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
/** The outcome in plain lines. */
|
|
184
|
+
function formatBuyResult(result) {
|
|
185
|
+
const lines = [`Paid ${result.amount_cia} $CIA for about ${result.days} ${result.days === 1 ? "day" : "days"}: ${result.deposit_url}`];
|
|
186
|
+
const grant = result.grant;
|
|
187
|
+
if (grant.outcome === "granted") lines.push(`Subscription active until ${grant.window_end ?? "unknown"} (about ${grant.days_left.toFixed(1)} days left).`);
|
|
188
|
+
else if (grant.outcome === "pending") lines.push("The server has seen the payment and is pricing it.", grant.next);
|
|
189
|
+
else lines.push("The server has not credited the payment yet.", grant.next);
|
|
190
|
+
return lines.join("\n");
|
|
191
|
+
}
|
|
192
|
+
//#endregion
|
|
193
|
+
exports.assertConfirmable = assertConfirmable;
|
|
194
|
+
exports.executeBuy = executeBuy;
|
|
195
|
+
exports.formatBuyResult = formatBuyResult;
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { t as PUBLIC_KEY_WALLET_TEXT } from "./known-keys-B87ZqOgE.mjs";
|
|
2
|
+
import { c as explorerTxUrl, f as readCiaAllowance, l as formatCia, m as readRobinhoodEthBalance, n as CIA_TOKEN_ADDRESS, o as approveCiaForSubscription, p as readCiaBalance, s as depositCiaForSubscription } from "./robinhood-CCTMagwM.mjs";
|
|
3
|
+
import { MAX_DAYS_HELD, SUBSCRIPTION_DAY_USD, quoteForDays } from "./quote-DfTut0cx.mjs";
|
|
4
|
+
//#region src/subscription/buy.ts
|
|
5
|
+
const GRANT_WAIT_MS = 12e4;
|
|
6
|
+
const GRANT_POLL_MS = 5e3;
|
|
7
|
+
const STATUS_COMMAND = "cia subscription status";
|
|
8
|
+
const TOPUP_COMMAND = "cia subscription topup";
|
|
9
|
+
/** Decides the buy from what was read. Pure: no I/O. */
|
|
10
|
+
function planBuy(state) {
|
|
11
|
+
const { quote } = state;
|
|
12
|
+
if (state.wallet.keyIsPublic === true) return {
|
|
13
|
+
ok: false,
|
|
14
|
+
reason: PUBLIC_KEY_WALLET_TEXT
|
|
15
|
+
};
|
|
16
|
+
if (!state.status) return {
|
|
17
|
+
ok: false,
|
|
18
|
+
reason: `The subscription status could not be read, so a payment cannot be checked. Nothing was sent. Run \`${STATUS_COMMAND}\` and try again.`
|
|
19
|
+
};
|
|
20
|
+
const pending = state.status.pending_payments[0];
|
|
21
|
+
if (pending) return {
|
|
22
|
+
ok: false,
|
|
23
|
+
reason: `A payment is still being credited: ${explorerTxUrl(pending.tx_hash)}. Nothing was sent. Wait a few minutes and run \`${STATUS_COMMAND}\`.`
|
|
24
|
+
};
|
|
25
|
+
const held = state.status.active ? Math.max(0, state.status.days_left) : 0;
|
|
26
|
+
const room = Math.floor(MAX_DAYS_HELD - held);
|
|
27
|
+
if (quote.days > room) return {
|
|
28
|
+
ok: false,
|
|
29
|
+
reason: room < 1 ? `The wallet already holds ${held.toFixed(1)} days, and a wallet may hold at most ${MAX_DAYS_HELD}. Nothing was sent. Buy again after a day has passed.` : `The wallet already holds ${held.toFixed(1)} days, and a wallet may hold at most ${MAX_DAYS_HELD}. Buy at most ${room} ${room === 1 ? "day" : "days"} now. Nothing was sent.`
|
|
30
|
+
};
|
|
31
|
+
if (state.ciaBalance === null || state.ethBalance === null) return {
|
|
32
|
+
ok: false,
|
|
33
|
+
reason: "The wallet balance on Robinhood Chain could not be read. Nothing was sent. Try again, or set ROBINHOOD_RPC_URL to a working Robinhood Chain RPC."
|
|
34
|
+
};
|
|
35
|
+
if (state.ciaBalance < quote.amount) {
|
|
36
|
+
const short = quote.amount - state.ciaBalance;
|
|
37
|
+
return {
|
|
38
|
+
ok: false,
|
|
39
|
+
reason: `Not enough $CIA: the buy needs ${formatCia(quote.amount)} $CIA and the wallet holds ${formatCia(state.ciaBalance)}, ${formatCia(short)} short. Run \`${TOPUP_COMMAND}\` to fund it.`
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
if (state.ethBalance === 0n) return {
|
|
43
|
+
ok: false,
|
|
44
|
+
reason: `The wallet holds no ETH on Robinhood Chain to pay the gas. Run \`${TOPUP_COMMAND}\` and send a little ETH.`
|
|
45
|
+
};
|
|
46
|
+
return {
|
|
47
|
+
ok: true,
|
|
48
|
+
approve: (state.allowance ?? 0n) < quote.amount,
|
|
49
|
+
quote
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function utcMinute(date) {
|
|
53
|
+
return `${date.toISOString().slice(0, 16).replace("T", " ")} UTC`;
|
|
54
|
+
}
|
|
55
|
+
/** The text the user says yes to. It never names the sink address. */
|
|
56
|
+
function confirmText(quote, approve) {
|
|
57
|
+
const transactions = approve ? `It signs up to two transactions on Robinhood Chain: approve exactly ${formatCia(quote.amount)} $CIA, then pay.` : "It signs up to two transactions on Robinhood Chain; this time one is enough: the payment.";
|
|
58
|
+
return [
|
|
59
|
+
`Buy about ${quote.days} ${quote.days === 1 ? "day" : "days"} of the Chain Insights subscription.`,
|
|
60
|
+
`Price: ${quote.days} x ${SUBSCRIPTION_DAY_USD} USD = ${quote.usd} USD.`,
|
|
61
|
+
`Pay: ${formatCia(quote.amount)} $CIA at ${quote.price.usd} USD per $CIA (CoinGecko, ${utcMinute(quote.price.updatedAt)}).`,
|
|
62
|
+
"The server prices the payment again when it credits it, so the days can differ by a few minutes.",
|
|
63
|
+
transactions
|
|
64
|
+
].join("\n");
|
|
65
|
+
}
|
|
66
|
+
/** A buy needs a typed yes on a terminal, or --yes. */
|
|
67
|
+
function assertConfirmable(options) {
|
|
68
|
+
if (options.yes || options.stdinIsTTY) return;
|
|
69
|
+
throw new Error("`cia subscription buy` needs a typed yes, and stdin is not a terminal. Run it in a terminal, or pass --yes.");
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Polls the status every 5 s for at most 120 s. granted: the window moved
|
|
73
|
+
* and the payment is no longer pending. pending: the server saw the payment
|
|
74
|
+
* and has not priced it yet. timeout: the server has not seen it. Never resends.
|
|
75
|
+
*/
|
|
76
|
+
async function waitForGrant(options) {
|
|
77
|
+
const now = options.now ?? (() => Date.now());
|
|
78
|
+
const sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
79
|
+
const timeoutMs = options.timeoutMs ?? GRANT_WAIT_MS;
|
|
80
|
+
const intervalMs = options.intervalMs ?? GRANT_POLL_MS;
|
|
81
|
+
const tx = options.txHash.toLowerCase();
|
|
82
|
+
const before = options.before;
|
|
83
|
+
const deadline = now() + timeoutMs;
|
|
84
|
+
let seen = false;
|
|
85
|
+
while (now() < deadline) {
|
|
86
|
+
await sleep(Math.min(intervalMs, deadline - now()));
|
|
87
|
+
let status = null;
|
|
88
|
+
try {
|
|
89
|
+
status = await options.readStatus();
|
|
90
|
+
} catch {
|
|
91
|
+
status = null;
|
|
92
|
+
}
|
|
93
|
+
if (!status) continue;
|
|
94
|
+
if (status.pending_payments.some((payment) => payment.tx_hash.toLowerCase() === tx)) {
|
|
95
|
+
seen = true;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
const moved = !before || !before.active || status.window_end !== before.window_end;
|
|
99
|
+
if (status.active && moved) return {
|
|
100
|
+
outcome: "granted",
|
|
101
|
+
tx_hash: options.txHash,
|
|
102
|
+
window_end: status.window_end,
|
|
103
|
+
days_left: status.days_left
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
const next = `Run \`${STATUS_COMMAND}\` in a few minutes. Do not pay again.`;
|
|
107
|
+
return seen ? {
|
|
108
|
+
outcome: "pending",
|
|
109
|
+
tx_hash: options.txHash,
|
|
110
|
+
next
|
|
111
|
+
} : {
|
|
112
|
+
outcome: "timeout",
|
|
113
|
+
tx_hash: options.txHash,
|
|
114
|
+
next
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/** The Robinhood Chain, through ROBINHOOD_RPC_URL or the public endpoints. */
|
|
118
|
+
function robinhoodBuyChain(env = process.env) {
|
|
119
|
+
return {
|
|
120
|
+
readCiaBalance: (address) => readCiaBalance(address, env),
|
|
121
|
+
readEthBalance: (address) => readRobinhoodEthBalance(address, env),
|
|
122
|
+
readAllowance: (owner) => readCiaAllowance(owner, env),
|
|
123
|
+
approve: (key, amount) => approveCiaForSubscription(key, amount, env),
|
|
124
|
+
deposit: (key, amount) => depositCiaForSubscription(key, amount, env)
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
/** Runs one buy. Throws, with nothing sent, on every refusal before the yes. */
|
|
128
|
+
async function executeBuy(deps) {
|
|
129
|
+
const chain = deps.chain ?? robinhoodBuyChain();
|
|
130
|
+
const log = deps.log ?? (() => {});
|
|
131
|
+
const { account } = deps;
|
|
132
|
+
if (account.keyIsPublic === true) throw new Error(PUBLIC_KEY_WALLET_TEXT);
|
|
133
|
+
const status = await deps.readStatus();
|
|
134
|
+
const price = await deps.fetchPrice();
|
|
135
|
+
const quote = quoteForDays(deps.days, price);
|
|
136
|
+
const [ciaBalance, ethBalance, allowance] = await Promise.all([
|
|
137
|
+
chain.readCiaBalance(account.address),
|
|
138
|
+
chain.readEthBalance(account.address),
|
|
139
|
+
chain.readAllowance(account.address)
|
|
140
|
+
]);
|
|
141
|
+
const plan = planBuy({
|
|
142
|
+
wallet: account,
|
|
143
|
+
status,
|
|
144
|
+
quote,
|
|
145
|
+
ciaBalance,
|
|
146
|
+
ethBalance,
|
|
147
|
+
allowance
|
|
148
|
+
});
|
|
149
|
+
if (!plan.ok) throw new Error(plan.reason);
|
|
150
|
+
if (!await deps.confirm(confirmText(quote, plan.approve))) throw new Error("Buy cancelled. Nothing was sent.");
|
|
151
|
+
let approveTx = null;
|
|
152
|
+
if (plan.approve) {
|
|
153
|
+
const approval = await chain.approve(account.privateKey, quote.amount);
|
|
154
|
+
approveTx = approval.txHash;
|
|
155
|
+
log(`Approve: ${explorerTxUrl(approval.txHash)}`);
|
|
156
|
+
if (approval.status !== "success") throw new Error(`The approval failed on chain: ${explorerTxUrl(approval.txHash)}. No $CIA was paid.`);
|
|
157
|
+
}
|
|
158
|
+
const deposit = await chain.deposit(account.privateKey, quote.amount);
|
|
159
|
+
log(`Payment: ${explorerTxUrl(deposit.txHash)}`);
|
|
160
|
+
if (deposit.status !== "success") throw new Error(`The payment failed on chain: ${explorerTxUrl(deposit.txHash)}. No $CIA was paid.`);
|
|
161
|
+
const received = deposit.received;
|
|
162
|
+
if (!received || received.from.toLowerCase() !== account.address.toLowerCase() || received.token.toLowerCase() !== CIA_TOKEN_ADDRESS || received.amount !== quote.amount) throw new Error(`The payment landed, but its receipt does not show ${formatCia(quote.amount)} $CIA from this wallet: ${explorerTxUrl(deposit.txHash)}. Do not pay again. Run \`${STATUS_COMMAND}\`.`);
|
|
163
|
+
const grant = await waitForGrant({
|
|
164
|
+
txHash: deposit.txHash,
|
|
165
|
+
before: status,
|
|
166
|
+
readStatus: deps.readStatus,
|
|
167
|
+
now: deps.now,
|
|
168
|
+
sleep: deps.sleep,
|
|
169
|
+
timeoutMs: deps.timeoutMs
|
|
170
|
+
});
|
|
171
|
+
return {
|
|
172
|
+
days: quote.days,
|
|
173
|
+
usd: quote.usd,
|
|
174
|
+
amount_cia: formatCia(quote.amount),
|
|
175
|
+
price_usd: price.usd,
|
|
176
|
+
price_time: price.updatedAt.toISOString(),
|
|
177
|
+
approve_tx: approveTx,
|
|
178
|
+
deposit_tx: deposit.txHash,
|
|
179
|
+
deposit_url: explorerTxUrl(deposit.txHash),
|
|
180
|
+
grant
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
/** The outcome in plain lines. */
|
|
184
|
+
function formatBuyResult(result) {
|
|
185
|
+
const lines = [`Paid ${result.amount_cia} $CIA for about ${result.days} ${result.days === 1 ? "day" : "days"}: ${result.deposit_url}`];
|
|
186
|
+
const grant = result.grant;
|
|
187
|
+
if (grant.outcome === "granted") lines.push(`Subscription active until ${grant.window_end ?? "unknown"} (about ${grant.days_left.toFixed(1)} days left).`);
|
|
188
|
+
else if (grant.outcome === "pending") lines.push("The server has seen the payment and is pricing it.", grant.next);
|
|
189
|
+
else lines.push("The server has not credited the payment yet.", grant.next);
|
|
190
|
+
return lines.join("\n");
|
|
191
|
+
}
|
|
192
|
+
//#endregion
|
|
193
|
+
export { assertConfirmable, executeBuy, formatBuyResult };
|
|
194
|
+
|
|
195
|
+
//# sourceMappingURL=buy-_mpdRieG.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"buy-_mpdRieG.mjs","names":[],"sources":["../src/subscription/buy.ts"],"sourcesContent":["import type { Hex } from 'viem'\nimport type { SubscriptionStatusFacts } from '../mcp/subscription-status.js'\nimport { PUBLIC_KEY_WALLET_TEXT } from '../wallet/known-keys.js'\nimport {\n CIA_TOKEN_ADDRESS,\n approveCiaForSubscription,\n depositCiaForSubscription,\n explorerTxUrl,\n formatCia,\n readCiaAllowance,\n readCiaBalance,\n readRobinhoodEthBalance,\n} from '../wallet/robinhood.js'\nimport { MAX_DAYS_HELD, SUBSCRIPTION_DAY_USD, quoteForDays, type CiaPrice, type Quote } from './quote.js'\n\n// `cia subscription buy --days N`: the one way to pay for a subscription. It\n// is a terminal command and never an MCP tool: an agent cannot buy. Order:\n// wallet, status, quote, balances, the user's yes, approve, deposit, wait.\n\nexport const GRANT_WAIT_MS = 120_000\nexport const GRANT_POLL_MS = 5_000\nexport const STATUS_COMMAND = 'cia subscription status'\nexport const TOPUP_COMMAND = 'cia subscription topup'\n\nexport interface BuyState {\n wallet: { address: string; keyIsPublic?: boolean }\n /** null when the server's subscription status could not be read. */\n status: SubscriptionStatusFacts | null\n quote: Quote\n ciaBalance: bigint | null\n ethBalance: bigint | null\n allowance: bigint | null\n}\n\nexport type BuyPlan = { ok: true; approve: boolean; quote: Quote } | { ok: false; reason: string }\n\n/** Decides the buy from what was read. Pure: no I/O. */\nexport function planBuy(state: BuyState): BuyPlan {\n const { quote } = state\n if (state.wallet.keyIsPublic === true) return { ok: false, reason: PUBLIC_KEY_WALLET_TEXT }\n if (!state.status) {\n return {\n ok: false,\n reason: `The subscription status could not be read, so a payment cannot be checked. Nothing was sent. Run \\`${STATUS_COMMAND}\\` and try again.`,\n }\n }\n const pending = state.status.pending_payments[0]\n if (pending) {\n return {\n ok: false,\n reason: `A payment is still being credited: ${explorerTxUrl(pending.tx_hash)}. Nothing was sent. Wait a few minutes and run \\`${STATUS_COMMAND}\\`.`,\n }\n }\n const held = state.status.active ? Math.max(0, state.status.days_left) : 0\n const room = Math.floor(MAX_DAYS_HELD - held)\n if (quote.days > room) {\n return {\n ok: false,\n reason:\n room < 1\n ? `The wallet already holds ${held.toFixed(1)} days, and a wallet may hold at most ${MAX_DAYS_HELD}. Nothing was sent. Buy again after a day has passed.`\n : `The wallet already holds ${held.toFixed(1)} days, and a wallet may hold at most ${MAX_DAYS_HELD}. Buy at most ${room} ${room === 1 ? 'day' : 'days'} now. Nothing was sent.`,\n }\n }\n if (state.ciaBalance === null || state.ethBalance === null) {\n return {\n ok: false,\n reason:\n 'The wallet balance on Robinhood Chain could not be read. Nothing was sent. Try again, or set ROBINHOOD_RPC_URL to a working Robinhood Chain RPC.',\n }\n }\n if (state.ciaBalance < quote.amount) {\n const short = quote.amount - state.ciaBalance\n return {\n ok: false,\n reason: `Not enough $CIA: the buy needs ${formatCia(quote.amount)} $CIA and the wallet holds ${formatCia(state.ciaBalance)}, ${formatCia(short)} short. Run \\`${TOPUP_COMMAND}\\` to fund it.`,\n }\n }\n if (state.ethBalance === 0n) {\n return {\n ok: false,\n reason: `The wallet holds no ETH on Robinhood Chain to pay the gas. Run \\`${TOPUP_COMMAND}\\` and send a little ETH.`,\n }\n }\n return { ok: true, approve: (state.allowance ?? 0n) < quote.amount, quote }\n}\n\nfunction utcMinute(date: Date): string {\n return `${date.toISOString().slice(0, 16).replace('T', ' ')} UTC`\n}\n\n/** The text the user says yes to. It never names the sink address. */\nexport function confirmText(quote: Quote, approve: boolean): string {\n const transactions = approve\n ? `It signs up to two transactions on Robinhood Chain: approve exactly ${formatCia(quote.amount)} $CIA, then pay.`\n : 'It signs up to two transactions on Robinhood Chain; this time one is enough: the payment.'\n return [\n `Buy about ${quote.days} ${quote.days === 1 ? 'day' : 'days'} of the Chain Insights subscription.`,\n `Price: ${quote.days} x ${SUBSCRIPTION_DAY_USD} USD = ${quote.usd} USD.`,\n `Pay: ${formatCia(quote.amount)} $CIA at ${quote.price.usd} USD per $CIA (CoinGecko, ${utcMinute(quote.price.updatedAt)}).`,\n 'The server prices the payment again when it credits it, so the days can differ by a few minutes.',\n transactions,\n ].join('\\n')\n}\n\n/** A buy needs a typed yes on a terminal, or --yes. */\nexport function assertConfirmable(options: { stdinIsTTY: boolean; yes: boolean }): void {\n if (options.yes || options.stdinIsTTY) return\n throw new Error(\n '`cia subscription buy` needs a typed yes, and stdin is not a terminal. Run it in a terminal, or pass --yes.'\n )\n}\n\nexport type GrantOutcome =\n | { outcome: 'granted'; tx_hash: string; window_end: string | null; days_left: number }\n | { outcome: 'pending'; tx_hash: string; next: string }\n | { outcome: 'timeout'; tx_hash: string; next: string }\n\nexport interface WaitForGrantOptions {\n txHash: string\n /** The status read before the payment. */\n before: SubscriptionStatusFacts | null\n readStatus: () => Promise<SubscriptionStatusFacts | null>\n now?: () => number\n sleep?: (ms: number) => Promise<void>\n timeoutMs?: number\n intervalMs?: number\n}\n\n/**\n * Polls the status every 5 s for at most 120 s. granted: the window moved\n * and the payment is no longer pending. pending: the server saw the payment\n * and has not priced it yet. timeout: the server has not seen it. Never resends.\n */\nexport async function waitForGrant(options: WaitForGrantOptions): Promise<GrantOutcome> {\n const now = options.now ?? (() => Date.now())\n const sleep = options.sleep ?? ((ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms)))\n const timeoutMs = options.timeoutMs ?? GRANT_WAIT_MS\n const intervalMs = options.intervalMs ?? GRANT_POLL_MS\n const tx = options.txHash.toLowerCase()\n const before = options.before\n const deadline = now() + timeoutMs\n let seen = false\n while (now() < deadline) {\n await sleep(Math.min(intervalMs, deadline - now()))\n let status: SubscriptionStatusFacts | null = null\n try {\n status = await options.readStatus()\n } catch {\n status = null\n }\n if (!status) continue\n if (status.pending_payments.some((payment) => payment.tx_hash.toLowerCase() === tx)) {\n seen = true\n continue\n }\n const moved = !before || !before.active || status.window_end !== before.window_end\n if (status.active && moved) {\n return { outcome: 'granted', tx_hash: options.txHash, window_end: status.window_end, days_left: status.days_left }\n }\n }\n const next = `Run \\`${STATUS_COMMAND}\\` in a few minutes. Do not pay again.`\n return seen\n ? { outcome: 'pending', tx_hash: options.txHash, next }\n : { outcome: 'timeout', tx_hash: options.txHash, next }\n}\n\nexport interface BuyChain {\n readCiaBalance(address: string): Promise<bigint | null>\n readEthBalance(address: string): Promise<bigint | null>\n readAllowance(owner: string): Promise<bigint | null>\n approve: typeof approveCiaForSubscription\n deposit: typeof depositCiaForSubscription\n}\n\n/** The Robinhood Chain, through ROBINHOOD_RPC_URL or the public endpoints. */\nexport function robinhoodBuyChain(env: NodeJS.ProcessEnv = process.env): BuyChain {\n return {\n readCiaBalance: (address) => readCiaBalance(address, env),\n readEthBalance: (address) => readRobinhoodEthBalance(address, env),\n readAllowance: (owner) => readCiaAllowance(owner, env),\n approve: (key, amount) => approveCiaForSubscription(key, amount, env),\n deposit: (key, amount) => depositCiaForSubscription(key, amount, env),\n }\n}\n\nexport interface BuyDependencies {\n account: { address: string; privateKey: Hex; keyIsPublic?: boolean }\n days: number\n readStatus: () => Promise<SubscriptionStatusFacts | null>\n fetchPrice: () => Promise<CiaPrice>\n /** Shows the text and resolves true on the user's yes. */\n confirm: (text: string) => Promise<boolean>\n chain?: BuyChain\n log?: (line: string) => void\n now?: () => number\n sleep?: (ms: number) => Promise<void>\n timeoutMs?: number\n}\n\nexport interface BuyResult {\n days: number\n usd: number\n amount_cia: string\n price_usd: string\n price_time: string\n approve_tx: string | null\n deposit_tx: string\n deposit_url: string\n grant: GrantOutcome\n}\n\n/** Runs one buy. Throws, with nothing sent, on every refusal before the yes. */\nexport async function executeBuy(deps: BuyDependencies): Promise<BuyResult> {\n const chain = deps.chain ?? robinhoodBuyChain()\n const log = deps.log ?? (() => {})\n const { account } = deps\n if (account.keyIsPublic === true) throw new Error(PUBLIC_KEY_WALLET_TEXT)\n\n const status = await deps.readStatus()\n const price = await deps.fetchPrice()\n const quote = quoteForDays(deps.days, price)\n const [ciaBalance, ethBalance, allowance] = await Promise.all([\n chain.readCiaBalance(account.address),\n chain.readEthBalance(account.address),\n chain.readAllowance(account.address),\n ])\n const plan = planBuy({ wallet: account, status, quote, ciaBalance, ethBalance, allowance })\n if (!plan.ok) throw new Error(plan.reason)\n if (!(await deps.confirm(confirmText(quote, plan.approve)))) {\n throw new Error('Buy cancelled. Nothing was sent.')\n }\n\n let approveTx: string | null = null\n if (plan.approve) {\n const approval = await chain.approve(account.privateKey, quote.amount)\n approveTx = approval.txHash\n log(`Approve: ${explorerTxUrl(approval.txHash)}`)\n if (approval.status !== 'success') {\n throw new Error(`The approval failed on chain: ${explorerTxUrl(approval.txHash)}. No $CIA was paid.`)\n }\n }\n const deposit = await chain.deposit(account.privateKey, quote.amount)\n log(`Payment: ${explorerTxUrl(deposit.txHash)}`)\n if (deposit.status !== 'success') {\n throw new Error(`The payment failed on chain: ${explorerTxUrl(deposit.txHash)}. No $CIA was paid.`)\n }\n const received = deposit.received\n if (\n !received ||\n received.from.toLowerCase() !== account.address.toLowerCase() ||\n received.token.toLowerCase() !== CIA_TOKEN_ADDRESS ||\n received.amount !== quote.amount\n ) {\n throw new Error(\n `The payment landed, but its receipt does not show ${formatCia(quote.amount)} $CIA from this wallet: ${explorerTxUrl(deposit.txHash)}. Do not pay again. Run \\`${STATUS_COMMAND}\\`.`\n )\n }\n\n const grant = await waitForGrant({\n txHash: deposit.txHash,\n before: status,\n readStatus: deps.readStatus,\n now: deps.now,\n sleep: deps.sleep,\n timeoutMs: deps.timeoutMs,\n })\n return {\n days: quote.days,\n usd: quote.usd,\n amount_cia: formatCia(quote.amount),\n price_usd: price.usd,\n price_time: price.updatedAt.toISOString(),\n approve_tx: approveTx,\n deposit_tx: deposit.txHash,\n deposit_url: explorerTxUrl(deposit.txHash),\n grant,\n }\n}\n\n/** The outcome in plain lines. */\nexport function formatBuyResult(result: BuyResult): string {\n const lines = [`Paid ${result.amount_cia} $CIA for about ${result.days} ${result.days === 1 ? 'day' : 'days'}: ${result.deposit_url}`]\n const grant = result.grant\n if (grant.outcome === 'granted') {\n lines.push(`Subscription active until ${grant.window_end ?? 'unknown'} (about ${grant.days_left.toFixed(1)} days left).`)\n } else if (grant.outcome === 'pending') {\n lines.push('The server has seen the payment and is pricing it.', grant.next)\n } else {\n lines.push('The server has not credited the payment yet.', grant.next)\n }\n return lines.join('\\n')\n}\n"],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const require_rolldown_runtime = require("./rolldown-runtime-C0BPl7ul.cjs");
|
|
2
|
-
const require_client = require("./client-
|
|
2
|
+
const require_client = require("./client-C9uwHnBS.cjs");
|
|
3
3
|
//#region src/mcp/capabilities.ts
|
|
4
4
|
var capabilities_exports = /* @__PURE__ */ require_rolldown_runtime.__exportAll({
|
|
5
5
|
familyDisplayName: () => familyDisplayName,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-8H4AJuhK.mjs";
|
|
2
|
-
import { a as resolveGraphMcpEndpoint, n as applyMcpAuthHeaders } from "./client-
|
|
2
|
+
import { a as resolveGraphMcpEndpoint, n as applyMcpAuthHeaders } from "./client-7qOfAdpH.mjs";
|
|
3
3
|
//#region src/mcp/capabilities.ts
|
|
4
4
|
var capabilities_exports = /* @__PURE__ */ __exportAll({
|
|
5
5
|
familyDisplayName: () => familyDisplayName,
|
|
@@ -383,4 +383,4 @@ function formatNetworkCapability(network) {
|
|
|
383
383
|
//#endregion
|
|
384
384
|
export { findNetworkCapability as n, mirrorGraphNetworkCapabilities as r, capabilities_exports as t };
|
|
385
385
|
|
|
386
|
-
//# sourceMappingURL=capabilities-
|
|
386
|
+
//# sourceMappingURL=capabilities-DxojbX4D.mjs.map
|