chain-insights 0.73.3 → 0.73.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.cjs +2 -2
- package/dist/cli.mjs +2 -2
- package/dist/index.cjs +2 -2
- package/dist/index.d.cts +13 -2
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +13 -2
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2 -2
- package/dist/mcp-proxy.cjs +1 -1
- package/dist/mcp-proxy.mjs +1 -1
- package/dist/{topup-C-Tczr2k.mjs → topup-B6gZbTmv.mjs} +3 -2
- package/dist/topup-B6gZbTmv.mjs.map +1 -0
- package/dist/{topup-Do_4EAld.cjs → topup-DVTCU2le.cjs} +3 -2
- package/dist/{topup-server-qdruzcre.mjs → topup-server-BcT7DIpd.mjs} +12 -6
- package/dist/topup-server-BcT7DIpd.mjs.map +1 -0
- package/dist/{topup-server-CSF5EjNt.mjs → topup-server-KQDviFXG.mjs} +32 -8
- package/dist/topup-server-KQDviFXG.mjs.map +1 -0
- package/dist/{topup-server-DH3qVMvO.cjs → topup-server-ShuYSyBY.cjs} +31 -7
- package/dist/{topup-server-B1cdQDFT.cjs → topup-server-f4Gx0vji.cjs} +11 -5
- package/package.json +1 -1
- package/dist/topup-C-Tczr2k.mjs.map +0 -1
- package/dist/topup-server-CSF5EjNt.mjs.map +0 -1
- package/dist/topup-server-qdruzcre.mjs.map +0 -1
package/dist/cli.cjs
CHANGED
|
@@ -122,7 +122,7 @@ function fail(err) {
|
|
|
122
122
|
function registerSubscriptionCommands(program, command) {
|
|
123
123
|
program.command("subscription").description("Buy and check the Chain Insights subscription, paid in $CIA on Robinhood Chain").addCommand(command("topup").description("Show the wallet address and QR code to fund with $CIA and ETH on Robinhood Chain").option("--no-open", "Print the top-up page URL without opening a browser").option("--json", "Print machine-readable top-up facts").action(async (opts) => {
|
|
124
124
|
try {
|
|
125
|
-
const { runSubscriptionTopup } = await Promise.resolve().then(() => require("./topup-
|
|
125
|
+
const { runSubscriptionTopup } = await Promise.resolve().then(() => require("./topup-DVTCU2le.cjs"));
|
|
126
126
|
await runSubscriptionTopup(opts);
|
|
127
127
|
} catch (err) {
|
|
128
128
|
fail(err);
|
|
@@ -592,7 +592,7 @@ program.command("wallet").description("Manage the local Base USDC payment wallet
|
|
|
592
592
|
})).addCommand(createCliCommand("topup").description("Open a local browser page to top up the payment wallet").option("--no-open", "Print the top-up URL without opening a browser").option("--json", "Print machine-readable top-up metadata").action(async (opts) => {
|
|
593
593
|
try {
|
|
594
594
|
const { assertWalletKeyNotPublic, buildTopupInfo, getWalletAccount } = await Promise.resolve().then(() => require("./tools-Cmnq7yip.cjs")).then((n) => n.tools_exports);
|
|
595
|
-
const { startTopupServer } = await Promise.resolve().then(() => require("./topup-server-
|
|
595
|
+
const { startTopupServer } = await Promise.resolve().then(() => require("./topup-server-f4Gx0vji.cjs")).then((n) => n.topup_server_exports);
|
|
596
596
|
const account = await getWalletAccount();
|
|
597
597
|
assertWalletKeyNotPublic(account);
|
|
598
598
|
const url = await startTopupServer(account);
|
package/dist/cli.mjs
CHANGED
|
@@ -120,7 +120,7 @@ function fail(err) {
|
|
|
120
120
|
function registerSubscriptionCommands(program, command) {
|
|
121
121
|
program.command("subscription").description("Buy and check the Chain Insights subscription, paid in $CIA on Robinhood Chain").addCommand(command("topup").description("Show the wallet address and QR code to fund with $CIA and ETH on Robinhood Chain").option("--no-open", "Print the top-up page URL without opening a browser").option("--json", "Print machine-readable top-up facts").action(async (opts) => {
|
|
122
122
|
try {
|
|
123
|
-
const { runSubscriptionTopup } = await import("./topup-
|
|
123
|
+
const { runSubscriptionTopup } = await import("./topup-B6gZbTmv.mjs");
|
|
124
124
|
await runSubscriptionTopup(opts);
|
|
125
125
|
} catch (err) {
|
|
126
126
|
fail(err);
|
|
@@ -590,7 +590,7 @@ program.command("wallet").description("Manage the local Base USDC payment wallet
|
|
|
590
590
|
})).addCommand(createCliCommand("topup").description("Open a local browser page to top up the payment wallet").option("--no-open", "Print the top-up URL without opening a browser").option("--json", "Print machine-readable top-up metadata").action(async (opts) => {
|
|
591
591
|
try {
|
|
592
592
|
const { assertWalletKeyNotPublic, buildTopupInfo, getWalletAccount } = await import("./tools-D9ep9E7Q.mjs").then((n) => n.p);
|
|
593
|
-
const { startTopupServer } = await import("./topup-server-
|
|
593
|
+
const { startTopupServer } = await import("./topup-server-BcT7DIpd.mjs").then((n) => n.r);
|
|
594
594
|
const account = await getWalletAccount();
|
|
595
595
|
assertWalletKeyNotPublic(account);
|
|
596
596
|
const url = await startTopupServer(account);
|
package/dist/index.cjs
CHANGED
|
@@ -3,8 +3,8 @@ const require_config = require("./config-BxtMoo9c.cjs");
|
|
|
3
3
|
const require_wallet = require("./wallet-CgIAfdxL.cjs");
|
|
4
4
|
const require_create = require("./create-DdggkF04.cjs");
|
|
5
5
|
const require_tools = require("./tools-Cmnq7yip.cjs");
|
|
6
|
-
const require_topup_server = require("./topup-server-
|
|
7
|
-
const require_topup_server$1 = require("./topup-server-
|
|
6
|
+
const require_topup_server = require("./topup-server-ShuYSyBY.cjs");
|
|
7
|
+
const require_topup_server$1 = require("./topup-server-f4Gx0vji.cjs");
|
|
8
8
|
const require_client = require("./client-C9uwHnBS.cjs");
|
|
9
9
|
exports.buildTopupInfo = require_tools.buildTopupInfo;
|
|
10
10
|
exports.createMcpFetchClient = require_client.createMcpFetchClient;
|
package/dist/index.d.cts
CHANGED
|
@@ -119,11 +119,22 @@ export declare function getWalletBalanceText(account?: PaymentWalletAccount): Pr
|
|
|
119
119
|
export declare function buildTopupInfo(address: string, topupUrl?: string): TopupInfo;
|
|
120
120
|
//#endregion
|
|
121
121
|
//#region src/wallet/mcp-proxy/topup-server.d.ts
|
|
122
|
-
|
|
122
|
+
/**
|
|
123
|
+
* The top-up rails. base-usdc funds pay-per-call queries with USDC on Base;
|
|
124
|
+
* robinhood-cia funds the subscription with $CIA and ETH on Robinhood Chain.
|
|
125
|
+
* One local server per rail.
|
|
126
|
+
*/
|
|
127
|
+
type TopupRail = 'base-usdc' | 'robinhood-cia';
|
|
128
|
+
export declare function generateArtifactHtml(walletAddressInput: string, topupUrl: string, rail?: TopupRail): string;
|
|
123
129
|
//#endregion
|
|
124
130
|
//#region src/wallet/topup-server.d.ts
|
|
125
131
|
export declare function getTopupUrl(): string | null;
|
|
126
|
-
|
|
132
|
+
/**
|
|
133
|
+
* The browser top-up page of `cia wallet topup` (rail base-usdc) and of
|
|
134
|
+
* `cia subscription topup` (rail robinhood-cia): one page, only its text
|
|
135
|
+
* differs by rail.
|
|
136
|
+
*/
|
|
137
|
+
export declare function startTopupServer(account: PaymentWalletAccount | string, rail?: TopupRail): Promise<string>;
|
|
127
138
|
//#endregion
|
|
128
139
|
//#region src/mcp/client.d.ts
|
|
129
140
|
/**
|
package/dist/index.d.cts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.cts","names":[],"sources":["../src/config/schema.ts","../src/config/index.ts","../src/wallet/index.ts","../src/wallet/create.ts","../src/wallet/tools.ts","../src/wallet/mcp-proxy/topup-server.ts","../src/wallet/topup-server.ts","../src/mcp/client.ts"],"mappings":";;;cA4Da,cAAY,EAAA;;;;;;;;;;;;;;;;GAcvB,EAAA,KAAA;KAEU,qBAAqB,EAAE,aAAa;;;wBC3B1B,cAAc,QAAQ;wBAMtB,WAAW,SAAS,QAAQ,sBAAsB;wBAsDlD,oBAAoB;;;wBCnF1B,0BAA0B,gBAAgB;wBAO1C,4BAA4B,qBAAqB;;;;;;;;wBAW3C,WAAW,qBAAqB;UAwBrC;;EAEf;;;;;;;;wBAuCoB,oBACpB,oBACA,UAAS,6BACR,QAAQ;;;;;;;wBAkEW,cAAc;;;;;wBAwCd,sBAAsB;;;UCnN3B;EACf;EACA;;wBAGc,4BAA4B;wBAI5B,wBAAwB;wBAQxB,0BACd,oBACA,UAAS;;;cCXE;cACA;UAgDI;EACf,SAAS;EACT,YAAY;;EAEZ;;UAGe;EACf;EACA;EACA,iBAAiB;EACjB;EACA,uBAAuB;EACvB;;UAoDe;EACf,SAAS;EACT;EACA;EACA,iBAAiB;EACjB;EACA;EACA;EACA;;UAGe;EACf;EACA;EACA;EACA;IACE,QAAQ;;;;;;;;wBASU,oBAAoB,QAAQ;wBAoB5B,eACpB,SAAS,kBACT,8BACC;wBAsBmB,cACpB,SAAS,kBACT,8BACC;wBAuQa,oBACd,iBACA,qBACA;wBAaoB,uBACpB,UAAU,uBACT,QAAQ;wBAyBK,0BAA0B,QAAQ;wBAK5B,qBAAqB,UAAU,uBAAuB;wBAI5D,eAAe,iBAAiB,oBAAoB
|
|
1
|
+
{"version":3,"file":"index.d.cts","names":[],"sources":["../src/config/schema.ts","../src/config/index.ts","../src/wallet/index.ts","../src/wallet/create.ts","../src/wallet/tools.ts","../src/wallet/mcp-proxy/topup-server.ts","../src/wallet/topup-server.ts","../src/mcp/client.ts"],"mappings":";;;cA4Da,cAAY,EAAA;;;;;;;;;;;;;;;;GAcvB,EAAA,KAAA;KAEU,qBAAqB,EAAE,aAAa;;;wBC3B1B,cAAc,QAAQ;wBAMtB,WAAW,SAAS,QAAQ,sBAAsB;wBAsDlD,oBAAoB;;;wBCnF1B,0BAA0B,gBAAgB;wBAO1C,4BAA4B,qBAAqB;;;;;;;;wBAW3C,WAAW,qBAAqB;UAwBrC;;EAEf;;;;;;;;wBAuCoB,oBACpB,oBACA,UAAS,6BACR,QAAQ;;;;;;;wBAkEW,cAAc;;;;;wBAwCd,sBAAsB;;;UCnN3B;EACf;EACA;;wBAGc,4BAA4B;wBAI5B,wBAAwB;wBAQxB,0BACd,oBACA,UAAS;;;cCXE;cACA;UAgDI;EACf,SAAS;EACT,YAAY;;EAEZ;;UAGe;EACf;EACA;EACA,iBAAiB;EACjB;EACA,uBAAuB;EACvB;;UAoDe;EACf,SAAS;EACT;EACA;EACA,iBAAiB;EACjB;EACA;EACA;EACA;;UAGe;EACf;EACA;EACA;EACA;IACE,QAAQ;;;;;;;;wBASU,oBAAoB,QAAQ;wBAoB5B,eACpB,SAAS,kBACT,8BACC;wBAsBmB,cACpB,SAAS,kBACT,8BACC;wBAuQa,oBACd,iBACA,qBACA;wBAaoB,uBACpB,UAAU,uBACT,QAAQ;wBAyBK,0BAA0B,QAAQ;wBAK5B,qBAAqB,UAAU,uBAAuB;wBAI5D,eAAe,iBAAiB,oBAAoB;;;;;;;;KC9cxD;wBAgKI,qBACd,4BACA,kBACA,OAAM;;;wBC9GQ;;;;;;wBAmBM,iBACpB,SAAS,+BACT,OAAM,YACL;;;;;;;;;;;;;;;wBCwJa,qBAAqB,2BAA2B,4BAAkB"}
|
package/dist/index.d.mts
CHANGED
|
@@ -119,11 +119,22 @@ export declare function getWalletBalanceText(account?: PaymentWalletAccount): Pr
|
|
|
119
119
|
export declare function buildTopupInfo(address: string, topupUrl?: string): TopupInfo;
|
|
120
120
|
//#endregion
|
|
121
121
|
//#region src/wallet/mcp-proxy/topup-server.d.ts
|
|
122
|
-
|
|
122
|
+
/**
|
|
123
|
+
* The top-up rails. base-usdc funds pay-per-call queries with USDC on Base;
|
|
124
|
+
* robinhood-cia funds the subscription with $CIA and ETH on Robinhood Chain.
|
|
125
|
+
* One local server per rail.
|
|
126
|
+
*/
|
|
127
|
+
type TopupRail = 'base-usdc' | 'robinhood-cia';
|
|
128
|
+
export declare function generateArtifactHtml(walletAddressInput: string, topupUrl: string, rail?: TopupRail): string;
|
|
123
129
|
//#endregion
|
|
124
130
|
//#region src/wallet/topup-server.d.ts
|
|
125
131
|
export declare function getTopupUrl(): string | null;
|
|
126
|
-
|
|
132
|
+
/**
|
|
133
|
+
* The browser top-up page of `cia wallet topup` (rail base-usdc) and of
|
|
134
|
+
* `cia subscription topup` (rail robinhood-cia): one page, only its text
|
|
135
|
+
* differs by rail.
|
|
136
|
+
*/
|
|
137
|
+
export declare function startTopupServer(account: PaymentWalletAccount | string, rail?: TopupRail): Promise<string>;
|
|
127
138
|
//#endregion
|
|
128
139
|
//#region src/mcp/client.d.ts
|
|
129
140
|
/**
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/config/schema.ts","../src/config/index.ts","../src/wallet/index.ts","../src/wallet/create.ts","../src/wallet/tools.ts","../src/wallet/mcp-proxy/topup-server.ts","../src/wallet/topup-server.ts","../src/mcp/client.ts"],"mappings":";;;cA4Da,cAAY,EAAA;;;;;;;;;;;;;;;;GAcvB,EAAA,KAAA;KAEU,qBAAqB,EAAE,aAAa;;;wBC3B1B,cAAc,QAAQ;wBAMtB,WAAW,SAAS,QAAQ,sBAAsB;wBAsDlD,oBAAoB;;;wBCnF1B,0BAA0B,gBAAgB;wBAO1C,4BAA4B,qBAAqB;;;;;;;;wBAW3C,WAAW,qBAAqB;UAwBrC;;EAEf;;;;;;;;wBAuCoB,oBACpB,oBACA,UAAS,6BACR,QAAQ;;;;;;;wBAkEW,cAAc;;;;;wBAwCd,sBAAsB;;;UCnN3B;EACf;EACA;;wBAGc,4BAA4B;wBAI5B,wBAAwB;wBAQxB,0BACd,oBACA,UAAS;;;cCXE;cACA;UAgDI;EACf,SAAS;EACT,YAAY;;EAEZ;;UAGe;EACf;EACA;EACA,iBAAiB;EACjB;EACA,uBAAuB;EACvB;;UAoDe;EACf,SAAS;EACT;EACA;EACA,iBAAiB;EACjB;EACA;EACA;EACA;;UAGe;EACf;EACA;EACA;EACA;IACE,QAAQ;;;;;;;;wBASU,oBAAoB,QAAQ;wBAoB5B,eACpB,SAAS,kBACT,8BACC;wBAsBmB,cACpB,SAAS,kBACT,8BACC;wBAuQa,oBACd,iBACA,qBACA;wBAaoB,uBACpB,UAAU,uBACT,QAAQ;wBAyBK,0BAA0B,QAAQ;wBAK5B,qBAAqB,UAAU,uBAAuB;wBAI5D,eAAe,iBAAiB,oBAAoB
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/config/schema.ts","../src/config/index.ts","../src/wallet/index.ts","../src/wallet/create.ts","../src/wallet/tools.ts","../src/wallet/mcp-proxy/topup-server.ts","../src/wallet/topup-server.ts","../src/mcp/client.ts"],"mappings":";;;cA4Da,cAAY,EAAA;;;;;;;;;;;;;;;;GAcvB,EAAA,KAAA;KAEU,qBAAqB,EAAE,aAAa;;;wBC3B1B,cAAc,QAAQ;wBAMtB,WAAW,SAAS,QAAQ,sBAAsB;wBAsDlD,oBAAoB;;;wBCnF1B,0BAA0B,gBAAgB;wBAO1C,4BAA4B,qBAAqB;;;;;;;;wBAW3C,WAAW,qBAAqB;UAwBrC;;EAEf;;;;;;;;wBAuCoB,oBACpB,oBACA,UAAS,6BACR,QAAQ;;;;;;;wBAkEW,cAAc;;;;;wBAwCd,sBAAsB;;;UCnN3B;EACf;EACA;;wBAGc,4BAA4B;wBAI5B,wBAAwB;wBAQxB,0BACd,oBACA,UAAS;;;cCXE;cACA;UAgDI;EACf,SAAS;EACT,YAAY;;EAEZ;;UAGe;EACf;EACA;EACA,iBAAiB;EACjB;EACA,uBAAuB;EACvB;;UAoDe;EACf,SAAS;EACT;EACA;EACA,iBAAiB;EACjB;EACA;EACA;EACA;;UAGe;EACf;EACA;EACA;EACA;IACE,QAAQ;;;;;;;;wBASU,oBAAoB,QAAQ;wBAoB5B,eACpB,SAAS,kBACT,8BACC;wBAsBmB,cACpB,SAAS,kBACT,8BACC;wBAuQa,oBACd,iBACA,qBACA;wBAaoB,uBACpB,UAAU,uBACT,QAAQ;wBAyBK,0BAA0B,QAAQ;wBAK5B,qBAAqB,UAAU,uBAAuB;wBAI5D,eAAe,iBAAiB,oBAAoB;;;;;;;;KC9cxD;wBAgKI,qBACd,4BACA,kBACA,OAAM;;;wBC9GQ;;;;;;wBAmBM,iBACpB,SAAS,+BACT,OAAM,YACL;;;;;;;;;;;;;;;wBCwJa,qBAAqB,2BAA2B,4BAAkB"}
|
package/dist/index.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import { i as saveConfig, n as loadConfig, r as resetConfigCache } from "./confi
|
|
|
2
2
|
import { a as setWalletPrivateKey, i as normalizeWalletPrivateKey, n as encryptKey, o as walletAddressFromPrivateKey, r as isWalletConfigured, t as decryptKey } from "./wallet-QTo5YCFR.mjs";
|
|
3
3
|
import { i as isWalletBackupConfirmed, n as formatWalletBackupWarning, r as generateWalletPrivateKey } from "./create-CpfZq2fx.mjs";
|
|
4
4
|
import { a as formatWalletBalanceResult, c as getWalletAccount, i as formatWalletBalance, l as getWalletBalanceResult, o as getBalanceEth, r as buildTopupInfo, s as getBalanceUsdc, u as getWalletBalanceText } from "./tools-D9ep9E7Q.mjs";
|
|
5
|
-
import { n as generateArtifactHtml } from "./topup-server-
|
|
6
|
-
import { n as startTopupServer, t as getTopupUrl } from "./topup-server-
|
|
5
|
+
import { n as generateArtifactHtml } from "./topup-server-KQDviFXG.mjs";
|
|
6
|
+
import { n as startTopupServer, t as getTopupUrl } from "./topup-server-BcT7DIpd.mjs";
|
|
7
7
|
import { i as createMcpFetchClient } from "./client-7qOfAdpH.mjs";
|
|
8
8
|
export { buildTopupInfo, createMcpFetchClient, decryptKey, encryptKey, formatWalletBackupWarning, formatWalletBalance, formatWalletBalanceResult, generateArtifactHtml, generateWalletPrivateKey, getBalanceEth, getBalanceUsdc, getTopupUrl, getWalletAccount, getWalletBalanceResult, getWalletBalanceText, isWalletBackupConfirmed, isWalletConfigured, loadConfig, normalizeWalletPrivateKey, resetConfigCache, saveConfig, setWalletPrivateKey, startTopupServer, walletAddressFromPrivateKey };
|
package/dist/mcp-proxy.cjs
CHANGED
|
@@ -1833,7 +1833,7 @@ async function createProxy() {
|
|
|
1833
1833
|
};
|
|
1834
1834
|
server.registerTool(tool.name, toolConfig, handler);
|
|
1835
1835
|
}
|
|
1836
|
-
const { registerTopupView, TOPUP_VIEW_URI } = await Promise.resolve().then(() => require("./topup-server-
|
|
1836
|
+
const { registerTopupView, TOPUP_VIEW_URI } = await Promise.resolve().then(() => require("./topup-server-ShuYSyBY.cjs")).then((n) => n.topup_server_exports);
|
|
1837
1837
|
await registerTopupView(server, { walletAddress: localTopupWalletAddress });
|
|
1838
1838
|
server.registerResource("Chain Insights view", CLAUDE_VIEW_URI, {
|
|
1839
1839
|
title: "Chain Insights view",
|
package/dist/mcp-proxy.mjs
CHANGED
|
@@ -1829,7 +1829,7 @@ async function createProxy() {
|
|
|
1829
1829
|
};
|
|
1830
1830
|
server.registerTool(tool.name, toolConfig, handler);
|
|
1831
1831
|
}
|
|
1832
|
-
const { registerTopupView, TOPUP_VIEW_URI } = await import("./topup-server-
|
|
1832
|
+
const { registerTopupView, TOPUP_VIEW_URI } = await import("./topup-server-KQDviFXG.mjs").then((n) => n.a);
|
|
1833
1833
|
await registerTopupView(server, { walletAddress: localTopupWalletAddress });
|
|
1834
1834
|
server.registerResource("Chain Insights view", CLAUDE_VIEW_URI, {
|
|
1835
1835
|
title: "Chain Insights view",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { o as qrTerminalText, t as SUBSCRIPTION_TOPUP_TEXT } from "./topup-server-KQDviFXG.mjs";
|
|
2
2
|
import { a as ROBINHOOD_NETWORK_DISPLAY, n as CIA_TOKEN_ADDRESS, r as ROBINHOOD_CHAIN_ID, t as CIA_SYMBOL } from "./robinhood-CCTMagwM.mjs";
|
|
3
|
+
import { n as startTopupServer } from "./topup-server-BcT7DIpd.mjs";
|
|
3
4
|
//#region src/subscription/topup.ts
|
|
4
5
|
function subscriptionTopupFacts(walletAddress, topupUrl) {
|
|
5
6
|
return {
|
|
@@ -42,4 +43,4 @@ async function runSubscriptionTopup(opts) {
|
|
|
42
43
|
//#endregion
|
|
43
44
|
export { runSubscriptionTopup };
|
|
44
45
|
|
|
45
|
-
//# sourceMappingURL=topup-
|
|
46
|
+
//# sourceMappingURL=topup-B6gZbTmv.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"topup-B6gZbTmv.mjs","names":[],"sources":["../src/subscription/topup.ts"],"sourcesContent":["import { qrTerminalText } from '../wallet/mcp-proxy/qr.js'\nimport { SUBSCRIPTION_TOPUP_TEXT } from '../wallet/mcp-proxy/topup-server.js'\nimport { startTopupServer } from '../wallet/topup-server.js'\nimport {\n CIA_SYMBOL,\n CIA_TOKEN_ADDRESS,\n ROBINHOOD_CHAIN_ID,\n ROBINHOOD_NETWORK_DISPLAY,\n} from '../wallet/robinhood.js'\n\nexport interface SubscriptionTopupFacts {\n wallet_address: string\n network: typeof ROBINHOOD_NETWORK_DISPLAY\n chain_id: typeof ROBINHOOD_CHAIN_ID\n token: typeof CIA_SYMBOL\n token_contract: typeof CIA_TOKEN_ADDRESS\n gas_token: 'ETH'\n topup_url?: string\n}\n\nexport function subscriptionTopupFacts(walletAddress: string, topupUrl?: string): SubscriptionTopupFacts {\n return {\n wallet_address: walletAddress,\n network: ROBINHOOD_NETWORK_DISPLAY,\n chain_id: ROBINHOOD_CHAIN_ID,\n token: CIA_SYMBOL,\n token_contract: CIA_TOKEN_ADDRESS,\n gas_token: 'ETH',\n ...(topupUrl ? { topup_url: topupUrl } : {}),\n }\n}\n\n/** The terminal text of `cia subscription topup`: facts, then the QR code. */\nexport function subscriptionTopupText(facts: SubscriptionTopupFacts): string {\n return [\n ...(facts.topup_url ? [`Top-up page: ${facts.topup_url}`] : []),\n `Wallet: ${facts.wallet_address}`,\n `Network: ${facts.network} (chain ID ${facts.chain_id})`,\n `Token: ${facts.token} ${facts.token_contract}, and a little ETH for gas`,\n '',\n qrTerminalText(facts.wallet_address),\n '',\n SUBSCRIPTION_TOPUP_TEXT,\n ].join('\\n')\n}\n\n/** `cia subscription topup`: the user's own wallet, never the payment address. */\nexport async function runSubscriptionTopup(opts: { open?: boolean; json?: boolean }): Promise<void> {\n const { assertWalletKeyNotPublic, getWalletAccount } = await import('../wallet/tools.js')\n const account = await getWalletAccount()\n assertWalletKeyNotPublic(account)\n const url = await startTopupServer(account.address, 'robinhood-cia')\n const facts = subscriptionTopupFacts(account.address, url)\n console.log(opts.json ? JSON.stringify(facts, null, 2) : subscriptionTopupText(facts))\n if (!opts.json) console.log('Press Ctrl+C to stop the top-up page.')\n if (opts.open !== false) {\n const open = (await import('open')).default\n await open(url)\n }\n}\n"],"mappings":";;;;AAoBA,SAAgB,uBAAuB,eAAuB,UAA2C;CACvG,OAAO;EACL,gBAAgB;EAChB,SAAS;EACT,UAAU;EACV,OAAO;EACP,gBAAgB;EAChB,WAAW;EACX,GAAI,WAAW,EAAE,WAAW,SAAS,IAAI,CAAC;CAC5C;AACF;;AAGA,SAAgB,sBAAsB,OAAuC;CAC3E,OAAO;EACL,GAAI,MAAM,YAAY,CAAC,gBAAgB,MAAM,WAAW,IAAI,CAAC;EAC7D,gBAAgB,MAAM;EACtB,gBAAgB,MAAM,QAAQ,aAAa,MAAM,SAAS;EAC1D,gBAAgB,MAAM,MAAM,GAAG,MAAM,eAAe;EACpD;EACA,eAAe,MAAM,cAAc;EACnC;EACA;CACF,CAAC,CAAC,KAAK,IAAI;AACb;;AAGA,eAAsB,qBAAqB,MAAyD;CAClG,MAAM,EAAE,0BAA0B,qBAAqB,MAAM,OAAO,uBAAqB,CAAA,MAAA,MAAA,EAAA,CAAA;CACzF,MAAM,UAAU,MAAM,iBAAiB;CACvC,yBAAyB,OAAO;CAChC,MAAM,MAAM,MAAM,iBAAiB,QAAQ,SAAS,eAAe;CACnE,MAAM,QAAQ,uBAAuB,QAAQ,SAAS,GAAG;CACzD,QAAQ,IAAI,KAAK,OAAO,KAAK,UAAU,OAAO,MAAM,CAAC,IAAI,sBAAsB,KAAK,CAAC;CACrF,IAAI,CAAC,KAAK,MAAM,QAAQ,IAAI,uCAAuC;CACnE,IAAI,KAAK,SAAS,OAAO;EACvB,MAAM,QAAQ,MAAM,OAAO,QAAA,CAAS;EACpC,MAAM,KAAK,GAAG;CAChB;AACF"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
const require_topup_server = require("./topup-server-
|
|
1
|
+
const require_topup_server = require("./topup-server-ShuYSyBY.cjs");
|
|
2
2
|
const require_robinhood = require("./robinhood-ChiT69sc.cjs");
|
|
3
|
+
const require_topup_server$1 = require("./topup-server-f4Gx0vji.cjs");
|
|
3
4
|
//#region src/subscription/topup.ts
|
|
4
5
|
function subscriptionTopupFacts(walletAddress, topupUrl) {
|
|
5
6
|
return {
|
|
@@ -30,7 +31,7 @@ async function runSubscriptionTopup(opts) {
|
|
|
30
31
|
const { assertWalletKeyNotPublic, getWalletAccount } = await Promise.resolve().then(() => require("./tools-Cmnq7yip.cjs")).then((n) => n.tools_exports);
|
|
31
32
|
const account = await getWalletAccount();
|
|
32
33
|
assertWalletKeyNotPublic(account);
|
|
33
|
-
const url = await require_topup_server.startTopupServer(account.address, "robinhood-cia");
|
|
34
|
+
const url = await require_topup_server$1.startTopupServer(account.address, "robinhood-cia");
|
|
34
35
|
const facts = subscriptionTopupFacts(account.address, url);
|
|
35
36
|
console.log(opts.json ? JSON.stringify(facts, null, 2) : subscriptionTopupText(facts));
|
|
36
37
|
if (!opts.json) console.log("Press Ctrl+C to stop the top-up page.");
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-8H4AJuhK.mjs";
|
|
2
|
-
import { i as startTopupServer$1, n as generateArtifactHtml, r as getTopupUrl$1 } from "./topup-server-
|
|
2
|
+
import { i as startTopupServer$1, n as generateArtifactHtml, r as getTopupUrl$1 } from "./topup-server-KQDviFXG.mjs";
|
|
3
3
|
import { isAddress } from "viem";
|
|
4
4
|
import { createServer } from "node:http";
|
|
5
5
|
//#region src/wallet/topup-server.ts
|
|
@@ -70,10 +70,15 @@ async function stopTopupServer() {
|
|
|
70
70
|
artifactServerState = null;
|
|
71
71
|
await new Promise((resolve) => server.close(() => resolve()));
|
|
72
72
|
}
|
|
73
|
-
|
|
73
|
+
/**
|
|
74
|
+
* The browser top-up page of `cia wallet topup` (rail base-usdc) and of
|
|
75
|
+
* `cia subscription topup` (rail robinhood-cia): one page, only its text
|
|
76
|
+
* differs by rail.
|
|
77
|
+
*/
|
|
78
|
+
async function startTopupServer(account, rail = "base-usdc") {
|
|
74
79
|
const walletAddress = toWalletAddress(account);
|
|
75
|
-
if (artifactServerState && artifactServerState.address.toLowerCase() === walletAddress.toLowerCase()) return artifactServerState.url;
|
|
76
|
-
const assetServerUrl = await startTopupServer$1(walletAddress);
|
|
80
|
+
if (artifactServerState && artifactServerState.rail === rail && artifactServerState.address.toLowerCase() === walletAddress.toLowerCase()) return artifactServerState.url;
|
|
81
|
+
const assetServerUrl = await startTopupServer$1(walletAddress, rail);
|
|
77
82
|
if (artifactServerState) await stopTopupServer();
|
|
78
83
|
const server = createServer((req, res) => {
|
|
79
84
|
const reqUrl = req.url ?? "/";
|
|
@@ -91,7 +96,7 @@ async function startTopupServer(account) {
|
|
|
91
96
|
const { pathname } = proxyTarget;
|
|
92
97
|
if (pathname === "/" || pathname === "/index.html") {
|
|
93
98
|
const artifactUrl = artifactServerState?.url ?? assetServerUrl;
|
|
94
|
-
send(res, 200, generateArtifactHtml(walletAddress, artifactUrl), "text/html; charset=utf-8");
|
|
99
|
+
send(res, 200, generateArtifactHtml(walletAddress, artifactUrl, rail), "text/html; charset=utf-8");
|
|
95
100
|
return;
|
|
96
101
|
}
|
|
97
102
|
if (pathname.startsWith("/assets/") || pathname.startsWith("/api/")) {
|
|
@@ -119,6 +124,7 @@ async function startTopupServer(account) {
|
|
|
119
124
|
});
|
|
120
125
|
artifactServerState = {
|
|
121
126
|
address: walletAddress,
|
|
127
|
+
rail,
|
|
122
128
|
assetServerUrl,
|
|
123
129
|
server,
|
|
124
130
|
url
|
|
@@ -128,4 +134,4 @@ async function startTopupServer(account) {
|
|
|
128
134
|
//#endregion
|
|
129
135
|
export { startTopupServer as n, topup_server_exports as r, getTopupUrl as t };
|
|
130
136
|
|
|
131
|
-
//# sourceMappingURL=topup-server-
|
|
137
|
+
//# sourceMappingURL=topup-server-BcT7DIpd.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"topup-server-BcT7DIpd.mjs","names":["getCopiedTopupUrl","startCopiedTopupServer"],"sources":["../src/wallet/topup-server.ts"],"sourcesContent":["import {\n generateArtifactHtml,\n getTopupUrl as getCopiedTopupUrl,\n startTopupServer as startCopiedTopupServer,\n type TopupRail,\n} from './mcp-proxy/topup-server.js'\nimport { createServer, type Server, type ServerResponse } from 'node:http'\nimport { isAddress } from 'viem'\nimport type { PaymentWalletAccount } from './tools.js'\n\ninterface ArtifactServerState {\n address: string\n rail: TopupRail\n assetServerUrl: string\n server: Server\n url: string\n}\n\nlet artifactServerState: ArtifactServerState | null = null\nconst REQUEST_TARGET_BASE = 'http://localhost'\n\nclass ProxyRequestError extends Error {\n status: number\n\n constructor(status: number, message: string) {\n super(message)\n this.name = 'ProxyRequestError'\n this.status = status\n }\n}\n\ninterface NormalizedProxyTarget {\n pathAndSearch: string\n pathname: string\n}\n\nfunction toWalletAddress(account: PaymentWalletAccount | string): string {\n const address = typeof account === 'string' ? account : account.address\n if (!isAddress(address)) {\n throw new Error('Wallet address must be a valid 0x-prefixed 20-byte EVM address')\n }\n return address\n}\n\nfunction send(\n res: ServerResponse,\n status: number,\n body: string | Buffer,\n contentType: string\n): void {\n res.writeHead(status, {\n 'content-type': contentType,\n 'cache-control': 'no-store',\n 'access-control-allow-origin': '*',\n })\n res.end(body)\n}\n\nfunction normalizeProxyTarget(reqUrl: string): NormalizedProxyTarget {\n if (!reqUrl.startsWith('/') || reqUrl.startsWith('//')) {\n throw new ProxyRequestError(400, 'Proxy request target must be an origin-form path')\n }\n\n const parsed = new URL(reqUrl, REQUEST_TARGET_BASE)\n if (parsed.origin !== REQUEST_TARGET_BASE) {\n throw new ProxyRequestError(400, 'Absolute and protocol-relative proxy targets are not allowed')\n }\n\n let decodedPathname: string\n try {\n decodedPathname = decodeURIComponent(parsed.pathname)\n } catch {\n throw new ProxyRequestError(400, 'Proxy request target contains invalid encoding')\n }\n\n if (decodedPathname.startsWith('//') || /^\\/[a-z][a-z0-9+.-]*:\\/\\//i.test(decodedPathname)) {\n throw new ProxyRequestError(400, 'Encoded host override targets are not allowed')\n }\n\n return {\n pathAndSearch: `${parsed.pathname}${parsed.search}`,\n pathname: parsed.pathname,\n }\n}\n\nasync function proxyToCopiedServer(\n proxyTarget: NormalizedProxyTarget,\n res: ServerResponse,\n assetServerUrl: string\n): Promise<void> {\n const allowedOrigin = new URL(assetServerUrl).origin\n const upstreamUrl = new URL(proxyTarget.pathAndSearch, assetServerUrl)\n if (upstreamUrl.origin !== allowedOrigin) {\n throw new ProxyRequestError(403, 'Upstream origin is not allowed')\n }\n\n const upstream = await fetch(upstreamUrl)\n const contentType = upstream.headers.get('content-type') ?? 'application/octet-stream'\n const body = Buffer.from(await upstream.arrayBuffer())\n send(res, upstream.status, body, contentType)\n}\n\nexport { generateArtifactHtml }\n\nexport function getTopupArtifactUrl(): string | null {\n return artifactServerState?.url ?? null\n}\n\nexport function getTopupUrl(): string | null {\n return getTopupArtifactUrl() ?? getCopiedTopupUrl()\n}\n\nexport async function stopTopupServer(): Promise<void> {\n if (!artifactServerState) {\n return\n }\n\n const { server } = artifactServerState\n artifactServerState = null\n await new Promise<void>((resolve) => server.close(() => resolve()))\n}\n\n/**\n * The browser top-up page of `cia wallet topup` (rail base-usdc) and of\n * `cia subscription topup` (rail robinhood-cia): one page, only its text\n * differs by rail.\n */\nexport async function startTopupServer(\n account: PaymentWalletAccount | string,\n rail: TopupRail = 'base-usdc'\n): Promise<string> {\n const walletAddress = toWalletAddress(account)\n\n if (\n artifactServerState &&\n artifactServerState.rail === rail &&\n artifactServerState.address.toLowerCase() === walletAddress.toLowerCase()\n ) {\n return artifactServerState.url\n }\n\n const assetServerUrl = await startCopiedTopupServer(walletAddress, rail)\n\n if (artifactServerState) {\n await stopTopupServer()\n }\n\n const server = createServer((req, res) => {\n const reqUrl = req.url ?? '/'\n let proxyTarget: NormalizedProxyTarget\n try {\n proxyTarget = normalizeProxyTarget(reqUrl)\n } catch (err) {\n if (err instanceof ProxyRequestError) {\n send(\n res,\n err.status,\n JSON.stringify({ error: err.message }) + '\\n',\n 'application/json; charset=utf-8'\n )\n return\n }\n send(\n res,\n 400,\n JSON.stringify({ error: 'Invalid request target' }) + '\\n',\n 'application/json; charset=utf-8'\n )\n return\n }\n const { pathname } = proxyTarget\n\n if (pathname === '/' || pathname === '/index.html') {\n const artifactUrl = artifactServerState?.url ?? assetServerUrl\n send(res, 200, generateArtifactHtml(walletAddress, artifactUrl, rail), 'text/html; charset=utf-8')\n return\n }\n\n if (pathname.startsWith('/assets/') || pathname.startsWith('/api/')) {\n void proxyToCopiedServer(proxyTarget, res, assetServerUrl).catch((err) => {\n if (err instanceof ProxyRequestError) {\n send(\n res,\n err.status,\n JSON.stringify({ error: err.message }) + '\\n',\n 'application/json; charset=utf-8'\n )\n return\n }\n send(\n res,\n 502,\n JSON.stringify({ error: (err as Error).message }) + '\\n',\n 'application/json; charset=utf-8'\n )\n })\n return\n }\n\n send(res, 404, JSON.stringify({ error: 'Not found' }) + '\\n', 'application/json; charset=utf-8')\n })\n\n const url = await new Promise<string>((resolve, reject) => {\n server.once('error', reject)\n server.listen(0, '127.0.0.1', () => {\n const addressInfo = server.address()\n if (!addressInfo || typeof addressInfo === 'string') {\n reject(new Error('Failed to start topup artifact server'))\n return\n }\n resolve(`http://localhost:${addressInfo.port}`)\n })\n })\n\n artifactServerState = {\n address: walletAddress,\n rail,\n assetServerUrl,\n server,\n url,\n }\n\n return url\n}\n"],"mappings":"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"}
|
|
@@ -421,11 +421,35 @@ async function startTopupServer(wallet, rail = "base-usdc") {
|
|
|
421
421
|
server.on("error", reject);
|
|
422
422
|
});
|
|
423
423
|
}
|
|
424
|
-
|
|
424
|
+
/**
|
|
425
|
+
* The text of the browser top-up page, per rail. The page is the wallet
|
|
426
|
+
* top-up page of `cia wallet topup` (aphex5, 2026-10-28: copy the wallet
|
|
427
|
+
* top-up page and change only Base Network to Robinhood Chain and $CIA):
|
|
428
|
+
* the logo, the background, the layout and the script never fork by rail.
|
|
429
|
+
*/
|
|
430
|
+
const ARTIFACT_PAGE_TEXT = {
|
|
431
|
+
"base-usdc": {
|
|
432
|
+
subtitle: "Fund your wallet with USDC on Base",
|
|
433
|
+
badge: "Base Network · USDC",
|
|
434
|
+
currency: "USDC",
|
|
435
|
+
balanceField: "balance_usdc",
|
|
436
|
+
gasNote: "Base ETH is used for one-time payment setup gas."
|
|
437
|
+
},
|
|
438
|
+
"robinhood-cia": {
|
|
439
|
+
subtitle: "Fund your wallet with $CIA on Robinhood Chain",
|
|
440
|
+
badge: "Robinhood Chain · $CIA",
|
|
441
|
+
currency: "$CIA",
|
|
442
|
+
balanceField: "balance_cia",
|
|
443
|
+
gasNote: "Robinhood Chain ETH is used for the buy transaction gas."
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
function generateArtifactHtml(walletAddressInput, topupUrl, rail = "base-usdc") {
|
|
447
|
+
const text = ARTIFACT_PAGE_TEXT[rail];
|
|
425
448
|
const walletAddress = assertWalletAddress(walletAddressInput);
|
|
426
449
|
const safeWalletAddress = escapeHtml(walletAddress);
|
|
427
450
|
const safeTopupUrl = escapeHtml(topupUrl);
|
|
428
451
|
const topupUrlJson = jsonForScript(topupUrl);
|
|
452
|
+
const qrSvg = generateQrSvg(walletAddress, { cellSize: 8 });
|
|
429
453
|
return `<!DOCTYPE html>
|
|
430
454
|
<html lang="en">
|
|
431
455
|
<head>
|
|
@@ -512,14 +536,14 @@ function generateArtifactHtml(walletAddressInput, topupUrl) {
|
|
|
512
536
|
<body>
|
|
513
537
|
<div class="card">
|
|
514
538
|
<div class="logo"><img src="${safeTopupUrl}/assets/logo.png" alt="Chain Insights"></div>
|
|
515
|
-
<p class="subtitle"
|
|
516
|
-
<div class="qr">${
|
|
539
|
+
<p class="subtitle">${text.subtitle}</p>
|
|
540
|
+
<div class="qr">${qrSvg}</div>
|
|
517
541
|
<div class="address-wrap">
|
|
518
542
|
<div class="address" id="addr" onclick="selectAndCopy()">${safeWalletAddress}</div>
|
|
519
543
|
<div class="address-hint" id="addrHint">Click to copy address</div>
|
|
520
544
|
</div>
|
|
521
|
-
<div class="badge"><span class="dot"></span
|
|
522
|
-
<p class="balance-line" id="balLine">Current balance: <span class="amount" id="bal">--</span>
|
|
545
|
+
<div class="badge"><span class="dot"></span>${text.badge}</div>
|
|
546
|
+
<p class="balance-line" id="balLine">Current balance: <span class="amount" id="bal">--</span> ${text.currency}<br>Gas balance: <span class="gas" id="gas">--</span> ETH<br>${text.gasNote}</p>
|
|
523
547
|
</div>
|
|
524
548
|
<script>
|
|
525
549
|
// MCP Apps protocol handshake for the wallet top-up page.
|
|
@@ -582,9 +606,9 @@ function fetchBal() {
|
|
|
582
606
|
var el = document.getElementById('bal');
|
|
583
607
|
var gas = document.getElementById('gas');
|
|
584
608
|
var line = document.getElementById('balLine');
|
|
585
|
-
var val = parseFloat(d.
|
|
609
|
+
var val = parseFloat(d.${text.balanceField} || '0').toFixed(2);
|
|
586
610
|
var gasVal = d.balance_eth === 'unknown' ? '--' : parseFloat(d.balance_eth || '0').toFixed(6);
|
|
587
|
-
if (d.
|
|
611
|
+
if (d.${text.balanceField} === 'unknown') { el.textContent = '--'; gas.textContent = gasVal; return; }
|
|
588
612
|
el.textContent = val;
|
|
589
613
|
gas.textContent = gasVal;
|
|
590
614
|
if (lastBal !== null && val !== lastBal) {
|
|
@@ -1417,4 +1441,4 @@ async function registerTopupView(server, deps) {
|
|
|
1417
1441
|
//#endregion
|
|
1418
1442
|
export { topup_server_exports as a, startTopupServer as i, generateArtifactHtml as n, qrTerminalText as o, getTopupUrl as r, SUBSCRIPTION_TOPUP_TEXT as t };
|
|
1419
1443
|
|
|
1420
|
-
//# sourceMappingURL=topup-server-
|
|
1444
|
+
//# sourceMappingURL=topup-server-KQDviFXG.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"topup-server-KQDviFXG.mjs","names":["USDC_ADDRESS"],"sources":["../src/wallet/mcp-proxy/qr.ts","../src/wallet/mcp-proxy/tools.ts","../src/wallet/mcp-proxy/topup-server.ts"],"sourcesContent":["/**\n * Minimal QR code generator. No external dependencies.\n *\n * Byte mode, error correction level L, versions 1 to 4 (single block), mask 0.\n * Version 3 holds a 42-character EVM address; version 4 holds up to 78 bytes.\n *\n * `qrModules` is self-contained on purpose: the top-up view inlines its source\n * (`qrModules.toString()`) and draws the same QR code inside the Claude view,\n * so the function must not reference anything outside its own body.\n */\n\n/**\n * The QR code modules for `text`: a square matrix, 1 for a dark module and 0\n * for a light one, without the quiet zone. Throws when `text` is longer than\n * 78 UTF-8 bytes.\n */\nexport function qrModules(text: string): number[][] {\n // Level L, single-block versions: [version, total codewords, EC codewords].\n const versions = [\n [1, 26, 7],\n [2, 44, 10],\n [3, 70, 15],\n [4, 100, 20],\n ]\n // Format information for level L and mask 0, BCH-coded and masked.\n const formatBits = 0x77c4\n const bytes = Array.from(new TextEncoder().encode(text))\n\n let version = 0\n let totalCodewords = 0\n let ecCodewords = 0\n for (const entry of versions) {\n const dataCodewords = entry[1] - entry[2]\n if (4 + 8 + bytes.length * 8 <= dataCodewords * 8) {\n version = entry[0]\n totalCodewords = entry[1]\n ecCodewords = entry[2]\n break\n }\n }\n if (version === 0) throw new Error('QR text is too long: at most 78 bytes')\n\n const dataCodewords = totalCodewords - ecCodewords\n const size = version * 4 + 17\n\n // Data bits: mode, count, bytes, terminator, byte padding, pad codewords.\n const bits: number[] = []\n const pushBits = (value: number, length: number) => {\n for (let i = length - 1; i >= 0; i--) bits.push((value >>> i) & 1)\n }\n pushBits(0b0100, 4)\n pushBits(bytes.length, 8)\n for (const value of bytes) pushBits(value, 8)\n const capacity = dataCodewords * 8\n pushBits(0, Math.min(4, capacity - bits.length))\n while (bits.length % 8 !== 0) bits.push(0)\n for (let pad = 0xec; bits.length < capacity; pad ^= 0xec ^ 0x11) pushBits(pad, 8)\n const data: number[] = []\n for (let i = 0; i < bits.length; i += 8) {\n let value = 0\n for (let j = 0; j < 8; j++) value = (value << 1) | bits[i + j]\n data.push(value)\n }\n\n // Reed-Solomon error correction over GF(256), polynomial 0x11d.\n const exp: number[] = []\n const log: number[] = []\n for (let i = 0, x = 1; i < 255; i++) {\n exp[i] = x\n log[x] = i\n x <<= 1\n if (x & 0x100) x ^= 0x11d\n }\n const multiply = (a: number, b: number) => (a === 0 || b === 0 ? 0 : exp[(log[a] + log[b]) % 255])\n let generator = [1]\n for (let i = 0; i < ecCodewords; i++) {\n const next = new Array(generator.length + 1).fill(0)\n for (let j = 0; j < generator.length; j++) {\n next[j] ^= generator[j]\n next[j + 1] ^= multiply(generator[j], exp[i])\n }\n generator = next\n }\n const remainder = [...data, ...new Array(ecCodewords).fill(0)]\n for (let i = 0; i < data.length; i++) {\n const factor = remainder[i]\n if (factor === 0) continue\n for (let j = 0; j < generator.length; j++) remainder[i + j] ^= multiply(generator[j], factor)\n }\n const codewords = [...data, ...remainder.slice(data.length)]\n\n // Function patterns. `reserved` marks every module data may not use.\n const modules: number[][] = []\n const reserved: boolean[][] = []\n for (let row = 0; row < size; row++) {\n modules.push(new Array(size).fill(0))\n reserved.push(new Array(size).fill(false))\n }\n const setFunction = (row: number, col: number, dark: boolean) => {\n modules[row][col] = dark ? 1 : 0\n reserved[row][col] = true\n }\n for (let i = 0; i < size; i++) {\n setFunction(6, i, i % 2 === 0)\n setFunction(i, 6, i % 2 === 0)\n }\n const finder = (centerRow: number, centerCol: number) => {\n for (let dr = -4; dr <= 4; dr++) {\n for (let dc = -4; dc <= 4; dc++) {\n const row = centerRow + dr\n const col = centerCol + dc\n if (row < 0 || row >= size || col < 0 || col >= size) continue\n const distance = Math.max(Math.abs(dr), Math.abs(dc))\n setFunction(row, col, distance !== 2 && distance !== 4)\n }\n }\n }\n finder(3, 3)\n finder(3, size - 4)\n finder(size - 4, 3)\n if (version >= 2) {\n const center = size - 7\n for (let dr = -2; dr <= 2; dr++) {\n for (let dc = -2; dc <= 2; dc++) {\n setFunction(center + dr, center + dc, Math.max(Math.abs(dr), Math.abs(dc)) !== 1)\n }\n }\n }\n const formatBit = (i: number) => ((formatBits >>> i) & 1) === 1\n for (let i = 0; i <= 5; i++) setFunction(i, 8, formatBit(i))\n setFunction(7, 8, formatBit(6))\n setFunction(8, 8, formatBit(7))\n setFunction(8, 7, formatBit(8))\n for (let i = 9; i < 15; i++) setFunction(8, 14 - i, formatBit(i))\n for (let i = 0; i < 8; i++) setFunction(8, size - 1 - i, formatBit(i))\n for (let i = 8; i < 15; i++) setFunction(size - 15 + i, 8, formatBit(i))\n setFunction(size - 8, 8, true)\n\n // Data in the zigzag order, then mask 0 on every data module.\n let bitIndex = 0\n for (let right = size - 1; right >= 1; right -= 2) {\n if (right === 6) right = 5\n const upward = ((right + 1) & 2) === 0\n for (let step = 0; step < size; step++) {\n const row = upward ? size - 1 - step : step\n for (let j = 0; j < 2; j++) {\n const col = right - j\n if (reserved[row][col]) continue\n if (bitIndex < codewords.length * 8) {\n modules[row][col] = (codewords[bitIndex >>> 3] >>> (7 - (bitIndex & 7))) & 1\n bitIndex++\n }\n if ((row + col) % 2 === 0) modules[row][col] ^= 1\n }\n }\n }\n return modules\n}\n\nexport interface QrOptions {\n cellSize?: number\n fgColor?: string\n bgColor?: string\n finderColor?: string\n logoBase64?: string // data URI for center logo\n logoWidth?: number // logo width in modules (default: 7)\n logoHeight?: number // logo height in modules (default: 5)\n}\n\nexport function generateQrSvg(text: string, opts: QrOptions | number = 4): string {\n // Backward compat: accept bare cellSize number\n const options: QrOptions = typeof opts === 'number' ? { cellSize: opts } : opts\n const cellSize = options.cellSize ?? 4\n const fgColor = options.fgColor ?? '#000'\n const bgColor = options.bgColor ?? '#fff'\n const finderColor = options.finderColor ?? fgColor\n\n const matrix = qrModules(text)\n const size = matrix.length\n\n // Logo exclusion zone (center of QR)\n const logoW = options.logoWidth ?? 7\n const logoH = options.logoHeight ?? 5\n const logoStartC = Math.floor((size - logoW) / 2)\n const logoStartR = Math.floor((size - logoH) / 2)\n const hasLogo = !!options.logoBase64\n\n const svgSize = size * cellSize\n let svg = `<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"${svgSize}\" height=\"${svgSize}\" viewBox=\"0 0 ${svgSize} ${svgSize}\">`\n svg += `<rect width=\"${svgSize}\" height=\"${svgSize}\" fill=\"${bgColor}\" rx=\"4\"/>`\n\n const isFinderModule = (r: number, c: number): boolean =>\n (r < 7 && c < 7) || (r < 7 && c >= size - 7) || (r >= size - 7 && c < 7)\n\n for (let r = 0; r < size; r++) {\n for (let c = 0; c < size; c++) {\n if (\n hasLogo &&\n r >= logoStartR &&\n r < logoStartR + logoH &&\n c >= logoStartC &&\n c < logoStartC + logoW\n ) {\n continue\n }\n if (matrix[r][c] === 1) {\n const color = isFinderModule(r, c) ? finderColor : fgColor\n svg += `<rect x=\"${c * cellSize}\" y=\"${r * cellSize}\" width=\"${cellSize}\" height=\"${cellSize}\" fill=\"${color}\" rx=\"0.5\"/>`\n }\n }\n }\n\n if (hasLogo && options.logoBase64) {\n const lx = logoStartC * cellSize\n const ly = logoStartR * cellSize\n const lw = logoW * cellSize\n const lh = logoH * cellSize\n svg += `<rect x=\"${lx - 1}\" y=\"${ly - 1}\" width=\"${lw + 2}\" height=\"${lh + 2}\" fill=\"${bgColor}\" rx=\"3\"/>`\n svg += `<image x=\"${lx + 2}\" y=\"${ly + 2}\" width=\"${lw - 4}\" height=\"${lh - 4}\" href=\"${options.logoBase64}\" xlink:href=\"${options.logoBase64}\" preserveAspectRatio=\"xMidYMid meet\"/>`\n }\n\n svg += '</svg>'\n return svg\n}\n\nexport interface QrTerminalOptions {\n /** Quiet zone in modules on each side (default 4, the QR standard). */\n quietZone?: number\n /** Draw light modules as blocks instead of dark ones, for a light-on-dark read. */\n invert?: boolean\n}\n\n/**\n * The QR code of `text` as terminal text. Each character covers two module\n * rows with half blocks (▀ ▄ █ and space), so the code keeps its square shape.\n * A block is a dark module; the quiet zone is light.\n */\nexport function qrTerminalText(text: string, options: QrTerminalOptions = {}): string {\n const quiet = options.quietZone ?? 4\n const modules = qrModules(text)\n const size = modules.length + quiet * 2\n const dark = (row: number, col: number): boolean => {\n const r = row - quiet\n const c = col - quiet\n const on = r >= 0 && c >= 0 && r < modules.length && c < modules.length && modules[r][c] === 1\n return options.invert ? !on : on\n }\n const lines: string[] = []\n for (let row = 0; row < size; row += 2) {\n let line = ''\n for (let col = 0; col < size; col++) {\n const top = dark(row, col)\n const bottom = row + 1 < size ? dark(row + 1, col) : options.invert === true\n line += top && bottom ? '█' : top ? '▀' : bottom ? '▄' : ' '\n }\n lines.push(line)\n }\n return lines.join('\\n')\n}\n","import type { WalletData } from './types.js'\nimport { createPublicClient, http, formatEther, formatUnits } from 'viem'\nimport { base } from 'viem/chains'\n\nconst USDC_ADDRESS = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913' as const\nconst DEFAULT_BASE_RPC_URL = 'https://mainnet.base.org'\nconst PUBLIC_BASE_RPC_URLS = [\n DEFAULT_BASE_RPC_URL,\n 'https://base-rpc.publicnode.com',\n 'https://base.drpc.org',\n 'https://1rpc.io/base',\n] as const\nconst USDC_ABI = [\n {\n name: 'balanceOf',\n type: 'function',\n stateMutability: 'view',\n inputs: [{ name: 'account', type: 'address' }],\n outputs: [{ name: '', type: 'uint256' }],\n },\n] as const\n\nfunction walletAddress(wallet: WalletData | string): `0x${string}` {\n return (typeof wallet === 'string' ? wallet : wallet.address) as `0x${string}`\n}\n\nexport async function getBalanceUsdc(wallet: WalletData | string): Promise<string> {\n const envRpcUrl = process.env.BASE_RPC_URL\n const rpcUrls = [\n ...(envRpcUrl ? [envRpcUrl] : []),\n ...PUBLIC_BASE_RPC_URLS.filter((url) => url !== envRpcUrl),\n ]\n\n for (const rpcUrl of rpcUrls) {\n try {\n const client = createPublicClient({ chain: base, transport: http(rpcUrl) })\n const balance = await client.readContract({\n address: USDC_ADDRESS,\n abi: USDC_ABI,\n functionName: 'balanceOf',\n args: [walletAddress(wallet)],\n })\n return formatUnits(balance, 6)\n } catch {\n // Try the next public Base RPC endpoint.\n }\n }\n\n return 'unknown'\n}\n\nexport async function getBalanceEth(wallet: WalletData | string): Promise<string> {\n const envRpcUrl = process.env.BASE_RPC_URL\n const rpcUrls = [\n ...(envRpcUrl ? [envRpcUrl] : []),\n ...PUBLIC_BASE_RPC_URLS.filter((url) => url !== envRpcUrl),\n ]\n\n for (const rpcUrl of rpcUrls) {\n try {\n const client = createPublicClient({ chain: base, transport: http(rpcUrl) })\n const balance = await client.getBalance({ address: walletAddress(wallet) })\n return formatEther(balance)\n } catch {\n // Try the next public Base RPC endpoint.\n }\n }\n\n return 'unknown'\n}\n\nexport async function getBalance(wallet: WalletData): Promise<string> {\n const [balanceUsdc, balanceEth] = await Promise.all([\n getBalanceUsdc(wallet),\n getBalanceEth(wallet),\n ])\n return [\n `Payment wallet: ${wallet.address}`,\n `USDC on Base: ${balanceUsdc}`,\n `Gas on Base: ${balanceEth} ETH`,\n `Payment network: Base`,\n `Base ETH is used only for one-time payment setup gas.`,\n ]\n .filter(Boolean)\n .join('\\n')\n}\n\nexport function getTopupInfo(wallet: WalletData): string {\n return JSON.stringify({\n wallet_address: wallet.address,\n network: 'Base (Chain ID 8453)',\n token: 'USDC',\n contract: USDC_ADDRESS,\n instructions: [`Send USDC on Base network to: ${wallet.address}`],\n })\n}\n","import { createServer, type Server } from 'node:http'\nimport { readFileSync } from 'node:fs'\nimport { createRequire } from 'node:module'\nimport { join, dirname } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { isAddress } from 'viem'\nimport type { WalletData } from './types.js'\nimport { generateQrSvg, qrModules } from './qr.js'\nimport { getBalanceEth, getBalanceUsdc } from './tools.js'\nimport {\n CIA_SYMBOL,\n CIA_TOKEN_ADDRESS,\n ROBINHOOD_CHAIN_ID,\n ROBINHOOD_NETWORK_DISPLAY,\n getCiaBalance,\n getRobinhoodEthBalance,\n} from '../robinhood.js'\nimport {\n SUBSCRIPTION_TOPUP_DESCRIPTION,\n SUBSCRIPTION_TOPUP_TOOL,\n WALLET_TOPUP_DESCRIPTION,\n} from '../../subscription/rules.js'\n\nconst USDC_ADDRESS = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913'\nconst BASE_CHAIN_ID = '0x2105'\n/** Robinhood Chain, chain ID 4663. */\nconst ROBINHOOD_CHAIN_ID_HEX = '0x1237'\n\nconst __dirname = dirname(fileURLToPath(import.meta.url))\n\n// Pre-load assets — try dist/assets first (installed), then src/assets (dev)\nfunction loadAsset(name: string): Buffer {\n const paths = [\n join(__dirname, 'assets', name), // dist/assets/ (global install)\n join(__dirname, '..', 'src', 'assets', name), // src/assets/ (dev)\n ]\n for (const p of paths) {\n try {\n return readFileSync(p)\n } catch {\n /* try next */\n }\n }\n console.error(`[chain-insights] Warning: asset ${name} not found`)\n return Buffer.alloc(0)\n}\n\nconst logoPng = loadAsset('logo.png')\nconst bgPatternPng = loadAsset('bg-pattern.png')\n\n/**\n * The top-up rails. base-usdc funds pay-per-call queries with USDC on Base;\n * robinhood-cia funds the subscription with $CIA and ETH on Robinhood Chain.\n * One local server per rail.\n */\nexport type TopupRail = 'base-usdc' | 'robinhood-cia'\n\nconst servers = new Map<TopupRail, { server: Server; port: number }>()\n\nfunction assertWalletAddress(wallet: WalletData | string): string {\n const walletAddress = typeof wallet === 'string' ? wallet : wallet.address\n if (!isAddress(walletAddress)) {\n throw new Error('Wallet address must be a valid 0x-prefixed 20-byte EVM address')\n }\n return walletAddress\n}\n\nfunction escapeHtml(value: string): string {\n return value\n .replaceAll('&', '&')\n .replaceAll('<', '<')\n .replaceAll('>', '>')\n .replaceAll('\"', '"')\n .replaceAll(\"'\", ''')\n}\n\nfunction jsonForScript(value: string): string {\n return JSON.stringify(value)\n .replaceAll('<', '\\\\u003c')\n .replaceAll('>', '\\\\u003e')\n .replaceAll('&', '\\\\u0026')\n}\n\nexport function getTopupUrl(rail: TopupRail = 'base-usdc'): string | null {\n const running = servers.get(rail)\n return running ? `http://localhost:${running.port}` : null\n}\n\nasync function railBalance(rail: TopupRail, walletAddress: string): Promise<Record<string, string>> {\n if (rail === 'robinhood-cia') {\n const [balanceCia, balanceEth] = await Promise.all([\n getCiaBalance(walletAddress),\n getRobinhoodEthBalance(walletAddress),\n ])\n return { balance_cia: balanceCia, balance_eth: balanceEth }\n }\n const [balanceUsdc, balanceEth] = await Promise.all([\n getBalanceUsdc(walletAddress),\n getBalanceEth(walletAddress),\n ])\n return { balance_usdc: balanceUsdc, balance_eth: balanceEth }\n}\n\nconst UNKNOWN_BALANCE: Record<TopupRail, Record<string, string>> = {\n 'base-usdc': { balance_usdc: 'unknown', balance_eth: 'unknown' },\n 'robinhood-cia': { balance_cia: 'unknown', balance_eth: 'unknown' },\n}\n\nexport async function startTopupServer(\n wallet: WalletData | string,\n rail: TopupRail = 'base-usdc'\n): Promise<string> {\n const walletAddress = assertWalletAddress(wallet)\n\n const running = getTopupUrl(rail)\n if (running) {\n return running\n }\n\n return new Promise((resolve, reject) => {\n const server = createServer((req, res) => {\n if (req.url === '/api/wallet') {\n res.writeHead(200, {\n 'Content-Type': 'application/json',\n 'Access-Control-Allow-Origin': '*',\n })\n res.end(JSON.stringify({ address: walletAddress }))\n return\n }\n\n if (req.url === '/api/balance') {\n railBalance(rail, walletAddress)\n .then((balance) => {\n res.writeHead(200, {\n 'Content-Type': 'application/json',\n 'Access-Control-Allow-Origin': '*',\n })\n res.end(JSON.stringify(balance))\n })\n .catch(() => {\n res.writeHead(200, {\n 'Content-Type': 'application/json',\n 'Access-Control-Allow-Origin': '*',\n })\n res.end(JSON.stringify(UNKNOWN_BALANCE[rail]))\n })\n return\n }\n\n if (req.url === '/assets/logo.png') {\n res.writeHead(200, {\n 'Content-Type': 'image/png',\n 'Cache-Control': 'public, max-age=86400',\n 'Access-Control-Allow-Origin': '*',\n })\n res.end(logoPng)\n return\n }\n\n if (req.url === '/assets/bg-pattern.png') {\n res.writeHead(200, {\n 'Content-Type': 'image/png',\n 'Cache-Control': 'public, max-age=86400',\n 'Access-Control-Allow-Origin': '*',\n })\n res.end(bgPatternPng)\n return\n }\n\n res.writeHead(200, { 'Content-Type': 'text/html' })\n res.end(rail === 'robinhood-cia' ? generateSubscriptionPage(walletAddress) : generatePage(walletAddress))\n })\n\n server.listen(0, '127.0.0.1', () => {\n const addr = server.address()\n if (addr && typeof addr === 'object') {\n servers.set(rail, { server, port: addr.port })\n const url = `http://localhost:${addr.port}`\n console.error(`[chain-insights] Topup server running at ${url}`)\n resolve(url)\n } else {\n reject(new Error('Failed to start topup server'))\n }\n })\n\n server.on('error', reject)\n })\n}\n\n/**\n * The text of the browser top-up page, per rail. The page is the wallet\n * top-up page of `cia wallet topup` (aphex5, 2026-10-28: copy the wallet\n * top-up page and change only Base Network to Robinhood Chain and $CIA):\n * the logo, the background, the layout and the script never fork by rail.\n */\nconst ARTIFACT_PAGE_TEXT: Record<\n TopupRail,\n { subtitle: string; badge: string; currency: string; balanceField: string; gasNote: string }\n> = {\n 'base-usdc': {\n subtitle: 'Fund your wallet with USDC on Base',\n badge: 'Base Network · USDC',\n currency: 'USDC',\n balanceField: 'balance_usdc',\n gasNote: 'Base ETH is used for one-time payment setup gas.',\n },\n 'robinhood-cia': {\n subtitle: 'Fund your wallet with $CIA on Robinhood Chain',\n badge: 'Robinhood Chain · $CIA',\n currency: '$CIA',\n balanceField: 'balance_cia',\n gasNote: 'Robinhood Chain ETH is used for the buy transaction gas.',\n },\n}\n\nexport function generateArtifactHtml(\n walletAddressInput: string,\n topupUrl: string,\n rail: TopupRail = 'base-usdc'\n): string {\n const text = ARTIFACT_PAGE_TEXT[rail]\n const walletAddress = assertWalletAddress(walletAddressInput)\n const safeWalletAddress = escapeHtml(walletAddress)\n const safeTopupUrl = escapeHtml(topupUrl)\n const topupUrlJson = jsonForScript(topupUrl)\n\n const LOGO_B64 =\n 'data:image/png;base64,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'\n\n // Generate QR code SVG server-side — standard B&W, 2x size\n const qrSvg = generateQrSvg(walletAddress, { cellSize: 8 })\n\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<style>\n * { margin: 0; padding: 0; box-sizing: border-box; }\n body {\n font-family: Inter, -apple-system, BlinkMacSystemFont, sans-serif;\n background: rgba(10, 12, 17, 1) url('${safeTopupUrl}/assets/bg-pattern.png') top left / contain no-repeat;\n color: rgba(255, 255, 255, 0.9);\n display: flex;\n justify-content: center;\n padding: 24px;\n line-height: 1.3;\n }\n .card {\n max-width: 400px;\n width: 100%;\n background: rgba(19, 19, 24, 1);\n border: 1px solid rgba(255, 255, 255, 0.06);\n border-radius: 12px;\n padding: 28px 24px;\n text-align: center;\n }\n .logo { margin-bottom: 8px; }\n .logo img { height: 24px; }\n .subtitle { color: rgba(255, 255, 255, 0.5); font-size: 13px; margin-bottom: 20px; }\n .qr { margin-bottom: 16px; }\n .qr svg { border-radius: 12px; background: #fff; padding: 10px; }\n .address-wrap {\n position: relative;\n margin-bottom: 16px;\n }\n .address {\n width: 100%;\n background: rgba(10, 12, 17, 1);\n border: 1px solid rgba(255, 255, 255, 0.06);\n border-radius: 8px;\n padding: 10px 14px;\n font-family: 'SF Mono', 'Fira Code', monospace;\n font-size: 11px; color: #f2dda6; word-break: break-all;\n cursor: pointer; text-align: center;\n transition: border-color 0.3s linear;\n -webkit-user-select: all; user-select: all;\n outline: none;\n }\n .address:focus { border-color: #ae9d71; }\n .address-hint {\n font-size: 10px; color: rgba(255,255,255,0.3);\n text-align: center; margin-top: 4px;\n }\n .badge {\n display: inline-flex; align-items: center; gap: 6px;\n background: rgba(255, 255, 255, 0.04);\n border: 1px solid rgba(255, 255, 255, 0.06);\n border-radius: 20px; padding: 5px 12px;\n font-size: 11px; color: rgba(255, 255, 255, 0.5); margin-bottom: 20px;\n }\n .badge .dot { width: 7px; height: 7px; border-radius: 50%; background: #f2dda6; }\n .balance-line {\n font-size: 13px;\n color: rgba(255, 255, 255, 0.5);\n margin-bottom: 20px;\n line-height: 1.4;\n }\n .balance-line .amount {\n color: #4feb69;\n font-weight: 600;\n }\n .balance-line .gas {\n color: #f2dda6;\n font-weight: 600;\n }\n @keyframes flash { 0%{opacity:0.4} 100%{opacity:1} }\n .balance-line.flash .amount { animation: flash 1s ease-out; }\n .hint {\n margin-top: 14px; font-size: 12px;\n color: rgba(255, 255, 255, 0.3);\n line-height: 1.5;\n }\n</style>\n</head>\n<body>\n<div class=\"card\">\n <div class=\"logo\"><img src=\"${safeTopupUrl}/assets/logo.png\" alt=\"Chain Insights\"></div>\n <p class=\"subtitle\">${text.subtitle}</p>\n <div class=\"qr\">${qrSvg}</div>\n <div class=\"address-wrap\">\n <div class=\"address\" id=\"addr\" onclick=\"selectAndCopy()\">${safeWalletAddress}</div>\n <div class=\"address-hint\" id=\"addrHint\">Click to copy address</div>\n </div>\n <div class=\"badge\"><span class=\"dot\"></span>${text.badge}</div>\n <p class=\"balance-line\" id=\"balLine\">Current balance: <span class=\"amount\" id=\"bal\">--</span> ${text.currency}<br>Gas balance: <span class=\"gas\" id=\"gas\">--</span> ETH<br>${text.gasNote}</p>\n</div>\n<script>\n// MCP Apps protocol handshake for the wallet top-up page.\n(function() {\n var initId = 1;\n\n // Listen for host messages\n window.addEventListener('message', function(event) {\n var data = event.data;\n if (!data || data.jsonrpc !== '2.0') return;\n\n // Initialize response\n if (data.id === initId && data.result) {\n // Send initialized notification\n window.parent.postMessage({\n jsonrpc: '2.0',\n method: 'ui/notifications/initialized',\n params: {}\n }, '*');\n\n // Send initial size\n var rect = document.documentElement.getBoundingClientRect();\n window.parent.postMessage({\n jsonrpc: '2.0',\n method: 'ui/notifications/size-changed',\n params: { width: Math.ceil(rect.width), height: Math.ceil(rect.height) }\n }, '*');\n }\n\n // Respond to pings\n if (data.method === 'ping' && data.id != null) {\n window.parent.postMessage({\n jsonrpc: '2.0',\n id: data.id,\n result: {}\n }, '*');\n }\n });\n\n // Send initialize request (must match App class protocol)\n window.parent.postMessage({\n jsonrpc: '2.0',\n id: initId,\n method: 'ui/initialize',\n params: {\n appInfo: { name: 'Chain Insights Topup', version: '1.0.0' },\n appCapabilities: {},\n protocolVersion: '2026-01-26'\n }\n }, '*');\n})();\n\n// Live balance polling\nvar lastBal = null;\nvar TOPUP_URL = ${topupUrlJson};\nfunction fetchBal() {\n fetch(TOPUP_URL + '/api/balance')\n .then(function(r) { return r.json(); })\n .then(function(d) {\n var el = document.getElementById('bal');\n var gas = document.getElementById('gas');\n var line = document.getElementById('balLine');\n var val = parseFloat(d.${text.balanceField} || '0').toFixed(2);\n var gasVal = d.balance_eth === 'unknown' ? '--' : parseFloat(d.balance_eth || '0').toFixed(6);\n if (d.${text.balanceField} === 'unknown') { el.textContent = '--'; gas.textContent = gasVal; return; }\n el.textContent = val;\n gas.textContent = gasVal;\n if (lastBal !== null && val !== lastBal) {\n line.classList.remove('flash');\n void line.offsetWidth;\n line.classList.add('flash');\n }\n lastBal = val;\n })\n .catch(function() {});\n}\nfetchBal();\nsetInterval(fetchBal, 10000);\n\nfunction selectAndCopy() {\n var addr = document.getElementById('addr');\n var hint = document.getElementById('addrHint');\n // Select the text\n var r = document.createRange();\n r.selectNodeContents(addr);\n var s = window.getSelection();\n s.removeAllRanges();\n s.addRange(r);\n // Try every clipboard method available\n var copied = false;\n try { copied = document.execCommand('copy'); } catch(e) {}\n if (!copied && navigator.clipboard && navigator.clipboard.writeText) {\n navigator.clipboard.writeText(addr.textContent).then(function() {\n hint.textContent = 'Copied!'; hint.style.color = '#4feb69';\n setTimeout(function() { hint.textContent = 'Click to copy address'; hint.style.color = ''; }, 2000);\n }).catch(function() {});\n }\n if (copied) {\n hint.textContent = 'Copied!'; hint.style.color = '#4feb69';\n setTimeout(function() { hint.textContent = 'Click to copy address'; hint.style.color = ''; }, 2000);\n } else {\n hint.textContent = 'Selected — press Ctrl+C'; hint.style.color = '#f2dda6';\n setTimeout(function() { hint.textContent = 'Click to copy address'; hint.style.color = ''; }, 3000);\n }\n}\n<\\/script>\n</body>\n</html>`\n}\n\n/**\n * The text of the one top-up page, per rail. The page is the Base wallet\n * page, unchanged (aphex5, 2026-10-28: \"wallet topup is your baseline\"): the\n * layout, the style and the script never fork by rail, only these strings do.\n */\ninterface TopupPageText {\n title: string\n subtitle: string\n badge: string\n currency: string\n /** The /api/balance field of the main balance. */\n balanceField: string\n gasNote: string\n info: string\n tokenAddress: string\n chainIdHex: string\n}\n\n/** The text of `cia subscription topup` in the terminal. */\nexport const SUBSCRIPTION_TOPUP_TEXT =\n 'Send $CIA and a little ETH on Robinhood Chain to your own wallet. Then run `cia subscription buy --days N`.'\n\nconst TOPUP_PAGE_TEXT: Record<TopupRail, TopupPageText> = {\n 'base-usdc': {\n title: 'Chain Insights — Fund Wallet',\n subtitle: 'Fund your wallet to use blockchain intelligence tools',\n badge: 'Base Network · USDC',\n currency: 'USDC',\n balanceField: 'balance_usdc',\n gasNote: 'Base ETH is used for one-time payment setup gas.',\n info: 'Send Base USDC to the wallet address above.',\n tokenAddress: USDC_ADDRESS,\n chainIdHex: BASE_CHAIN_ID,\n },\n 'robinhood-cia': {\n title: 'Chain Insights — Fund Subscription Wallet',\n subtitle: 'Fund your wallet to buy a Chain Insights subscription',\n badge: 'Robinhood Chain · $CIA',\n currency: '$CIA',\n balanceField: 'balance_cia',\n gasNote: 'Robinhood Chain ETH is used for the buy transaction gas.',\n info: 'Send $CIA and Robinhood Chain ETH to the wallet address above.',\n tokenAddress: CIA_TOKEN_ADDRESS,\n chainIdHex: ROBINHOOD_CHAIN_ID_HEX,\n },\n}\n\n/**\n * The one local top-up page: the wallet address, its QR code, the network\n * and the balances. It names only the user's own wallet, never where a\n * payment goes.\n */\nexport function generateTopupPage(walletAddressInput: string, rail: TopupRail = 'base-usdc'): string {\n const text = TOPUP_PAGE_TEXT[rail]\n const walletAddress = assertWalletAddress(walletAddressInput)\n const safeWalletAddress = escapeHtml(walletAddress)\n const walletAddressJson = jsonForScript(walletAddress)\n\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>${text.title}</title>\n<script src=\"https://cdn.jsdelivr.net/npm/qrcode-generator@1.4.4/qrcode.min.js\"></script>\n<style>\n * { margin: 0; padding: 0; box-sizing: border-box; }\n\n body {\n font-family: Inter, -apple-system, BlinkMacSystemFont, sans-serif;\n background: rgba(10, 12, 17, 1);\n color: rgba(255, 255, 255, 0.9);\n min-height: 100vh;\n display: flex;\n justify-content: center;\n align-items: center;\n line-height: 1.3;\n }\n\n .container {\n max-width: 480px;\n width: 100%;\n padding: 24px;\n }\n\n .logo {\n text-align: center;\n margin-bottom: 32px;\n }\n\n .logo h1 {\n font-size: 24px;\n font-weight: 600;\n color: #f2dda6;\n letter-spacing: -0.5px;\n }\n\n .logo p {\n color: rgba(255, 255, 255, 0.5);\n font-size: 14px;\n margin-top: 4px;\n }\n\n .card {\n background: rgba(19, 19, 24, 1);\n border: 1px solid rgba(255, 255, 255, 0.06);\n border-radius: 12px;\n padding: 32px 24px;\n }\n\n .qr-container {\n display: flex;\n justify-content: center;\n margin-bottom: 24px;\n }\n\n .qr-container canvas, .qr-container img {\n border-radius: 12px;\n background: #fff;\n padding: 12px;\n }\n\n .address-box {\n background: rgba(10, 12, 17, 1);\n border: 1px solid rgba(255, 255, 255, 0.06);\n border-radius: 8px;\n padding: 12px 16px;\n display: flex;\n align-items: center;\n gap: 8px;\n margin-bottom: 24px;\n }\n\n .address-box code {\n font-family: 'SF Mono', 'Fira Code', monospace;\n font-size: 12px;\n color: #f2dda6;\n word-break: break-all;\n flex: 1;\n }\n\n .copy-btn {\n background: none;\n border: none;\n color: rgba(255, 255, 255, 0.4);\n cursor: pointer;\n padding: 4px;\n font-size: 18px;\n transition: color 0.3s linear;\n }\n\n .copy-btn:hover { color: #f2dda6; }\n .copy-btn.copied { color: #4feb69; }\n\n .network-badge {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n background: rgba(255, 255, 255, 0.04);\n border: 1px solid rgba(255, 255, 255, 0.06);\n border-radius: 20px;\n padding: 6px 12px;\n font-size: 12px;\n color: rgba(255, 255, 255, 0.5);\n margin-bottom: 24px;\n }\n\n .network-badge .dot {\n width: 8px;\n height: 8px;\n border-radius: 50%;\n background: #f2dda6;\n }\n\n .balance-row {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 12px 0;\n border-top: 1px solid rgba(255, 255, 255, 0.06);\n margin-bottom: 16px;\n }\n\n .balance-label { color: rgba(255, 255, 255, 0.5); font-size: 14px; }\n\n .balance-value {\n font-size: 20px;\n font-weight: 600;\n color: rgba(255, 255, 255, 0.9);\n }\n\n .balance-value .currency {\n font-size: 14px;\n color: rgba(255, 255, 255, 0.5);\n font-weight: 400;\n margin-left: 4px;\n }\n\n .gas-note {\n margin-top: -8px;\n margin-bottom: 16px;\n color: rgba(255,255,255,0.45);\n font-size: 12px;\n line-height: 1.4;\n text-align: left;\n }\n\n .metamask-btn {\n width: 100%;\n padding: 14px;\n border: none;\n border-radius: 10px;\n font-size: 15px;\n font-weight: 600;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 10px;\n transition: all 0.2s;\n background: #f6851b;\n color: #fff;\n }\n\n .metamask-btn:hover { background: #e2761b; transform: translateY(-1px); }\n .metamask-btn:disabled { opacity: 0.5; cursor: not-allowed; transform: none; }\n\n .metamask-btn svg { width: 22px; height: 22px; }\n\n .amount-input {\n width: 100%;\n padding: 12px 16px;\n background: #0a0c11;\n border: 1px solid rgba(255, 255, 255, 0.06);\n border-radius: 8px;\n color: rgba(255, 255, 255, 0.9);\n font-size: 16px;\n margin-bottom: 16px;\n outline: none;\n transition: border-color 0.2s;\n }\n\n .amount-input:focus { border-color: #f2dda6; }\n\n .amount-label {\n font-size: 13px;\n color: rgba(255, 255, 255, 0.5);\n margin-bottom: 6px;\n display: block;\n }\n\n .status {\n text-align: center;\n padding: 12px;\n border-radius: 8px;\n font-size: 14px;\n margin-top: 16px;\n display: none;\n }\n\n .status.success { display: block; background: #0a2e1a; color: #4feb69; border: 1px solid #1a4a2e; }\n .status.error { display: block; background: #2e0a0a; color: #eb4f4f; border: 1px solid #4a1a1a; }\n .status.pending { display: block; background: #1a1a0a; color: #f2dda6; border: 1px solid #4a4a1a; }\n\n .info {\n text-align: center;\n margin-top: 24px;\n font-size: 12px;\n color: rgba(255, 255, 255, 0.3);\n line-height: 1.6;\n }\n</style>\n</head>\n<body>\n<div class=\"container\">\n <div class=\"logo\">\n <h1>Chain Insights</h1>\n <p>${text.subtitle}</p>\n </div>\n\n <div class=\"card\">\n <div class=\"qr-container\" id=\"qr\"></div>\n\n <div class=\"address-box\">\n <code id=\"address\">${safeWalletAddress}</code>\n <button class=\"copy-btn\" onclick=\"copyAddress()\" id=\"copyBtn\" title=\"Copy address\">⎘</button>\n </div>\n\n <div class=\"network-badge\">\n <span class=\"dot\"></span>\n ${text.badge}\n </div>\n\n <div class=\"balance-row\">\n <span class=\"balance-label\">Current balance</span>\n <span class=\"balance-value\" id=\"balance\">—<span class=\"currency\">${text.currency}</span></span>\n </div>\n <div class=\"balance-row\">\n <span class=\"balance-label\">Gas balance</span>\n <span class=\"balance-value\" id=\"gasBalance\">—<span class=\"currency\">ETH</span></span>\n </div>\n <p class=\"gas-note\">${text.gasNote}</p>\n\n <div class=\"status\" id=\"status\"></div>\n </div>\n\n <p class=\"info\">${text.info}</p>\n</div>\n\n<script>\nconst WALLET = ${walletAddressJson};\nconst USDC = '${text.tokenAddress}';\nconst CHAIN_ID = '${text.chainIdHex}';\n\n// QR code\n(function() {\n var qr = qrcode(0, 'M');\n qr.addData(WALLET);\n qr.make();\n document.getElementById('qr').innerHTML = qr.createSvgTag(5, 0);\n var svg = document.querySelector('#qr svg');\n if (svg) { svg.style.borderRadius = '12px'; svg.style.background = '#fff'; svg.style.padding = '12px'; }\n})();\n\n// Copy address — fallback for sandboxed iframes where navigator.clipboard is blocked\nfunction copyAddress() {\n var ok = false;\n // Try modern clipboard API first\n if (navigator.clipboard && navigator.clipboard.writeText) {\n navigator.clipboard.writeText(WALLET).then(function() { ok = true; }).catch(function() {});\n }\n // Fallback: hidden textarea + execCommand\n if (!ok) {\n var ta = document.createElement('textarea');\n ta.value = WALLET;\n ta.style.position = 'fixed';\n ta.style.left = '-9999px';\n document.body.appendChild(ta);\n ta.select();\n try { document.execCommand('copy'); } catch(e) {}\n document.body.removeChild(ta);\n }\n var btn = document.getElementById('copyBtn');\n btn.classList.add('copied');\n btn.innerHTML = '✓';\n setTimeout(function() { btn.classList.remove('copied'); btn.innerHTML = '⎘'; }, 2000);\n}\n\n// Fetch balance\nasync function fetchBalance() {\n try {\n var resp = await fetch('/api/balance');\n var json = await resp.json();\n if (json.${text.balanceField} === 'unknown') throw new Error('balance unavailable');\n var balance = Number(json.${text.balanceField} || 0).toFixed(2);\n document.getElementById('balance').innerHTML = balance + '<span class=\"currency\">${text.currency}</span>';\n var gasBalance = json.balance_eth === 'unknown' ? '—' : Number(json.balance_eth || 0).toFixed(6);\n document.getElementById('gasBalance').innerHTML = gasBalance + '<span class=\"currency\">ETH</span>';\n } catch(e) {\n document.getElementById('balance').innerHTML = '—<span class=\"currency\">${text.currency}</span>';\n document.getElementById('gasBalance').innerHTML = '—<span class=\"currency\">ETH</span>';\n }\n}\nfetchBalance();\nsetInterval(fetchBalance, 15000);\n\n</script>\n</body>\n</html>`\n}\n\n/** The Base USDC page. */\nfunction generatePage(walletAddressInput: string): string {\n return generateTopupPage(walletAddressInput, 'base-usdc')\n}\n\n/** The robinhood-cia page: the same page, the subscription text. */\nexport function generateSubscriptionPage(walletAddressInput: string): string {\n return generateTopupPage(walletAddressInput, 'robinhood-cia')\n}\n\n// ---------------------------------------------------------------------------\n// The local top-up view: an MCP App drawn by Claude Desktop next to the\n// wallet_topup answer of the local proxy. It shows the wallet address, its QR\n// code, Base Mainnet and USDC. It fetches nothing, signs nothing and sends\n// nothing: the page holds no wallet code, only an address to read.\n// The hosted connector never serves it (ruling 2026-10-05).\n// ---------------------------------------------------------------------------\n\nexport const TOPUP_TOOL_NAME = 'wallet_topup'\nexport const TOPUP_VIEW_URI = 'ui://chain-insights/topup'\nexport const TOPUP_NETWORK_DISPLAY = 'Base Mainnet'\nconst TOPUP_CHAIN_ID = 8453\nconst EXT_APPS_GLOBAL = '__chainInsightsExtApps'\nconst NO_WALLET_TEXT =\n 'No local payment wallet exists yet. Run `cia wallet create` to make one, then ask to top up again.'\n\nexport interface TopupViewFacts {\n wallet_address: string\n network: 'Base'\n network_display: typeof TOPUP_NETWORK_DISPLAY\n chain_id: typeof TOPUP_CHAIN_ID\n token: 'USDC'\n token_contract: typeof USDC_ADDRESS\n}\n\nexport interface TopupToolResult {\n [key: string]: unknown\n content: Array<{ type: 'text'; text: string }>\n structuredContent: {\n schema: 'chain-insights.result.v1'\n tool: typeof TOPUP_TOOL_NAME\n hint: string | null\n facts: { wallet_configured: boolean; topup: TopupViewFacts | null }\n }\n isError: false\n}\n\n/**\n * The wallet_topup answer. The text is complete on its own, for hosts that\n * draw no view. `walletAddress` null means no local wallet exists.\n */\nexport function topupToolResult(walletAddress: string | null): TopupToolResult {\n if (walletAddress === null) {\n return {\n content: [{ type: 'text', text: NO_WALLET_TEXT }],\n structuredContent: {\n schema: 'chain-insights.result.v1',\n tool: TOPUP_TOOL_NAME,\n hint: 'Run `cia wallet create`.',\n facts: { wallet_configured: false, topup: null },\n },\n isError: false,\n }\n }\n const address = assertWalletAddress(walletAddress)\n const topup: TopupViewFacts = {\n wallet_address: address,\n network: 'Base',\n network_display: TOPUP_NETWORK_DISPLAY,\n chain_id: TOPUP_CHAIN_ID,\n token: 'USDC',\n token_contract: USDC_ADDRESS,\n }\n return {\n content: [\n {\n type: 'text',\n text: [\n `Payment wallet: ${address}`,\n `Network: ${TOPUP_NETWORK_DISPLAY} (chain ID ${TOPUP_CHAIN_ID})`,\n `Token: USDC (contract ${USDC_ADDRESS})`,\n `To top up, send USDC on ${TOPUP_NETWORK_DISPLAY} to the wallet address. Chain Insights makes no payment from here.`,\n ].join('\\n'),\n },\n ],\n structuredContent: {\n schema: 'chain-insights.result.v1',\n tool: TOPUP_TOOL_NAME,\n hint: null,\n facts: { wallet_configured: true, topup },\n },\n isError: false,\n }\n}\n\nexport interface SubscriptionTopupViewFacts {\n wallet_address: string\n network: typeof ROBINHOOD_NETWORK_DISPLAY\n network_display: typeof ROBINHOOD_NETWORK_DISPLAY\n chain_id: typeof ROBINHOOD_CHAIN_ID\n token: typeof CIA_SYMBOL\n token_contract: typeof CIA_TOKEN_ADDRESS\n gas_token: 'ETH'\n cia_balance: string\n eth_balance: string\n}\n\nexport interface SubscriptionTopupToolResult {\n [key: string]: unknown\n content: Array<{ type: 'text'; text: string }>\n structuredContent: {\n schema: 'chain-insights.result.v1'\n tool: typeof SUBSCRIPTION_TOPUP_TOOL\n hint: string | null\n facts: { wallet_configured: boolean; topup: SubscriptionTopupViewFacts | null }\n }\n isError: false\n}\n\n/** The rule the subscription_topup answer ends with. */\nexport const SUBSCRIPTION_TOPUP_RULE =\n 'Fund this wallet. The user buys days in a terminal with `cia subscription buy --days N`. An agent cannot buy. Never send $CIA by hand to any other address: it is not credited.'\n\n/**\n * The subscription_topup answer: the local wallet on Robinhood Chain, $CIA,\n * the balances ('unknown' when no RPC answers). It names the user's own\n * wallet only. `walletAddress` null means no local wallet exists.\n */\nexport function subscriptionTopupToolResult(\n walletAddress: string | null,\n balances: { cia: string; eth: string } = { cia: 'unknown', eth: 'unknown' }\n): SubscriptionTopupToolResult {\n if (walletAddress === null) {\n return {\n content: [{ type: 'text', text: 'No local wallet exists yet. Run `cia wallet create` to make one, then ask to top up again.' }],\n structuredContent: {\n schema: 'chain-insights.result.v1',\n tool: SUBSCRIPTION_TOPUP_TOOL,\n hint: 'Run `cia wallet create`.',\n facts: { wallet_configured: false, topup: null },\n },\n isError: false,\n }\n }\n const address = assertWalletAddress(walletAddress)\n const topup: SubscriptionTopupViewFacts = {\n wallet_address: address,\n network: ROBINHOOD_NETWORK_DISPLAY,\n network_display: ROBINHOOD_NETWORK_DISPLAY,\n chain_id: ROBINHOOD_CHAIN_ID,\n token: CIA_SYMBOL,\n token_contract: CIA_TOKEN_ADDRESS,\n gas_token: 'ETH',\n cia_balance: balances.cia,\n eth_balance: balances.eth,\n }\n return {\n content: [\n {\n type: 'text',\n text: [\n `Subscription wallet: ${address}`,\n `Network: ${ROBINHOOD_NETWORK_DISPLAY} (chain ID ${ROBINHOOD_CHAIN_ID})`,\n `Token: ${CIA_SYMBOL} (contract ${CIA_TOKEN_ADDRESS}), with ETH for gas`,\n `Balance: ${balances.cia} ${CIA_SYMBOL}, ${balances.eth} ETH`,\n SUBSCRIPTION_TOPUP_RULE,\n ].join('\\n'),\n },\n ],\n structuredContent: {\n schema: 'chain-insights.result.v1',\n tool: SUBSCRIPTION_TOPUP_TOOL,\n hint: null,\n facts: { wallet_configured: true, topup },\n },\n isError: false,\n }\n}\n\nlet extAppsScript: string | null = null\n\n/**\n * The browser build of the ext-apps App client (the same 1.x pin as\n * the Chain Insights UI package), turned from an ES module into a script that publishes\n * its exports on globalThis, so the view stays one self-contained file.\n */\nexport function extAppsBrowserScript(): string {\n if (extAppsScript !== null) return extAppsScript\n const require = createRequire(import.meta.url)\n const source = readFileSync(\n require.resolve('@modelcontextprotocol/ext-apps/app-with-deps'),\n 'utf8'\n )\n const match = /export\\s*\\{([^}]*)\\}\\s*;?\\s*(?:\\/\\/# sourceMappingURL=\\S*\\s*)?$/.exec(source)\n if (!match) throw new Error('The ext-apps app bundle does not end with one export list')\n const fields = match[1]\n .split(',')\n .map((part) => part.trim())\n .filter(Boolean)\n .map((part) => {\n const [local, exported] = part.split(/\\s+as\\s+/)\n return `${JSON.stringify(exported ?? local)}:${local}`\n })\n extAppsScript = `${source.slice(0, match.index)}\\nglobalThis.${EXT_APPS_GLOBAL}={${fields.join(',')}};\\n`\n extAppsScript = extAppsScript.replaceAll('</script', '<\\\\/script')\n return extAppsScript\n}\n\nconst TOPUP_VIEW_STYLE = `\n:root {\n color-scheme: light;\n --ci-text: var(--color-text-primary, #1d1b16);\n --ci-muted: var(--color-text-secondary, #5c5647);\n --ci-surface: var(--color-background-secondary, #f7f4ec);\n --ci-border: var(--color-border-primary, #d9d2c0);\n --ci-accent: #8a6a12;\n --ci-on-accent: #ffffff;\n --ci-qr-dark: #000000;\n --ci-qr-light: #ffffff;\n --ci-font: var(--font-sans, ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif);\n --ci-mono: var(--font-mono, ui-monospace, 'SF Mono', Menlo, Consolas, monospace);\n}\nhtml[data-theme='dark'] {\n color-scheme: dark;\n --ci-text: var(--color-text-primary, #f2eee4);\n --ci-muted: var(--color-text-secondary, #b8b09c);\n --ci-surface: var(--color-background-secondary, #1f1d19);\n --ci-border: var(--color-border-primary, #3d392f);\n --ci-accent: #e3c46a;\n --ci-on-accent: #1d1b16;\n}\n* { box-sizing: border-box; margin: 0; padding: 0; }\nhtml, body { background: transparent; }\nbody { font-family: var(--ci-font); color: var(--ci-text); font-size: 14px; line-height: 1.45; padding: 4px; }\n.card { max-width: 420px; margin: 0 auto; padding: 20px; border: 1px solid var(--ci-border); border-radius: 12px; background: var(--ci-surface); }\nh1 { font-size: 16px; font-weight: 600; margin-bottom: 4px; }\n.lead, .note, .muted { color: var(--ci-muted); }\n.lead { margin-bottom: 16px; }\n.qr-tile { display: flex; justify-content: center; margin-bottom: 16px; }\n.qr { width: 200px; height: 200px; border-radius: 8px; }\n.qr-dark { fill: var(--ci-qr-dark); }\n.qr-light { fill: var(--ci-qr-light); }\ndl { display: grid; grid-template-columns: max-content 1fr; gap: 6px 12px; margin-bottom: 16px; }\ndt { color: var(--ci-muted); }\ndd { overflow-wrap: anywhere; }\ncode { font-family: var(--ci-mono); font-size: 12px; }\n.address { display: block; margin-bottom: 6px; }\nbutton { font: inherit; font-size: 13px; padding: 4px 12px; border-radius: 6px; border: 0; background: var(--ci-accent); color: var(--ci-on-accent); cursor: pointer; }\nbutton:focus-visible { outline: 2px solid var(--ci-accent); outline-offset: 2px; }\n.copy-state { margin-left: 8px; font-size: 12px; color: var(--ci-muted); }\n.skeleton { height: 14px; border-radius: 4px; background: var(--ci-border); margin-bottom: 10px; }\n.skeleton.short { width: 60%; }\n`\n\nconst TOPUP_VIEW_SCRIPT = `\n(function () {\n var extApps = globalThis.${EXT_APPS_GLOBAL};\n var qrModules = __QR_MODULES__;\n var root = document.getElementById('topup');\n var ADDRESS = /^0x[0-9a-fA-F]{40}$/;\n\n function el(tag, attrs, children) {\n var node = document.createElement(tag);\n Object.keys(attrs || {}).forEach(function (key) {\n if (key === 'text') node.textContent = attrs[key];\n else node.setAttribute(key, attrs[key]);\n });\n (children || []).forEach(function (child) {\n node.append(typeof child === 'string' ? document.createTextNode(child) : child);\n });\n return node;\n }\n\n function qrSvg(address) {\n var modules = qrModules(address);\n var quiet = 4;\n var total = modules.length + quiet * 2;\n var ns = 'http://www.w3.org/2000/svg';\n var svg = document.createElementNS(ns, 'svg');\n svg.setAttribute('class', 'qr');\n svg.setAttribute('viewBox', '0 0 ' + total + ' ' + total);\n svg.setAttribute('shape-rendering', 'crispEdges');\n svg.setAttribute('role', 'img');\n svg.setAttribute('aria-label', 'QR code of the wallet address ' + address);\n var light = document.createElementNS(ns, 'rect');\n light.setAttribute('class', 'qr-light');\n light.setAttribute('width', String(total));\n light.setAttribute('height', String(total));\n svg.appendChild(light);\n var d = '';\n for (var row = 0; row < modules.length; row++) {\n for (var col = 0; col < modules.length; col++) {\n if (modules[row][col] === 1) d += 'M' + (col + quiet) + ' ' + (row + quiet) + 'h1v1h-1z';\n }\n }\n var dark = document.createElementNS(ns, 'path');\n dark.setAttribute('class', 'qr-dark');\n dark.setAttribute('d', d);\n svg.appendChild(dark);\n return svg;\n }\n\n function show(children) {\n root.replaceChildren(el('div', { class: 'card' }, children));\n }\n\n function resultText(result) {\n return ((result && result.content) || [])\n .filter(function (item) { return item && item.type === 'text' && typeof item.text === 'string'; })\n .map(function (item) { return item.text; })\n .join('\\\\n');\n }\n\n function copyButton(address, target) {\n var state = el('span', { class: 'copy-state', 'aria-live': 'polite' });\n var button = el('button', { type: 'button', text: 'Copy address' });\n button.addEventListener('click', function () {\n function selectIt(message) {\n var range = document.createRange();\n range.selectNodeContents(target);\n var selection = window.getSelection();\n selection.removeAllRanges();\n selection.addRange(range);\n state.textContent = message;\n }\n if (navigator.clipboard && navigator.clipboard.writeText) {\n navigator.clipboard.writeText(address).then(\n function () { state.textContent = 'Copied'; },\n function () { selectIt('Selected. Press Ctrl+C to copy.'); }\n );\n } else {\n selectIt('Selected. Press Ctrl+C to copy.');\n }\n });\n return el('div', {}, [button, state]);\n }\n\n function render(result) {\n var structured = result && result.structuredContent;\n var facts = structured && (structured.tool === '${TOPUP_TOOL_NAME}' || structured.tool === '${SUBSCRIPTION_TOPUP_TOOL}') ? structured.facts : null;\n if (!result || result.isError || !facts) {\n show([el('h1', { text: 'Wallet top-up' }), el('p', { class: 'muted', text: resultText(result) || 'The wallet details did not arrive.' })]);\n return;\n }\n if (!facts.topup) {\n show([\n el('h1', { text: 'No payment wallet yet' }),\n el('p', { class: 'lead' }, ['Create one with ', el('code', { text: 'cia wallet create' }), ', then ask to top up again.'])\n ]);\n return;\n }\n var topup = facts.topup;\n if (!ADDRESS.test(topup.wallet_address)) {\n show([el('h1', { text: 'Wallet top-up' }), el('p', { class: 'muted', text: 'The wallet address is not a valid 0x address.' })]);\n return;\n }\n var address = el('code', { class: 'address', text: topup.wallet_address });\n show([\n el('h1', { text: 'Top up your Chain Insights wallet' }),\n el('p', { class: 'lead', text: 'Send ' + topup.token + ' on ' + topup.network_display + ' to this address.' }),\n el('div', { class: 'qr-tile' }, [qrSvg(topup.wallet_address)]),\n el('dl', {}, [\n el('dt', { text: 'Network' }), el('dd', { text: topup.network_display + ' (chain ID ' + topup.chain_id + ')' }),\n el('dt', { text: 'Token' }), el('dd', { text: topup.token }),\n el('dt', { text: 'Token contract' }), el('dd', {}, [el('code', { text: topup.token_contract })]),\n el('dt', { text: 'Wallet' }), el('dd', {}, [address, copyButton(topup.wallet_address, address)])\n ]),\n el('p', { class: 'note', text: 'This view shows an address only. It signs nothing and sends nothing.' })\n ]);\n }\n\n function applyContext(context) {\n if (!context) return;\n if (context.theme === 'light' || context.theme === 'dark') extApps.applyDocumentTheme(context.theme);\n var styles = context.styles;\n if (styles && styles.variables) extApps.applyHostStyleVariables(styles.variables);\n if (styles && styles.css && styles.css.fonts) extApps.applyHostFonts(styles.css.fonts);\n }\n\n // Until the host names its theme, follow the system; the host's theme wins.\n if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {\n document.documentElement.setAttribute('data-theme', 'dark');\n }\n\n var app = new extApps.App({ name: 'chain-insights-topup', version: '1.0.0' }, {});\n app.ontoolresult = function (result) { render(result); };\n app.onhostcontextchanged = function (change) { applyContext(change); };\n app.onteardown = function () { return {}; };\n app.connect(new extApps.PostMessageTransport(window.parent, window.parent)).then(\n function () { applyContext(app.getHostContext()); },\n function () {\n show([el('h1', { text: 'Wallet top-up' }), el('p', { class: 'muted', text: 'This view opens inside Claude, next to the ${TOPUP_TOOL_NAME} answer.' })]);\n }\n );\n})();\n`\n\n/**\n * The QR encoder as browser source: an expression that evaluates to\n * `qrModules`. A transform that keeps function names (esbuild keepNames, as\n * tsx runs it) wraps inner functions in `__name(...)`; the wrapper gives that\n * helper a do-nothing body so the source runs anywhere.\n */\nexport function qrModulesSource(): string {\n return `(function () { var __name = function (target) { return target; }; return (${qrModules.toString()}); })()`\n}\n\n/** The one self-contained HTML file of ui://chain-insights/topup. */\nexport function topupViewHtml(): string {\n const script = TOPUP_VIEW_SCRIPT.replace('__QR_MODULES__', () => qrModulesSource())\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>Wallet top-up</title>\n<style>${TOPUP_VIEW_STYLE}</style>\n</head>\n<body>\n<main id=\"topup\" aria-live=\"polite\"><div class=\"card\"><div class=\"skeleton\"></div><div class=\"skeleton short\"></div><p class=\"muted\">Waiting for the wallet details.</p></div></main>\n<script type=\"module\">${extAppsBrowserScript()}</script>\n<script type=\"module\">${script.replaceAll('</script', '<\\\\/script')}</script>\n</body>\n</html>`\n}\n\n/** The parts of McpServer the top-up view registers on. */\nexport type TopupViewServer = Pick<\n import('@modelcontextprotocol/sdk/server/mcp.js').McpServer,\n 'registerTool' | 'registerResource'\n>\n\nexport interface TopupViewDependencies {\n /** The local wallet address, or null when no wallet exists. Throws when it cannot be read. */\n walletAddress: () => Promise<string | null>\n /** The $CIA and ETH balances on Robinhood Chain, 'unknown' when no RPC answers. */\n robinhoodBalances?: (address: string) => Promise<{ cia: string; eth: string }>\n}\n\nasync function readRobinhoodBalances(address: string): Promise<{ cia: string; eth: string }> {\n const [cia, eth] = await Promise.all([getCiaBalance(address), getRobinhoodEthBalance(address)])\n return { cia, eth }\n}\n\n/**\n * Register the wallet_topup tool and its view on the local proxy. The tool\n * reads the local wallet address and nothing else.\n */\nexport async function registerTopupView(\n server: TopupViewServer,\n deps: TopupViewDependencies\n): Promise<void> {\n const { registerAppResource, registerAppTool, RESOURCE_MIME_TYPE } =\n await import('@modelcontextprotocol/ext-apps/server')\n const uiMeta = { prefersBorder: false, csp: { resourceDomains: ['https://assets.claude.ai'] } }\n\n registerAppTool(\n server,\n TOPUP_TOOL_NAME,\n {\n title: 'Top Up Wallet',\n description: WALLET_TOPUP_DESCRIPTION,\n annotations: {\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: false,\n },\n _meta: { ui: { resourceUri: TOPUP_VIEW_URI } },\n },\n async () => {\n try {\n return topupToolResult(await deps.walletAddress())\n } catch (err) {\n return {\n content: [{ type: 'text' as const, text: `Top-up failed: ${(err as Error).message}` }],\n isError: true,\n }\n }\n }\n )\n\n registerAppTool(\n server,\n SUBSCRIPTION_TOPUP_TOOL,\n {\n title: 'Top Up Subscription Wallet',\n description: SUBSCRIPTION_TOPUP_DESCRIPTION,\n annotations: {\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n // It reads the balances on Robinhood Chain.\n openWorldHint: true,\n },\n _meta: { ui: { resourceUri: TOPUP_VIEW_URI } },\n },\n async () => {\n try {\n const address = await deps.walletAddress()\n if (address === null) return subscriptionTopupToolResult(null)\n const balances = await (deps.robinhoodBalances ?? readRobinhoodBalances)(address)\n return subscriptionTopupToolResult(address, balances)\n } catch (err) {\n return {\n content: [{ type: 'text' as const, text: `Top-up failed: ${(err as Error).message}` }],\n isError: true,\n }\n }\n }\n )\n\n registerAppResource(\n server,\n 'Chain Insights wallet top-up',\n TOPUP_VIEW_URI,\n {\n description:\n 'The local wallet address and its QR code: on Base Mainnet with USDC, or on Robinhood Chain with $CIA.',\n _meta: { ui: uiMeta },\n },\n async () => ({\n contents: [\n {\n uri: TOPUP_VIEW_URI,\n mimeType: RESOURCE_MIME_TYPE,\n text: topupViewHtml(),\n _meta: { ui: uiMeta },\n },\n ],\n })\n )\n}\n"],"mappings":"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"}
|
|
@@ -421,11 +421,35 @@ async function startTopupServer(wallet, rail = "base-usdc") {
|
|
|
421
421
|
server.on("error", reject);
|
|
422
422
|
});
|
|
423
423
|
}
|
|
424
|
-
|
|
424
|
+
/**
|
|
425
|
+
* The text of the browser top-up page, per rail. The page is the wallet
|
|
426
|
+
* top-up page of `cia wallet topup` (aphex5, 2026-10-28: copy the wallet
|
|
427
|
+
* top-up page and change only Base Network to Robinhood Chain and $CIA):
|
|
428
|
+
* the logo, the background, the layout and the script never fork by rail.
|
|
429
|
+
*/
|
|
430
|
+
const ARTIFACT_PAGE_TEXT = {
|
|
431
|
+
"base-usdc": {
|
|
432
|
+
subtitle: "Fund your wallet with USDC on Base",
|
|
433
|
+
badge: "Base Network · USDC",
|
|
434
|
+
currency: "USDC",
|
|
435
|
+
balanceField: "balance_usdc",
|
|
436
|
+
gasNote: "Base ETH is used for one-time payment setup gas."
|
|
437
|
+
},
|
|
438
|
+
"robinhood-cia": {
|
|
439
|
+
subtitle: "Fund your wallet with $CIA on Robinhood Chain",
|
|
440
|
+
badge: "Robinhood Chain · $CIA",
|
|
441
|
+
currency: "$CIA",
|
|
442
|
+
balanceField: "balance_cia",
|
|
443
|
+
gasNote: "Robinhood Chain ETH is used for the buy transaction gas."
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
function generateArtifactHtml(walletAddressInput, topupUrl, rail = "base-usdc") {
|
|
447
|
+
const text = ARTIFACT_PAGE_TEXT[rail];
|
|
425
448
|
const walletAddress = assertWalletAddress(walletAddressInput);
|
|
426
449
|
const safeWalletAddress = escapeHtml(walletAddress);
|
|
427
450
|
const safeTopupUrl = escapeHtml(topupUrl);
|
|
428
451
|
const topupUrlJson = jsonForScript(topupUrl);
|
|
452
|
+
const qrSvg = generateQrSvg(walletAddress, { cellSize: 8 });
|
|
429
453
|
return `<!DOCTYPE html>
|
|
430
454
|
<html lang="en">
|
|
431
455
|
<head>
|
|
@@ -512,14 +536,14 @@ function generateArtifactHtml(walletAddressInput, topupUrl) {
|
|
|
512
536
|
<body>
|
|
513
537
|
<div class="card">
|
|
514
538
|
<div class="logo"><img src="${safeTopupUrl}/assets/logo.png" alt="Chain Insights"></div>
|
|
515
|
-
<p class="subtitle"
|
|
516
|
-
<div class="qr">${
|
|
539
|
+
<p class="subtitle">${text.subtitle}</p>
|
|
540
|
+
<div class="qr">${qrSvg}</div>
|
|
517
541
|
<div class="address-wrap">
|
|
518
542
|
<div class="address" id="addr" onclick="selectAndCopy()">${safeWalletAddress}</div>
|
|
519
543
|
<div class="address-hint" id="addrHint">Click to copy address</div>
|
|
520
544
|
</div>
|
|
521
|
-
<div class="badge"><span class="dot"></span
|
|
522
|
-
<p class="balance-line" id="balLine">Current balance: <span class="amount" id="bal">--</span>
|
|
545
|
+
<div class="badge"><span class="dot"></span>${text.badge}</div>
|
|
546
|
+
<p class="balance-line" id="balLine">Current balance: <span class="amount" id="bal">--</span> ${text.currency}<br>Gas balance: <span class="gas" id="gas">--</span> ETH<br>${text.gasNote}</p>
|
|
523
547
|
</div>
|
|
524
548
|
<script>
|
|
525
549
|
// MCP Apps protocol handshake for the wallet top-up page.
|
|
@@ -582,9 +606,9 @@ function fetchBal() {
|
|
|
582
606
|
var el = document.getElementById('bal');
|
|
583
607
|
var gas = document.getElementById('gas');
|
|
584
608
|
var line = document.getElementById('balLine');
|
|
585
|
-
var val = parseFloat(d.
|
|
609
|
+
var val = parseFloat(d.${text.balanceField} || '0').toFixed(2);
|
|
586
610
|
var gasVal = d.balance_eth === 'unknown' ? '--' : parseFloat(d.balance_eth || '0').toFixed(6);
|
|
587
|
-
if (d.
|
|
611
|
+
if (d.${text.balanceField} === 'unknown') { el.textContent = '--'; gas.textContent = gasVal; return; }
|
|
588
612
|
el.textContent = val;
|
|
589
613
|
gas.textContent = gasVal;
|
|
590
614
|
if (lastBal !== null && val !== lastBal) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const require_rolldown_runtime = require("./rolldown-runtime-C0BPl7ul.cjs");
|
|
2
|
-
const require_topup_server = require("./topup-server-
|
|
2
|
+
const require_topup_server = require("./topup-server-ShuYSyBY.cjs");
|
|
3
3
|
let viem = require("viem");
|
|
4
4
|
let node_http = require("node:http");
|
|
5
5
|
//#region src/wallet/topup-server.ts
|
|
@@ -70,10 +70,15 @@ async function stopTopupServer() {
|
|
|
70
70
|
artifactServerState = null;
|
|
71
71
|
await new Promise((resolve) => server.close(() => resolve()));
|
|
72
72
|
}
|
|
73
|
-
|
|
73
|
+
/**
|
|
74
|
+
* The browser top-up page of `cia wallet topup` (rail base-usdc) and of
|
|
75
|
+
* `cia subscription topup` (rail robinhood-cia): one page, only its text
|
|
76
|
+
* differs by rail.
|
|
77
|
+
*/
|
|
78
|
+
async function startTopupServer(account, rail = "base-usdc") {
|
|
74
79
|
const walletAddress = toWalletAddress(account);
|
|
75
|
-
if (artifactServerState && artifactServerState.address.toLowerCase() === walletAddress.toLowerCase()) return artifactServerState.url;
|
|
76
|
-
const assetServerUrl = await require_topup_server.startTopupServer(walletAddress);
|
|
80
|
+
if (artifactServerState && artifactServerState.rail === rail && artifactServerState.address.toLowerCase() === walletAddress.toLowerCase()) return artifactServerState.url;
|
|
81
|
+
const assetServerUrl = await require_topup_server.startTopupServer(walletAddress, rail);
|
|
77
82
|
if (artifactServerState) await stopTopupServer();
|
|
78
83
|
const server = (0, node_http.createServer)((req, res) => {
|
|
79
84
|
const reqUrl = req.url ?? "/";
|
|
@@ -91,7 +96,7 @@ async function startTopupServer(account) {
|
|
|
91
96
|
const { pathname } = proxyTarget;
|
|
92
97
|
if (pathname === "/" || pathname === "/index.html") {
|
|
93
98
|
const artifactUrl = artifactServerState?.url ?? assetServerUrl;
|
|
94
|
-
send(res, 200, require_topup_server.generateArtifactHtml(walletAddress, artifactUrl), "text/html; charset=utf-8");
|
|
99
|
+
send(res, 200, require_topup_server.generateArtifactHtml(walletAddress, artifactUrl, rail), "text/html; charset=utf-8");
|
|
95
100
|
return;
|
|
96
101
|
}
|
|
97
102
|
if (pathname.startsWith("/assets/") || pathname.startsWith("/api/")) {
|
|
@@ -119,6 +124,7 @@ async function startTopupServer(account) {
|
|
|
119
124
|
});
|
|
120
125
|
artifactServerState = {
|
|
121
126
|
address: walletAddress,
|
|
127
|
+
rail,
|
|
122
128
|
assetServerUrl,
|
|
123
129
|
server,
|
|
124
130
|
url
|
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"topup-C-Tczr2k.mjs","names":[],"sources":["../src/subscription/topup.ts"],"sourcesContent":["import { qrTerminalText } from '../wallet/mcp-proxy/qr.js'\nimport { SUBSCRIPTION_TOPUP_TEXT, startTopupServer } from '../wallet/mcp-proxy/topup-server.js'\nimport {\n CIA_SYMBOL,\n CIA_TOKEN_ADDRESS,\n ROBINHOOD_CHAIN_ID,\n ROBINHOOD_NETWORK_DISPLAY,\n} from '../wallet/robinhood.js'\n\nexport interface SubscriptionTopupFacts {\n wallet_address: string\n network: typeof ROBINHOOD_NETWORK_DISPLAY\n chain_id: typeof ROBINHOOD_CHAIN_ID\n token: typeof CIA_SYMBOL\n token_contract: typeof CIA_TOKEN_ADDRESS\n gas_token: 'ETH'\n topup_url?: string\n}\n\nexport function subscriptionTopupFacts(walletAddress: string, topupUrl?: string): SubscriptionTopupFacts {\n return {\n wallet_address: walletAddress,\n network: ROBINHOOD_NETWORK_DISPLAY,\n chain_id: ROBINHOOD_CHAIN_ID,\n token: CIA_SYMBOL,\n token_contract: CIA_TOKEN_ADDRESS,\n gas_token: 'ETH',\n ...(topupUrl ? { topup_url: topupUrl } : {}),\n }\n}\n\n/** The terminal text of `cia subscription topup`: facts, then the QR code. */\nexport function subscriptionTopupText(facts: SubscriptionTopupFacts): string {\n return [\n ...(facts.topup_url ? [`Top-up page: ${facts.topup_url}`] : []),\n `Wallet: ${facts.wallet_address}`,\n `Network: ${facts.network} (chain ID ${facts.chain_id})`,\n `Token: ${facts.token} ${facts.token_contract}, and a little ETH for gas`,\n '',\n qrTerminalText(facts.wallet_address),\n '',\n SUBSCRIPTION_TOPUP_TEXT,\n ].join('\\n')\n}\n\n/** `cia subscription topup`: the user's own wallet, never the payment address. */\nexport async function runSubscriptionTopup(opts: { open?: boolean; json?: boolean }): Promise<void> {\n const { assertWalletKeyNotPublic, getWalletAccount } = await import('../wallet/tools.js')\n const account = await getWalletAccount()\n assertWalletKeyNotPublic(account)\n const url = await startTopupServer(account.address, 'robinhood-cia')\n const facts = subscriptionTopupFacts(account.address, url)\n console.log(opts.json ? JSON.stringify(facts, null, 2) : subscriptionTopupText(facts))\n if (!opts.json) console.log('Press Ctrl+C to stop the top-up page.')\n if (opts.open !== false) {\n const open = (await import('open')).default\n await open(url)\n }\n}\n"],"mappings":";;;AAmBA,SAAgB,uBAAuB,eAAuB,UAA2C;CACvG,OAAO;EACL,gBAAgB;EAChB,SAAS;EACT,UAAU;EACV,OAAO;EACP,gBAAgB;EAChB,WAAW;EACX,GAAI,WAAW,EAAE,WAAW,SAAS,IAAI,CAAC;CAC5C;AACF;;AAGA,SAAgB,sBAAsB,OAAuC;CAC3E,OAAO;EACL,GAAI,MAAM,YAAY,CAAC,gBAAgB,MAAM,WAAW,IAAI,CAAC;EAC7D,gBAAgB,MAAM;EACtB,gBAAgB,MAAM,QAAQ,aAAa,MAAM,SAAS;EAC1D,gBAAgB,MAAM,MAAM,GAAG,MAAM,eAAe;EACpD;EACA,eAAe,MAAM,cAAc;EACnC;EACA;CACF,CAAC,CAAC,KAAK,IAAI;AACb;;AAGA,eAAsB,qBAAqB,MAAyD;CAClG,MAAM,EAAE,0BAA0B,qBAAqB,MAAM,OAAO,uBAAqB,CAAA,MAAA,MAAA,EAAA,CAAA;CACzF,MAAM,UAAU,MAAM,iBAAiB;CACvC,yBAAyB,OAAO;CAChC,MAAM,MAAM,MAAM,iBAAiB,QAAQ,SAAS,eAAe;CACnE,MAAM,QAAQ,uBAAuB,QAAQ,SAAS,GAAG;CACzD,QAAQ,IAAI,KAAK,OAAO,KAAK,UAAU,OAAO,MAAM,CAAC,IAAI,sBAAsB,KAAK,CAAC;CACrF,IAAI,CAAC,KAAK,MAAM,QAAQ,IAAI,uCAAuC;CACnE,IAAI,KAAK,SAAS,OAAO;EACvB,MAAM,QAAQ,MAAM,OAAO,QAAA,CAAS;EACpC,MAAM,KAAK,GAAG;CAChB;AACF"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"topup-server-CSF5EjNt.mjs","names":["USDC_ADDRESS"],"sources":["../src/wallet/mcp-proxy/qr.ts","../src/wallet/mcp-proxy/tools.ts","../src/wallet/mcp-proxy/topup-server.ts"],"sourcesContent":["/**\n * Minimal QR code generator. No external dependencies.\n *\n * Byte mode, error correction level L, versions 1 to 4 (single block), mask 0.\n * Version 3 holds a 42-character EVM address; version 4 holds up to 78 bytes.\n *\n * `qrModules` is self-contained on purpose: the top-up view inlines its source\n * (`qrModules.toString()`) and draws the same QR code inside the Claude view,\n * so the function must not reference anything outside its own body.\n */\n\n/**\n * The QR code modules for `text`: a square matrix, 1 for a dark module and 0\n * for a light one, without the quiet zone. Throws when `text` is longer than\n * 78 UTF-8 bytes.\n */\nexport function qrModules(text: string): number[][] {\n // Level L, single-block versions: [version, total codewords, EC codewords].\n const versions = [\n [1, 26, 7],\n [2, 44, 10],\n [3, 70, 15],\n [4, 100, 20],\n ]\n // Format information for level L and mask 0, BCH-coded and masked.\n const formatBits = 0x77c4\n const bytes = Array.from(new TextEncoder().encode(text))\n\n let version = 0\n let totalCodewords = 0\n let ecCodewords = 0\n for (const entry of versions) {\n const dataCodewords = entry[1] - entry[2]\n if (4 + 8 + bytes.length * 8 <= dataCodewords * 8) {\n version = entry[0]\n totalCodewords = entry[1]\n ecCodewords = entry[2]\n break\n }\n }\n if (version === 0) throw new Error('QR text is too long: at most 78 bytes')\n\n const dataCodewords = totalCodewords - ecCodewords\n const size = version * 4 + 17\n\n // Data bits: mode, count, bytes, terminator, byte padding, pad codewords.\n const bits: number[] = []\n const pushBits = (value: number, length: number) => {\n for (let i = length - 1; i >= 0; i--) bits.push((value >>> i) & 1)\n }\n pushBits(0b0100, 4)\n pushBits(bytes.length, 8)\n for (const value of bytes) pushBits(value, 8)\n const capacity = dataCodewords * 8\n pushBits(0, Math.min(4, capacity - bits.length))\n while (bits.length % 8 !== 0) bits.push(0)\n for (let pad = 0xec; bits.length < capacity; pad ^= 0xec ^ 0x11) pushBits(pad, 8)\n const data: number[] = []\n for (let i = 0; i < bits.length; i += 8) {\n let value = 0\n for (let j = 0; j < 8; j++) value = (value << 1) | bits[i + j]\n data.push(value)\n }\n\n // Reed-Solomon error correction over GF(256), polynomial 0x11d.\n const exp: number[] = []\n const log: number[] = []\n for (let i = 0, x = 1; i < 255; i++) {\n exp[i] = x\n log[x] = i\n x <<= 1\n if (x & 0x100) x ^= 0x11d\n }\n const multiply = (a: number, b: number) => (a === 0 || b === 0 ? 0 : exp[(log[a] + log[b]) % 255])\n let generator = [1]\n for (let i = 0; i < ecCodewords; i++) {\n const next = new Array(generator.length + 1).fill(0)\n for (let j = 0; j < generator.length; j++) {\n next[j] ^= generator[j]\n next[j + 1] ^= multiply(generator[j], exp[i])\n }\n generator = next\n }\n const remainder = [...data, ...new Array(ecCodewords).fill(0)]\n for (let i = 0; i < data.length; i++) {\n const factor = remainder[i]\n if (factor === 0) continue\n for (let j = 0; j < generator.length; j++) remainder[i + j] ^= multiply(generator[j], factor)\n }\n const codewords = [...data, ...remainder.slice(data.length)]\n\n // Function patterns. `reserved` marks every module data may not use.\n const modules: number[][] = []\n const reserved: boolean[][] = []\n for (let row = 0; row < size; row++) {\n modules.push(new Array(size).fill(0))\n reserved.push(new Array(size).fill(false))\n }\n const setFunction = (row: number, col: number, dark: boolean) => {\n modules[row][col] = dark ? 1 : 0\n reserved[row][col] = true\n }\n for (let i = 0; i < size; i++) {\n setFunction(6, i, i % 2 === 0)\n setFunction(i, 6, i % 2 === 0)\n }\n const finder = (centerRow: number, centerCol: number) => {\n for (let dr = -4; dr <= 4; dr++) {\n for (let dc = -4; dc <= 4; dc++) {\n const row = centerRow + dr\n const col = centerCol + dc\n if (row < 0 || row >= size || col < 0 || col >= size) continue\n const distance = Math.max(Math.abs(dr), Math.abs(dc))\n setFunction(row, col, distance !== 2 && distance !== 4)\n }\n }\n }\n finder(3, 3)\n finder(3, size - 4)\n finder(size - 4, 3)\n if (version >= 2) {\n const center = size - 7\n for (let dr = -2; dr <= 2; dr++) {\n for (let dc = -2; dc <= 2; dc++) {\n setFunction(center + dr, center + dc, Math.max(Math.abs(dr), Math.abs(dc)) !== 1)\n }\n }\n }\n const formatBit = (i: number) => ((formatBits >>> i) & 1) === 1\n for (let i = 0; i <= 5; i++) setFunction(i, 8, formatBit(i))\n setFunction(7, 8, formatBit(6))\n setFunction(8, 8, formatBit(7))\n setFunction(8, 7, formatBit(8))\n for (let i = 9; i < 15; i++) setFunction(8, 14 - i, formatBit(i))\n for (let i = 0; i < 8; i++) setFunction(8, size - 1 - i, formatBit(i))\n for (let i = 8; i < 15; i++) setFunction(size - 15 + i, 8, formatBit(i))\n setFunction(size - 8, 8, true)\n\n // Data in the zigzag order, then mask 0 on every data module.\n let bitIndex = 0\n for (let right = size - 1; right >= 1; right -= 2) {\n if (right === 6) right = 5\n const upward = ((right + 1) & 2) === 0\n for (let step = 0; step < size; step++) {\n const row = upward ? size - 1 - step : step\n for (let j = 0; j < 2; j++) {\n const col = right - j\n if (reserved[row][col]) continue\n if (bitIndex < codewords.length * 8) {\n modules[row][col] = (codewords[bitIndex >>> 3] >>> (7 - (bitIndex & 7))) & 1\n bitIndex++\n }\n if ((row + col) % 2 === 0) modules[row][col] ^= 1\n }\n }\n }\n return modules\n}\n\nexport interface QrOptions {\n cellSize?: number\n fgColor?: string\n bgColor?: string\n finderColor?: string\n logoBase64?: string // data URI for center logo\n logoWidth?: number // logo width in modules (default: 7)\n logoHeight?: number // logo height in modules (default: 5)\n}\n\nexport function generateQrSvg(text: string, opts: QrOptions | number = 4): string {\n // Backward compat: accept bare cellSize number\n const options: QrOptions = typeof opts === 'number' ? { cellSize: opts } : opts\n const cellSize = options.cellSize ?? 4\n const fgColor = options.fgColor ?? '#000'\n const bgColor = options.bgColor ?? '#fff'\n const finderColor = options.finderColor ?? fgColor\n\n const matrix = qrModules(text)\n const size = matrix.length\n\n // Logo exclusion zone (center of QR)\n const logoW = options.logoWidth ?? 7\n const logoH = options.logoHeight ?? 5\n const logoStartC = Math.floor((size - logoW) / 2)\n const logoStartR = Math.floor((size - logoH) / 2)\n const hasLogo = !!options.logoBase64\n\n const svgSize = size * cellSize\n let svg = `<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"${svgSize}\" height=\"${svgSize}\" viewBox=\"0 0 ${svgSize} ${svgSize}\">`\n svg += `<rect width=\"${svgSize}\" height=\"${svgSize}\" fill=\"${bgColor}\" rx=\"4\"/>`\n\n const isFinderModule = (r: number, c: number): boolean =>\n (r < 7 && c < 7) || (r < 7 && c >= size - 7) || (r >= size - 7 && c < 7)\n\n for (let r = 0; r < size; r++) {\n for (let c = 0; c < size; c++) {\n if (\n hasLogo &&\n r >= logoStartR &&\n r < logoStartR + logoH &&\n c >= logoStartC &&\n c < logoStartC + logoW\n ) {\n continue\n }\n if (matrix[r][c] === 1) {\n const color = isFinderModule(r, c) ? finderColor : fgColor\n svg += `<rect x=\"${c * cellSize}\" y=\"${r * cellSize}\" width=\"${cellSize}\" height=\"${cellSize}\" fill=\"${color}\" rx=\"0.5\"/>`\n }\n }\n }\n\n if (hasLogo && options.logoBase64) {\n const lx = logoStartC * cellSize\n const ly = logoStartR * cellSize\n const lw = logoW * cellSize\n const lh = logoH * cellSize\n svg += `<rect x=\"${lx - 1}\" y=\"${ly - 1}\" width=\"${lw + 2}\" height=\"${lh + 2}\" fill=\"${bgColor}\" rx=\"3\"/>`\n svg += `<image x=\"${lx + 2}\" y=\"${ly + 2}\" width=\"${lw - 4}\" height=\"${lh - 4}\" href=\"${options.logoBase64}\" xlink:href=\"${options.logoBase64}\" preserveAspectRatio=\"xMidYMid meet\"/>`\n }\n\n svg += '</svg>'\n return svg\n}\n\nexport interface QrTerminalOptions {\n /** Quiet zone in modules on each side (default 4, the QR standard). */\n quietZone?: number\n /** Draw light modules as blocks instead of dark ones, for a light-on-dark read. */\n invert?: boolean\n}\n\n/**\n * The QR code of `text` as terminal text. Each character covers two module\n * rows with half blocks (▀ ▄ █ and space), so the code keeps its square shape.\n * A block is a dark module; the quiet zone is light.\n */\nexport function qrTerminalText(text: string, options: QrTerminalOptions = {}): string {\n const quiet = options.quietZone ?? 4\n const modules = qrModules(text)\n const size = modules.length + quiet * 2\n const dark = (row: number, col: number): boolean => {\n const r = row - quiet\n const c = col - quiet\n const on = r >= 0 && c >= 0 && r < modules.length && c < modules.length && modules[r][c] === 1\n return options.invert ? !on : on\n }\n const lines: string[] = []\n for (let row = 0; row < size; row += 2) {\n let line = ''\n for (let col = 0; col < size; col++) {\n const top = dark(row, col)\n const bottom = row + 1 < size ? dark(row + 1, col) : options.invert === true\n line += top && bottom ? '█' : top ? '▀' : bottom ? '▄' : ' '\n }\n lines.push(line)\n }\n return lines.join('\\n')\n}\n","import type { WalletData } from './types.js'\nimport { createPublicClient, http, formatEther, formatUnits } from 'viem'\nimport { base } from 'viem/chains'\n\nconst USDC_ADDRESS = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913' as const\nconst DEFAULT_BASE_RPC_URL = 'https://mainnet.base.org'\nconst PUBLIC_BASE_RPC_URLS = [\n DEFAULT_BASE_RPC_URL,\n 'https://base-rpc.publicnode.com',\n 'https://base.drpc.org',\n 'https://1rpc.io/base',\n] as const\nconst USDC_ABI = [\n {\n name: 'balanceOf',\n type: 'function',\n stateMutability: 'view',\n inputs: [{ name: 'account', type: 'address' }],\n outputs: [{ name: '', type: 'uint256' }],\n },\n] as const\n\nfunction walletAddress(wallet: WalletData | string): `0x${string}` {\n return (typeof wallet === 'string' ? wallet : wallet.address) as `0x${string}`\n}\n\nexport async function getBalanceUsdc(wallet: WalletData | string): Promise<string> {\n const envRpcUrl = process.env.BASE_RPC_URL\n const rpcUrls = [\n ...(envRpcUrl ? [envRpcUrl] : []),\n ...PUBLIC_BASE_RPC_URLS.filter((url) => url !== envRpcUrl),\n ]\n\n for (const rpcUrl of rpcUrls) {\n try {\n const client = createPublicClient({ chain: base, transport: http(rpcUrl) })\n const balance = await client.readContract({\n address: USDC_ADDRESS,\n abi: USDC_ABI,\n functionName: 'balanceOf',\n args: [walletAddress(wallet)],\n })\n return formatUnits(balance, 6)\n } catch {\n // Try the next public Base RPC endpoint.\n }\n }\n\n return 'unknown'\n}\n\nexport async function getBalanceEth(wallet: WalletData | string): Promise<string> {\n const envRpcUrl = process.env.BASE_RPC_URL\n const rpcUrls = [\n ...(envRpcUrl ? [envRpcUrl] : []),\n ...PUBLIC_BASE_RPC_URLS.filter((url) => url !== envRpcUrl),\n ]\n\n for (const rpcUrl of rpcUrls) {\n try {\n const client = createPublicClient({ chain: base, transport: http(rpcUrl) })\n const balance = await client.getBalance({ address: walletAddress(wallet) })\n return formatEther(balance)\n } catch {\n // Try the next public Base RPC endpoint.\n }\n }\n\n return 'unknown'\n}\n\nexport async function getBalance(wallet: WalletData): Promise<string> {\n const [balanceUsdc, balanceEth] = await Promise.all([\n getBalanceUsdc(wallet),\n getBalanceEth(wallet),\n ])\n return [\n `Payment wallet: ${wallet.address}`,\n `USDC on Base: ${balanceUsdc}`,\n `Gas on Base: ${balanceEth} ETH`,\n `Payment network: Base`,\n `Base ETH is used only for one-time payment setup gas.`,\n ]\n .filter(Boolean)\n .join('\\n')\n}\n\nexport function getTopupInfo(wallet: WalletData): string {\n return JSON.stringify({\n wallet_address: wallet.address,\n network: 'Base (Chain ID 8453)',\n token: 'USDC',\n contract: USDC_ADDRESS,\n instructions: [`Send USDC on Base network to: ${wallet.address}`],\n })\n}\n","import { createServer, type Server } from 'node:http'\nimport { readFileSync } from 'node:fs'\nimport { createRequire } from 'node:module'\nimport { join, dirname } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { isAddress } from 'viem'\nimport type { WalletData } from './types.js'\nimport { generateQrSvg, qrModules } from './qr.js'\nimport { getBalanceEth, getBalanceUsdc } from './tools.js'\nimport {\n CIA_SYMBOL,\n CIA_TOKEN_ADDRESS,\n ROBINHOOD_CHAIN_ID,\n ROBINHOOD_NETWORK_DISPLAY,\n getCiaBalance,\n getRobinhoodEthBalance,\n} from '../robinhood.js'\nimport {\n SUBSCRIPTION_TOPUP_DESCRIPTION,\n SUBSCRIPTION_TOPUP_TOOL,\n WALLET_TOPUP_DESCRIPTION,\n} from '../../subscription/rules.js'\n\nconst USDC_ADDRESS = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913'\nconst BASE_CHAIN_ID = '0x2105'\n/** Robinhood Chain, chain ID 4663. */\nconst ROBINHOOD_CHAIN_ID_HEX = '0x1237'\n\nconst __dirname = dirname(fileURLToPath(import.meta.url))\n\n// Pre-load assets — try dist/assets first (installed), then src/assets (dev)\nfunction loadAsset(name: string): Buffer {\n const paths = [\n join(__dirname, 'assets', name), // dist/assets/ (global install)\n join(__dirname, '..', 'src', 'assets', name), // src/assets/ (dev)\n ]\n for (const p of paths) {\n try {\n return readFileSync(p)\n } catch {\n /* try next */\n }\n }\n console.error(`[chain-insights] Warning: asset ${name} not found`)\n return Buffer.alloc(0)\n}\n\nconst logoPng = loadAsset('logo.png')\nconst bgPatternPng = loadAsset('bg-pattern.png')\n\n/**\n * The top-up rails. base-usdc funds pay-per-call queries with USDC on Base;\n * robinhood-cia funds the subscription with $CIA and ETH on Robinhood Chain.\n * One local server per rail.\n */\nexport type TopupRail = 'base-usdc' | 'robinhood-cia'\n\nconst servers = new Map<TopupRail, { server: Server; port: number }>()\n\nfunction assertWalletAddress(wallet: WalletData | string): string {\n const walletAddress = typeof wallet === 'string' ? wallet : wallet.address\n if (!isAddress(walletAddress)) {\n throw new Error('Wallet address must be a valid 0x-prefixed 20-byte EVM address')\n }\n return walletAddress\n}\n\nfunction escapeHtml(value: string): string {\n return value\n .replaceAll('&', '&')\n .replaceAll('<', '<')\n .replaceAll('>', '>')\n .replaceAll('\"', '"')\n .replaceAll(\"'\", ''')\n}\n\nfunction jsonForScript(value: string): string {\n return JSON.stringify(value)\n .replaceAll('<', '\\\\u003c')\n .replaceAll('>', '\\\\u003e')\n .replaceAll('&', '\\\\u0026')\n}\n\nexport function getTopupUrl(rail: TopupRail = 'base-usdc'): string | null {\n const running = servers.get(rail)\n return running ? `http://localhost:${running.port}` : null\n}\n\nasync function railBalance(rail: TopupRail, walletAddress: string): Promise<Record<string, string>> {\n if (rail === 'robinhood-cia') {\n const [balanceCia, balanceEth] = await Promise.all([\n getCiaBalance(walletAddress),\n getRobinhoodEthBalance(walletAddress),\n ])\n return { balance_cia: balanceCia, balance_eth: balanceEth }\n }\n const [balanceUsdc, balanceEth] = await Promise.all([\n getBalanceUsdc(walletAddress),\n getBalanceEth(walletAddress),\n ])\n return { balance_usdc: balanceUsdc, balance_eth: balanceEth }\n}\n\nconst UNKNOWN_BALANCE: Record<TopupRail, Record<string, string>> = {\n 'base-usdc': { balance_usdc: 'unknown', balance_eth: 'unknown' },\n 'robinhood-cia': { balance_cia: 'unknown', balance_eth: 'unknown' },\n}\n\nexport async function startTopupServer(\n wallet: WalletData | string,\n rail: TopupRail = 'base-usdc'\n): Promise<string> {\n const walletAddress = assertWalletAddress(wallet)\n\n const running = getTopupUrl(rail)\n if (running) {\n return running\n }\n\n return new Promise((resolve, reject) => {\n const server = createServer((req, res) => {\n if (req.url === '/api/wallet') {\n res.writeHead(200, {\n 'Content-Type': 'application/json',\n 'Access-Control-Allow-Origin': '*',\n })\n res.end(JSON.stringify({ address: walletAddress }))\n return\n }\n\n if (req.url === '/api/balance') {\n railBalance(rail, walletAddress)\n .then((balance) => {\n res.writeHead(200, {\n 'Content-Type': 'application/json',\n 'Access-Control-Allow-Origin': '*',\n })\n res.end(JSON.stringify(balance))\n })\n .catch(() => {\n res.writeHead(200, {\n 'Content-Type': 'application/json',\n 'Access-Control-Allow-Origin': '*',\n })\n res.end(JSON.stringify(UNKNOWN_BALANCE[rail]))\n })\n return\n }\n\n if (req.url === '/assets/logo.png') {\n res.writeHead(200, {\n 'Content-Type': 'image/png',\n 'Cache-Control': 'public, max-age=86400',\n 'Access-Control-Allow-Origin': '*',\n })\n res.end(logoPng)\n return\n }\n\n if (req.url === '/assets/bg-pattern.png') {\n res.writeHead(200, {\n 'Content-Type': 'image/png',\n 'Cache-Control': 'public, max-age=86400',\n 'Access-Control-Allow-Origin': '*',\n })\n res.end(bgPatternPng)\n return\n }\n\n res.writeHead(200, { 'Content-Type': 'text/html' })\n res.end(rail === 'robinhood-cia' ? generateSubscriptionPage(walletAddress) : generatePage(walletAddress))\n })\n\n server.listen(0, '127.0.0.1', () => {\n const addr = server.address()\n if (addr && typeof addr === 'object') {\n servers.set(rail, { server, port: addr.port })\n const url = `http://localhost:${addr.port}`\n console.error(`[chain-insights] Topup server running at ${url}`)\n resolve(url)\n } else {\n reject(new Error('Failed to start topup server'))\n }\n })\n\n server.on('error', reject)\n })\n}\n\nexport function generateArtifactHtml(walletAddressInput: string, topupUrl: string): string {\n const walletAddress = assertWalletAddress(walletAddressInput)\n const safeWalletAddress = escapeHtml(walletAddress)\n const safeTopupUrl = escapeHtml(topupUrl)\n const topupUrlJson = jsonForScript(topupUrl)\n\n const LOGO_B64 =\n 'data:image/png;base64,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'\n\n // Generate QR code SVG server-side — standard B&W, 2x size\n const qrSvg = generateQrSvg(walletAddress, { cellSize: 8 })\n\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<style>\n * { margin: 0; padding: 0; box-sizing: border-box; }\n body {\n font-family: Inter, -apple-system, BlinkMacSystemFont, sans-serif;\n background: rgba(10, 12, 17, 1) url('${safeTopupUrl}/assets/bg-pattern.png') top left / contain no-repeat;\n color: rgba(255, 255, 255, 0.9);\n display: flex;\n justify-content: center;\n padding: 24px;\n line-height: 1.3;\n }\n .card {\n max-width: 400px;\n width: 100%;\n background: rgba(19, 19, 24, 1);\n border: 1px solid rgba(255, 255, 255, 0.06);\n border-radius: 12px;\n padding: 28px 24px;\n text-align: center;\n }\n .logo { margin-bottom: 8px; }\n .logo img { height: 24px; }\n .subtitle { color: rgba(255, 255, 255, 0.5); font-size: 13px; margin-bottom: 20px; }\n .qr { margin-bottom: 16px; }\n .qr svg { border-radius: 12px; background: #fff; padding: 10px; }\n .address-wrap {\n position: relative;\n margin-bottom: 16px;\n }\n .address {\n width: 100%;\n background: rgba(10, 12, 17, 1);\n border: 1px solid rgba(255, 255, 255, 0.06);\n border-radius: 8px;\n padding: 10px 14px;\n font-family: 'SF Mono', 'Fira Code', monospace;\n font-size: 11px; color: #f2dda6; word-break: break-all;\n cursor: pointer; text-align: center;\n transition: border-color 0.3s linear;\n -webkit-user-select: all; user-select: all;\n outline: none;\n }\n .address:focus { border-color: #ae9d71; }\n .address-hint {\n font-size: 10px; color: rgba(255,255,255,0.3);\n text-align: center; margin-top: 4px;\n }\n .badge {\n display: inline-flex; align-items: center; gap: 6px;\n background: rgba(255, 255, 255, 0.04);\n border: 1px solid rgba(255, 255, 255, 0.06);\n border-radius: 20px; padding: 5px 12px;\n font-size: 11px; color: rgba(255, 255, 255, 0.5); margin-bottom: 20px;\n }\n .badge .dot { width: 7px; height: 7px; border-radius: 50%; background: #f2dda6; }\n .balance-line {\n font-size: 13px;\n color: rgba(255, 255, 255, 0.5);\n margin-bottom: 20px;\n line-height: 1.4;\n }\n .balance-line .amount {\n color: #4feb69;\n font-weight: 600;\n }\n .balance-line .gas {\n color: #f2dda6;\n font-weight: 600;\n }\n @keyframes flash { 0%{opacity:0.4} 100%{opacity:1} }\n .balance-line.flash .amount { animation: flash 1s ease-out; }\n .hint {\n margin-top: 14px; font-size: 12px;\n color: rgba(255, 255, 255, 0.3);\n line-height: 1.5;\n }\n</style>\n</head>\n<body>\n<div class=\"card\">\n <div class=\"logo\"><img src=\"${safeTopupUrl}/assets/logo.png\" alt=\"Chain Insights\"></div>\n <p class=\"subtitle\">Fund your wallet with USDC on Base</p>\n <div class=\"qr\">${qrSvg}</div>\n <div class=\"address-wrap\">\n <div class=\"address\" id=\"addr\" onclick=\"selectAndCopy()\">${safeWalletAddress}</div>\n <div class=\"address-hint\" id=\"addrHint\">Click to copy address</div>\n </div>\n <div class=\"badge\"><span class=\"dot\"></span>Base Network · USDC</div>\n <p class=\"balance-line\" id=\"balLine\">Current balance: <span class=\"amount\" id=\"bal\">--</span> USDC<br>Gas balance: <span class=\"gas\" id=\"gas\">--</span> ETH<br>Base ETH is used for one-time payment setup gas.</p>\n</div>\n<script>\n// MCP Apps protocol handshake for the wallet top-up page.\n(function() {\n var initId = 1;\n\n // Listen for host messages\n window.addEventListener('message', function(event) {\n var data = event.data;\n if (!data || data.jsonrpc !== '2.0') return;\n\n // Initialize response\n if (data.id === initId && data.result) {\n // Send initialized notification\n window.parent.postMessage({\n jsonrpc: '2.0',\n method: 'ui/notifications/initialized',\n params: {}\n }, '*');\n\n // Send initial size\n var rect = document.documentElement.getBoundingClientRect();\n window.parent.postMessage({\n jsonrpc: '2.0',\n method: 'ui/notifications/size-changed',\n params: { width: Math.ceil(rect.width), height: Math.ceil(rect.height) }\n }, '*');\n }\n\n // Respond to pings\n if (data.method === 'ping' && data.id != null) {\n window.parent.postMessage({\n jsonrpc: '2.0',\n id: data.id,\n result: {}\n }, '*');\n }\n });\n\n // Send initialize request (must match App class protocol)\n window.parent.postMessage({\n jsonrpc: '2.0',\n id: initId,\n method: 'ui/initialize',\n params: {\n appInfo: { name: 'Chain Insights Topup', version: '1.0.0' },\n appCapabilities: {},\n protocolVersion: '2026-01-26'\n }\n }, '*');\n})();\n\n// Live balance polling\nvar lastBal = null;\nvar TOPUP_URL = ${topupUrlJson};\nfunction fetchBal() {\n fetch(TOPUP_URL + '/api/balance')\n .then(function(r) { return r.json(); })\n .then(function(d) {\n var el = document.getElementById('bal');\n var gas = document.getElementById('gas');\n var line = document.getElementById('balLine');\n var val = parseFloat(d.balance_usdc || '0').toFixed(2);\n var gasVal = d.balance_eth === 'unknown' ? '--' : parseFloat(d.balance_eth || '0').toFixed(6);\n if (d.balance_usdc === 'unknown') { el.textContent = '--'; gas.textContent = gasVal; return; }\n el.textContent = val;\n gas.textContent = gasVal;\n if (lastBal !== null && val !== lastBal) {\n line.classList.remove('flash');\n void line.offsetWidth;\n line.classList.add('flash');\n }\n lastBal = val;\n })\n .catch(function() {});\n}\nfetchBal();\nsetInterval(fetchBal, 10000);\n\nfunction selectAndCopy() {\n var addr = document.getElementById('addr');\n var hint = document.getElementById('addrHint');\n // Select the text\n var r = document.createRange();\n r.selectNodeContents(addr);\n var s = window.getSelection();\n s.removeAllRanges();\n s.addRange(r);\n // Try every clipboard method available\n var copied = false;\n try { copied = document.execCommand('copy'); } catch(e) {}\n if (!copied && navigator.clipboard && navigator.clipboard.writeText) {\n navigator.clipboard.writeText(addr.textContent).then(function() {\n hint.textContent = 'Copied!'; hint.style.color = '#4feb69';\n setTimeout(function() { hint.textContent = 'Click to copy address'; hint.style.color = ''; }, 2000);\n }).catch(function() {});\n }\n if (copied) {\n hint.textContent = 'Copied!'; hint.style.color = '#4feb69';\n setTimeout(function() { hint.textContent = 'Click to copy address'; hint.style.color = ''; }, 2000);\n } else {\n hint.textContent = 'Selected — press Ctrl+C'; hint.style.color = '#f2dda6';\n setTimeout(function() { hint.textContent = 'Click to copy address'; hint.style.color = ''; }, 3000);\n }\n}\n<\\/script>\n</body>\n</html>`\n}\n\n/**\n * The text of the one top-up page, per rail. The page is the Base wallet\n * page, unchanged (aphex5, 2026-10-28: \"wallet topup is your baseline\"): the\n * layout, the style and the script never fork by rail, only these strings do.\n */\ninterface TopupPageText {\n title: string\n subtitle: string\n badge: string\n currency: string\n /** The /api/balance field of the main balance. */\n balanceField: string\n gasNote: string\n info: string\n tokenAddress: string\n chainIdHex: string\n}\n\n/** The text of `cia subscription topup` in the terminal. */\nexport const SUBSCRIPTION_TOPUP_TEXT =\n 'Send $CIA and a little ETH on Robinhood Chain to your own wallet. Then run `cia subscription buy --days N`.'\n\nconst TOPUP_PAGE_TEXT: Record<TopupRail, TopupPageText> = {\n 'base-usdc': {\n title: 'Chain Insights — Fund Wallet',\n subtitle: 'Fund your wallet to use blockchain intelligence tools',\n badge: 'Base Network · USDC',\n currency: 'USDC',\n balanceField: 'balance_usdc',\n gasNote: 'Base ETH is used for one-time payment setup gas.',\n info: 'Send Base USDC to the wallet address above.',\n tokenAddress: USDC_ADDRESS,\n chainIdHex: BASE_CHAIN_ID,\n },\n 'robinhood-cia': {\n title: 'Chain Insights — Fund Subscription Wallet',\n subtitle: 'Fund your wallet to buy a Chain Insights subscription',\n badge: 'Robinhood Chain · $CIA',\n currency: '$CIA',\n balanceField: 'balance_cia',\n gasNote: 'Robinhood Chain ETH is used for the buy transaction gas.',\n info: 'Send $CIA and Robinhood Chain ETH to the wallet address above.',\n tokenAddress: CIA_TOKEN_ADDRESS,\n chainIdHex: ROBINHOOD_CHAIN_ID_HEX,\n },\n}\n\n/**\n * The one local top-up page: the wallet address, its QR code, the network\n * and the balances. It names only the user's own wallet, never where a\n * payment goes.\n */\nexport function generateTopupPage(walletAddressInput: string, rail: TopupRail = 'base-usdc'): string {\n const text = TOPUP_PAGE_TEXT[rail]\n const walletAddress = assertWalletAddress(walletAddressInput)\n const safeWalletAddress = escapeHtml(walletAddress)\n const walletAddressJson = jsonForScript(walletAddress)\n\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>${text.title}</title>\n<script src=\"https://cdn.jsdelivr.net/npm/qrcode-generator@1.4.4/qrcode.min.js\"></script>\n<style>\n * { margin: 0; padding: 0; box-sizing: border-box; }\n\n body {\n font-family: Inter, -apple-system, BlinkMacSystemFont, sans-serif;\n background: rgba(10, 12, 17, 1);\n color: rgba(255, 255, 255, 0.9);\n min-height: 100vh;\n display: flex;\n justify-content: center;\n align-items: center;\n line-height: 1.3;\n }\n\n .container {\n max-width: 480px;\n width: 100%;\n padding: 24px;\n }\n\n .logo {\n text-align: center;\n margin-bottom: 32px;\n }\n\n .logo h1 {\n font-size: 24px;\n font-weight: 600;\n color: #f2dda6;\n letter-spacing: -0.5px;\n }\n\n .logo p {\n color: rgba(255, 255, 255, 0.5);\n font-size: 14px;\n margin-top: 4px;\n }\n\n .card {\n background: rgba(19, 19, 24, 1);\n border: 1px solid rgba(255, 255, 255, 0.06);\n border-radius: 12px;\n padding: 32px 24px;\n }\n\n .qr-container {\n display: flex;\n justify-content: center;\n margin-bottom: 24px;\n }\n\n .qr-container canvas, .qr-container img {\n border-radius: 12px;\n background: #fff;\n padding: 12px;\n }\n\n .address-box {\n background: rgba(10, 12, 17, 1);\n border: 1px solid rgba(255, 255, 255, 0.06);\n border-radius: 8px;\n padding: 12px 16px;\n display: flex;\n align-items: center;\n gap: 8px;\n margin-bottom: 24px;\n }\n\n .address-box code {\n font-family: 'SF Mono', 'Fira Code', monospace;\n font-size: 12px;\n color: #f2dda6;\n word-break: break-all;\n flex: 1;\n }\n\n .copy-btn {\n background: none;\n border: none;\n color: rgba(255, 255, 255, 0.4);\n cursor: pointer;\n padding: 4px;\n font-size: 18px;\n transition: color 0.3s linear;\n }\n\n .copy-btn:hover { color: #f2dda6; }\n .copy-btn.copied { color: #4feb69; }\n\n .network-badge {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n background: rgba(255, 255, 255, 0.04);\n border: 1px solid rgba(255, 255, 255, 0.06);\n border-radius: 20px;\n padding: 6px 12px;\n font-size: 12px;\n color: rgba(255, 255, 255, 0.5);\n margin-bottom: 24px;\n }\n\n .network-badge .dot {\n width: 8px;\n height: 8px;\n border-radius: 50%;\n background: #f2dda6;\n }\n\n .balance-row {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 12px 0;\n border-top: 1px solid rgba(255, 255, 255, 0.06);\n margin-bottom: 16px;\n }\n\n .balance-label { color: rgba(255, 255, 255, 0.5); font-size: 14px; }\n\n .balance-value {\n font-size: 20px;\n font-weight: 600;\n color: rgba(255, 255, 255, 0.9);\n }\n\n .balance-value .currency {\n font-size: 14px;\n color: rgba(255, 255, 255, 0.5);\n font-weight: 400;\n margin-left: 4px;\n }\n\n .gas-note {\n margin-top: -8px;\n margin-bottom: 16px;\n color: rgba(255,255,255,0.45);\n font-size: 12px;\n line-height: 1.4;\n text-align: left;\n }\n\n .metamask-btn {\n width: 100%;\n padding: 14px;\n border: none;\n border-radius: 10px;\n font-size: 15px;\n font-weight: 600;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 10px;\n transition: all 0.2s;\n background: #f6851b;\n color: #fff;\n }\n\n .metamask-btn:hover { background: #e2761b; transform: translateY(-1px); }\n .metamask-btn:disabled { opacity: 0.5; cursor: not-allowed; transform: none; }\n\n .metamask-btn svg { width: 22px; height: 22px; }\n\n .amount-input {\n width: 100%;\n padding: 12px 16px;\n background: #0a0c11;\n border: 1px solid rgba(255, 255, 255, 0.06);\n border-radius: 8px;\n color: rgba(255, 255, 255, 0.9);\n font-size: 16px;\n margin-bottom: 16px;\n outline: none;\n transition: border-color 0.2s;\n }\n\n .amount-input:focus { border-color: #f2dda6; }\n\n .amount-label {\n font-size: 13px;\n color: rgba(255, 255, 255, 0.5);\n margin-bottom: 6px;\n display: block;\n }\n\n .status {\n text-align: center;\n padding: 12px;\n border-radius: 8px;\n font-size: 14px;\n margin-top: 16px;\n display: none;\n }\n\n .status.success { display: block; background: #0a2e1a; color: #4feb69; border: 1px solid #1a4a2e; }\n .status.error { display: block; background: #2e0a0a; color: #eb4f4f; border: 1px solid #4a1a1a; }\n .status.pending { display: block; background: #1a1a0a; color: #f2dda6; border: 1px solid #4a4a1a; }\n\n .info {\n text-align: center;\n margin-top: 24px;\n font-size: 12px;\n color: rgba(255, 255, 255, 0.3);\n line-height: 1.6;\n }\n</style>\n</head>\n<body>\n<div class=\"container\">\n <div class=\"logo\">\n <h1>Chain Insights</h1>\n <p>${text.subtitle}</p>\n </div>\n\n <div class=\"card\">\n <div class=\"qr-container\" id=\"qr\"></div>\n\n <div class=\"address-box\">\n <code id=\"address\">${safeWalletAddress}</code>\n <button class=\"copy-btn\" onclick=\"copyAddress()\" id=\"copyBtn\" title=\"Copy address\">⎘</button>\n </div>\n\n <div class=\"network-badge\">\n <span class=\"dot\"></span>\n ${text.badge}\n </div>\n\n <div class=\"balance-row\">\n <span class=\"balance-label\">Current balance</span>\n <span class=\"balance-value\" id=\"balance\">—<span class=\"currency\">${text.currency}</span></span>\n </div>\n <div class=\"balance-row\">\n <span class=\"balance-label\">Gas balance</span>\n <span class=\"balance-value\" id=\"gasBalance\">—<span class=\"currency\">ETH</span></span>\n </div>\n <p class=\"gas-note\">${text.gasNote}</p>\n\n <div class=\"status\" id=\"status\"></div>\n </div>\n\n <p class=\"info\">${text.info}</p>\n</div>\n\n<script>\nconst WALLET = ${walletAddressJson};\nconst USDC = '${text.tokenAddress}';\nconst CHAIN_ID = '${text.chainIdHex}';\n\n// QR code\n(function() {\n var qr = qrcode(0, 'M');\n qr.addData(WALLET);\n qr.make();\n document.getElementById('qr').innerHTML = qr.createSvgTag(5, 0);\n var svg = document.querySelector('#qr svg');\n if (svg) { svg.style.borderRadius = '12px'; svg.style.background = '#fff'; svg.style.padding = '12px'; }\n})();\n\n// Copy address — fallback for sandboxed iframes where navigator.clipboard is blocked\nfunction copyAddress() {\n var ok = false;\n // Try modern clipboard API first\n if (navigator.clipboard && navigator.clipboard.writeText) {\n navigator.clipboard.writeText(WALLET).then(function() { ok = true; }).catch(function() {});\n }\n // Fallback: hidden textarea + execCommand\n if (!ok) {\n var ta = document.createElement('textarea');\n ta.value = WALLET;\n ta.style.position = 'fixed';\n ta.style.left = '-9999px';\n document.body.appendChild(ta);\n ta.select();\n try { document.execCommand('copy'); } catch(e) {}\n document.body.removeChild(ta);\n }\n var btn = document.getElementById('copyBtn');\n btn.classList.add('copied');\n btn.innerHTML = '✓';\n setTimeout(function() { btn.classList.remove('copied'); btn.innerHTML = '⎘'; }, 2000);\n}\n\n// Fetch balance\nasync function fetchBalance() {\n try {\n var resp = await fetch('/api/balance');\n var json = await resp.json();\n if (json.${text.balanceField} === 'unknown') throw new Error('balance unavailable');\n var balance = Number(json.${text.balanceField} || 0).toFixed(2);\n document.getElementById('balance').innerHTML = balance + '<span class=\"currency\">${text.currency}</span>';\n var gasBalance = json.balance_eth === 'unknown' ? '—' : Number(json.balance_eth || 0).toFixed(6);\n document.getElementById('gasBalance').innerHTML = gasBalance + '<span class=\"currency\">ETH</span>';\n } catch(e) {\n document.getElementById('balance').innerHTML = '—<span class=\"currency\">${text.currency}</span>';\n document.getElementById('gasBalance').innerHTML = '—<span class=\"currency\">ETH</span>';\n }\n}\nfetchBalance();\nsetInterval(fetchBalance, 15000);\n\n</script>\n</body>\n</html>`\n}\n\n/** The Base USDC page. */\nfunction generatePage(walletAddressInput: string): string {\n return generateTopupPage(walletAddressInput, 'base-usdc')\n}\n\n/** The robinhood-cia page: the same page, the subscription text. */\nexport function generateSubscriptionPage(walletAddressInput: string): string {\n return generateTopupPage(walletAddressInput, 'robinhood-cia')\n}\n\n// ---------------------------------------------------------------------------\n// The local top-up view: an MCP App drawn by Claude Desktop next to the\n// wallet_topup answer of the local proxy. It shows the wallet address, its QR\n// code, Base Mainnet and USDC. It fetches nothing, signs nothing and sends\n// nothing: the page holds no wallet code, only an address to read.\n// The hosted connector never serves it (ruling 2026-10-05).\n// ---------------------------------------------------------------------------\n\nexport const TOPUP_TOOL_NAME = 'wallet_topup'\nexport const TOPUP_VIEW_URI = 'ui://chain-insights/topup'\nexport const TOPUP_NETWORK_DISPLAY = 'Base Mainnet'\nconst TOPUP_CHAIN_ID = 8453\nconst EXT_APPS_GLOBAL = '__chainInsightsExtApps'\nconst NO_WALLET_TEXT =\n 'No local payment wallet exists yet. Run `cia wallet create` to make one, then ask to top up again.'\n\nexport interface TopupViewFacts {\n wallet_address: string\n network: 'Base'\n network_display: typeof TOPUP_NETWORK_DISPLAY\n chain_id: typeof TOPUP_CHAIN_ID\n token: 'USDC'\n token_contract: typeof USDC_ADDRESS\n}\n\nexport interface TopupToolResult {\n [key: string]: unknown\n content: Array<{ type: 'text'; text: string }>\n structuredContent: {\n schema: 'chain-insights.result.v1'\n tool: typeof TOPUP_TOOL_NAME\n hint: string | null\n facts: { wallet_configured: boolean; topup: TopupViewFacts | null }\n }\n isError: false\n}\n\n/**\n * The wallet_topup answer. The text is complete on its own, for hosts that\n * draw no view. `walletAddress` null means no local wallet exists.\n */\nexport function topupToolResult(walletAddress: string | null): TopupToolResult {\n if (walletAddress === null) {\n return {\n content: [{ type: 'text', text: NO_WALLET_TEXT }],\n structuredContent: {\n schema: 'chain-insights.result.v1',\n tool: TOPUP_TOOL_NAME,\n hint: 'Run `cia wallet create`.',\n facts: { wallet_configured: false, topup: null },\n },\n isError: false,\n }\n }\n const address = assertWalletAddress(walletAddress)\n const topup: TopupViewFacts = {\n wallet_address: address,\n network: 'Base',\n network_display: TOPUP_NETWORK_DISPLAY,\n chain_id: TOPUP_CHAIN_ID,\n token: 'USDC',\n token_contract: USDC_ADDRESS,\n }\n return {\n content: [\n {\n type: 'text',\n text: [\n `Payment wallet: ${address}`,\n `Network: ${TOPUP_NETWORK_DISPLAY} (chain ID ${TOPUP_CHAIN_ID})`,\n `Token: USDC (contract ${USDC_ADDRESS})`,\n `To top up, send USDC on ${TOPUP_NETWORK_DISPLAY} to the wallet address. Chain Insights makes no payment from here.`,\n ].join('\\n'),\n },\n ],\n structuredContent: {\n schema: 'chain-insights.result.v1',\n tool: TOPUP_TOOL_NAME,\n hint: null,\n facts: { wallet_configured: true, topup },\n },\n isError: false,\n }\n}\n\nexport interface SubscriptionTopupViewFacts {\n wallet_address: string\n network: typeof ROBINHOOD_NETWORK_DISPLAY\n network_display: typeof ROBINHOOD_NETWORK_DISPLAY\n chain_id: typeof ROBINHOOD_CHAIN_ID\n token: typeof CIA_SYMBOL\n token_contract: typeof CIA_TOKEN_ADDRESS\n gas_token: 'ETH'\n cia_balance: string\n eth_balance: string\n}\n\nexport interface SubscriptionTopupToolResult {\n [key: string]: unknown\n content: Array<{ type: 'text'; text: string }>\n structuredContent: {\n schema: 'chain-insights.result.v1'\n tool: typeof SUBSCRIPTION_TOPUP_TOOL\n hint: string | null\n facts: { wallet_configured: boolean; topup: SubscriptionTopupViewFacts | null }\n }\n isError: false\n}\n\n/** The rule the subscription_topup answer ends with. */\nexport const SUBSCRIPTION_TOPUP_RULE =\n 'Fund this wallet. The user buys days in a terminal with `cia subscription buy --days N`. An agent cannot buy. Never send $CIA by hand to any other address: it is not credited.'\n\n/**\n * The subscription_topup answer: the local wallet on Robinhood Chain, $CIA,\n * the balances ('unknown' when no RPC answers). It names the user's own\n * wallet only. `walletAddress` null means no local wallet exists.\n */\nexport function subscriptionTopupToolResult(\n walletAddress: string | null,\n balances: { cia: string; eth: string } = { cia: 'unknown', eth: 'unknown' }\n): SubscriptionTopupToolResult {\n if (walletAddress === null) {\n return {\n content: [{ type: 'text', text: 'No local wallet exists yet. Run `cia wallet create` to make one, then ask to top up again.' }],\n structuredContent: {\n schema: 'chain-insights.result.v1',\n tool: SUBSCRIPTION_TOPUP_TOOL,\n hint: 'Run `cia wallet create`.',\n facts: { wallet_configured: false, topup: null },\n },\n isError: false,\n }\n }\n const address = assertWalletAddress(walletAddress)\n const topup: SubscriptionTopupViewFacts = {\n wallet_address: address,\n network: ROBINHOOD_NETWORK_DISPLAY,\n network_display: ROBINHOOD_NETWORK_DISPLAY,\n chain_id: ROBINHOOD_CHAIN_ID,\n token: CIA_SYMBOL,\n token_contract: CIA_TOKEN_ADDRESS,\n gas_token: 'ETH',\n cia_balance: balances.cia,\n eth_balance: balances.eth,\n }\n return {\n content: [\n {\n type: 'text',\n text: [\n `Subscription wallet: ${address}`,\n `Network: ${ROBINHOOD_NETWORK_DISPLAY} (chain ID ${ROBINHOOD_CHAIN_ID})`,\n `Token: ${CIA_SYMBOL} (contract ${CIA_TOKEN_ADDRESS}), with ETH for gas`,\n `Balance: ${balances.cia} ${CIA_SYMBOL}, ${balances.eth} ETH`,\n SUBSCRIPTION_TOPUP_RULE,\n ].join('\\n'),\n },\n ],\n structuredContent: {\n schema: 'chain-insights.result.v1',\n tool: SUBSCRIPTION_TOPUP_TOOL,\n hint: null,\n facts: { wallet_configured: true, topup },\n },\n isError: false,\n }\n}\n\nlet extAppsScript: string | null = null\n\n/**\n * The browser build of the ext-apps App client (the same 1.x pin as\n * the Chain Insights UI package), turned from an ES module into a script that publishes\n * its exports on globalThis, so the view stays one self-contained file.\n */\nexport function extAppsBrowserScript(): string {\n if (extAppsScript !== null) return extAppsScript\n const require = createRequire(import.meta.url)\n const source = readFileSync(\n require.resolve('@modelcontextprotocol/ext-apps/app-with-deps'),\n 'utf8'\n )\n const match = /export\\s*\\{([^}]*)\\}\\s*;?\\s*(?:\\/\\/# sourceMappingURL=\\S*\\s*)?$/.exec(source)\n if (!match) throw new Error('The ext-apps app bundle does not end with one export list')\n const fields = match[1]\n .split(',')\n .map((part) => part.trim())\n .filter(Boolean)\n .map((part) => {\n const [local, exported] = part.split(/\\s+as\\s+/)\n return `${JSON.stringify(exported ?? local)}:${local}`\n })\n extAppsScript = `${source.slice(0, match.index)}\\nglobalThis.${EXT_APPS_GLOBAL}={${fields.join(',')}};\\n`\n extAppsScript = extAppsScript.replaceAll('</script', '<\\\\/script')\n return extAppsScript\n}\n\nconst TOPUP_VIEW_STYLE = `\n:root {\n color-scheme: light;\n --ci-text: var(--color-text-primary, #1d1b16);\n --ci-muted: var(--color-text-secondary, #5c5647);\n --ci-surface: var(--color-background-secondary, #f7f4ec);\n --ci-border: var(--color-border-primary, #d9d2c0);\n --ci-accent: #8a6a12;\n --ci-on-accent: #ffffff;\n --ci-qr-dark: #000000;\n --ci-qr-light: #ffffff;\n --ci-font: var(--font-sans, ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif);\n --ci-mono: var(--font-mono, ui-monospace, 'SF Mono', Menlo, Consolas, monospace);\n}\nhtml[data-theme='dark'] {\n color-scheme: dark;\n --ci-text: var(--color-text-primary, #f2eee4);\n --ci-muted: var(--color-text-secondary, #b8b09c);\n --ci-surface: var(--color-background-secondary, #1f1d19);\n --ci-border: var(--color-border-primary, #3d392f);\n --ci-accent: #e3c46a;\n --ci-on-accent: #1d1b16;\n}\n* { box-sizing: border-box; margin: 0; padding: 0; }\nhtml, body { background: transparent; }\nbody { font-family: var(--ci-font); color: var(--ci-text); font-size: 14px; line-height: 1.45; padding: 4px; }\n.card { max-width: 420px; margin: 0 auto; padding: 20px; border: 1px solid var(--ci-border); border-radius: 12px; background: var(--ci-surface); }\nh1 { font-size: 16px; font-weight: 600; margin-bottom: 4px; }\n.lead, .note, .muted { color: var(--ci-muted); }\n.lead { margin-bottom: 16px; }\n.qr-tile { display: flex; justify-content: center; margin-bottom: 16px; }\n.qr { width: 200px; height: 200px; border-radius: 8px; }\n.qr-dark { fill: var(--ci-qr-dark); }\n.qr-light { fill: var(--ci-qr-light); }\ndl { display: grid; grid-template-columns: max-content 1fr; gap: 6px 12px; margin-bottom: 16px; }\ndt { color: var(--ci-muted); }\ndd { overflow-wrap: anywhere; }\ncode { font-family: var(--ci-mono); font-size: 12px; }\n.address { display: block; margin-bottom: 6px; }\nbutton { font: inherit; font-size: 13px; padding: 4px 12px; border-radius: 6px; border: 0; background: var(--ci-accent); color: var(--ci-on-accent); cursor: pointer; }\nbutton:focus-visible { outline: 2px solid var(--ci-accent); outline-offset: 2px; }\n.copy-state { margin-left: 8px; font-size: 12px; color: var(--ci-muted); }\n.skeleton { height: 14px; border-radius: 4px; background: var(--ci-border); margin-bottom: 10px; }\n.skeleton.short { width: 60%; }\n`\n\nconst TOPUP_VIEW_SCRIPT = `\n(function () {\n var extApps = globalThis.${EXT_APPS_GLOBAL};\n var qrModules = __QR_MODULES__;\n var root = document.getElementById('topup');\n var ADDRESS = /^0x[0-9a-fA-F]{40}$/;\n\n function el(tag, attrs, children) {\n var node = document.createElement(tag);\n Object.keys(attrs || {}).forEach(function (key) {\n if (key === 'text') node.textContent = attrs[key];\n else node.setAttribute(key, attrs[key]);\n });\n (children || []).forEach(function (child) {\n node.append(typeof child === 'string' ? document.createTextNode(child) : child);\n });\n return node;\n }\n\n function qrSvg(address) {\n var modules = qrModules(address);\n var quiet = 4;\n var total = modules.length + quiet * 2;\n var ns = 'http://www.w3.org/2000/svg';\n var svg = document.createElementNS(ns, 'svg');\n svg.setAttribute('class', 'qr');\n svg.setAttribute('viewBox', '0 0 ' + total + ' ' + total);\n svg.setAttribute('shape-rendering', 'crispEdges');\n svg.setAttribute('role', 'img');\n svg.setAttribute('aria-label', 'QR code of the wallet address ' + address);\n var light = document.createElementNS(ns, 'rect');\n light.setAttribute('class', 'qr-light');\n light.setAttribute('width', String(total));\n light.setAttribute('height', String(total));\n svg.appendChild(light);\n var d = '';\n for (var row = 0; row < modules.length; row++) {\n for (var col = 0; col < modules.length; col++) {\n if (modules[row][col] === 1) d += 'M' + (col + quiet) + ' ' + (row + quiet) + 'h1v1h-1z';\n }\n }\n var dark = document.createElementNS(ns, 'path');\n dark.setAttribute('class', 'qr-dark');\n dark.setAttribute('d', d);\n svg.appendChild(dark);\n return svg;\n }\n\n function show(children) {\n root.replaceChildren(el('div', { class: 'card' }, children));\n }\n\n function resultText(result) {\n return ((result && result.content) || [])\n .filter(function (item) { return item && item.type === 'text' && typeof item.text === 'string'; })\n .map(function (item) { return item.text; })\n .join('\\\\n');\n }\n\n function copyButton(address, target) {\n var state = el('span', { class: 'copy-state', 'aria-live': 'polite' });\n var button = el('button', { type: 'button', text: 'Copy address' });\n button.addEventListener('click', function () {\n function selectIt(message) {\n var range = document.createRange();\n range.selectNodeContents(target);\n var selection = window.getSelection();\n selection.removeAllRanges();\n selection.addRange(range);\n state.textContent = message;\n }\n if (navigator.clipboard && navigator.clipboard.writeText) {\n navigator.clipboard.writeText(address).then(\n function () { state.textContent = 'Copied'; },\n function () { selectIt('Selected. Press Ctrl+C to copy.'); }\n );\n } else {\n selectIt('Selected. Press Ctrl+C to copy.');\n }\n });\n return el('div', {}, [button, state]);\n }\n\n function render(result) {\n var structured = result && result.structuredContent;\n var facts = structured && (structured.tool === '${TOPUP_TOOL_NAME}' || structured.tool === '${SUBSCRIPTION_TOPUP_TOOL}') ? structured.facts : null;\n if (!result || result.isError || !facts) {\n show([el('h1', { text: 'Wallet top-up' }), el('p', { class: 'muted', text: resultText(result) || 'The wallet details did not arrive.' })]);\n return;\n }\n if (!facts.topup) {\n show([\n el('h1', { text: 'No payment wallet yet' }),\n el('p', { class: 'lead' }, ['Create one with ', el('code', { text: 'cia wallet create' }), ', then ask to top up again.'])\n ]);\n return;\n }\n var topup = facts.topup;\n if (!ADDRESS.test(topup.wallet_address)) {\n show([el('h1', { text: 'Wallet top-up' }), el('p', { class: 'muted', text: 'The wallet address is not a valid 0x address.' })]);\n return;\n }\n var address = el('code', { class: 'address', text: topup.wallet_address });\n show([\n el('h1', { text: 'Top up your Chain Insights wallet' }),\n el('p', { class: 'lead', text: 'Send ' + topup.token + ' on ' + topup.network_display + ' to this address.' }),\n el('div', { class: 'qr-tile' }, [qrSvg(topup.wallet_address)]),\n el('dl', {}, [\n el('dt', { text: 'Network' }), el('dd', { text: topup.network_display + ' (chain ID ' + topup.chain_id + ')' }),\n el('dt', { text: 'Token' }), el('dd', { text: topup.token }),\n el('dt', { text: 'Token contract' }), el('dd', {}, [el('code', { text: topup.token_contract })]),\n el('dt', { text: 'Wallet' }), el('dd', {}, [address, copyButton(topup.wallet_address, address)])\n ]),\n el('p', { class: 'note', text: 'This view shows an address only. It signs nothing and sends nothing.' })\n ]);\n }\n\n function applyContext(context) {\n if (!context) return;\n if (context.theme === 'light' || context.theme === 'dark') extApps.applyDocumentTheme(context.theme);\n var styles = context.styles;\n if (styles && styles.variables) extApps.applyHostStyleVariables(styles.variables);\n if (styles && styles.css && styles.css.fonts) extApps.applyHostFonts(styles.css.fonts);\n }\n\n // Until the host names its theme, follow the system; the host's theme wins.\n if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {\n document.documentElement.setAttribute('data-theme', 'dark');\n }\n\n var app = new extApps.App({ name: 'chain-insights-topup', version: '1.0.0' }, {});\n app.ontoolresult = function (result) { render(result); };\n app.onhostcontextchanged = function (change) { applyContext(change); };\n app.onteardown = function () { return {}; };\n app.connect(new extApps.PostMessageTransport(window.parent, window.parent)).then(\n function () { applyContext(app.getHostContext()); },\n function () {\n show([el('h1', { text: 'Wallet top-up' }), el('p', { class: 'muted', text: 'This view opens inside Claude, next to the ${TOPUP_TOOL_NAME} answer.' })]);\n }\n );\n})();\n`\n\n/**\n * The QR encoder as browser source: an expression that evaluates to\n * `qrModules`. A transform that keeps function names (esbuild keepNames, as\n * tsx runs it) wraps inner functions in `__name(...)`; the wrapper gives that\n * helper a do-nothing body so the source runs anywhere.\n */\nexport function qrModulesSource(): string {\n return `(function () { var __name = function (target) { return target; }; return (${qrModules.toString()}); })()`\n}\n\n/** The one self-contained HTML file of ui://chain-insights/topup. */\nexport function topupViewHtml(): string {\n const script = TOPUP_VIEW_SCRIPT.replace('__QR_MODULES__', () => qrModulesSource())\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>Wallet top-up</title>\n<style>${TOPUP_VIEW_STYLE}</style>\n</head>\n<body>\n<main id=\"topup\" aria-live=\"polite\"><div class=\"card\"><div class=\"skeleton\"></div><div class=\"skeleton short\"></div><p class=\"muted\">Waiting for the wallet details.</p></div></main>\n<script type=\"module\">${extAppsBrowserScript()}</script>\n<script type=\"module\">${script.replaceAll('</script', '<\\\\/script')}</script>\n</body>\n</html>`\n}\n\n/** The parts of McpServer the top-up view registers on. */\nexport type TopupViewServer = Pick<\n import('@modelcontextprotocol/sdk/server/mcp.js').McpServer,\n 'registerTool' | 'registerResource'\n>\n\nexport interface TopupViewDependencies {\n /** The local wallet address, or null when no wallet exists. Throws when it cannot be read. */\n walletAddress: () => Promise<string | null>\n /** The $CIA and ETH balances on Robinhood Chain, 'unknown' when no RPC answers. */\n robinhoodBalances?: (address: string) => Promise<{ cia: string; eth: string }>\n}\n\nasync function readRobinhoodBalances(address: string): Promise<{ cia: string; eth: string }> {\n const [cia, eth] = await Promise.all([getCiaBalance(address), getRobinhoodEthBalance(address)])\n return { cia, eth }\n}\n\n/**\n * Register the wallet_topup tool and its view on the local proxy. The tool\n * reads the local wallet address and nothing else.\n */\nexport async function registerTopupView(\n server: TopupViewServer,\n deps: TopupViewDependencies\n): Promise<void> {\n const { registerAppResource, registerAppTool, RESOURCE_MIME_TYPE } =\n await import('@modelcontextprotocol/ext-apps/server')\n const uiMeta = { prefersBorder: false, csp: { resourceDomains: ['https://assets.claude.ai'] } }\n\n registerAppTool(\n server,\n TOPUP_TOOL_NAME,\n {\n title: 'Top Up Wallet',\n description: WALLET_TOPUP_DESCRIPTION,\n annotations: {\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: false,\n },\n _meta: { ui: { resourceUri: TOPUP_VIEW_URI } },\n },\n async () => {\n try {\n return topupToolResult(await deps.walletAddress())\n } catch (err) {\n return {\n content: [{ type: 'text' as const, text: `Top-up failed: ${(err as Error).message}` }],\n isError: true,\n }\n }\n }\n )\n\n registerAppTool(\n server,\n SUBSCRIPTION_TOPUP_TOOL,\n {\n title: 'Top Up Subscription Wallet',\n description: SUBSCRIPTION_TOPUP_DESCRIPTION,\n annotations: {\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n // It reads the balances on Robinhood Chain.\n openWorldHint: true,\n },\n _meta: { ui: { resourceUri: TOPUP_VIEW_URI } },\n },\n async () => {\n try {\n const address = await deps.walletAddress()\n if (address === null) return subscriptionTopupToolResult(null)\n const balances = await (deps.robinhoodBalances ?? readRobinhoodBalances)(address)\n return subscriptionTopupToolResult(address, balances)\n } catch (err) {\n return {\n content: [{ type: 'text' as const, text: `Top-up failed: ${(err as Error).message}` }],\n isError: true,\n }\n }\n }\n )\n\n registerAppResource(\n server,\n 'Chain Insights wallet top-up',\n TOPUP_VIEW_URI,\n {\n description:\n 'The local wallet address and its QR code: on Base Mainnet with USDC, or on Robinhood Chain with $CIA.',\n _meta: { ui: uiMeta },\n },\n async () => ({\n contents: [\n {\n uri: TOPUP_VIEW_URI,\n mimeType: RESOURCE_MIME_TYPE,\n text: topupViewHtml(),\n _meta: { ui: uiMeta },\n },\n ],\n })\n )\n}\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"topup-server-qdruzcre.mjs","names":["getCopiedTopupUrl","startCopiedTopupServer"],"sources":["../src/wallet/topup-server.ts"],"sourcesContent":["import {\n generateArtifactHtml,\n getTopupUrl as getCopiedTopupUrl,\n startTopupServer as startCopiedTopupServer,\n} from './mcp-proxy/topup-server.js'\nimport { createServer, type Server, type ServerResponse } from 'node:http'\nimport { isAddress } from 'viem'\nimport type { PaymentWalletAccount } from './tools.js'\n\ninterface ArtifactServerState {\n address: string\n assetServerUrl: string\n server: Server\n url: string\n}\n\nlet artifactServerState: ArtifactServerState | null = null\nconst REQUEST_TARGET_BASE = 'http://localhost'\n\nclass ProxyRequestError extends Error {\n status: number\n\n constructor(status: number, message: string) {\n super(message)\n this.name = 'ProxyRequestError'\n this.status = status\n }\n}\n\ninterface NormalizedProxyTarget {\n pathAndSearch: string\n pathname: string\n}\n\nfunction toWalletAddress(account: PaymentWalletAccount | string): string {\n const address = typeof account === 'string' ? account : account.address\n if (!isAddress(address)) {\n throw new Error('Wallet address must be a valid 0x-prefixed 20-byte EVM address')\n }\n return address\n}\n\nfunction send(\n res: ServerResponse,\n status: number,\n body: string | Buffer,\n contentType: string\n): void {\n res.writeHead(status, {\n 'content-type': contentType,\n 'cache-control': 'no-store',\n 'access-control-allow-origin': '*',\n })\n res.end(body)\n}\n\nfunction normalizeProxyTarget(reqUrl: string): NormalizedProxyTarget {\n if (!reqUrl.startsWith('/') || reqUrl.startsWith('//')) {\n throw new ProxyRequestError(400, 'Proxy request target must be an origin-form path')\n }\n\n const parsed = new URL(reqUrl, REQUEST_TARGET_BASE)\n if (parsed.origin !== REQUEST_TARGET_BASE) {\n throw new ProxyRequestError(400, 'Absolute and protocol-relative proxy targets are not allowed')\n }\n\n let decodedPathname: string\n try {\n decodedPathname = decodeURIComponent(parsed.pathname)\n } catch {\n throw new ProxyRequestError(400, 'Proxy request target contains invalid encoding')\n }\n\n if (decodedPathname.startsWith('//') || /^\\/[a-z][a-z0-9+.-]*:\\/\\//i.test(decodedPathname)) {\n throw new ProxyRequestError(400, 'Encoded host override targets are not allowed')\n }\n\n return {\n pathAndSearch: `${parsed.pathname}${parsed.search}`,\n pathname: parsed.pathname,\n }\n}\n\nasync function proxyToCopiedServer(\n proxyTarget: NormalizedProxyTarget,\n res: ServerResponse,\n assetServerUrl: string\n): Promise<void> {\n const allowedOrigin = new URL(assetServerUrl).origin\n const upstreamUrl = new URL(proxyTarget.pathAndSearch, assetServerUrl)\n if (upstreamUrl.origin !== allowedOrigin) {\n throw new ProxyRequestError(403, 'Upstream origin is not allowed')\n }\n\n const upstream = await fetch(upstreamUrl)\n const contentType = upstream.headers.get('content-type') ?? 'application/octet-stream'\n const body = Buffer.from(await upstream.arrayBuffer())\n send(res, upstream.status, body, contentType)\n}\n\nexport { generateArtifactHtml }\n\nexport function getTopupArtifactUrl(): string | null {\n return artifactServerState?.url ?? null\n}\n\nexport function getTopupUrl(): string | null {\n return getTopupArtifactUrl() ?? getCopiedTopupUrl()\n}\n\nexport async function stopTopupServer(): Promise<void> {\n if (!artifactServerState) {\n return\n }\n\n const { server } = artifactServerState\n artifactServerState = null\n await new Promise<void>((resolve) => server.close(() => resolve()))\n}\n\nexport async function startTopupServer(account: PaymentWalletAccount | string): Promise<string> {\n const walletAddress = toWalletAddress(account)\n\n if (\n artifactServerState &&\n artifactServerState.address.toLowerCase() === walletAddress.toLowerCase()\n ) {\n return artifactServerState.url\n }\n\n const assetServerUrl = await startCopiedTopupServer(walletAddress)\n\n if (artifactServerState) {\n await stopTopupServer()\n }\n\n const server = createServer((req, res) => {\n const reqUrl = req.url ?? '/'\n let proxyTarget: NormalizedProxyTarget\n try {\n proxyTarget = normalizeProxyTarget(reqUrl)\n } catch (err) {\n if (err instanceof ProxyRequestError) {\n send(\n res,\n err.status,\n JSON.stringify({ error: err.message }) + '\\n',\n 'application/json; charset=utf-8'\n )\n return\n }\n send(\n res,\n 400,\n JSON.stringify({ error: 'Invalid request target' }) + '\\n',\n 'application/json; charset=utf-8'\n )\n return\n }\n const { pathname } = proxyTarget\n\n if (pathname === '/' || pathname === '/index.html') {\n const artifactUrl = artifactServerState?.url ?? assetServerUrl\n send(res, 200, generateArtifactHtml(walletAddress, artifactUrl), 'text/html; charset=utf-8')\n return\n }\n\n if (pathname.startsWith('/assets/') || pathname.startsWith('/api/')) {\n void proxyToCopiedServer(proxyTarget, res, assetServerUrl).catch((err) => {\n if (err instanceof ProxyRequestError) {\n send(\n res,\n err.status,\n JSON.stringify({ error: err.message }) + '\\n',\n 'application/json; charset=utf-8'\n )\n return\n }\n send(\n res,\n 502,\n JSON.stringify({ error: (err as Error).message }) + '\\n',\n 'application/json; charset=utf-8'\n )\n })\n return\n }\n\n send(res, 404, JSON.stringify({ error: 'Not found' }) + '\\n', 'application/json; charset=utf-8')\n })\n\n const url = await new Promise<string>((resolve, reject) => {\n server.once('error', reject)\n server.listen(0, '127.0.0.1', () => {\n const addressInfo = server.address()\n if (!addressInfo || typeof addressInfo === 'string') {\n reject(new Error('Failed to start topup artifact server'))\n return\n }\n resolve(`http://localhost:${addressInfo.port}`)\n })\n })\n\n artifactServerState = {\n address: walletAddress,\n assetServerUrl,\n server,\n url,\n }\n\n return url\n}\n"],"mappings":"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"}
|