kaspa-mcp 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.
Files changed (47) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +148 -0
  3. package/dist/e2e.test.d.ts +1 -0
  4. package/dist/e2e.test.js +184 -0
  5. package/dist/index.d.ts +2 -0
  6. package/dist/index.js +63 -0
  7. package/dist/integration.test.d.ts +1 -0
  8. package/dist/integration.test.js +159 -0
  9. package/dist/kaspa/api.d.ts +11 -0
  10. package/dist/kaspa/api.js +49 -0
  11. package/dist/kaspa/api.test.d.ts +1 -0
  12. package/dist/kaspa/api.test.js +177 -0
  13. package/dist/kaspa/setup.d.ts +1 -0
  14. package/dist/kaspa/setup.js +4 -0
  15. package/dist/kaspa/transaction.d.ts +5 -0
  16. package/dist/kaspa/transaction.js +60 -0
  17. package/dist/kaspa/transaction.test.d.ts +1 -0
  18. package/dist/kaspa/transaction.test.js +170 -0
  19. package/dist/kaspa/wallet.d.ts +15 -0
  20. package/dist/kaspa/wallet.js +97 -0
  21. package/dist/kaspa/wallet.test.d.ts +1 -0
  22. package/dist/kaspa/wallet.test.js +160 -0
  23. package/dist/test-setup.d.ts +1 -0
  24. package/dist/test-setup.js +4 -0
  25. package/dist/tools/get-balance.d.ts +9 -0
  26. package/dist/tools/get-balance.js +21 -0
  27. package/dist/tools/get-balance.test.d.ts +1 -0
  28. package/dist/tools/get-balance.test.js +93 -0
  29. package/dist/tools/get-fee-estimate.d.ts +6 -0
  30. package/dist/tools/get-fee-estimate.js +13 -0
  31. package/dist/tools/get-fee-estimate.test.d.ts +1 -0
  32. package/dist/tools/get-fee-estimate.test.js +74 -0
  33. package/dist/tools/get-my-address.d.ts +4 -0
  34. package/dist/tools/get-my-address.js +9 -0
  35. package/dist/tools/get-my-address.test.d.ts +1 -0
  36. package/dist/tools/get-my-address.test.js +32 -0
  37. package/dist/tools/get-transaction.d.ts +21 -0
  38. package/dist/tools/get-transaction.js +36 -0
  39. package/dist/tools/get-transaction.test.d.ts +1 -0
  40. package/dist/tools/get-transaction.test.js +119 -0
  41. package/dist/tools/send-kaspa.d.ts +10 -0
  42. package/dist/tools/send-kaspa.js +53 -0
  43. package/dist/tools/send-kaspa.test.d.ts +1 -0
  44. package/dist/tools/send-kaspa.test.js +147 -0
  45. package/dist/types.d.ts +53 -0
  46. package/dist/types.js +3 -0
  47. package/package.json +39 -0
