@shicodiez/crypto-regime-mcp 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +198 -2
- package/package.json +46 -4
- package/src/index.js +139 -0
- package/src/spend.js +40 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Shicodiez
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,199 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @shicodiez/crypto-regime-mcp
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Puente **MCP stdio** para [`signal-api`](https://signal-api.shicoars.workers.dev).
|
|
4
|
+
Conecta Claude Desktop, Claude Code o Cursor al endpoint remoto
|
|
5
|
+
`/mcp` y paga automáticamente las tools x402 con una wallet EVM local.
|
|
6
|
+
|
|
7
|
+
> **No es Signal Messenger.** `signal-api` es el nombre del servicio de señales
|
|
8
|
+
> cripto; este paquete no tiene relación con Signal Messenger ni con su app de
|
|
9
|
+
> mensajería.
|
|
10
|
+
|
|
11
|
+
- Tools gratis: `list_assets`, `get_catalog`.
|
|
12
|
+
- Tools de pago: `get_signal` (0,01 USDC), `get_trend` (0,005 USDC),
|
|
13
|
+
`get_regime` (0,005 USDC).
|
|
14
|
+
- Red por defecto: **Base mainnet** (`eip155:8453`), USDC.
|
|
15
|
+
- Límite de gasto por sesión: `MAX_SPEND_USDC` (por defecto **1 USDC**).
|
|
16
|
+
|
|
17
|
+
> **No publiques la clave privada.** Usa una wallet **dedicada**, con poco saldo,
|
|
18
|
+
> solo para este puente. La clave se guarda en la configuración de Claude/Cursor
|
|
19
|
+
> en texto plano y el puente la usa para firmar pagos x402.
|
|
20
|
+
|
|
21
|
+
## Instalación
|
|
22
|
+
|
|
23
|
+
El paquete aún **no está publicado en npm**. Para probarlo:
|
|
24
|
+
|
|
25
|
+
```powershell
|
|
26
|
+
cd mcp-bridge
|
|
27
|
+
npm install
|
|
28
|
+
node src/index.js
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Para usarlo como comando local:
|
|
32
|
+
|
|
33
|
+
```powershell
|
|
34
|
+
npm link
|
|
35
|
+
crypto-regime-mcp
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
La idea es publicarlo más adelante como `@shicodiez/crypto-regime-mcp`; entonces
|
|
39
|
+
valdrá `npx -y @shicodiez/crypto-regime-mcp`.
|
|
40
|
+
|
|
41
|
+
## Variables de entorno
|
|
42
|
+
|
|
43
|
+
| Variable | Obligatoria | Default | Descripción |
|
|
44
|
+
|---|---|---|---|
|
|
45
|
+
| `EVM_PRIVATE_KEY` | Sí | — | Clave privada `0x...` de la wallet dedicada. Nunca la commitees. |
|
|
46
|
+
| `MAX_SPEND_USDC` | No | `1` | Gasto máximo acumulado por sesión, en USDC. |
|
|
47
|
+
| `SIGNAL_API_MCP_URL` | No | `https://signal-api.shicoars.workers.dev/mcp` | Endpoint MCP remoto. |
|
|
48
|
+
| `NETWORK` | No | `eip155:8453` | Red x402. Usa `eip155:84532` para testnet. |
|
|
49
|
+
|
|
50
|
+
El límite `MAX_SPEND_USDC` se aplica **antes** de cada pago: si el pago haría
|
|
51
|
+
superar el acumulado, se bloquea y la tool devuelve error. Se asume USDC de 6
|
|
52
|
+
decimales (el activo de signal-api). El contador es conservador: si un settlement
|
|
53
|
+
falla después de autorizar, el importe sigue contado en la sesión.
|
|
54
|
+
|
|
55
|
+
## Claude Desktop
|
|
56
|
+
|
|
57
|
+
Edita:
|
|
58
|
+
|
|
59
|
+
- Windows: `%APPDATA%\Claude\claude_desktop_config.json`
|
|
60
|
+
- macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
61
|
+
- Linux: `~/.config/Claude/claude_desktop_config.json`
|
|
62
|
+
|
|
63
|
+
```json
|
|
64
|
+
{
|
|
65
|
+
"mcpServers": {
|
|
66
|
+
"crypto-regime": {
|
|
67
|
+
"command": "npx",
|
|
68
|
+
"args": ["-y", "@shicodiez/crypto-regime-mcp"],
|
|
69
|
+
"env": {
|
|
70
|
+
"EVM_PRIVATE_KEY": "0x...",
|
|
71
|
+
"MAX_SPEND_USDC": "1"
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Si aún no está publicado, apunta al script local:
|
|
79
|
+
|
|
80
|
+
```json
|
|
81
|
+
{
|
|
82
|
+
"mcpServers": {
|
|
83
|
+
"crypto-regime": {
|
|
84
|
+
"command": "node",
|
|
85
|
+
"args": ["C:\\Users\\TU_USUARIO\\signal-bot\\mcp-bridge\\src\\index.js"],
|
|
86
|
+
"env": {
|
|
87
|
+
"EVM_PRIVATE_KEY": "0x...",
|
|
88
|
+
"MAX_SPEND_USDC": "1"
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Reinicia Claude Desktop después de guardar.
|
|
96
|
+
|
|
97
|
+
## Claude Code
|
|
98
|
+
|
|
99
|
+
Con el paquete publicado:
|
|
100
|
+
|
|
101
|
+
```powershell
|
|
102
|
+
claude mcp add crypto-regime `
|
|
103
|
+
--env EVM_PRIVATE_KEY=0x... `
|
|
104
|
+
--env MAX_SPEND_USDC=1 `
|
|
105
|
+
-- npx -y @shicodiez/crypto-regime-mcp
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Con el script local:
|
|
109
|
+
|
|
110
|
+
```powershell
|
|
111
|
+
claude mcp add crypto-regime `
|
|
112
|
+
--env EVM_PRIVATE_KEY=0x... `
|
|
113
|
+
--env MAX_SPEND_USDC=1 `
|
|
114
|
+
-- node C:\Users\TU_USUARIO\signal-bot\mcp-bridge\src\index.js
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Comprueba con `claude mcp list` y `claude mcp get crypto-regime`.
|
|
118
|
+
|
|
119
|
+
## Cursor
|
|
120
|
+
|
|
121
|
+
Crea `.cursor/mcp.json` en el proyecto (o usa la configuración global de Cursor):
|
|
122
|
+
|
|
123
|
+
```json
|
|
124
|
+
{
|
|
125
|
+
"mcpServers": {
|
|
126
|
+
"crypto-regime": {
|
|
127
|
+
"command": "npx",
|
|
128
|
+
"args": ["-y", "@shicodiez/crypto-regime-mcp"],
|
|
129
|
+
"env": {
|
|
130
|
+
"EVM_PRIVATE_KEY": "0x...",
|
|
131
|
+
"MAX_SPEND_USDC": "1"
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Para el script local, cambia `command` por `node` y `args` por la ruta absoluta
|
|
139
|
+
de `src/index.js`. Recarga la ventana de MCP de Cursor.
|
|
140
|
+
|
|
141
|
+
## Cómo funciona
|
|
142
|
+
|
|
143
|
+
El puente:
|
|
144
|
+
|
|
145
|
+
1. Abre el `/mcp` remoto con `StreamableHTTPClientTransport`.
|
|
146
|
+
2. Lo envuelve con **`createx402MCPClient`** y `ExactEvmScheme` de `@x402/evm`.
|
|
147
|
+
3. Expone un servidor MCP **stdio** con `ListTools`/`CallTool` que reenvía al
|
|
148
|
+
cliente remoto.
|
|
149
|
+
4. Cuando una tool devuelve `PaymentRequired` (x402 v2), el cliente firma el
|
|
150
|
+
pago con `EVM_PRIVATE_KEY`, lo reintenta con `_meta["x402/payment"]` y
|
|
151
|
+
devuelve el resultado junto con `_meta["x402/payment-response"]`.
|
|
152
|
+
|
|
153
|
+
El servidor remoto verifica y liquida con su facilitator; el puente solo firma
|
|
154
|
+
el pago. No hay custodia de fondos.
|
|
155
|
+
|
|
156
|
+
## Probar con MCP Inspector (stdio)
|
|
157
|
+
|
|
158
|
+
```powershell
|
|
159
|
+
cd mcp-bridge
|
|
160
|
+
npm install
|
|
161
|
+
$env:EVM_PRIVATE_KEY = "0x..." # no lo imprimas
|
|
162
|
+
$env:MAX_SPEND_USDC = "1"
|
|
163
|
+
|
|
164
|
+
npx @modelcontextprotocol/inspector@latest --cli `
|
|
165
|
+
--transport stdio `
|
|
166
|
+
--method tools/list `
|
|
167
|
+
--format json `
|
|
168
|
+
-- node C:\Users\TU_USUARIO\signal-bot\mcp-bridge\src\index.js
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
También puedes arrancar la UI web de Inspector sin `--cli` y elegir
|
|
172
|
+
`Standalone` → `stdio` → comando `node`, argumento `src/index.js`.
|
|
173
|
+
|
|
174
|
+
Para probar una tool gratis:
|
|
175
|
+
|
|
176
|
+
```powershell
|
|
177
|
+
npx @modelcontextprotocol/inspector@latest --cli `
|
|
178
|
+
--transport stdio `
|
|
179
|
+
--method tools/call `
|
|
180
|
+
--tool-name list_assets `
|
|
181
|
+
--format json `
|
|
182
|
+
-- node C:\Users\TU_USUARIO\signal-bot\mcp-bridge\src\index.js
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Las tools de pago requieren saldo USDC en la wallet y consumen
|
|
186
|
+
`MAX_SPEND_USDC`. Para testnet usa `NETWORK=eip155:84532` y un endpoint local
|
|
187
|
+
`SIGNAL_API_MCP_URL=http://localhost:8787/mcp`.
|
|
188
|
+
|
|
189
|
+
## Seguridad
|
|
190
|
+
|
|
191
|
+
- Usa una wallet **exclusiva** para este puente y mantén solo el saldo que estás
|
|
192
|
+
dispuesto a gastar.
|
|
193
|
+
- No pongas la clave en el repositorio, en `README.md`, en capturas ni en logs.
|
|
194
|
+
- `MAX_SPEND_USDC` reduce el daño de un agente descontrolado, pero no sustituye
|
|
195
|
+
a una wallet con poco saldo.
|
|
196
|
+
- El puente no puede retirar fondos: solo firma autorizaciones x402 del importe
|
|
197
|
+
que pide el servidor, sujeto al límite.
|
|
198
|
+
- Si ves un `payment blocked`, revisa `MAX_SPEND_USDC` y el acumulado de la
|
|
199
|
+
sesión; reinicia el cliente MCP para empezar una sesión nueva.
|
package/package.json
CHANGED
|
@@ -1,6 +1,48 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shicodiez/crypto-regime-mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Stdio MCP bridge for crypto trend/regime tools with automatic x402 payments.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"crypto-regime-mcp": "src/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"src",
|
|
11
|
+
"README.md",
|
|
12
|
+
"LICENSE"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"start": "node src/index.js",
|
|
16
|
+
"test": "node --test"
|
|
17
|
+
},
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=20"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"mcp",
|
|
23
|
+
"x402",
|
|
24
|
+
"crypto",
|
|
25
|
+
"trend",
|
|
26
|
+
"regime",
|
|
27
|
+
"volatility",
|
|
28
|
+
"base",
|
|
29
|
+
"usdc"
|
|
30
|
+
],
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
},
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "git+https://github.com/Shicodiez/signal-bot.git",
|
|
38
|
+
"directory": "mcp-bridge"
|
|
39
|
+
},
|
|
40
|
+
"homepage": "https://signal-api.shicoars.workers.dev",
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@modelcontextprotocol/sdk": "1.32.2",
|
|
43
|
+
"@x402/core": "2.30.0",
|
|
44
|
+
"@x402/evm": "2.30.0",
|
|
45
|
+
"@x402/mcp": "2.30.0",
|
|
46
|
+
"viem": "^2.57.16"
|
|
47
|
+
}
|
|
48
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
4
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
5
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
6
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
7
|
+
import {
|
|
8
|
+
CallToolRequestSchema,
|
|
9
|
+
ListToolsRequestSchema,
|
|
10
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
11
|
+
import { ExactEvmScheme } from "@x402/evm/exact/client";
|
|
12
|
+
import { createx402MCPClient } from "@x402/mcp";
|
|
13
|
+
import { privateKeyToAccount } from "viem/accounts";
|
|
14
|
+
|
|
15
|
+
import { createSpendTracker, usdcFromAtomic } from "./spend.js";
|
|
16
|
+
|
|
17
|
+
const VERSION = "0.1.0";
|
|
18
|
+
const DEFAULT_MCP_URL = "https://signal-api.shicoars.workers.dev/mcp";
|
|
19
|
+
const DEFAULT_NETWORK = "eip155:8453";
|
|
20
|
+
const DEFAULT_MAX_SPEND_USDC = 1;
|
|
21
|
+
|
|
22
|
+
function log(message) {
|
|
23
|
+
// stdout is reserved for the MCP stdio protocol.
|
|
24
|
+
console.error(`[crypto-regime-mcp] ${message}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function parseMaxSpend() {
|
|
28
|
+
const raw = process.env.MAX_SPEND_USDC;
|
|
29
|
+
if (raw === undefined || raw.trim() === "") {
|
|
30
|
+
return DEFAULT_MAX_SPEND_USDC;
|
|
31
|
+
}
|
|
32
|
+
const value = Number(raw);
|
|
33
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
34
|
+
throw new Error("MAX_SPEND_USDC must be a non-negative number");
|
|
35
|
+
}
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function getPrivateKey() {
|
|
40
|
+
const raw = process.env.EVM_PRIVATE_KEY?.trim();
|
|
41
|
+
if (!raw || !/^0x[0-9a-fA-F]{64}$/.test(raw)) {
|
|
42
|
+
throw new Error("EVM_PRIVATE_KEY must be a 32-byte hex private key (0x...)");
|
|
43
|
+
}
|
|
44
|
+
return raw;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function main() {
|
|
48
|
+
const mcpUrl = process.env.SIGNAL_API_MCP_URL ?? DEFAULT_MCP_URL;
|
|
49
|
+
const network = process.env.NETWORK ?? DEFAULT_NETWORK;
|
|
50
|
+
const maxSpendUsdc = parseMaxSpend();
|
|
51
|
+
const account = privateKeyToAccount(getPrivateKey());
|
|
52
|
+
const tracker = createSpendTracker(maxSpendUsdc);
|
|
53
|
+
|
|
54
|
+
const paymentClient = createx402MCPClient({
|
|
55
|
+
name: "crypto-regime-mcp",
|
|
56
|
+
version: VERSION,
|
|
57
|
+
schemes: [
|
|
58
|
+
{
|
|
59
|
+
network,
|
|
60
|
+
client: new ExactEvmScheme(account),
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
spendControls:
|
|
64
|
+
maxSpendUsdc > 0
|
|
65
|
+
? { maxAmountPerPayment: `$${maxSpendUsdc}` }
|
|
66
|
+
: false,
|
|
67
|
+
autoPayment: true,
|
|
68
|
+
onPaymentRequested: async ({ paymentRequired }) => {
|
|
69
|
+
const accept = paymentRequired.accepts?.[0];
|
|
70
|
+
if (!accept) {
|
|
71
|
+
log("payment blocked: no accepts entry");
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
const amountUsd = usdcFromAtomic(accept.amount);
|
|
75
|
+
if (amountUsd === null) {
|
|
76
|
+
log(`payment blocked: invalid amount ${accept.amount}`);
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
if (!tracker.canSpend(amountUsd)) {
|
|
80
|
+
log(
|
|
81
|
+
`payment blocked: $${amountUsd} would exceed MAX_SPEND_USDC=$${maxSpendUsdc} (spent $${tracker.spentUsdc})`,
|
|
82
|
+
);
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
tracker.record(amountUsd);
|
|
86
|
+
log(
|
|
87
|
+
`authorizing $${amountUsd} on ${accept.network} to ${accept.payTo} (spent $${tracker.spentUsdc}/$${maxSpendUsdc})`,
|
|
88
|
+
);
|
|
89
|
+
return true;
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const transport = new StreamableHTTPClientTransport(new URL(mcpUrl));
|
|
94
|
+
await paymentClient.connect(transport);
|
|
95
|
+
|
|
96
|
+
const server = new Server(
|
|
97
|
+
{ name: "crypto-regime-mcp", version: VERSION },
|
|
98
|
+
{ capabilities: { tools: {} } },
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
102
|
+
const result = await paymentClient.listTools();
|
|
103
|
+
return { tools: result.tools };
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
107
|
+
const { name, arguments: args } = request.params;
|
|
108
|
+
try {
|
|
109
|
+
const result = await paymentClient.callTool(name, args ?? {});
|
|
110
|
+
const meta = { ...(result._meta ?? {}) };
|
|
111
|
+
if (result.paymentResponse) {
|
|
112
|
+
meta["x402/payment-response"] = result.paymentResponse;
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
content: result.content,
|
|
116
|
+
isError: result.isError,
|
|
117
|
+
structuredContent: result.structuredContent,
|
|
118
|
+
_meta: Object.keys(meta).length > 0 ? meta : undefined,
|
|
119
|
+
};
|
|
120
|
+
} catch (error) {
|
|
121
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
122
|
+
log(`tool ${name} failed: ${message}`);
|
|
123
|
+
return {
|
|
124
|
+
content: [{ type: "text", text: JSON.stringify({ error: message }) }],
|
|
125
|
+
isError: true,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
await server.connect(new StdioServerTransport());
|
|
131
|
+
log(
|
|
132
|
+
`connected to ${mcpUrl} as ${account.address}; MAX_SPEND_USDC=$${maxSpendUsdc}`,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
main().catch((error) => {
|
|
137
|
+
log(error instanceof Error ? error.message : String(error));
|
|
138
|
+
process.exit(1);
|
|
139
|
+
});
|
package/src/spend.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export function createSpendTracker(maxSpendUsdc) {
|
|
2
|
+
let spentUsdc = 0;
|
|
3
|
+
const epsilon = 1e-9;
|
|
4
|
+
|
|
5
|
+
return {
|
|
6
|
+
get maxUsdc() {
|
|
7
|
+
return maxSpendUsdc;
|
|
8
|
+
},
|
|
9
|
+
get spentUsdc() {
|
|
10
|
+
return spentUsdc;
|
|
11
|
+
},
|
|
12
|
+
remainingUsdc() {
|
|
13
|
+
return Math.max(
|
|
14
|
+
0,
|
|
15
|
+
Math.round((maxSpendUsdc - spentUsdc) * 1e6) / 1e6,
|
|
16
|
+
);
|
|
17
|
+
},
|
|
18
|
+
canSpend(amountUsdc) {
|
|
19
|
+
return (
|
|
20
|
+
Number.isFinite(amountUsdc) &&
|
|
21
|
+
amountUsdc >= 0 &&
|
|
22
|
+
spentUsdc + amountUsdc <= maxSpendUsdc + epsilon
|
|
23
|
+
);
|
|
24
|
+
},
|
|
25
|
+
record(amountUsdc) {
|
|
26
|
+
if (!Number.isFinite(amountUsdc) || amountUsdc < 0) {
|
|
27
|
+
throw new Error("amountUsdc must be a non-negative finite number");
|
|
28
|
+
}
|
|
29
|
+
spentUsdc = Math.round((spentUsdc + amountUsdc) * 1e6) / 1e6;
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function usdcFromAtomic(amount, decimals = 6) {
|
|
35
|
+
const value = Number(amount);
|
|
36
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
return value / 10 ** decimals;
|
|
40
|
+
}
|