globodai-mcp-payment-manager 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +23 -0
- package/.github/workflows/ci.yml +26 -0
- package/.github/workflows/release.yml +82 -0
- package/LICENSE +21 -0
- package/README.md +362 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +122 -0
- package/dist/lib/blockchain.d.ts +50 -0
- package/dist/lib/blockchain.js +287 -0
- package/dist/lib/cards.d.ts +83 -0
- package/dist/lib/cards.js +276 -0
- package/dist/lib/cli-runner.d.ts +31 -0
- package/dist/lib/cli-runner.js +77 -0
- package/dist/lib/crypto.d.ts +39 -0
- package/dist/lib/crypto.js +228 -0
- package/dist/lib/cvv-crypto.d.ts +23 -0
- package/dist/lib/cvv-crypto.js +67 -0
- package/dist/lib/mcp-core.d.ts +46 -0
- package/dist/lib/mcp-core.js +86 -0
- package/dist/lib/pin-manager.d.ts +69 -0
- package/dist/lib/pin-manager.js +199 -0
- package/dist/lib/wallets.d.ts +91 -0
- package/dist/lib/wallets.js +227 -0
- package/dist/tools/add-card.d.ts +65 -0
- package/dist/tools/add-card.js +97 -0
- package/dist/tools/add-wallet.d.ts +65 -0
- package/dist/tools/add-wallet.js +104 -0
- package/dist/tools/card-status.d.ts +20 -0
- package/dist/tools/card-status.js +26 -0
- package/dist/tools/confirm-payment.d.ts +44 -0
- package/dist/tools/confirm-payment.js +88 -0
- package/dist/tools/get-total-balance.d.ts +41 -0
- package/dist/tools/get-total-balance.js +98 -0
- package/dist/tools/get-transactions.d.ts +39 -0
- package/dist/tools/get-transactions.js +40 -0
- package/dist/tools/get-wallet-balance.d.ts +43 -0
- package/dist/tools/get-wallet-balance.js +69 -0
- package/dist/tools/list-cards.d.ts +36 -0
- package/dist/tools/list-cards.js +39 -0
- package/dist/tools/list-wallet-transactions.d.ts +63 -0
- package/dist/tools/list-wallet-transactions.js +76 -0
- package/dist/tools/list-wallets.d.ts +41 -0
- package/dist/tools/list-wallets.js +50 -0
- package/dist/tools/lock-cards.d.ts +16 -0
- package/dist/tools/lock-cards.js +23 -0
- package/dist/tools/prepare-crypto-tx.d.ts +69 -0
- package/dist/tools/prepare-crypto-tx.js +93 -0
- package/dist/tools/prepare-payment.d.ts +57 -0
- package/dist/tools/prepare-payment.js +93 -0
- package/dist/tools/remove-card.d.ts +25 -0
- package/dist/tools/remove-card.js +39 -0
- package/dist/tools/remove-wallet.d.ts +27 -0
- package/dist/tools/remove-wallet.js +40 -0
- package/dist/tools/setup-pin.d.ts +26 -0
- package/dist/tools/setup-pin.js +33 -0
- package/dist/tools/sign-crypto-tx.d.ts +42 -0
- package/dist/tools/sign-crypto-tx.js +75 -0
- package/dist/tools/unlock-cards.d.ts +35 -0
- package/dist/tools/unlock-cards.js +41 -0
- package/package.json +50 -0
- package/src/index.ts +139 -0
- package/src/lib/blockchain.ts +375 -0
- package/src/lib/cards.ts +372 -0
- package/src/lib/cli-runner.ts +113 -0
- package/src/lib/crypto.ts +284 -0
- package/src/lib/cvv-crypto.ts +81 -0
- package/src/lib/mcp-core.ts +127 -0
- package/src/lib/pin-manager.ts +252 -0
- package/src/lib/wallets.ts +331 -0
- package/src/tools/add-card.ts +108 -0
- package/src/tools/add-wallet.ts +114 -0
- package/src/tools/card-status.ts +32 -0
- package/src/tools/confirm-payment.ts +103 -0
- package/src/tools/get-total-balance.ts +123 -0
- package/src/tools/get-transactions.ts +49 -0
- package/src/tools/get-wallet-balance.ts +75 -0
- package/src/tools/list-cards.ts +52 -0
- package/src/tools/list-wallet-transactions.ts +83 -0
- package/src/tools/list-wallets.ts +63 -0
- package/src/tools/lock-cards.ts +31 -0
- package/src/tools/prepare-crypto-tx.ts +108 -0
- package/src/tools/prepare-payment.ts +108 -0
- package/src/tools/remove-card.ts +46 -0
- package/src/tools/remove-wallet.ts +47 -0
- package/src/tools/setup-pin.ts +39 -0
- package/src/tools/sign-crypto-tx.ts +90 -0
- package/src/tools/unlock-cards.ts +48 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Blockchain API Client
|
|
3
|
+
*
|
|
4
|
+
* Fetches balances and transactions from various blockchains.
|
|
5
|
+
* Uses public RPC endpoints and explorer APIs.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { CHAIN_CONFIG, type Chain } from "./wallets";
|
|
9
|
+
|
|
10
|
+
// Default public RPC endpoints (can be overridden via env vars)
|
|
11
|
+
const DEFAULT_RPC: Record<Chain, string> = {
|
|
12
|
+
ethereum: "https://eth.llamarpc.com",
|
|
13
|
+
polygon: "https://polygon-rpc.com",
|
|
14
|
+
arbitrum: "https://arb1.arbitrum.io/rpc",
|
|
15
|
+
optimism: "https://mainnet.optimism.io",
|
|
16
|
+
base: "https://mainnet.base.org",
|
|
17
|
+
avalanche: "https://api.avax.network/ext/bc/C/rpc",
|
|
18
|
+
bsc: "https://bsc-dataseed.binance.org",
|
|
19
|
+
bitcoin: "", // Needs special handling
|
|
20
|
+
solana: "https://api.mainnet-beta.solana.com",
|
|
21
|
+
starknet: "https://starknet-mainnet.public.blastapi.io",
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// Explorer APIs for transactions
|
|
25
|
+
const EXPLORER_API: Partial<Record<Chain, string>> = {
|
|
26
|
+
ethereum: "https://api.etherscan.io/api",
|
|
27
|
+
polygon: "https://api.polygonscan.com/api",
|
|
28
|
+
arbitrum: "https://api.arbiscan.io/api",
|
|
29
|
+
optimism: "https://api-optimistic.etherscan.io/api",
|
|
30
|
+
base: "https://api.basescan.org/api",
|
|
31
|
+
bsc: "https://api.bscscan.com/api",
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export interface WalletBalance {
|
|
35
|
+
address: string;
|
|
36
|
+
chain: Chain;
|
|
37
|
+
nativeBalance: string;
|
|
38
|
+
nativeBalanceFormatted: string;
|
|
39
|
+
nativeCurrency: string;
|
|
40
|
+
usdValue?: number;
|
|
41
|
+
tokens?: TokenBalance[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface TokenBalance {
|
|
45
|
+
symbol: string;
|
|
46
|
+
name: string;
|
|
47
|
+
balance: string;
|
|
48
|
+
balanceFormatted: string;
|
|
49
|
+
contractAddress: string;
|
|
50
|
+
decimals: number;
|
|
51
|
+
usdValue?: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface WalletTransaction {
|
|
55
|
+
hash: string;
|
|
56
|
+
from: string;
|
|
57
|
+
to: string;
|
|
58
|
+
value: string;
|
|
59
|
+
valueFormatted: string;
|
|
60
|
+
timestamp: number;
|
|
61
|
+
blockNumber: number;
|
|
62
|
+
isIncoming: boolean;
|
|
63
|
+
status: "success" | "failed" | "pending";
|
|
64
|
+
fee?: string;
|
|
65
|
+
tokenSymbol?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Get RPC URL for a chain
|
|
70
|
+
*/
|
|
71
|
+
function getRpcUrl(chain: Chain): string {
|
|
72
|
+
const envVar = CHAIN_CONFIG[chain]?.rpcEnvVar;
|
|
73
|
+
if (envVar && process.env[envVar]) {
|
|
74
|
+
return process.env[envVar]!;
|
|
75
|
+
}
|
|
76
|
+
return DEFAULT_RPC[chain] || "";
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Get explorer API key (optional, for higher rate limits)
|
|
81
|
+
*/
|
|
82
|
+
function getExplorerApiKey(chain: Chain): string | undefined {
|
|
83
|
+
const keyMap: Partial<Record<Chain, string>> = {
|
|
84
|
+
ethereum: "ETHERSCAN_API_KEY",
|
|
85
|
+
polygon: "POLYGONSCAN_API_KEY",
|
|
86
|
+
arbitrum: "ARBISCAN_API_KEY",
|
|
87
|
+
bsc: "BSCSCAN_API_KEY",
|
|
88
|
+
};
|
|
89
|
+
const envVar = keyMap[chain];
|
|
90
|
+
return envVar ? process.env[envVar] : undefined;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Make JSON-RPC call
|
|
95
|
+
*/
|
|
96
|
+
async function rpcCall(chain: Chain, method: string, params: unknown[]): Promise<unknown> {
|
|
97
|
+
const url = getRpcUrl(chain);
|
|
98
|
+
if (!url) {
|
|
99
|
+
throw new Error(`No RPC URL configured for ${chain}`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const response = await fetch(url, {
|
|
103
|
+
method: "POST",
|
|
104
|
+
headers: { "Content-Type": "application/json" },
|
|
105
|
+
body: JSON.stringify({
|
|
106
|
+
jsonrpc: "2.0",
|
|
107
|
+
id: 1,
|
|
108
|
+
method,
|
|
109
|
+
params,
|
|
110
|
+
}),
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const data = await response.json();
|
|
114
|
+
if (data.error) {
|
|
115
|
+
throw new Error(data.error.message || "RPC error");
|
|
116
|
+
}
|
|
117
|
+
return data.result;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Format wei to ETH (or equivalent)
|
|
122
|
+
*/
|
|
123
|
+
function formatWei(wei: string, decimals = 18): string {
|
|
124
|
+
const weiNum = BigInt(wei);
|
|
125
|
+
const divisor = BigInt(10 ** decimals);
|
|
126
|
+
const whole = weiNum / divisor;
|
|
127
|
+
const fraction = weiNum % divisor;
|
|
128
|
+
const fractionStr = fraction.toString().padStart(decimals, "0").slice(0, 6);
|
|
129
|
+
return `${whole}.${fractionStr}`.replace(/\.?0+$/, "") || "0";
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Get native balance for EVM chains
|
|
134
|
+
*/
|
|
135
|
+
async function getEvmBalance(address: string, chain: Chain): Promise<WalletBalance> {
|
|
136
|
+
const result = await rpcCall(chain, "eth_getBalance", [address, "latest"]) as string;
|
|
137
|
+
const config = CHAIN_CONFIG[chain];
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
address,
|
|
141
|
+
chain,
|
|
142
|
+
nativeBalance: result,
|
|
143
|
+
nativeBalanceFormatted: formatWei(result),
|
|
144
|
+
nativeCurrency: config?.nativeCurrency || "ETH",
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Get Solana balance
|
|
150
|
+
*/
|
|
151
|
+
async function getSolanaBalance(address: string): Promise<WalletBalance> {
|
|
152
|
+
const url = getRpcUrl("solana");
|
|
153
|
+
|
|
154
|
+
const response = await fetch(url, {
|
|
155
|
+
method: "POST",
|
|
156
|
+
headers: { "Content-Type": "application/json" },
|
|
157
|
+
body: JSON.stringify({
|
|
158
|
+
jsonrpc: "2.0",
|
|
159
|
+
id: 1,
|
|
160
|
+
method: "getBalance",
|
|
161
|
+
params: [address],
|
|
162
|
+
}),
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const data = await response.json();
|
|
166
|
+
const lamports = data.result?.value || 0;
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
address,
|
|
170
|
+
chain: "solana",
|
|
171
|
+
nativeBalance: lamports.toString(),
|
|
172
|
+
nativeBalanceFormatted: (lamports / 1e9).toFixed(4),
|
|
173
|
+
nativeCurrency: "SOL",
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Get Bitcoin balance via public API
|
|
179
|
+
*/
|
|
180
|
+
async function getBitcoinBalance(address: string): Promise<WalletBalance> {
|
|
181
|
+
// Using blockchain.info API
|
|
182
|
+
const response = await fetch(`https://blockchain.info/balance?active=${address}`);
|
|
183
|
+
const data = await response.json();
|
|
184
|
+
|
|
185
|
+
const satoshis = data[address]?.final_balance || 0;
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
address,
|
|
189
|
+
chain: "bitcoin",
|
|
190
|
+
nativeBalance: satoshis.toString(),
|
|
191
|
+
nativeBalanceFormatted: (satoshis / 1e8).toFixed(8),
|
|
192
|
+
nativeCurrency: "BTC",
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Get wallet balance
|
|
198
|
+
*/
|
|
199
|
+
export async function getWalletBalance(address: string, chain: Chain): Promise<WalletBalance> {
|
|
200
|
+
switch (chain) {
|
|
201
|
+
case "bitcoin":
|
|
202
|
+
return getBitcoinBalance(address);
|
|
203
|
+
case "solana":
|
|
204
|
+
return getSolanaBalance(address);
|
|
205
|
+
default:
|
|
206
|
+
// All EVM chains
|
|
207
|
+
return getEvmBalance(address, chain);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Get EVM transactions via explorer API
|
|
213
|
+
*/
|
|
214
|
+
async function getEvmTransactions(
|
|
215
|
+
address: string,
|
|
216
|
+
chain: Chain,
|
|
217
|
+
limit = 20
|
|
218
|
+
): Promise<WalletTransaction[]> {
|
|
219
|
+
const apiUrl = EXPLORER_API[chain];
|
|
220
|
+
if (!apiUrl) {
|
|
221
|
+
return []; // No explorer API for this chain
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const apiKey = getExplorerApiKey(chain);
|
|
225
|
+
const url = new URL(apiUrl);
|
|
226
|
+
url.searchParams.set("module", "account");
|
|
227
|
+
url.searchParams.set("action", "txlist");
|
|
228
|
+
url.searchParams.set("address", address);
|
|
229
|
+
url.searchParams.set("sort", "desc");
|
|
230
|
+
url.searchParams.set("page", "1");
|
|
231
|
+
url.searchParams.set("offset", limit.toString());
|
|
232
|
+
if (apiKey) {
|
|
233
|
+
url.searchParams.set("apikey", apiKey);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const response = await fetch(url.toString());
|
|
237
|
+
const data = await response.json();
|
|
238
|
+
|
|
239
|
+
if (data.status !== "1" || !Array.isArray(data.result)) {
|
|
240
|
+
return [];
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const config = CHAIN_CONFIG[chain];
|
|
244
|
+
|
|
245
|
+
return data.result.map((tx: any) => ({
|
|
246
|
+
hash: tx.hash,
|
|
247
|
+
from: tx.from,
|
|
248
|
+
to: tx.to,
|
|
249
|
+
value: tx.value,
|
|
250
|
+
valueFormatted: `${formatWei(tx.value)} ${config?.nativeCurrency || "ETH"}`,
|
|
251
|
+
timestamp: parseInt(tx.timeStamp) * 1000,
|
|
252
|
+
blockNumber: parseInt(tx.blockNumber),
|
|
253
|
+
isIncoming: tx.to.toLowerCase() === address.toLowerCase(),
|
|
254
|
+
status: tx.isError === "0" ? "success" : "failed",
|
|
255
|
+
fee: formatWei((BigInt(tx.gasUsed) * BigInt(tx.gasPrice)).toString()),
|
|
256
|
+
}));
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Get Solana transactions
|
|
261
|
+
*/
|
|
262
|
+
async function getSolanaTransactions(
|
|
263
|
+
address: string,
|
|
264
|
+
limit = 20
|
|
265
|
+
): Promise<WalletTransaction[]> {
|
|
266
|
+
const url = getRpcUrl("solana");
|
|
267
|
+
|
|
268
|
+
// Get signatures
|
|
269
|
+
const sigResponse = await fetch(url, {
|
|
270
|
+
method: "POST",
|
|
271
|
+
headers: { "Content-Type": "application/json" },
|
|
272
|
+
body: JSON.stringify({
|
|
273
|
+
jsonrpc: "2.0",
|
|
274
|
+
id: 1,
|
|
275
|
+
method: "getSignaturesForAddress",
|
|
276
|
+
params: [address, { limit }],
|
|
277
|
+
}),
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
const sigData = await sigResponse.json();
|
|
281
|
+
const signatures = sigData.result || [];
|
|
282
|
+
|
|
283
|
+
return signatures.map((sig: any) => ({
|
|
284
|
+
hash: sig.signature,
|
|
285
|
+
from: address,
|
|
286
|
+
to: "",
|
|
287
|
+
value: "0",
|
|
288
|
+
valueFormatted: "N/A",
|
|
289
|
+
timestamp: sig.blockTime ? sig.blockTime * 1000 : Date.now(),
|
|
290
|
+
blockNumber: sig.slot,
|
|
291
|
+
isIncoming: false,
|
|
292
|
+
status: sig.err ? "failed" : "success",
|
|
293
|
+
}));
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Get Bitcoin transactions
|
|
298
|
+
*/
|
|
299
|
+
async function getBitcoinTransactions(
|
|
300
|
+
address: string,
|
|
301
|
+
limit = 20
|
|
302
|
+
): Promise<WalletTransaction[]> {
|
|
303
|
+
const response = await fetch(
|
|
304
|
+
`https://blockchain.info/rawaddr/${address}?limit=${limit}`
|
|
305
|
+
);
|
|
306
|
+
const data = await response.json();
|
|
307
|
+
|
|
308
|
+
if (!data.txs) return [];
|
|
309
|
+
|
|
310
|
+
return data.txs.map((tx: any) => {
|
|
311
|
+
const isIncoming = tx.out.some(
|
|
312
|
+
(o: any) => o.addr === address
|
|
313
|
+
);
|
|
314
|
+
const value = tx.out
|
|
315
|
+
.filter((o: any) => (isIncoming ? o.addr === address : o.addr !== address))
|
|
316
|
+
.reduce((sum: number, o: any) => sum + o.value, 0);
|
|
317
|
+
|
|
318
|
+
return {
|
|
319
|
+
hash: tx.hash,
|
|
320
|
+
from: tx.inputs[0]?.prev_out?.addr || "coinbase",
|
|
321
|
+
to: tx.out[0]?.addr || "",
|
|
322
|
+
value: value.toString(),
|
|
323
|
+
valueFormatted: `${(value / 1e8).toFixed(8)} BTC`,
|
|
324
|
+
timestamp: tx.time * 1000,
|
|
325
|
+
blockNumber: tx.block_height || 0,
|
|
326
|
+
isIncoming,
|
|
327
|
+
status: "success",
|
|
328
|
+
};
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Get wallet transactions
|
|
334
|
+
*/
|
|
335
|
+
export async function getWalletTransactions(
|
|
336
|
+
address: string,
|
|
337
|
+
chain: Chain,
|
|
338
|
+
limit = 20
|
|
339
|
+
): Promise<WalletTransaction[]> {
|
|
340
|
+
switch (chain) {
|
|
341
|
+
case "bitcoin":
|
|
342
|
+
return getBitcoinTransactions(address, limit);
|
|
343
|
+
case "solana":
|
|
344
|
+
return getSolanaTransactions(address, limit);
|
|
345
|
+
default:
|
|
346
|
+
return getEvmTransactions(address, chain, limit);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Get USD price for a currency (simple coingecko fetch)
|
|
352
|
+
*/
|
|
353
|
+
export async function getUsdPrice(currency: string): Promise<number | null> {
|
|
354
|
+
const coinIds: Record<string, string> = {
|
|
355
|
+
ETH: "ethereum",
|
|
356
|
+
BTC: "bitcoin",
|
|
357
|
+
SOL: "solana",
|
|
358
|
+
MATIC: "matic-network",
|
|
359
|
+
AVAX: "avalanche-2",
|
|
360
|
+
BNB: "binancecoin",
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
const coinId = coinIds[currency.toUpperCase()];
|
|
364
|
+
if (!coinId) return null;
|
|
365
|
+
|
|
366
|
+
try {
|
|
367
|
+
const response = await fetch(
|
|
368
|
+
`https://api.coingecko.com/api/v3/simple/price?ids=${coinId}&vs_currencies=usd`
|
|
369
|
+
);
|
|
370
|
+
const data = await response.json();
|
|
371
|
+
return data[coinId]?.usd || null;
|
|
372
|
+
} catch {
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
}
|