package/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2025
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
10
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
11
+ AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
12
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
13
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
14
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
15
+ PERFORMANCE OF THIS SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,148 @@
1
+ # Kaspa MCP
2
+
3
+ MCP server for sending KAS on the Kaspa blockDAG.
4
+
5
+ ## About Kaspa
6
+
7
+ [Kaspa](https://kaspa.org) is a fast, scalable Layer-1 cryptocurrency built on proof-of-work (PoW) and powered by the GHOSTDAG protocol — a novel consensus mechanism that extends Nakamoto's original design. Unlike traditional blockchains that discard competing blocks, GHOSTDAG allows parallel blocks to coexist and orders them within a Directed Acyclic Graph (blockDAG), enabling high throughput while preserving decentralization and security.
8
+
9
+ **Key Features:**
10
+ - **10 blocks per second** with sub-second finality (Crescendo upgrade, May 2025)
11
+ - **Proof of Work** using kHeavyHash algorithm
12
+ - **Fair launch** - no premine, no ICO, no token allocations
13
+ - **Decentralized** - runs on standard hardware
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ npm install
19
+ npm run build
20
+ ```
21
+
22
+ ## Configuration
23
+
24
+ Set these environment variables:
25
+
26
+ | Variable | Required | Description |
27
+ |----------|----------|-------------|
28
+ | `KASPA_MNEMONIC` | Yes* | BIP39 mnemonic phrase (24 words) |
29
+ | `KASPA_PRIVATE_KEY` | Yes* | Hex-encoded private key (alternative to mnemonic) |
30
+ | `KASPA_NETWORK` | No | Network: `mainnet`, `testnet-10`, `testnet-11`. Defaults to `mainnet` |
31
+ | `KASPA_ACCOUNT_INDEX` | No | BIP44 account index when using mnemonic. Defaults to `0` |
32
+
33
+ *Either `KASPA_MNEMONIC` or `KASPA_PRIVATE_KEY` must be set.
34
+
35
+ ## Usage with Claude Desktop
36
+
37
+ Add to your Claude Desktop config (`~/Library/Application Support/Claude/claude_desktop_config.json`):
38
+
39
+ ```json
40
+ {
41
+ "mcpServers": {
42
+ "kaspa": {
43
+ "command": "npx",
44
+ "args": ["kaspa-mcp"],
45
+ "env": {
46
+ "KASPA_MNEMONIC": "your twenty four word mnemonic phrase here ...",
47
+ "KASPA_NETWORK": "mainnet"
48
+ }
49
+ }
50
+ }
51
+ }
52
+ ```
53
+
54
+ ## Tools
55
+
56
+ ### `get_my_address`
57
+ Get the Kaspa address derived from your configured private key or mnemonic.
58
+
59
+ **Returns:** `{ address: string }`
60
+
61
+ ### `get_balance`
62
+ Get balance for a Kaspa address.
63
+
64
+ **Parameters:**
65
+ - `address` (optional): Address to check. Defaults to your wallet address.
66
+
67
+ **Returns:** `{ address: string, balance: string, utxoCount: number }`
68
+
69
+ ### `get_fee_estimate`
70
+ Get current fee estimates from the network.
71
+
72
+ **Returns:** `{ priorityFee: string, normalFee: string, lowFee: string }`
73
+
74
+ ### `send_kaspa`
75
+ Send KAS tokens to a recipient.
76
+
77
+ **Parameters:**
78
+ - `to`: Recipient Kaspa address (must match wallet network)
79
+ - `amount`: Amount in KAS as string (e.g., "10.5", max 8 decimal places)
80
+ - `priorityFee` (optional): Priority fee in sompi
81
+
82
+ **Returns:** `{ txId: string, fee: string }`
83
+
84
+ **Validations:**
85
+ - Address format and network prefix validation
86
+ - Amount must be a valid positive decimal number
87
+ - Maximum 8 decimal places (1 sompi = 0.00000001 KAS)
88
+ - Insufficient balance check before broadcast
89
+
90
+ ### `get_transaction`
91
+ Get transaction details including inputs and outputs.
92
+
93
+ **Parameters:**
94
+ - `txId`: Transaction ID
95
+
96
+ **Returns:**
97
+ ```typescript
98
+ {
99
+ txId: string,
100
+ accepted: boolean,
101
+ blockHash?: string,
102
+ blockTime?: number,
103
+ inputs: Array<{ transactionId: string, index: number }>,
104
+ outputs: Array<{ index: number, amount: string, address: string }>
105
+ }
106
+ ```
107
+
108
+ ## Example
109
+
110
+ ![Demo of kaspa-mcp in Claude Code](assets/demo.png)
111
+
112
+ ```
113
+ "Send 5 KAS to kaspa:qz..."
114
+ ```
115
+
116
+ The MCP will:
117
+ 1. Validate the recipient address matches your network
118
+ 2. Check your balance is sufficient
119
+ 3. Build the transaction with KIP-9 compliant fees
120
+ 4. Sign with your private key
121
+ 5. Broadcast to the network via public nodes
122
+ 6. Return the transaction ID
123
+
124
+ ## Technical Details
125
+
126
+ - Uses [kaspa-wasm](https://github.com/aspect-build/aspect-cli) for cryptographic operations
127
+ - Connects to public nodes via Resolver for automatic node discovery
128
+ - Implements BIP44 derivation path `m/44'/111111'/account'` for mnemonic wallets
129
+ - Transaction building uses Generator for KIP-9 compliant UTXO management
130
+
131
+ ## Security
132
+
133
+ - Private keys and mnemonics are only used locally for signing
134
+ - Keys are never sent to any external service
135
+ - Error messages are sanitized to prevent secret leakage
136
+ - All transactions require explicit user action via MCP tools
137
+
138
+ ## Networks
139
+
140
+ | Network | Address Prefix | API Endpoint |
141
+ |---------|---------------|--------------|
142
+ | `mainnet` | `kaspa:` | api.kaspa.org |
143
+ | `testnet-10` | `kaspatest:` | api-tn10.kaspa.org |
144
+ | `testnet-11` | `kaspatest:` | api-tn11.kaspa.org |
145
+
146
+ ## License
147
+
148
+ ISC
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,184 @@
1
+ // ABOUTME: End-to-end tests for Kaspa MCP server
2
+ // ABOUTME: Tests the MCP server tool handlers with real wallet operations
3
+ import { describe, it, expect, vi, beforeAll, afterAll, beforeEach, afterEach } from 'vitest';
4
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
5
+ import { z } from 'zod';
6
+ // Valid test mnemonic (24 words)
7
+ const TEST_MNEMONIC = 'matter client cigar north mixed hard rail kitten flat shrug view group diagram release goose thumb benefit fire confirm swamp skill merry genre visa';
8
+ async function wrapToolHandler(handler) {
9
+ try {
10
+ const result = await handler();
11
+ return {
12
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
13
+ };
14
+ }
15
+ catch (error) {
16
+ return {
17
+ content: [{ type: 'text', text: `Error: ${error}` }],
18
+ isError: true,
19
+ };
20
+ }
21
+ }
22
+ describe('End-to-End Tests', () => {
23
+ const mockFetch = vi.fn();
24
+ const originalFetch = globalThis.fetch;
25
+ const originalEnv = process.env;
26
+ beforeAll(() => {
27
+ process.env = {
28
+ ...originalEnv,
29
+ KASPA_MNEMONIC: TEST_MNEMONIC,
30
+ KASPA_NETWORK: 'mainnet',
31
+ };
32
+ });
33
+ afterAll(() => {
34
+ process.env = originalEnv;
35
+ });
36
+ beforeEach(() => {
37
+ globalThis.fetch = mockFetch;
38
+ mockFetch.mockReset();
39
+ vi.resetModules();
40
+ });
41
+ afterEach(() => {
42
+ globalThis.fetch = originalFetch;
43
+ });
44
+ describe('wrapToolHandler', () => {
45
+ it('wraps successful result in MCP format', async () => {
46
+ const handler = async () => ({ address: 'kaspa:qptest' });
47
+ const response = await wrapToolHandler(handler);
48
+ expect(response.isError).toBeUndefined();
49
+ expect(response.content).toHaveLength(1);
50
+ expect(response.content[0].type).toBe('text');
51
+ const parsed = JSON.parse(response.content[0].text);
52
+ expect(parsed.address).toBe('kaspa:qptest');
53
+ });
54
+ it('wraps error in MCP format with isError flag', async () => {
55
+ const handler = async () => {
56
+ throw new Error('Test error');
57
+ };
58
+ const response = await wrapToolHandler(handler);
59
+ expect(response.isError).toBe(true);
60
+ expect(response.content).toHaveLength(1);
61
+ expect(response.content[0].text).toContain('Error:');
62
+ expect(response.content[0].text).toContain('Test error');
63
+ });
64
+ });
65
+ describe('MCP Server Tool Registration', () => {
66
+ it('can create MCP server with tool capabilities', () => {
67
+ const server = new McpServer({ name: 'test-kaspa-mcp', version: '0.1.0' }, { capabilities: { tools: {} } });
68
+ expect(server).toBeDefined();
69
+ });
70
+ it('can register get_my_address tool', async () => {
71
+ const server = new McpServer({ name: 'test-kaspa-mcp', version: '0.1.0' }, { capabilities: { tools: {} } });
72
+ const { getMyAddress } = await import('./tools/get-my-address.js');
73
+ server.tool('get_my_address', 'Get the Kaspa address derived from the configured private key', async () => wrapToolHandler(() => getMyAddress()));
74
+ expect(server).toBeDefined();
75
+ });
76
+ it('can register get_balance tool with schema', async () => {
77
+ const server = new McpServer({ name: 'test-kaspa-mcp', version: '0.1.0' }, { capabilities: { tools: {} } });
78
+ const { getBalance } = await import('./tools/get-balance.js');
79
+ server.tool('get_balance', 'Get balance for a Kaspa address', {
80
+ address: z.string().optional().describe('Kaspa address to check'),
81
+ }, async (params) => wrapToolHandler(() => getBalance({ address: params.address })));
82
+ expect(server).toBeDefined();
83
+ });
84
+ it('can register send_kaspa tool with required params', async () => {
85
+ const server = new McpServer({ name: 'test-kaspa-mcp', version: '0.1.0' }, { capabilities: { tools: {} } });
86
+ const { sendKaspa } = await import('./tools/send-kaspa.js');
87
+ server.tool('send_kaspa', 'Send KAS tokens', {
88
+ to: z.string().describe('Recipient address'),
89
+ amount: z.string().describe('Amount in KAS'),
90
+ priorityFee: z.number().optional().describe('Priority fee in sompi'),
91
+ }, async (params) => wrapToolHandler(() => sendKaspa({
92
+ to: params.to,
93
+ amount: params.amount,
94
+ priorityFee: params.priorityFee,
95
+ })));
96
+ expect(server).toBeDefined();
97
+ });
98
+ });
99
+ describe('Full Tool Flow E2E', () => {
100
+ it('get_my_address returns valid mainnet address', async () => {
101
+ const { getMyAddress } = await import('./tools/get-my-address.js');
102
+ const response = await wrapToolHandler(() => getMyAddress());
103
+ expect(response.isError).toBeUndefined();
104
+ const result = JSON.parse(response.content[0].text);
105
+ expect(result.address).toMatch(/^kaspa:/);
106
+ });
107
+ it('get_balance returns formatted balance', async () => {
108
+ mockFetch
109
+ .mockResolvedValueOnce({
110
+ ok: true,
111
+ json: () => Promise.resolve({ balance: '10000000000' }),
112
+ })
113
+ .mockResolvedValueOnce({
114
+ ok: true,
115
+ json: () => Promise.resolve([{ utxo: 1 }, { utxo: 2 }, { utxo: 3 }]),
116
+ });
117
+ const { getBalance } = await import('./tools/get-balance.js');
118
+ const response = await wrapToolHandler(() => getBalance({}));
119
+ expect(response.isError).toBeUndefined();
120
+ const result = JSON.parse(response.content[0].text);
121
+ expect(result.balance).toBe('100');
122
+ expect(result.utxoCount).toBe(3);
123
+ });
124
+ it('get_fee_estimate returns all fee tiers', async () => {
125
+ mockFetch.mockResolvedValueOnce({
126
+ ok: true,
127
+ json: () => Promise.resolve({
128
+ priorityBucket: { feerate: 2.0 },
129
+ normalBuckets: [{ feerate: 1.5 }],
130
+ lowBuckets: [{ feerate: 1.0 }],
131
+ }),
132
+ });
133
+ const { getFeeEstimate } = await import('./tools/get-fee-estimate.js');
134
+ const response = await wrapToolHandler(() => getFeeEstimate());
135
+ expect(response.isError).toBeUndefined();
136
+ const result = JSON.parse(response.content[0].text);
137
+ expect(result.priorityFee).toBe('2');
138
+ expect(result.normalFee).toBe('1.5');
139
+ expect(result.lowFee).toBe('1');
140
+ });
141
+ it('get_transaction returns transaction status', async () => {
142
+ mockFetch.mockResolvedValueOnce({
143
+ ok: true,
144
+ json: () => Promise.resolve({
145
+ transaction_id: 'txid123',
146
+ is_accepted: true,
147
+ block_hash: ['hash1'],
148
+ block_time: 1234567890,
149
+ inputs: [{ previous_outpoint_hash: 'prev_tx', previous_outpoint_index: 0 }],
150
+ outputs: [{ amount: '10000000000', script_public_key_address: 'kaspa:qptest' }],
151
+ }),
152
+ });
153
+ const { getTransaction } = await import('./tools/get-transaction.js');
154
+ const response = await wrapToolHandler(() => getTransaction({ txId: 'txid123' }));
155
+ expect(response.isError).toBeUndefined();
156
+ const result = JSON.parse(response.content[0].text);
157
+ expect(result.txId).toBe('txid123');
158
+ expect(result.accepted).toBe(true);
159
+ expect(result.outputs).toHaveLength(1);
160
+ expect(result.outputs[0].address).toBe('kaspa:qptest');
161
+ });
162
+ it('send_kaspa validation error returns isError', async () => {
163
+ const { sendKaspa } = await import('./tools/send-kaspa.js');
164
+ const response = await wrapToolHandler(() => sendKaspa({ to: 'invalid', amount: '1' }));
165
+ expect(response.isError).toBe(true);
166
+ expect(response.content[0].text).toContain('Invalid Kaspa address');
167
+ });
168
+ });
169
+ describe('Error Handling E2E', () => {
170
+ it('API timeout is handled gracefully', async () => {
171
+ mockFetch.mockRejectedValueOnce(new Error('Network timeout'));
172
+ const { getFeeEstimate } = await import('./tools/get-fee-estimate.js');
173
+ const response = await wrapToolHandler(() => getFeeEstimate());
174
+ expect(response.isError).toBe(true);
175
+ expect(response.content[0].text).toContain('Network timeout');
176
+ });
177
+ it('missing txId parameter returns error', async () => {
178
+ const { getTransaction } = await import('./tools/get-transaction.js');
179
+ const response = await wrapToolHandler(() => getTransaction({ txId: '' }));
180
+ expect(response.isError).toBe(true);
181
+ expect(response.content[0].text).toContain('Transaction ID (txId) is required');
182
+ });
183
+ });
184
+ });
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import './kaspa/setup.js';
package/dist/index.js ADDED
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env node
2
+ // ABOUTME: MCP server entry point for Kaspa transactions
3
+ // ABOUTME: Registers all tools and starts the stdio transport
4
+ import './kaspa/setup.js';
5
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
6
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
7
+ import { z } from 'zod';
8
+ import { getMyAddress } from './tools/get-my-address.js';
9
+ import { getBalance } from './tools/get-balance.js';
10
+ import { getFeeEstimate } from './tools/get-fee-estimate.js';
11
+ import { sendKaspa } from './tools/send-kaspa.js';
12
+ import { getTransaction } from './tools/get-transaction.js';
13
+ async function wrapToolHandler(handler) {
14
+ try {
15
+ const result = await handler();
16
+ return {
17
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
18
+ };
19
+ }
20
+ catch (error) {
21
+ const errorMessage = error instanceof Error ? error.message : 'An unknown error occurred';
22
+ return {
23
+ content: [{ type: 'text', text: `Error: ${errorMessage}` }],
24
+ isError: true,
25
+ };
26
+ }
27
+ }
28
+ const server = new McpServer({
29
+ name: 'kaspa-mcp',
30
+ version: '0.1.0',
31
+ }, {
32
+ capabilities: {
33
+ tools: {},
34
+ },
35
+ });
36
+ server.tool('get_my_address', 'Get the Kaspa address derived from the configured private key', async () => wrapToolHandler(() => getMyAddress()));
37
+ server.tool('get_balance', 'Get balance for a Kaspa address (defaults to your wallet address)', {
38
+ address: z.string().optional().describe('Kaspa address to check (optional, defaults to your address)'),
39
+ }, async (params) => wrapToolHandler(() => getBalance({ address: params.address })));
40
+ server.tool('get_fee_estimate', 'Get current fee estimates from the Kaspa network', async () => wrapToolHandler(() => getFeeEstimate()));
41
+ server.tool('send_kaspa', 'Send KAS tokens to a recipient address', {
42
+ to: z.string().describe('Recipient Kaspa address'),
43
+ amount: z.string().describe('Amount to send in KAS'),
44
+ priorityFee: z.number().optional().describe('Priority fee in sompi (optional)'),
45
+ }, async (params) => wrapToolHandler(() => sendKaspa({
46
+ to: params.to,
47
+ amount: params.amount,
48
+ priorityFee: params.priorityFee,
49
+ })));
50
+ server.tool('get_transaction', 'Get transaction status and details', {
51
+ txId: z.string().describe('Transaction ID'),
52
+ }, async (params) => wrapToolHandler(() => getTransaction({ txId: params.txId })));
53
+ async function main() {
54
+ const transport = new StdioServerTransport();
55
+ await server.connect(transport);
56
+ console.error('Kaspa MCP server started');
57
+ }
58
+ main().catch((error) => {
59
+ // Log only the message to prevent secret leakage in stack traces
60
+ const safeMessage = error instanceof Error ? error.message : 'Unknown error';
61
+ console.error('Fatal error:', safeMessage);
62
+ process.exit(1);
63
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,159 @@
1
+ // ABOUTME: Integration tests for Kaspa MCP tools
2
+ // ABOUTME: Tests tool flows with real kaspa-wasm but mocked network calls
3
+ import { describe, it, expect, vi, beforeEach, afterEach, beforeAll, afterAll } from 'vitest';
4
+ // Valid test mnemonic (24 words)
5
+ const TEST_MNEMONIC = 'matter client cigar north mixed hard rail kitten flat shrug view group diagram release goose thumb benefit fire confirm swamp skill merry genre visa';
6
+ describe('Integration Tests', () => {
7
+ const mockFetch = vi.fn();
8
+ const originalFetch = globalThis.fetch;
9
+ const originalEnv = process.env;
10
+ beforeAll(() => {
11
+ process.env = {
12
+ ...originalEnv,
13
+ KASPA_MNEMONIC: TEST_MNEMONIC,
14
+ KASPA_NETWORK: 'mainnet',
15
+ };
16
+ });
17
+ afterAll(() => {
18
+ process.env = originalEnv;
19
+ });
20
+ beforeEach(() => {
21
+ globalThis.fetch = mockFetch;
22
+ mockFetch.mockReset();
23
+ vi.resetModules();
24
+ });
25
+ afterEach(() => {
26
+ globalThis.fetch = originalFetch;
27
+ });
28
+ describe('get-my-address tool', () => {
29
+ it('returns wallet address derived from mnemonic', async () => {
30
+ const { getMyAddress } = await import('./tools/get-my-address.js');
31
+ const result = await getMyAddress();
32
+ expect(result.address).toMatch(/^kaspa:/);
33
+ expect(result.address).toHaveLength(67);
34
+ });
35
+ });
36
+ describe('get-balance tool', () => {
37
+ it('returns balance for wallet address', async () => {
38
+ mockFetch
39
+ .mockResolvedValueOnce({
40
+ ok: true,
41
+ json: () => Promise.resolve({ address: 'kaspa:test', balance: '5000000000' }),
42
+ })
43
+ .mockResolvedValueOnce({
44
+ ok: true,
45
+ json: () => Promise.resolve([
46
+ { address: 'kaspa:test', outpoint: { transactionId: 'tx1', index: 0 }, utxoEntry: { amount: '5000000000' } },
47
+ ]),
48
+ });
49
+ const { getBalance } = await import('./tools/get-balance.js');
50
+ const result = await getBalance({});
51
+ expect(result.address).toMatch(/^kaspa:/);
52
+ expect(result.balance).toBe('50');
53
+ expect(result.utxoCount).toBe(1);
54
+ });
55
+ it('returns balance for specified address', async () => {
56
+ const testAddress = 'kaspa:qptest123456789abcdef';
57
+ mockFetch
58
+ .mockResolvedValueOnce({
59
+ ok: true,
60
+ json: () => Promise.resolve({ address: testAddress, balance: '1000000000' }),
61
+ })
62
+ .mockResolvedValueOnce({
63
+ ok: true,
64
+ json: () => Promise.resolve([]),
65
+ });
66
+ const { getBalance } = await import('./tools/get-balance.js');
67
+ const result = await getBalance({ address: testAddress });
68
+ expect(result.address).toBe(testAddress);
69
+ expect(result.balance).toBe('10');
70
+ expect(result.utxoCount).toBe(0);
71
+ });
72
+ });
73
+ describe('get-fee-estimate tool', () => {
74
+ it('returns formatted fee estimates', async () => {
75
+ mockFetch.mockResolvedValueOnce({
76
+ ok: true,
77
+ json: () => Promise.resolve({
78
+ priorityBucket: { feerate: 1.5, estimatedSeconds: 10 },
79
+ normalBuckets: [{ feerate: 1.0, estimatedSeconds: 30 }],
80
+ lowBuckets: [{ feerate: 0.5, estimatedSeconds: 60 }],
81
+ }),
82
+ });
83
+ const { getFeeEstimate } = await import('./tools/get-fee-estimate.js');
84
+ const result = await getFeeEstimate();
85
+ expect(result.priorityFee).toBe('1.5');
86
+ expect(result.normalFee).toBe('1');
87
+ expect(result.lowFee).toBe('0.5');
88
+ });
89
+ });
90
+ describe('get-transaction tool', () => {
91
+ it('returns transaction details', async () => {
92
+ mockFetch.mockResolvedValueOnce({
93
+ ok: true,
94
+ json: () => Promise.resolve({
95
+ transaction_id: 'abc123def456',
96
+ is_accepted: true,
97
+ block_hash: ['blockhash1'],
98
+ block_time: 1234567890,
99
+ inputs: [],
100
+ outputs: [],
101
+ }),
102
+ });
103
+ const { getTransaction } = await import('./tools/get-transaction.js');
104
+ const result = await getTransaction({ txId: 'abc123def456' });
105
+ expect(result.txId).toBe('abc123def456');
106
+ expect(result.accepted).toBe(true);
107
+ expect(result.blockHash).toBe('blockhash1');
108
+ });
109
+ it('throws for missing transaction', async () => {
110
+ mockFetch.mockResolvedValueOnce({
111
+ ok: false,
112
+ status: 404,
113
+ text: () => Promise.resolve('Not found'),
114
+ });
115
+ const { getTransaction } = await import('./tools/get-transaction.js');
116
+ await expect(getTransaction({ txId: 'notfound' })).rejects.toThrow('Transaction not found: notfound');
117
+ });
118
+ });
119
+ describe('send-kaspa tool validation', () => {
120
+ it('validates recipient address format', async () => {
121
+ const { sendKaspa } = await import('./tools/send-kaspa.js');
122
+ await expect(sendKaspa({ to: 'invalid', amount: '1' })).rejects.toThrow('Invalid Kaspa address');
123
+ });
124
+ it('validates amount format', async () => {
125
+ const { sendKaspa } = await import('./tools/send-kaspa.js');
126
+ // Use a valid mainnet address format for this test
127
+ const validMainnetAddress = 'kaspa:qpamkvhgh0kzx50gwvvp5xs8ktmqutcy3dfs9dc3w7lm9rq0zs76vf959mmrp';
128
+ await expect(sendKaspa({ to: validMainnetAddress, amount: 'abc' })).rejects.toThrow('Amount must be a valid decimal number');
129
+ });
130
+ it('validates network mismatch', async () => {
131
+ // Create a testnet wallet to get a valid testnet address
132
+ const { KaspaWallet } = await import('./kaspa/wallet.js');
133
+ const testnetWallet = KaspaWallet.fromMnemonic(TEST_MNEMONIC, 'testnet-10');
134
+ const validTestnetAddress = testnetWallet.getAddress();
135
+ const { sendKaspa } = await import('./tools/send-kaspa.js');
136
+ // Wallet is on mainnet (from beforeAll), so sending to testnet address should fail
137
+ await expect(sendKaspa({ to: validTestnetAddress, amount: '1' })).rejects.toThrow('Address network mismatch');
138
+ });
139
+ });
140
+ describe('wallet consistency', () => {
141
+ it('generates same address from same mnemonic', async () => {
142
+ const { getMyAddress } = await import('./tools/get-my-address.js');
143
+ const result1 = await getMyAddress();
144
+ const result2 = await getMyAddress();
145
+ expect(result1.address).toBe(result2.address);
146
+ });
147
+ });
148
+ describe('API error handling', () => {
149
+ it('handles API errors gracefully', async () => {
150
+ mockFetch.mockResolvedValueOnce({
151
+ ok: false,
152
+ status: 500,
153
+ text: () => Promise.resolve('Internal server error'),
154
+ });
155
+ const { getFeeEstimate } = await import('./tools/get-fee-estimate.js');
156
+ await expect(getFeeEstimate()).rejects.toThrow('API error 500');
157
+ });
158
+ });
159
+ });
@@ -0,0 +1,11 @@
1
+ import type { UtxoResponse, BalanceResponse, FeeEstimateResponse, TransactionResponse } from '../types.js';
2
+ export declare class KaspaApi {
3
+ private baseUrl;
4
+ constructor(network?: string);
5
+ private fetch;
6
+ getBalance(address: string): Promise<BalanceResponse>;
7
+ getUtxos(address: string): Promise<UtxoResponse[]>;
8
+ getFeeEstimate(): Promise<FeeEstimateResponse>;
9
+ getTransaction(txId: string): Promise<TransactionResponse>;
10
+ }
11
+ export declare function getApi(network: string): KaspaApi;
@@ -0,0 +1,49 @@
1
+ // ABOUTME: REST API client for Kaspa REST APIs
2
+ // ABOUTME: Handles UTXO queries, balance checks, fee estimates - supports mainnet and testnet
3
+ const API_ENDPOINTS = {
4
+ mainnet: 'https://api.kaspa.org',
5
+ 'testnet-10': 'https://api-tn10.kaspa.org',
6
+ 'testnet-11': 'https://api-tn11.kaspa.org',
7
+ };
8
+ export class KaspaApi {
9
+ baseUrl;
10
+ constructor(network = 'mainnet') {
11
+ this.baseUrl = API_ENDPOINTS[network] || API_ENDPOINTS.mainnet;
12
+ }
13
+ async fetch(path, options) {
14
+ const url = `${this.baseUrl}${path}`;
15
+ const response = await fetch(url, {
16
+ ...options,
17
+ headers: {
18
+ 'Content-Type': 'application/json',
19
+ ...options?.headers,
20
+ },
21
+ });
22
+ if (!response.ok) {
23
+ const text = await response.text();
24
+ throw new Error(`API error ${response.status}: ${text}`);
25
+ }
26
+ return response.json();
27
+ }
28
+ async getBalance(address) {
29
+ return this.fetch(`/addresses/${address}/balance`);
30
+ }
31
+ async getUtxos(address) {
32
+ return this.fetch(`/addresses/${address}/utxos`);
33
+ }
34
+ async getFeeEstimate() {
35
+ return this.fetch('/info/fee-estimate');
36
+ }
37
+ async getTransaction(txId) {
38
+ return this.fetch(`/transactions/${txId}`);
39
+ }
40
+ }
41
+ let apiInstance = null;
42
+ let apiNetwork = null;
43
+ export function getApi(network) {
44
+ if (!apiInstance || apiNetwork !== network) {
45
+ apiInstance = new KaspaApi(network);
46
+ apiNetwork = network;
47
+ }
48
+ return apiInstance;
49
+ }
@@ -0,0 +1 @@
1
+ export {};