@aibtc/mcp-server 1.58.0 → 1.60.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -0
- package/dist/bridge/index.d.ts +31 -0
- package/dist/bridge/index.js +285 -0
- package/dist/index.js +28 -5
- package/dist/services/credentials.js +7 -2
- package/dist/services/defi.service.js +23 -11
- package/dist/services/hiro-api.d.ts +10 -0
- package/dist/services/hiro-api.js +30 -8
- package/dist/services/nft.service.js +14 -5
- package/dist/services/pillar-api.service.d.ts +8 -0
- package/dist/services/pillar-api.service.js +13 -1
- package/dist/services/spend-limiter.d.ts +45 -0
- package/dist/services/spend-limiter.js +218 -0
- package/dist/services/wallet-manager.js +5 -0
- package/dist/services/x402.service.js +63 -13
- package/dist/tools/bitcoin.tools.js +19 -0
- package/dist/tools/endpoint.tools.js +3 -1
- package/dist/tools/inbox.tools.js +9 -12
- package/dist/tools/news.tools.js +14 -16
- package/dist/tools/pillar-direct.tools.js +29 -22
- package/dist/tools/sbtc.tools.js +2 -9
- package/dist/tools/scaffold.tools.js +9 -14
- package/dist/tools/stacking-lottery.tools.js +8 -5
- package/dist/tools/yield-dashboard.tools.js +10 -14
- package/dist/transactions/builder.js +5 -0
- package/dist/utils/encryption.js +2 -0
- package/package.json +11 -5
- package/skill/SKILL.md +1 -1
- package/skill/references/genesis-lifecycle.md +105 -92
- package/dist/config/bitcoin-constants.d.ts.map +0 -1
- package/dist/config/bitcoin-constants.js.map +0 -1
- package/dist/config/caip.d.ts.map +0 -1
- package/dist/config/caip.js.map +0 -1
- package/dist/config/competition.d.ts.map +0 -1
- package/dist/config/competition.js.map +0 -1
- package/dist/config/contracts.d.ts.map +0 -1
- package/dist/config/contracts.js.map +0 -1
- package/dist/config/index.d.ts.map +0 -1
- package/dist/config/index.js.map +0 -1
- package/dist/config/networks.d.ts.map +0 -1
- package/dist/config/networks.js.map +0 -1
- package/dist/config/pillar.d.ts.map +0 -1
- package/dist/config/pillar.js.map +0 -1
- package/dist/config/sponsor.d.ts.map +0 -1
- package/dist/config/sponsor.js.map +0 -1
- package/dist/endpoints/index.d.ts.map +0 -1
- package/dist/endpoints/index.js.map +0 -1
- package/dist/endpoints/registry.d.ts.map +0 -1
- package/dist/endpoints/registry.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/services/bitflow.service.d.ts.map +0 -1
- package/dist/services/bitflow.service.js.map +0 -1
- package/dist/services/bns.service.d.ts.map +0 -1
- package/dist/services/bns.service.js.map +0 -1
- package/dist/services/credentials.d.ts.map +0 -1
- package/dist/services/credentials.js.map +0 -1
- package/dist/services/defi.service.d.ts.map +0 -1
- package/dist/services/defi.service.js.map +0 -1
- package/dist/services/erc8004.service.d.ts.map +0 -1
- package/dist/services/erc8004.service.js.map +0 -1
- package/dist/services/hiro-api.d.ts.map +0 -1
- package/dist/services/hiro-api.js.map +0 -1
- package/dist/services/inscription-parser.d.ts.map +0 -1
- package/dist/services/inscription-parser.js.map +0 -1
- package/dist/services/lightning/provider.d.ts.map +0 -1
- package/dist/services/lightning/provider.js.map +0 -1
- package/dist/services/lightning/spark-provider.d.ts.map +0 -1
- package/dist/services/lightning/spark-provider.js.map +0 -1
- package/dist/services/lightning-manager.d.ts.map +0 -1
- package/dist/services/lightning-manager.js.map +0 -1
- package/dist/services/mempool-api.d.ts.map +0 -1
- package/dist/services/mempool-api.js.map +0 -1
- package/dist/services/nft.service.d.ts.map +0 -1
- package/dist/services/nft.service.js.map +0 -1
- package/dist/services/nonce-tracker.d.ts.map +0 -1
- package/dist/services/nonce-tracker.js.map +0 -1
- package/dist/services/pillar-api.service.d.ts.map +0 -1
- package/dist/services/pillar-api.service.js.map +0 -1
- package/dist/services/sbtc-deposit.service.d.ts.map +0 -1
- package/dist/services/sbtc-deposit.service.js.map +0 -1
- package/dist/services/sbtc.service.d.ts.map +0 -1
- package/dist/services/sbtc.service.js.map +0 -1
- package/dist/services/scaffold.service.d.ts.map +0 -1
- package/dist/services/scaffold.service.js.map +0 -1
- package/dist/services/signing-key.service.d.ts.map +0 -1
- package/dist/services/signing-key.service.js.map +0 -1
- package/dist/services/stacking.service.d.ts.map +0 -1
- package/dist/services/stacking.service.js.map +0 -1
- package/dist/services/tenero-api.d.ts.map +0 -1
- package/dist/services/tenero-api.js.map +0 -1
- package/dist/services/tokens.service.d.ts.map +0 -1
- package/dist/services/tokens.service.js.map +0 -1
- package/dist/services/unisat-indexer.d.ts.map +0 -1
- package/dist/services/unisat-indexer.js.map +0 -1
- package/dist/services/wallet-manager.d.ts.map +0 -1
- package/dist/services/wallet-manager.js.map +0 -1
- package/dist/services/x402.service.d.ts.map +0 -1
- package/dist/services/x402.service.js.map +0 -1
- package/dist/tools/arxiv-research.tools.d.ts.map +0 -1
- package/dist/tools/arxiv-research.tools.js.map +0 -1
- package/dist/tools/bitcoin.tools.d.ts.map +0 -1
- package/dist/tools/bitcoin.tools.js.map +0 -1
- package/dist/tools/bitflow.tools.d.ts.map +0 -1
- package/dist/tools/bitflow.tools.js.map +0 -1
- package/dist/tools/bns.tools.d.ts.map +0 -1
- package/dist/tools/bns.tools.js.map +0 -1
- package/dist/tools/bounty-scanner.tools.d.ts.map +0 -1
- package/dist/tools/bounty-scanner.tools.js.map +0 -1
- package/dist/tools/child-inscription.tools.d.ts.map +0 -1
- package/dist/tools/child-inscription.tools.js.map +0 -1
- package/dist/tools/competition.tools.d.ts.map +0 -1
- package/dist/tools/competition.tools.js.map +0 -1
- package/dist/tools/contract.tools.d.ts.map +0 -1
- package/dist/tools/contract.tools.js.map +0 -1
- package/dist/tools/credentials.tools.d.ts.map +0 -1
- package/dist/tools/credentials.tools.js.map +0 -1
- package/dist/tools/defi.tools.d.ts.map +0 -1
- package/dist/tools/defi.tools.js.map +0 -1
- package/dist/tools/dual-stacking.tools.d.ts.map +0 -1
- package/dist/tools/dual-stacking.tools.js.map +0 -1
- package/dist/tools/earning.tools.d.ts.map +0 -1
- package/dist/tools/earning.tools.js.map +0 -1
- package/dist/tools/endpoint.tools.d.ts.map +0 -1
- package/dist/tools/endpoint.tools.js.map +0 -1
- package/dist/tools/erc8004.tools.d.ts.map +0 -1
- package/dist/tools/erc8004.tools.js.map +0 -1
- package/dist/tools/identity.tools.d.ts.map +0 -1
- package/dist/tools/identity.tools.js.map +0 -1
- package/dist/tools/inbox-x402.tools.d.ts.map +0 -1
- package/dist/tools/inbox-x402.tools.js.map +0 -1
- package/dist/tools/inbox.tools.d.ts.map +0 -1
- package/dist/tools/inbox.tools.js.map +0 -1
- package/dist/tools/index.d.ts.map +0 -1
- package/dist/tools/index.js.map +0 -1
- package/dist/tools/jingswap.tools.d.ts.map +0 -1
- package/dist/tools/jingswap.tools.js.map +0 -1
- package/dist/tools/lightning.tools.d.ts.map +0 -1
- package/dist/tools/lightning.tools.js.map +0 -1
- package/dist/tools/mempool.tools.d.ts.map +0 -1
- package/dist/tools/mempool.tools.js.map +0 -1
- package/dist/tools/news.tools.d.ts.map +0 -1
- package/dist/tools/news.tools.js.map +0 -1
- package/dist/tools/nft.tools.d.ts.map +0 -1
- package/dist/tools/nft.tools.js.map +0 -1
- package/dist/tools/nonce.tools.d.ts.map +0 -1
- package/dist/tools/nonce.tools.js.map +0 -1
- package/dist/tools/nostr.tools.d.ts.map +0 -1
- package/dist/tools/nostr.tools.js.map +0 -1
- package/dist/tools/openrouter.tools.d.ts.map +0 -1
- package/dist/tools/openrouter.tools.js.map +0 -1
- package/dist/tools/ordinals-marketplace.tools.d.ts.map +0 -1
- package/dist/tools/ordinals-marketplace.tools.js.map +0 -1
- package/dist/tools/ordinals-p2p.tools.d.ts.map +0 -1
- package/dist/tools/ordinals-p2p.tools.js.map +0 -1
- package/dist/tools/ordinals.tools.d.ts.map +0 -1
- package/dist/tools/ordinals.tools.js.map +0 -1
- package/dist/tools/pillar-direct.tools.d.ts.map +0 -1
- package/dist/tools/pillar-direct.tools.js.map +0 -1
- package/dist/tools/pillar.tools.d.ts.map +0 -1
- package/dist/tools/pillar.tools.js.map +0 -1
- package/dist/tools/psbt.helpers.d.ts.map +0 -1
- package/dist/tools/psbt.helpers.js.map +0 -1
- package/dist/tools/psbt.tools.d.ts.map +0 -1
- package/dist/tools/psbt.tools.js.map +0 -1
- package/dist/tools/query.tools.d.ts.map +0 -1
- package/dist/tools/query.tools.js.map +0 -1
- package/dist/tools/relay-diagnostic.tools.d.ts.map +0 -1
- package/dist/tools/relay-diagnostic.tools.js.map +0 -1
- package/dist/tools/reputation.tools.d.ts.map +0 -1
- package/dist/tools/reputation.tools.js.map +0 -1
- package/dist/tools/runes.tools.d.ts.map +0 -1
- package/dist/tools/runes.tools.js.map +0 -1
- package/dist/tools/sbtc.tools.d.ts.map +0 -1
- package/dist/tools/sbtc.tools.js.map +0 -1
- package/dist/tools/scaffold.tools.d.ts.map +0 -1
- package/dist/tools/scaffold.tools.js.map +0 -1
- package/dist/tools/schemas.d.ts.map +0 -1
- package/dist/tools/schemas.js.map +0 -1
- package/dist/tools/settings.tools.d.ts.map +0 -1
- package/dist/tools/settings.tools.js.map +0 -1
- package/dist/tools/signing.tools.d.ts.map +0 -1
- package/dist/tools/signing.tools.js.map +0 -1
- package/dist/tools/skill-mappings.d.ts.map +0 -1
- package/dist/tools/skill-mappings.js.map +0 -1
- package/dist/tools/souldinals.tools.d.ts.map +0 -1
- package/dist/tools/souldinals.tools.js.map +0 -1
- package/dist/tools/stacking-lottery.tools.d.ts.map +0 -1
- package/dist/tools/stacking-lottery.tools.js.map +0 -1
- package/dist/tools/stacking.tools.d.ts.map +0 -1
- package/dist/tools/stacking.tools.js.map +0 -1
- package/dist/tools/stacks-market.tools.d.ts.map +0 -1
- package/dist/tools/stacks-market.tools.js.map +0 -1
- package/dist/tools/styx.tools.d.ts.map +0 -1
- package/dist/tools/styx.tools.js.map +0 -1
- package/dist/tools/taproot-multisig.tools.d.ts.map +0 -1
- package/dist/tools/taproot-multisig.tools.js.map +0 -1
- package/dist/tools/tenero.tools.d.ts.map +0 -1
- package/dist/tools/tenero.tools.js.map +0 -1
- package/dist/tools/tokens.tools.d.ts.map +0 -1
- package/dist/tools/tokens.tools.js.map +0 -1
- package/dist/tools/transfer.tools.d.ts.map +0 -1
- package/dist/tools/transfer.tools.js.map +0 -1
- package/dist/tools/wallet-management.tools.d.ts.map +0 -1
- package/dist/tools/wallet-management.tools.js.map +0 -1
- package/dist/tools/wallet.tools.d.ts.map +0 -1
- package/dist/tools/wallet.tools.js.map +0 -1
- package/dist/tools/yield-dashboard.tools.d.ts.map +0 -1
- package/dist/tools/yield-dashboard.tools.js.map +0 -1
- package/dist/tools/yield-hunter.tools.d.ts.map +0 -1
- package/dist/tools/yield-hunter.tools.js.map +0 -1
- package/dist/transactions/bitcoin-builder.d.ts.map +0 -1
- package/dist/transactions/bitcoin-builder.js.map +0 -1
- package/dist/transactions/builder.d.ts.map +0 -1
- package/dist/transactions/builder.js.map +0 -1
- package/dist/transactions/child-inscription-builder.d.ts.map +0 -1
- package/dist/transactions/child-inscription-builder.js.map +0 -1
- package/dist/transactions/clarity-values.d.ts.map +0 -1
- package/dist/transactions/clarity-values.js.map +0 -1
- package/dist/transactions/inscription-builder.d.ts.map +0 -1
- package/dist/transactions/inscription-builder.js.map +0 -1
- package/dist/transactions/post-conditions.d.ts.map +0 -1
- package/dist/transactions/post-conditions.js.map +0 -1
- package/dist/transactions/rune-transfer-builder.d.ts.map +0 -1
- package/dist/transactions/rune-transfer-builder.js.map +0 -1
- package/dist/transactions/runestone-builder.d.ts.map +0 -1
- package/dist/transactions/runestone-builder.js.map +0 -1
- package/dist/transactions/sponsor-builder.d.ts.map +0 -1
- package/dist/transactions/sponsor-builder.js.map +0 -1
- package/dist/utils/bip322.d.ts.map +0 -1
- package/dist/utils/bip322.js.map +0 -1
- package/dist/utils/bitcoin.d.ts.map +0 -1
- package/dist/utils/bitcoin.js.map +0 -1
- package/dist/utils/encryption.d.ts.map +0 -1
- package/dist/utils/encryption.js.map +0 -1
- package/dist/utils/erc8004-helpers.d.ts.map +0 -1
- package/dist/utils/erc8004-helpers.js.map +0 -1
- package/dist/utils/errors.d.ts.map +0 -1
- package/dist/utils/errors.js.map +0 -1
- package/dist/utils/fee.d.ts.map +0 -1
- package/dist/utils/fee.js.map +0 -1
- package/dist/utils/formatting.d.ts.map +0 -1
- package/dist/utils/formatting.js.map +0 -1
- package/dist/utils/index.d.ts.map +0 -1
- package/dist/utils/index.js.map +0 -1
- package/dist/utils/l402-protocol.d.ts.map +0 -1
- package/dist/utils/l402-protocol.js.map +0 -1
- package/dist/utils/redact.d.ts.map +0 -1
- package/dist/utils/redact.js.map +0 -1
- package/dist/utils/relay-health.d.ts.map +0 -1
- package/dist/utils/relay-health.js.map +0 -1
- package/dist/utils/storage.d.ts.map +0 -1
- package/dist/utils/storage.js.map +0 -1
- package/dist/utils/tokens.d.ts.map +0 -1
- package/dist/utils/tokens.js.map +0 -1
- package/dist/utils/x402-payment-logging.d.ts.map +0 -1
- package/dist/utils/x402-payment-logging.js.map +0 -1
- package/dist/utils/x402-payment-state.d.ts.map +0 -1
- package/dist/utils/x402-payment-state.js.map +0 -1
- package/dist/utils/x402-protocol.d.ts.map +0 -1
- package/dist/utils/x402-protocol.js.map +0 -1
- package/dist/utils/x402-recovery.d.ts.map +0 -1
- package/dist/utils/x402-recovery.js.map +0 -1
- package/dist/yield-hunter/index.d.ts.map +0 -1
- package/dist/yield-hunter/index.js.map +0 -1
package/README.md
CHANGED
|
@@ -69,6 +69,38 @@ npx @aibtc/mcp-server@latest --install --vscode # VS Code (writes ./.vscode/
|
|
|
69
69
|
|
|
70
70
|
Each installer merges into the existing config — it won't clobber other servers or settings. Restart the client afterward.
|
|
71
71
|
|
|
72
|
+
### OpenRouter (any model)
|
|
73
|
+
|
|
74
|
+
The clients above are MCP hosts — they connect to this server for you. To drive the tools with an [OpenRouter](https://openrouter.ai) model instead, the server ships a built-in bridge: it spawns itself in server mode, exposes the tools to the model as function tools, and runs the tool-call loop. This is the client-side pattern from [OpenRouter's MCP cookbook](https://openrouter.ai/docs/cookbook/coding-agents/mcp-servers), packaged into the binary.
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
export OPENROUTER_API_KEY=sk-or-...
|
|
78
|
+
npx @aibtc/mcp-server@latest bridge "what's the STX balance of SP3...?"
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Safety flags (this server moves real funds, so the bridge defaults to nothing extra and lets you constrain it):
|
|
82
|
+
|
|
83
|
+
| Flag | Effect |
|
|
84
|
+
|------|--------|
|
|
85
|
+
| `--read-only` | Expose only read-only tools (no transfer/swap/deploy/etc.) |
|
|
86
|
+
| `--allow a,b` | Force-allow specific tool names (added to the set) |
|
|
87
|
+
| `--block a,b` | Force-remove specific tool names (wins over `--allow`) |
|
|
88
|
+
| `--max-spend-ustx <n>` / `--max-spend-sats <n>` | Cap spend via the server's spend-limit rail (enforced before signing) |
|
|
89
|
+
| `--list-tools` | Print the exposed tool set and exit (no API key needed) |
|
|
90
|
+
| `--model <id>` | OpenRouter model (default `anthropic/claude-3.5-haiku`) |
|
|
91
|
+
| `--network <net>` | `mainnet` or `testnet` (default `mainnet`) |
|
|
92
|
+
| `--max-turns <n>` | Tool-call loop cap (default 10) |
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
# Preview what a read-only session would expose, no key needed:
|
|
96
|
+
npx @aibtc/mcp-server@latest bridge --read-only --list-tools
|
|
97
|
+
|
|
98
|
+
# Read-only chat, plus one explicitly allowed write tool:
|
|
99
|
+
npx @aibtc/mcp-server@latest bridge --read-only --allow transfer_stx "send 1 STX to SP3..."
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
The allowlist is re-enforced at execution time, so a model can never call a tool outside the exposed set. Any MCP-capable agent framework (`@openrouter/agent`, OpenAI Agents SDK, Claude Agent SDK) can also point at this server directly — the bridge is for driving it through OpenRouter's raw API without adopting a framework.
|
|
103
|
+
|
|
72
104
|
### Testnet Mode
|
|
73
105
|
|
|
74
106
|
Add `--testnet` to any install command:
|
|
@@ -512,9 +544,14 @@ Or use any SIP-010 token by contract ID: `SP2X...::token-name`
|
|
|
512
544
|
| `API_URL` | Default x402 API base URL | `https://x402.biwas.xyz` |
|
|
513
545
|
| `CLIENT_MNEMONIC` | (Optional) Pre-configured mnemonic | - |
|
|
514
546
|
| `HIRO_API_KEY` | (Optional) Hiro API key for higher rate limits | - |
|
|
547
|
+
| `SPEND_LIMIT_ENABLED` | Set `false` to disable the wallet spending limit | `true` |
|
|
548
|
+
| `SPEND_LIMIT_DAILY_USTX` / `SPEND_LIMIT_SESSION_USTX` | STX spend cap per day / per unlock (micro-STX) | `10000000` (10 STX) |
|
|
549
|
+
| `SPEND_LIMIT_DAILY_SATS` / `SPEND_LIMIT_SESSION_SATS` | BTC spend cap per day / per unlock (sats) | `50000` |
|
|
515
550
|
|
|
516
551
|
**Note on `NETWORK`:** The `--install` command writes `NETWORK=mainnet` by default (pass `--testnet` to use testnet). If you omit `NETWORK` from your config entirely, the runtime fallback is `testnet`. Most users should set this explicitly.
|
|
517
552
|
|
|
553
|
+
**Note on spending limits:** A default-on safety rail meters every outbound spend (`transfer_stx`, `transfer_btc`, x402/L402 auto-payments) against a cumulative per-session and per-day cap, so a single bad instruction or a malicious endpoint can't drain the wallet. A spend over the cap is blocked and reports the remaining budget. Raise the caps via the env vars above, or disable with `SPEND_LIMIT_ENABLED=false`. See [SECURITY.md](SECURITY.md#limit-blast-radius).
|
|
554
|
+
|
|
518
555
|
**Note:** `CLIENT_MNEMONIC` is optional. The recommended approach is to let Claude create its own wallet. `HIRO_API_KEY` is optional but recommended for production use — without it, you may hit Hiro's public rate limits (429 responses). Get a key at [platform.hiro.so](https://platform.hiro.so).
|
|
519
556
|
|
|
520
557
|
## Architecture
|
|
@@ -538,8 +575,12 @@ You ←→ Claude ←→ aibtc-mcp-server
|
|
|
538
575
|
- Mnemonics shown only once at creation
|
|
539
576
|
- Auto-lock after 15 minutes (configurable)
|
|
540
577
|
- Transactions signed locally before broadcast
|
|
578
|
+
- **Spending limit (default-on):** outbound spends are capped per session and per day so a single bad instruction can't drain the wallet — see [Configuration](#configuration)
|
|
579
|
+
- **Secret-scanning pre-commit hook:** contributors get a hook (auto-installed via `npm install`) that blocks committing a seed phrase or private key
|
|
541
580
|
- For mainnet: Fund with small amounts first
|
|
542
581
|
|
|
582
|
+
See [SECURITY.md](SECURITY.md) for the full wallet-key protection model and key-leak recovery steps.
|
|
583
|
+
|
|
543
584
|
## Advanced: Pre-configured Mnemonic
|
|
544
585
|
|
|
545
586
|
For automated setups where Claude needs immediate wallet access, add the `CLIENT_MNEMONIC` environment variable to your MCP server config (in `~/.claude.json` for Claude Code, or `claude_desktop_config.json` for Claude Desktop):
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenRouter <-> MCP bridge (`aibtc-mcp-server bridge`).
|
|
3
|
+
*
|
|
4
|
+
* Lets any OpenRouter model call this server's tools. This is the canonical
|
|
5
|
+
* integration described in OpenRouter's own cookbook
|
|
6
|
+
* (https://openrouter.ai/docs/cookbook/coding-agents/mcp-servers): the CLIENT
|
|
7
|
+
* connects to the MCP server and routes LLM calls through OpenRouter — there is
|
|
8
|
+
* no OpenRouter-side MCP hosting. This is a Node port of that pattern with two
|
|
9
|
+
* upgrades: it handles every tool_call (not just the first) and loops to
|
|
10
|
+
* completion (not a single round).
|
|
11
|
+
*
|
|
12
|
+
* Flow:
|
|
13
|
+
* 1. Spawn this same binary in server mode over stdio, connect, list tools.
|
|
14
|
+
* 2. Apply safety filters, then convert each MCP tool -> OpenAI function tool.
|
|
15
|
+
* 3. POST conversation + tools to OpenRouter /chat/completions.
|
|
16
|
+
* 4. On tool_calls, execute via MCP and feed results back as role:"tool".
|
|
17
|
+
* 5. Loop until the model answers in plain text (capped by --max-turns).
|
|
18
|
+
*
|
|
19
|
+
* Safety flags:
|
|
20
|
+
* --read-only expose only read-only tools (heuristic allowlist)
|
|
21
|
+
* --allow a,b,c force-allow these tool names (added to the set)
|
|
22
|
+
* --block a,b,c force-remove these tool names (applied last, wins)
|
|
23
|
+
* --max-spend-ustx <n> forward to the server's SPEND_LIMIT_SESSION_USTX rail
|
|
24
|
+
* --max-spend-sats <n> forward to the server's SPEND_LIMIT_SESSION_SATS rail
|
|
25
|
+
* --list-tools print the exposed tool set and exit (no API key needed)
|
|
26
|
+
* --model <id> OpenRouter model (default anthropic/claude-3.5-haiku)
|
|
27
|
+
* --network <net> mainnet|testnet for the spawned server (default mainnet)
|
|
28
|
+
* --max-turns <n> tool-call loop cap (default 10)
|
|
29
|
+
*/
|
|
30
|
+
export declare function runBridge(argv: string[]): Promise<void>;
|
|
31
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenRouter <-> MCP bridge (`aibtc-mcp-server bridge`).
|
|
3
|
+
*
|
|
4
|
+
* Lets any OpenRouter model call this server's tools. This is the canonical
|
|
5
|
+
* integration described in OpenRouter's own cookbook
|
|
6
|
+
* (https://openrouter.ai/docs/cookbook/coding-agents/mcp-servers): the CLIENT
|
|
7
|
+
* connects to the MCP server and routes LLM calls through OpenRouter — there is
|
|
8
|
+
* no OpenRouter-side MCP hosting. This is a Node port of that pattern with two
|
|
9
|
+
* upgrades: it handles every tool_call (not just the first) and loops to
|
|
10
|
+
* completion (not a single round).
|
|
11
|
+
*
|
|
12
|
+
* Flow:
|
|
13
|
+
* 1. Spawn this same binary in server mode over stdio, connect, list tools.
|
|
14
|
+
* 2. Apply safety filters, then convert each MCP tool -> OpenAI function tool.
|
|
15
|
+
* 3. POST conversation + tools to OpenRouter /chat/completions.
|
|
16
|
+
* 4. On tool_calls, execute via MCP and feed results back as role:"tool".
|
|
17
|
+
* 5. Loop until the model answers in plain text (capped by --max-turns).
|
|
18
|
+
*
|
|
19
|
+
* Safety flags:
|
|
20
|
+
* --read-only expose only read-only tools (heuristic allowlist)
|
|
21
|
+
* --allow a,b,c force-allow these tool names (added to the set)
|
|
22
|
+
* --block a,b,c force-remove these tool names (applied last, wins)
|
|
23
|
+
* --max-spend-ustx <n> forward to the server's SPEND_LIMIT_SESSION_USTX rail
|
|
24
|
+
* --max-spend-sats <n> forward to the server's SPEND_LIMIT_SESSION_SATS rail
|
|
25
|
+
* --list-tools print the exposed tool set and exit (no API key needed)
|
|
26
|
+
* --model <id> OpenRouter model (default anthropic/claude-3.5-haiku)
|
|
27
|
+
* --network <net> mainnet|testnet for the spawned server (default mainnet)
|
|
28
|
+
* --max-turns <n> tool-call loop cap (default 10)
|
|
29
|
+
*/
|
|
30
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
31
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
32
|
+
const OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions";
|
|
33
|
+
const DEFAULT_MODEL = "anthropic/claude-3.5-haiku";
|
|
34
|
+
/**
|
|
35
|
+
* Read-only heuristic. A tool counts as read-only only if it matches a known
|
|
36
|
+
* read pattern AND contains no mutating verb — deliberately conservative so a
|
|
37
|
+
* spend/write tool is never auto-included. Use --allow/--block for precision.
|
|
38
|
+
*/
|
|
39
|
+
const READ_PATTERNS = [
|
|
40
|
+
/^get_/, /^list_/, /^check_/, /^lookup_/, /^reverse_/, /^probe_/, /^read_/,
|
|
41
|
+
/_status$/, /_info$/, /_quote$/, /_quote_/, /_models$/, /_guide$/, /_list$/,
|
|
42
|
+
/_balance$/, /_fees$/, /_utxos$/, /_history$/, /_holdings$/, /_overview$/,
|
|
43
|
+
/_summary$/, /_health$/, /_price$/, /_prices$/, /_state$/, /_positions$/,
|
|
44
|
+
/_breakdown$/, /_dashboard_/, /_stats$/, /_metadata$/, /_owner$/, /_holders$/,
|
|
45
|
+
/_search$/, /_feed$/, /_profile$/, /_pubkey$/, /_availability$/, /_count$/,
|
|
46
|
+
];
|
|
47
|
+
const WRITE_VERB = /(transfer|send|swap|deploy|broadcast|buy|sell|pay|supply|withdraw|borrow|repay|create|delete|register|claim|deposit|stack|boost|unwind|invite|set_|sign|submit|enroll|opt_|file_|preorder|mint|inscribe|approve|revoke|connect|disconnect|rotate|export|import|unlock|lock|start|stop|join|settle|redeem|fund|accept|cancel|counter|append|give|add_|deactivate|record|compile|configure|heal|fill_gap|recover|rebalance|hunter|enable)/;
|
|
48
|
+
function isReadOnly(name) {
|
|
49
|
+
if (WRITE_VERB.test(name))
|
|
50
|
+
return false;
|
|
51
|
+
return READ_PATTERNS.some((p) => p.test(name));
|
|
52
|
+
}
|
|
53
|
+
/** MCP tool -> OpenRouter (OpenAI) function-tool schema (verbatim JSON Schema). */
|
|
54
|
+
function toOpenAITool(tool) {
|
|
55
|
+
// Clone so we never mutate the tool descriptor handed back by the MCP SDK.
|
|
56
|
+
const raw = tool.inputSchema ?? {};
|
|
57
|
+
const schema = { type: "object", properties: {}, ...raw };
|
|
58
|
+
return {
|
|
59
|
+
type: "function",
|
|
60
|
+
function: {
|
|
61
|
+
name: tool.name,
|
|
62
|
+
description: tool.description ?? "",
|
|
63
|
+
parameters: schema,
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function parseArgs(argv) {
|
|
68
|
+
const opts = {
|
|
69
|
+
prompt: "",
|
|
70
|
+
model: process.env.OPENROUTER_MODEL || DEFAULT_MODEL,
|
|
71
|
+
network: process.env.NETWORK || "mainnet",
|
|
72
|
+
readOnly: false,
|
|
73
|
+
allow: new Set(),
|
|
74
|
+
block: new Set(),
|
|
75
|
+
maxTurns: 10,
|
|
76
|
+
listTools: false,
|
|
77
|
+
};
|
|
78
|
+
const promptWords = [];
|
|
79
|
+
const csv = (v) => v.split(",").map((s) => s.trim()).filter(Boolean);
|
|
80
|
+
for (let i = 0; i < argv.length; i++) {
|
|
81
|
+
const a = argv[i];
|
|
82
|
+
// Value-taking flags must be followed by a value — otherwise the next loop
|
|
83
|
+
// iteration would silently consume the following flag (or leave the option
|
|
84
|
+
// unset, which for a spend cap means the rail is quietly disabled).
|
|
85
|
+
const value = (flag) => {
|
|
86
|
+
const v = argv[++i];
|
|
87
|
+
if (v === undefined || v.startsWith("--")) {
|
|
88
|
+
console.error(`Missing value for ${flag}`);
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
return v;
|
|
92
|
+
};
|
|
93
|
+
switch (a) {
|
|
94
|
+
case "--read-only":
|
|
95
|
+
opts.readOnly = true;
|
|
96
|
+
break;
|
|
97
|
+
case "--list-tools":
|
|
98
|
+
opts.listTools = true;
|
|
99
|
+
break;
|
|
100
|
+
case "--model":
|
|
101
|
+
opts.model = value(a);
|
|
102
|
+
break;
|
|
103
|
+
case "--network":
|
|
104
|
+
opts.network = value(a);
|
|
105
|
+
break;
|
|
106
|
+
case "--allow":
|
|
107
|
+
csv(value(a)).forEach((t) => opts.allow.add(t));
|
|
108
|
+
break;
|
|
109
|
+
case "--block":
|
|
110
|
+
csv(value(a)).forEach((t) => opts.block.add(t));
|
|
111
|
+
break;
|
|
112
|
+
case "--max-spend-ustx":
|
|
113
|
+
opts.maxSpendUstx = value(a);
|
|
114
|
+
break;
|
|
115
|
+
case "--max-spend-sats":
|
|
116
|
+
opts.maxSpendSats = value(a);
|
|
117
|
+
break;
|
|
118
|
+
case "--max-turns": {
|
|
119
|
+
const raw = value(a);
|
|
120
|
+
const n = parseInt(raw, 10);
|
|
121
|
+
if (!Number.isInteger(n) || n < 1) {
|
|
122
|
+
console.error(`--max-turns must be a positive integer (got "${raw}")`);
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
opts.maxTurns = n;
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
default:
|
|
129
|
+
if (a.startsWith("--")) {
|
|
130
|
+
console.error(`Unknown flag: ${a}`);
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
133
|
+
promptWords.push(a);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
opts.prompt = promptWords.join(" ").trim();
|
|
137
|
+
return opts;
|
|
138
|
+
}
|
|
139
|
+
/** Apply read-only + allow + block to the raw MCP tool list. */
|
|
140
|
+
function selectTools(tools, opts) {
|
|
141
|
+
return tools.filter((t) => {
|
|
142
|
+
if (opts.block.has(t.name))
|
|
143
|
+
return false;
|
|
144
|
+
if (opts.allow.has(t.name))
|
|
145
|
+
return true;
|
|
146
|
+
if (opts.readOnly)
|
|
147
|
+
return isReadOnly(t.name);
|
|
148
|
+
return true;
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
async function callOpenRouter(apiKey, model, messages, tools) {
|
|
152
|
+
const res = await fetch(OPENROUTER_URL, {
|
|
153
|
+
method: "POST",
|
|
154
|
+
headers: {
|
|
155
|
+
Authorization: `Bearer ${apiKey}`,
|
|
156
|
+
"Content-Type": "application/json",
|
|
157
|
+
"HTTP-Referer": "https://github.com/aibtcdev/aibtc-mcp",
|
|
158
|
+
"X-Title": "aibtc-mcp bridge",
|
|
159
|
+
},
|
|
160
|
+
body: JSON.stringify({ model, messages, tools, tool_choice: "auto" }),
|
|
161
|
+
});
|
|
162
|
+
if (!res.ok) {
|
|
163
|
+
throw new Error(`OpenRouter ${res.status}: ${await res.text()}`);
|
|
164
|
+
}
|
|
165
|
+
// OpenRouter can return HTTP 200 with an error payload (rate limit, moderation,
|
|
166
|
+
// provider failure) and no choices — surface it instead of crashing on [0].
|
|
167
|
+
const data = (await res.json());
|
|
168
|
+
const message = data.choices?.[0]?.message;
|
|
169
|
+
if (!message) {
|
|
170
|
+
const detail = data.error?.message ?? JSON.stringify(data);
|
|
171
|
+
throw new Error(`OpenRouter returned no message: ${detail}`);
|
|
172
|
+
}
|
|
173
|
+
return message;
|
|
174
|
+
}
|
|
175
|
+
export async function runBridge(argv) {
|
|
176
|
+
const opts = parseArgs(argv);
|
|
177
|
+
// Spawn this same binary in plain server mode (no extra args -> MCP server).
|
|
178
|
+
const serverEnv = {
|
|
179
|
+
...process.env,
|
|
180
|
+
NETWORK: opts.network,
|
|
181
|
+
};
|
|
182
|
+
if (opts.maxSpendUstx)
|
|
183
|
+
serverEnv.SPEND_LIMIT_SESSION_USTX = opts.maxSpendUstx;
|
|
184
|
+
if (opts.maxSpendSats)
|
|
185
|
+
serverEnv.SPEND_LIMIT_SESSION_SATS = opts.maxSpendSats;
|
|
186
|
+
const transport = new StdioClientTransport({
|
|
187
|
+
command: process.execPath,
|
|
188
|
+
args: [process.argv[1]],
|
|
189
|
+
env: serverEnv,
|
|
190
|
+
});
|
|
191
|
+
const client = new Client({ name: "aibtc-openrouter-bridge", version: "1.0.0" });
|
|
192
|
+
await client.connect(transport);
|
|
193
|
+
const { tools: allTools } = await client.listTools();
|
|
194
|
+
const exposed = selectTools(allTools, opts);
|
|
195
|
+
if (opts.listTools) {
|
|
196
|
+
console.log(`Exposed ${exposed.length}/${allTools.length} tools` +
|
|
197
|
+
(opts.readOnly ? " (read-only)" : "") + ":");
|
|
198
|
+
for (const t of exposed)
|
|
199
|
+
console.log(` ${t.name}`);
|
|
200
|
+
await client.close();
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
const apiKey = process.env.OPENROUTER_API_KEY;
|
|
204
|
+
if (!apiKey) {
|
|
205
|
+
console.error("Set OPENROUTER_API_KEY (or use --list-tools to preview without a model).");
|
|
206
|
+
await client.close();
|
|
207
|
+
process.exit(1);
|
|
208
|
+
}
|
|
209
|
+
if (!opts.prompt) {
|
|
210
|
+
console.error('Provide a prompt, e.g. aibtc-mcp-server bridge "what is the STX balance of SP3..."');
|
|
211
|
+
await client.close();
|
|
212
|
+
process.exit(1);
|
|
213
|
+
}
|
|
214
|
+
const writeCount = exposed.filter((t) => !isReadOnly(t.name)).length;
|
|
215
|
+
console.error(`Bridge: ${exposed.length} tools -> ${opts.model}` +
|
|
216
|
+
(opts.readOnly ? " [read-only]" : ` [${writeCount} can write/spend]`) +
|
|
217
|
+
(opts.maxSpendUstx || opts.maxSpendSats
|
|
218
|
+
? ` (cap ${opts.maxSpendUstx ?? "-"} uSTX / ${opts.maxSpendSats ?? "-"} sats)`
|
|
219
|
+
: ""));
|
|
220
|
+
const openaiTools = exposed.map(toOpenAITool);
|
|
221
|
+
const messages = [
|
|
222
|
+
{
|
|
223
|
+
role: "system",
|
|
224
|
+
content: "You are an agent with access to aibtc-mcp tools for Stacks/Bitcoin and " +
|
|
225
|
+
"x402 actions. Use tools when needed. Be concise.",
|
|
226
|
+
},
|
|
227
|
+
{ role: "user", content: opts.prompt },
|
|
228
|
+
];
|
|
229
|
+
try {
|
|
230
|
+
let completed = false;
|
|
231
|
+
for (let turn = 0; turn < opts.maxTurns; turn++) {
|
|
232
|
+
const reply = await callOpenRouter(apiKey, opts.model, messages, openaiTools);
|
|
233
|
+
messages.push(reply);
|
|
234
|
+
const toolCalls = reply.tool_calls ?? [];
|
|
235
|
+
if (toolCalls.length === 0) {
|
|
236
|
+
console.log("\n" + (reply.content ?? ""));
|
|
237
|
+
completed = true;
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
for (const call of toolCalls) {
|
|
241
|
+
const name = call.function.name;
|
|
242
|
+
// Enforce the allowlist at execution time too — never trust the model
|
|
243
|
+
// to stay inside the exposed set.
|
|
244
|
+
if (!exposed.some((t) => t.name === name)) {
|
|
245
|
+
messages.push({
|
|
246
|
+
role: "tool",
|
|
247
|
+
tool_call_id: call.id,
|
|
248
|
+
name,
|
|
249
|
+
content: `Tool ${name} is not permitted by the bridge.`,
|
|
250
|
+
});
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
let args = {};
|
|
254
|
+
try {
|
|
255
|
+
args = JSON.parse(call.function.arguments || "{}");
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
/* malformed JSON -> empty args */
|
|
259
|
+
}
|
|
260
|
+
console.error(`-> ${name}(${call.function.arguments || "{}"})`);
|
|
261
|
+
let content;
|
|
262
|
+
try {
|
|
263
|
+
const result = await client.callTool({ name, arguments: args });
|
|
264
|
+
content = (result.content ?? [])
|
|
265
|
+
.map((c) => (c?.type === "text" ? c.text : JSON.stringify(c)))
|
|
266
|
+
.join("\n");
|
|
267
|
+
}
|
|
268
|
+
catch (err) {
|
|
269
|
+
content = `Tool error: ${err.message}`;
|
|
270
|
+
}
|
|
271
|
+
messages.push({ role: "tool", tool_call_id: call.id, name, content });
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
// Don't let the cap swallow the session silently — the model was still
|
|
275
|
+
// calling tools when we ran out of turns.
|
|
276
|
+
if (!completed) {
|
|
277
|
+
console.error(`Bridge: reached --max-turns limit (${opts.maxTurns}) without a final response. ` +
|
|
278
|
+
"Re-run with a higher --max-turns to let it finish.");
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
finally {
|
|
282
|
+
await client.close();
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
//# sourceMappingURL=index.js.map
|
package/dist/index.js
CHANGED
|
@@ -36,13 +36,24 @@ function getClaudeDesktopConfigPath() {
|
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
async function readJsonConfig(filePath) {
|
|
39
|
+
let content;
|
|
40
|
+
try {
|
|
41
|
+
content = await fs.readFile(filePath, "utf8");
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
if (error.code === "ENOENT") {
|
|
45
|
+
return {}; // No config yet - fresh install
|
|
46
|
+
}
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
39
49
|
try {
|
|
40
|
-
const content = await fs.readFile(filePath, "utf8");
|
|
41
50
|
return JSON.parse(content);
|
|
42
51
|
}
|
|
43
52
|
catch {
|
|
44
|
-
//
|
|
45
|
-
|
|
53
|
+
// Never silently replace an unparseable config: it may hold the user's
|
|
54
|
+
// other MCP servers, and "merging" into {} would discard them on write.
|
|
55
|
+
throw new Error(`Existing config at ${filePath} is not valid JSON. ` +
|
|
56
|
+
`Fix or remove it, then re-run the install.`);
|
|
46
57
|
}
|
|
47
58
|
}
|
|
48
59
|
async function writeJsonConfig(filePath, config) {
|
|
@@ -99,8 +110,10 @@ async function writeCodexToml(configPath, network) {
|
|
|
99
110
|
try {
|
|
100
111
|
existing = await fs.readFile(configPath, "utf8");
|
|
101
112
|
}
|
|
102
|
-
catch {
|
|
103
|
-
|
|
113
|
+
catch (error) {
|
|
114
|
+
if (error.code !== "ENOENT") {
|
|
115
|
+
throw error; // Unreadable existing config must not be clobbered
|
|
116
|
+
}
|
|
104
117
|
}
|
|
105
118
|
const preserved = stripCodexSection(existing).replace(/\s+$/, "");
|
|
106
119
|
const block = [
|
|
@@ -216,6 +229,16 @@ if (process.argv[2] === "yield-hunter") {
|
|
|
216
229
|
process.exit(1);
|
|
217
230
|
});
|
|
218
231
|
}
|
|
232
|
+
// Check for bridge command (OpenRouter <-> MCP)
|
|
233
|
+
else if (process.argv[2] === "bridge") {
|
|
234
|
+
import("./bridge/index.js")
|
|
235
|
+
.then(({ runBridge }) => runBridge(process.argv.slice(3)))
|
|
236
|
+
.then(() => process.exit(0))
|
|
237
|
+
.catch((error) => {
|
|
238
|
+
console.error("❌ Bridge error:", redactSensitive(String(error)));
|
|
239
|
+
process.exit(1);
|
|
240
|
+
});
|
|
241
|
+
}
|
|
219
242
|
// Check for --install flag
|
|
220
243
|
else if (process.argv.includes("--install") || process.argv.includes("install")) {
|
|
221
244
|
runInstall()
|
|
@@ -39,6 +39,7 @@ async function encrypt(data, password) {
|
|
|
39
39
|
const iv = crypto.randomBytes(12);
|
|
40
40
|
const key = await deriveKey(password, salt);
|
|
41
41
|
const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
|
|
42
|
+
key.fill(0); // cipher holds its own copy
|
|
42
43
|
const encrypted = Buffer.concat([cipher.update(data, "utf8"), cipher.final()]);
|
|
43
44
|
const authTag = cipher.getAuthTag();
|
|
44
45
|
return {
|
|
@@ -53,6 +54,7 @@ async function encrypt(data, password) {
|
|
|
53
54
|
async function decrypt(encrypted, password) {
|
|
54
55
|
const key = await deriveKey(password, Buffer.from(encrypted.salt, "base64"));
|
|
55
56
|
const decipher = crypto.createDecipheriv("aes-256-gcm", key, Buffer.from(encrypted.iv, "base64"));
|
|
57
|
+
key.fill(0); // decipher holds its own copy
|
|
56
58
|
decipher.setAuthTag(Buffer.from(encrypted.authTag, "base64"));
|
|
57
59
|
const decrypted = Buffer.concat([
|
|
58
60
|
decipher.update(Buffer.from(encrypted.ciphertext, "base64")),
|
|
@@ -67,12 +69,15 @@ function emptyStore() {
|
|
|
67
69
|
async function save() {
|
|
68
70
|
if (!_store || !_password)
|
|
69
71
|
throw new Error("Store not unlocked");
|
|
70
|
-
await fs.mkdir(getStoreDir(), { recursive: true });
|
|
72
|
+
await fs.mkdir(getStoreDir(), { recursive: true, mode: 0o700 });
|
|
71
73
|
const file = {
|
|
72
74
|
version: VERSION,
|
|
73
75
|
encrypted: await encrypt(JSON.stringify(_store), _password),
|
|
74
76
|
};
|
|
75
|
-
|
|
77
|
+
// Owner-only perms + atomic rename, same pattern as the wallet keystores
|
|
78
|
+
const tempFile = getStoreFile() + ".tmp";
|
|
79
|
+
await fs.writeFile(tempFile, JSON.stringify(file, null, 2), { mode: 0o600 });
|
|
80
|
+
await fs.rename(tempFile, getStoreFile());
|
|
76
81
|
}
|
|
77
82
|
async function load(password) {
|
|
78
83
|
const raw = await fs.readFile(getStoreFile(), "utf-8");
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { uintCV, contractPrincipalCV, cvToJSON, hexToCV, PostConditionMode, Pc, principalCV, noneCV, someCV, listCV, bufferCV, } from "@stacks/transactions";
|
|
2
2
|
import { AlexSDK, Currency } from "alex-sdk";
|
|
3
|
-
import { getHiroApi } from "./hiro-api.js";
|
|
3
|
+
import { getHiroApi, isNotFoundError } from "./hiro-api.js";
|
|
4
4
|
import { getAlexContracts, getZestContracts, parseContractId, ZEST_ASSETS, ZEST_V2_MARKET, ZEST_V2_MARKET_VAULT, } from "../config/index.js";
|
|
5
5
|
import { callContract } from "../transactions/builder.js";
|
|
6
6
|
// ============================================================================
|
|
@@ -129,8 +129,10 @@ export class AlexDexService {
|
|
|
129
129
|
}
|
|
130
130
|
return null;
|
|
131
131
|
}
|
|
132
|
-
catch {
|
|
133
|
-
|
|
132
|
+
catch (error) {
|
|
133
|
+
if (isNotFoundError(error))
|
|
134
|
+
return null;
|
|
135
|
+
throw error;
|
|
134
136
|
}
|
|
135
137
|
}
|
|
136
138
|
/**
|
|
@@ -360,8 +362,10 @@ export class ZestProtocolService {
|
|
|
360
362
|
healthFactor: position["health-factor"]?.value,
|
|
361
363
|
};
|
|
362
364
|
}
|
|
363
|
-
catch {
|
|
364
|
-
|
|
365
|
+
catch (error) {
|
|
366
|
+
if (isNotFoundError(error))
|
|
367
|
+
return null;
|
|
368
|
+
throw error;
|
|
365
369
|
}
|
|
366
370
|
}
|
|
367
371
|
/**
|
|
@@ -377,8 +381,10 @@ export class ZestProtocolService {
|
|
|
377
381
|
}
|
|
378
382
|
return cvToJSON(hexToCV(result.result));
|
|
379
383
|
}
|
|
380
|
-
catch {
|
|
381
|
-
|
|
384
|
+
catch (error) {
|
|
385
|
+
if (isNotFoundError(error))
|
|
386
|
+
return null;
|
|
387
|
+
throw error;
|
|
382
388
|
}
|
|
383
389
|
}
|
|
384
390
|
/**
|
|
@@ -411,8 +417,12 @@ export class ZestProtocolService {
|
|
|
411
417
|
}
|
|
412
418
|
}
|
|
413
419
|
}
|
|
414
|
-
catch {
|
|
415
|
-
//
|
|
420
|
+
catch (error) {
|
|
421
|
+
// Only fall back to the shares lower-bound estimate when the vault
|
|
422
|
+
// function is missing — network/server failures must surface, or the
|
|
423
|
+
// post-conditions built from this value would be silently wrong.
|
|
424
|
+
if (!isNotFoundError(error))
|
|
425
|
+
throw error;
|
|
416
426
|
}
|
|
417
427
|
return shares;
|
|
418
428
|
}
|
|
@@ -431,8 +441,10 @@ export class ZestProtocolService {
|
|
|
431
441
|
}
|
|
432
442
|
}
|
|
433
443
|
}
|
|
434
|
-
catch {
|
|
435
|
-
//
|
|
444
|
+
catch (error) {
|
|
445
|
+
// Same rationale as getUnderlyingFromShares: estimate only on 404.
|
|
446
|
+
if (!isNotFoundError(error))
|
|
447
|
+
throw error;
|
|
436
448
|
}
|
|
437
449
|
return amount;
|
|
438
450
|
}
|
|
@@ -7,6 +7,16 @@ export declare class HiroApiRateLimitError extends Error {
|
|
|
7
7
|
readonly retryAfterSeconds: number;
|
|
8
8
|
constructor(message: string, retryAfterSeconds: number);
|
|
9
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* HTTP error from the Hiro / BNS V2 APIs, carrying the status code so
|
|
12
|
+
* callers can treat 404 as "not found" without swallowing other failures.
|
|
13
|
+
*/
|
|
14
|
+
export declare class HiroApiError extends Error {
|
|
15
|
+
readonly status: number;
|
|
16
|
+
constructor(message: string, status: number);
|
|
17
|
+
}
|
|
18
|
+
/** True when the error is an expected "resource does not exist" response. */
|
|
19
|
+
export declare function isNotFoundError(error: unknown): boolean;
|
|
10
20
|
export interface AccountInfo {
|
|
11
21
|
balance: string;
|
|
12
22
|
locked: string;
|
|
@@ -16,6 +16,22 @@ export class HiroApiRateLimitError extends Error {
|
|
|
16
16
|
this.name = "HiroApiRateLimitError";
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* HTTP error from the Hiro / BNS V2 APIs, carrying the status code so
|
|
21
|
+
* callers can treat 404 as "not found" without swallowing other failures.
|
|
22
|
+
*/
|
|
23
|
+
export class HiroApiError extends Error {
|
|
24
|
+
status;
|
|
25
|
+
constructor(message, status) {
|
|
26
|
+
super(message);
|
|
27
|
+
this.status = status;
|
|
28
|
+
this.name = "HiroApiError";
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/** True when the error is an expected "resource does not exist" response. */
|
|
32
|
+
export function isNotFoundError(error) {
|
|
33
|
+
return error instanceof HiroApiError && error.status === 404;
|
|
34
|
+
}
|
|
19
35
|
// ============================================================================
|
|
20
36
|
// Hiro API Service
|
|
21
37
|
// ============================================================================
|
|
@@ -51,7 +67,7 @@ export class HiroApiService {
|
|
|
51
67
|
throw new HiroApiRateLimitError(`Hiro API rate limit exceeded. Retry after ${retryAfterSeconds}s`, retryAfterSeconds);
|
|
52
68
|
}
|
|
53
69
|
const errorText = await response.text();
|
|
54
|
-
throw new
|
|
70
|
+
throw new HiroApiError(`Hiro API error (${response.status}): ${errorText}`, response.status);
|
|
55
71
|
}
|
|
56
72
|
return response.json();
|
|
57
73
|
}
|
|
@@ -123,8 +139,10 @@ export class HiroApiService {
|
|
|
123
139
|
try {
|
|
124
140
|
return await this.fetch(`/metadata/v1/ft/${contractId}`);
|
|
125
141
|
}
|
|
126
|
-
catch {
|
|
127
|
-
|
|
142
|
+
catch (error) {
|
|
143
|
+
if (isNotFoundError(error))
|
|
144
|
+
return null;
|
|
145
|
+
throw error;
|
|
128
146
|
}
|
|
129
147
|
}
|
|
130
148
|
async getTokenHolders(contractId, options) {
|
|
@@ -329,8 +347,10 @@ export class HiroApiService {
|
|
|
329
347
|
const info = await this.getBnsNameInfo(name);
|
|
330
348
|
return info.address;
|
|
331
349
|
}
|
|
332
|
-
catch {
|
|
333
|
-
|
|
350
|
+
catch (error) {
|
|
351
|
+
if (isNotFoundError(error))
|
|
352
|
+
return null;
|
|
353
|
+
throw error;
|
|
334
354
|
}
|
|
335
355
|
}
|
|
336
356
|
// ==========================================================================
|
|
@@ -385,7 +405,7 @@ export class BnsV2ApiService {
|
|
|
385
405
|
});
|
|
386
406
|
if (!response.ok) {
|
|
387
407
|
const errorText = await response.text();
|
|
388
|
-
throw new
|
|
408
|
+
throw new HiroApiError(`BNS V2 API error (${response.status}): ${errorText}`, response.status);
|
|
389
409
|
}
|
|
390
410
|
return response.json();
|
|
391
411
|
}
|
|
@@ -431,8 +451,10 @@ export class BnsV2ApiService {
|
|
|
431
451
|
}
|
|
432
452
|
return null;
|
|
433
453
|
}
|
|
434
|
-
catch {
|
|
435
|
-
|
|
454
|
+
catch (error) {
|
|
455
|
+
if (isNotFoundError(error))
|
|
456
|
+
return null;
|
|
457
|
+
throw error;
|
|
436
458
|
}
|
|
437
459
|
}
|
|
438
460
|
}
|