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.
- package/LICENSE +15 -0
- package/README.md +148 -0
- package/dist/e2e.test.d.ts +1 -0
- package/dist/e2e.test.js +184 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +63 -0
- package/dist/integration.test.d.ts +1 -0
- package/dist/integration.test.js +159 -0
- package/dist/kaspa/api.d.ts +11 -0
- package/dist/kaspa/api.js +49 -0
- package/dist/kaspa/api.test.d.ts +1 -0
- package/dist/kaspa/api.test.js +177 -0
- package/dist/kaspa/setup.d.ts +1 -0
- package/dist/kaspa/setup.js +4 -0
- package/dist/kaspa/transaction.d.ts +5 -0
- package/dist/kaspa/transaction.js +60 -0
- package/dist/kaspa/transaction.test.d.ts +1 -0
- package/dist/kaspa/transaction.test.js +170 -0
- package/dist/kaspa/wallet.d.ts +15 -0
- package/dist/kaspa/wallet.js +97 -0
- package/dist/kaspa/wallet.test.d.ts +1 -0
- package/dist/kaspa/wallet.test.js +160 -0
- package/dist/test-setup.d.ts +1 -0
- package/dist/test-setup.js +4 -0
- package/dist/tools/get-balance.d.ts +9 -0
- package/dist/tools/get-balance.js +21 -0
- package/dist/tools/get-balance.test.d.ts +1 -0
- package/dist/tools/get-balance.test.js +93 -0
- package/dist/tools/get-fee-estimate.d.ts +6 -0
- package/dist/tools/get-fee-estimate.js +13 -0
- package/dist/tools/get-fee-estimate.test.d.ts +1 -0
- package/dist/tools/get-fee-estimate.test.js +74 -0
- package/dist/tools/get-my-address.d.ts +4 -0
- package/dist/tools/get-my-address.js +9 -0
- package/dist/tools/get-my-address.test.d.ts +1 -0
- package/dist/tools/get-my-address.test.js +32 -0
- package/dist/tools/get-transaction.d.ts +21 -0
- package/dist/tools/get-transaction.js +36 -0
- package/dist/tools/get-transaction.test.d.ts +1 -0
- package/dist/tools/get-transaction.test.js +119 -0
- package/dist/tools/send-kaspa.d.ts +10 -0
- package/dist/tools/send-kaspa.js +53 -0
- package/dist/tools/send-kaspa.test.d.ts +1 -0
- package/dist/tools/send-kaspa.test.js +147 -0
- package/dist/types.d.ts +53 -0
- package/dist/types.js +3 -0
- package/package.json +39 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// ABOUTME: Unit tests for KaspaWallet key management
|
|
2
|
+
// ABOUTME: Tests mnemonic derivation, private key handling, and address generation
|
|
3
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
4
|
+
import { KaspaWallet } from './wallet.js';
|
|
5
|
+
// WARNING: These are PUBLIC TEST-ONLY credentials. DO NOT use on mainnet with real funds.
|
|
6
|
+
// They are published in this repository and offer no security.
|
|
7
|
+
// Valid test mnemonic (24 words)
|
|
8
|
+
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';
|
|
9
|
+
// Valid testnet private key (64 hex characters)
|
|
10
|
+
const TEST_PRIVATE_KEY = 'a9f8d7e6c5b4a3029187654321fedcba9876543210fedcba9876543210fedcba';
|
|
11
|
+
describe('KaspaWallet', () => {
|
|
12
|
+
describe('fromMnemonic', () => {
|
|
13
|
+
it('creates wallet from valid mnemonic for mainnet', () => {
|
|
14
|
+
const wallet = KaspaWallet.fromMnemonic(TEST_MNEMONIC, 'mainnet');
|
|
15
|
+
expect(wallet).toBeInstanceOf(KaspaWallet);
|
|
16
|
+
expect(wallet.getAddress()).toMatch(/^kaspa:/);
|
|
17
|
+
});
|
|
18
|
+
it('creates wallet from valid mnemonic for testnet-10', () => {
|
|
19
|
+
const wallet = KaspaWallet.fromMnemonic(TEST_MNEMONIC, 'testnet-10');
|
|
20
|
+
expect(wallet).toBeInstanceOf(KaspaWallet);
|
|
21
|
+
expect(wallet.getAddress()).toMatch(/^kaspatest:/);
|
|
22
|
+
});
|
|
23
|
+
it('creates wallet from valid mnemonic for testnet-11', () => {
|
|
24
|
+
const wallet = KaspaWallet.fromMnemonic(TEST_MNEMONIC, 'testnet-11');
|
|
25
|
+
expect(wallet).toBeInstanceOf(KaspaWallet);
|
|
26
|
+
expect(wallet.getAddress()).toMatch(/^kaspatest:/);
|
|
27
|
+
});
|
|
28
|
+
it('creates wallet with different account index', () => {
|
|
29
|
+
const wallet0 = KaspaWallet.fromMnemonic(TEST_MNEMONIC, 'mainnet', 0);
|
|
30
|
+
const wallet1 = KaspaWallet.fromMnemonic(TEST_MNEMONIC, 'mainnet', 1);
|
|
31
|
+
expect(wallet0.getAddress()).not.toBe(wallet1.getAddress());
|
|
32
|
+
});
|
|
33
|
+
it('throws error for empty mnemonic', () => {
|
|
34
|
+
expect(() => KaspaWallet.fromMnemonic('', 'mainnet')).toThrow('Mnemonic phrase is required');
|
|
35
|
+
});
|
|
36
|
+
it('throws error for invalid mnemonic', () => {
|
|
37
|
+
expect(() => KaspaWallet.fromMnemonic('invalid mnemonic words', 'mainnet')).toThrow('Invalid mnemonic phrase');
|
|
38
|
+
});
|
|
39
|
+
it('defaults to mainnet when no network specified', () => {
|
|
40
|
+
const wallet = KaspaWallet.fromMnemonic(TEST_MNEMONIC);
|
|
41
|
+
expect(wallet.getAddress()).toMatch(/^kaspa:/);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
describe('fromPrivateKey', () => {
|
|
45
|
+
it('creates wallet from valid private key', () => {
|
|
46
|
+
const wallet = KaspaWallet.fromPrivateKey(TEST_PRIVATE_KEY, 'mainnet');
|
|
47
|
+
expect(wallet).toBeInstanceOf(KaspaWallet);
|
|
48
|
+
expect(wallet.getAddress()).toMatch(/^kaspa:/);
|
|
49
|
+
});
|
|
50
|
+
it('creates wallet for testnet-10', () => {
|
|
51
|
+
const wallet = KaspaWallet.fromPrivateKey(TEST_PRIVATE_KEY, 'testnet-10');
|
|
52
|
+
expect(wallet.getAddress()).toMatch(/^kaspatest:/);
|
|
53
|
+
});
|
|
54
|
+
it('throws error for empty private key', () => {
|
|
55
|
+
expect(() => KaspaWallet.fromPrivateKey('', 'mainnet')).toThrow('Private key is required');
|
|
56
|
+
});
|
|
57
|
+
it('throws error for invalid private key format', () => {
|
|
58
|
+
expect(() => KaspaWallet.fromPrivateKey('not-a-valid-key', 'mainnet')).toThrow('Invalid private key format');
|
|
59
|
+
});
|
|
60
|
+
it('defaults to mainnet when no network specified', () => {
|
|
61
|
+
const wallet = KaspaWallet.fromPrivateKey(TEST_PRIVATE_KEY);
|
|
62
|
+
expect(wallet.getAddress()).toMatch(/^kaspa:/);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
describe('getAddress', () => {
|
|
66
|
+
it('returns consistent address for same wallet', () => {
|
|
67
|
+
const wallet = KaspaWallet.fromMnemonic(TEST_MNEMONIC, 'mainnet');
|
|
68
|
+
expect(wallet.getAddress()).toBe(wallet.getAddress());
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
describe('getPrivateKey', () => {
|
|
72
|
+
it('returns the private key object', () => {
|
|
73
|
+
const wallet = KaspaWallet.fromMnemonic(TEST_MNEMONIC, 'mainnet');
|
|
74
|
+
const pk = wallet.getPrivateKey();
|
|
75
|
+
expect(pk).toBeDefined();
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
describe('getNetworkType', () => {
|
|
79
|
+
it('returns Mainnet for mainnet wallet', () => {
|
|
80
|
+
const wallet = KaspaWallet.fromMnemonic(TEST_MNEMONIC, 'mainnet');
|
|
81
|
+
const networkType = wallet.getNetworkType();
|
|
82
|
+
expect(networkType).toBeDefined();
|
|
83
|
+
});
|
|
84
|
+
it('returns Testnet for testnet-10 wallet', () => {
|
|
85
|
+
const wallet = KaspaWallet.fromMnemonic(TEST_MNEMONIC, 'testnet-10');
|
|
86
|
+
const networkType = wallet.getNetworkType();
|
|
87
|
+
expect(networkType).toBeDefined();
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
describe('getNetworkId', () => {
|
|
91
|
+
it('returns mainnet for mainnet wallet', () => {
|
|
92
|
+
const wallet = KaspaWallet.fromMnemonic(TEST_MNEMONIC, 'mainnet');
|
|
93
|
+
expect(wallet.getNetworkId()).toBe('mainnet');
|
|
94
|
+
});
|
|
95
|
+
it('returns testnet-10 for testnet-10 wallet', () => {
|
|
96
|
+
const wallet = KaspaWallet.fromMnemonic(TEST_MNEMONIC, 'testnet-10');
|
|
97
|
+
expect(wallet.getNetworkId()).toBe('testnet-10');
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
describe('getWallet', () => {
|
|
102
|
+
const originalEnv = process.env;
|
|
103
|
+
beforeEach(() => {
|
|
104
|
+
// Reset module state by clearing the cached wallet
|
|
105
|
+
vi.resetModules();
|
|
106
|
+
process.env = { ...originalEnv };
|
|
107
|
+
});
|
|
108
|
+
afterEach(() => {
|
|
109
|
+
process.env = originalEnv;
|
|
110
|
+
});
|
|
111
|
+
it('creates wallet from KASPA_MNEMONIC environment variable', async () => {
|
|
112
|
+
process.env.KASPA_MNEMONIC = TEST_MNEMONIC;
|
|
113
|
+
process.env.KASPA_NETWORK = 'testnet-10';
|
|
114
|
+
delete process.env.KASPA_PRIVATE_KEY;
|
|
115
|
+
const { getWallet: freshGetWallet } = await import('./wallet.js');
|
|
116
|
+
const wallet = freshGetWallet();
|
|
117
|
+
expect(wallet.getAddress()).toMatch(/^kaspatest:/);
|
|
118
|
+
});
|
|
119
|
+
it('creates wallet from KASPA_PRIVATE_KEY when no mnemonic', async () => {
|
|
120
|
+
delete process.env.KASPA_MNEMONIC;
|
|
121
|
+
process.env.KASPA_PRIVATE_KEY = TEST_PRIVATE_KEY;
|
|
122
|
+
process.env.KASPA_NETWORK = 'mainnet';
|
|
123
|
+
const { getWallet: freshGetWallet } = await import('./wallet.js');
|
|
124
|
+
const wallet = freshGetWallet();
|
|
125
|
+
expect(wallet.getAddress()).toMatch(/^kaspa:/);
|
|
126
|
+
});
|
|
127
|
+
it('uses account index from KASPA_ACCOUNT_INDEX', async () => {
|
|
128
|
+
process.env.KASPA_MNEMONIC = TEST_MNEMONIC;
|
|
129
|
+
process.env.KASPA_NETWORK = 'mainnet';
|
|
130
|
+
process.env.KASPA_ACCOUNT_INDEX = '1';
|
|
131
|
+
delete process.env.KASPA_PRIVATE_KEY;
|
|
132
|
+
const { getWallet: freshGetWallet } = await import('./wallet.js');
|
|
133
|
+
const wallet = freshGetWallet();
|
|
134
|
+
// Use duck typing since module re-import creates different class reference
|
|
135
|
+
expect(wallet.getAddress()).toMatch(/^kaspa:/);
|
|
136
|
+
expect(wallet.getNetworkId()).toBe('mainnet');
|
|
137
|
+
});
|
|
138
|
+
it('defaults to mainnet when KASPA_NETWORK not set', async () => {
|
|
139
|
+
process.env.KASPA_MNEMONIC = TEST_MNEMONIC;
|
|
140
|
+
delete process.env.KASPA_NETWORK;
|
|
141
|
+
delete process.env.KASPA_PRIVATE_KEY;
|
|
142
|
+
const { getWallet: freshGetWallet } = await import('./wallet.js');
|
|
143
|
+
const wallet = freshGetWallet();
|
|
144
|
+
expect(wallet.getAddress()).toMatch(/^kaspa:/);
|
|
145
|
+
});
|
|
146
|
+
it('throws error when no credentials set', async () => {
|
|
147
|
+
delete process.env.KASPA_MNEMONIC;
|
|
148
|
+
delete process.env.KASPA_PRIVATE_KEY;
|
|
149
|
+
const { getWallet: freshGetWallet } = await import('./wallet.js');
|
|
150
|
+
expect(() => freshGetWallet()).toThrow('Either KASPA_MNEMONIC or KASPA_PRIVATE_KEY environment variable must be set');
|
|
151
|
+
});
|
|
152
|
+
it('returns cached wallet on subsequent calls', async () => {
|
|
153
|
+
process.env.KASPA_MNEMONIC = TEST_MNEMONIC;
|
|
154
|
+
process.env.KASPA_NETWORK = 'mainnet';
|
|
155
|
+
const { getWallet: freshGetWallet } = await import('./wallet.js');
|
|
156
|
+
const wallet1 = freshGetWallet();
|
|
157
|
+
const wallet2 = freshGetWallet();
|
|
158
|
+
expect(wallet1).toBe(wallet2);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// ABOUTME: MCP tool to get balance for a Kaspa address
|
|
2
|
+
// ABOUTME: Returns balance in KAS and UTXO count
|
|
3
|
+
import * as kaspa from 'kaspa-wasm';
|
|
4
|
+
import { getApi } from '../kaspa/api.js';
|
|
5
|
+
import { getWallet } from '../kaspa/wallet.js';
|
|
6
|
+
const { sompiToKaspaString } = kaspa;
|
|
7
|
+
export async function getBalance(params) {
|
|
8
|
+
const wallet = getWallet();
|
|
9
|
+
const address = params.address || wallet.getAddress();
|
|
10
|
+
const api = getApi(wallet.getNetworkId());
|
|
11
|
+
const [balanceResponse, utxos] = await Promise.all([
|
|
12
|
+
api.getBalance(address),
|
|
13
|
+
api.getUtxos(address),
|
|
14
|
+
]);
|
|
15
|
+
const balanceKas = sompiToKaspaString(BigInt(balanceResponse.balance));
|
|
16
|
+
return {
|
|
17
|
+
address,
|
|
18
|
+
balance: balanceKas,
|
|
19
|
+
utxoCount: utxos.length,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// ABOUTME: Unit tests for get-balance MCP tool
|
|
2
|
+
// ABOUTME: Tests balance retrieval with sompi conversion
|
|
3
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
4
|
+
vi.mock('../kaspa/wallet.js', () => ({
|
|
5
|
+
getWallet: vi.fn(),
|
|
6
|
+
}));
|
|
7
|
+
vi.mock('../kaspa/api.js', () => ({
|
|
8
|
+
getApi: vi.fn(),
|
|
9
|
+
}));
|
|
10
|
+
vi.mock('kaspa-wasm', () => ({
|
|
11
|
+
sompiToKaspaString: vi.fn((sompi) => {
|
|
12
|
+
const kas = Number(sompi) / 100_000_000;
|
|
13
|
+
return kas.toString();
|
|
14
|
+
}),
|
|
15
|
+
}));
|
|
16
|
+
import { getBalance } from './get-balance.js';
|
|
17
|
+
import { getWallet } from '../kaspa/wallet.js';
|
|
18
|
+
import { getApi } from '../kaspa/api.js';
|
|
19
|
+
describe('getBalance', () => {
|
|
20
|
+
const mockWallet = {
|
|
21
|
+
getAddress: vi.fn(),
|
|
22
|
+
getNetworkId: vi.fn(),
|
|
23
|
+
};
|
|
24
|
+
const mockApi = {
|
|
25
|
+
getBalance: vi.fn(),
|
|
26
|
+
getUtxos: vi.fn(),
|
|
27
|
+
};
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
vi.mocked(getWallet).mockReturnValue(mockWallet);
|
|
30
|
+
vi.mocked(getApi).mockReturnValue(mockApi);
|
|
31
|
+
mockWallet.getAddress.mockReset();
|
|
32
|
+
mockWallet.getNetworkId.mockReset();
|
|
33
|
+
mockApi.getBalance.mockReset();
|
|
34
|
+
mockApi.getUtxos.mockReset();
|
|
35
|
+
});
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
vi.restoreAllMocks();
|
|
38
|
+
});
|
|
39
|
+
it('returns balance for wallet address when no address provided', async () => {
|
|
40
|
+
mockWallet.getAddress.mockReturnValue('kaspa:qpwallet123');
|
|
41
|
+
mockWallet.getNetworkId.mockReturnValue('mainnet');
|
|
42
|
+
mockApi.getBalance.mockResolvedValue({ balance: '1000000000' });
|
|
43
|
+
mockApi.getUtxos.mockResolvedValue([{ utxo: 1 }, { utxo: 2 }]);
|
|
44
|
+
const result = await getBalance({});
|
|
45
|
+
expect(result).toEqual({
|
|
46
|
+
address: 'kaspa:qpwallet123',
|
|
47
|
+
balance: '10',
|
|
48
|
+
utxoCount: 2,
|
|
49
|
+
});
|
|
50
|
+
expect(getApi).toHaveBeenCalledWith('mainnet');
|
|
51
|
+
});
|
|
52
|
+
it('returns balance for specified address', async () => {
|
|
53
|
+
mockWallet.getAddress.mockReturnValue('kaspa:qpwallet123');
|
|
54
|
+
mockWallet.getNetworkId.mockReturnValue('testnet-10');
|
|
55
|
+
mockApi.getBalance.mockResolvedValue({ balance: '500000000' });
|
|
56
|
+
mockApi.getUtxos.mockResolvedValue([{ utxo: 1 }]);
|
|
57
|
+
const result = await getBalance({ address: 'kaspatest:qpother456' });
|
|
58
|
+
expect(result).toEqual({
|
|
59
|
+
address: 'kaspatest:qpother456',
|
|
60
|
+
balance: '5',
|
|
61
|
+
utxoCount: 1,
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
it('returns zero balance with no UTXOs', async () => {
|
|
65
|
+
mockWallet.getAddress.mockReturnValue('kaspa:qpempty');
|
|
66
|
+
mockWallet.getNetworkId.mockReturnValue('mainnet');
|
|
67
|
+
mockApi.getBalance.mockResolvedValue({ balance: '0' });
|
|
68
|
+
mockApi.getUtxos.mockResolvedValue([]);
|
|
69
|
+
const result = await getBalance({});
|
|
70
|
+
expect(result).toEqual({
|
|
71
|
+
address: 'kaspa:qpempty',
|
|
72
|
+
balance: '0',
|
|
73
|
+
utxoCount: 0,
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
it('handles large balance amounts', async () => {
|
|
77
|
+
mockWallet.getAddress.mockReturnValue('kaspa:qprich');
|
|
78
|
+
mockWallet.getNetworkId.mockReturnValue('mainnet');
|
|
79
|
+
mockApi.getBalance.mockResolvedValue({ balance: '100000000000000' });
|
|
80
|
+
mockApi.getUtxos.mockResolvedValue(new Array(100).fill({ utxo: 1 }));
|
|
81
|
+
const result = await getBalance({});
|
|
82
|
+
expect(result.utxoCount).toBe(100);
|
|
83
|
+
expect(result.address).toBe('kaspa:qprich');
|
|
84
|
+
});
|
|
85
|
+
it('calls API with correct network', async () => {
|
|
86
|
+
mockWallet.getAddress.mockReturnValue('kaspatest:qptest');
|
|
87
|
+
mockWallet.getNetworkId.mockReturnValue('testnet-11');
|
|
88
|
+
mockApi.getBalance.mockResolvedValue({ balance: '1000000' });
|
|
89
|
+
mockApi.getUtxos.mockResolvedValue([]);
|
|
90
|
+
await getBalance({});
|
|
91
|
+
expect(getApi).toHaveBeenCalledWith('testnet-11');
|
|
92
|
+
});
|
|
93
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// ABOUTME: MCP tool to get current fee estimates from the Kaspa network
|
|
2
|
+
// ABOUTME: Returns priority and normal fee rates in sompi/gram
|
|
3
|
+
import { getApi } from '../kaspa/api.js';
|
|
4
|
+
import { getWallet } from '../kaspa/wallet.js';
|
|
5
|
+
export async function getFeeEstimate() {
|
|
6
|
+
const api = getApi(getWallet().getNetworkId());
|
|
7
|
+
const feeEstimate = await api.getFeeEstimate();
|
|
8
|
+
return {
|
|
9
|
+
priorityFee: feeEstimate.priorityBucket.feerate.toString(),
|
|
10
|
+
normalFee: feeEstimate.normalBuckets[0]?.feerate.toString() || '0',
|
|
11
|
+
lowFee: feeEstimate.lowBuckets[0]?.feerate.toString() || '0',
|
|
12
|
+
};
|
|
13
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// ABOUTME: Unit tests for get-fee-estimate MCP tool
|
|
2
|
+
// ABOUTME: Tests fee estimate retrieval and formatting
|
|
3
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
4
|
+
vi.mock('../kaspa/wallet.js', () => ({
|
|
5
|
+
getWallet: vi.fn(),
|
|
6
|
+
}));
|
|
7
|
+
vi.mock('../kaspa/api.js', () => ({
|
|
8
|
+
getApi: vi.fn(),
|
|
9
|
+
}));
|
|
10
|
+
import { getFeeEstimate } from './get-fee-estimate.js';
|
|
11
|
+
import { getWallet } from '../kaspa/wallet.js';
|
|
12
|
+
import { getApi } from '../kaspa/api.js';
|
|
13
|
+
describe('getFeeEstimate', () => {
|
|
14
|
+
const mockWallet = {
|
|
15
|
+
getNetworkId: vi.fn(),
|
|
16
|
+
};
|
|
17
|
+
const mockApi = {
|
|
18
|
+
getFeeEstimate: vi.fn(),
|
|
19
|
+
};
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
vi.mocked(getWallet).mockReturnValue(mockWallet);
|
|
22
|
+
vi.mocked(getApi).mockReturnValue(mockApi);
|
|
23
|
+
mockWallet.getNetworkId.mockReset();
|
|
24
|
+
mockApi.getFeeEstimate.mockReset();
|
|
25
|
+
});
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
vi.restoreAllMocks();
|
|
28
|
+
});
|
|
29
|
+
it('returns fee estimates from API', async () => {
|
|
30
|
+
mockWallet.getNetworkId.mockReturnValue('mainnet');
|
|
31
|
+
mockApi.getFeeEstimate.mockResolvedValue({
|
|
32
|
+
priorityBucket: { feerate: 1.5 },
|
|
33
|
+
normalBuckets: [{ feerate: 1.0 }],
|
|
34
|
+
lowBuckets: [{ feerate: 0.5 }],
|
|
35
|
+
});
|
|
36
|
+
const result = await getFeeEstimate();
|
|
37
|
+
expect(result).toEqual({
|
|
38
|
+
priorityFee: '1.5',
|
|
39
|
+
normalFee: '1',
|
|
40
|
+
lowFee: '0.5',
|
|
41
|
+
});
|
|
42
|
+
expect(getApi).toHaveBeenCalledWith('mainnet');
|
|
43
|
+
});
|
|
44
|
+
it('returns 0 for normal fee when normalBuckets is empty', async () => {
|
|
45
|
+
mockWallet.getNetworkId.mockReturnValue('testnet-10');
|
|
46
|
+
mockApi.getFeeEstimate.mockResolvedValue({
|
|
47
|
+
priorityBucket: { feerate: 2.0 },
|
|
48
|
+
normalBuckets: [],
|
|
49
|
+
lowBuckets: [{ feerate: 0.5 }],
|
|
50
|
+
});
|
|
51
|
+
const result = await getFeeEstimate();
|
|
52
|
+
expect(result.normalFee).toBe('0');
|
|
53
|
+
});
|
|
54
|
+
it('returns 0 for low fee when lowBuckets is empty', async () => {
|
|
55
|
+
mockWallet.getNetworkId.mockReturnValue('testnet-11');
|
|
56
|
+
mockApi.getFeeEstimate.mockResolvedValue({
|
|
57
|
+
priorityBucket: { feerate: 2.0 },
|
|
58
|
+
normalBuckets: [{ feerate: 1.0 }],
|
|
59
|
+
lowBuckets: [],
|
|
60
|
+
});
|
|
61
|
+
const result = await getFeeEstimate();
|
|
62
|
+
expect(result.lowFee).toBe('0');
|
|
63
|
+
});
|
|
64
|
+
it('uses correct network from wallet', async () => {
|
|
65
|
+
mockWallet.getNetworkId.mockReturnValue('testnet-10');
|
|
66
|
+
mockApi.getFeeEstimate.mockResolvedValue({
|
|
67
|
+
priorityBucket: { feerate: 1.0 },
|
|
68
|
+
normalBuckets: [{ feerate: 0.5 }],
|
|
69
|
+
lowBuckets: [{ feerate: 0.25 }],
|
|
70
|
+
});
|
|
71
|
+
await getFeeEstimate();
|
|
72
|
+
expect(getApi).toHaveBeenCalledWith('testnet-10');
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// ABOUTME: MCP tool to get the wallet address derived from the configured private key
|
|
2
|
+
// ABOUTME: Returns the Kaspa address for the current wallet
|
|
3
|
+
import { getWallet } from '../kaspa/wallet.js';
|
|
4
|
+
export async function getMyAddress() {
|
|
5
|
+
const wallet = getWallet();
|
|
6
|
+
return {
|
|
7
|
+
address: wallet.getAddress(),
|
|
8
|
+
};
|
|
9
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// ABOUTME: Unit tests for get-my-address MCP tool
|
|
2
|
+
// ABOUTME: Tests wallet address retrieval functionality
|
|
3
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
4
|
+
vi.mock('../kaspa/wallet.js', () => ({
|
|
5
|
+
getWallet: vi.fn(),
|
|
6
|
+
}));
|
|
7
|
+
import { getMyAddress } from './get-my-address.js';
|
|
8
|
+
import { getWallet } from '../kaspa/wallet.js';
|
|
9
|
+
describe('getMyAddress', () => {
|
|
10
|
+
const mockWallet = {
|
|
11
|
+
getAddress: vi.fn(),
|
|
12
|
+
};
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
vi.mocked(getWallet).mockReturnValue(mockWallet);
|
|
15
|
+
mockWallet.getAddress.mockReset();
|
|
16
|
+
});
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
vi.restoreAllMocks();
|
|
19
|
+
});
|
|
20
|
+
it('returns the wallet address', async () => {
|
|
21
|
+
mockWallet.getAddress.mockReturnValue('kaspa:qptest123');
|
|
22
|
+
const result = await getMyAddress();
|
|
23
|
+
expect(result).toEqual({ address: 'kaspa:qptest123' });
|
|
24
|
+
expect(getWallet).toHaveBeenCalled();
|
|
25
|
+
expect(mockWallet.getAddress).toHaveBeenCalled();
|
|
26
|
+
});
|
|
27
|
+
it('returns testnet address when wallet is on testnet', async () => {
|
|
28
|
+
mockWallet.getAddress.mockReturnValue('kaspatest:qptest456');
|
|
29
|
+
const result = await getMyAddress();
|
|
30
|
+
expect(result).toEqual({ address: 'kaspatest:qptest456' });
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface GetTransactionParams {
|
|
2
|
+
txId: string;
|
|
3
|
+
}
|
|
4
|
+
export interface TransactionOutput {
|
|
5
|
+
index: number;
|
|
6
|
+
amount: string;
|
|
7
|
+
address: string;
|
|
8
|
+
}
|
|
9
|
+
export interface TransactionInput {
|
|
10
|
+
transactionId: string;
|
|
11
|
+
index: number;
|
|
12
|
+
}
|
|
13
|
+
export interface GetTransactionResult {
|
|
14
|
+
txId: string;
|
|
15
|
+
accepted: boolean;
|
|
16
|
+
blockHash?: string;
|
|
17
|
+
blockTime?: number;
|
|
18
|
+
inputs: TransactionInput[];
|
|
19
|
+
outputs: TransactionOutput[];
|
|
20
|
+
}
|
|
21
|
+
export declare function getTransaction(params: GetTransactionParams): Promise<GetTransactionResult>;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// ABOUTME: MCP tool to get transaction status and details
|
|
2
|
+
// ABOUTME: Returns transaction details including inputs, outputs, and acceptance status
|
|
3
|
+
import * as kaspa from 'kaspa-wasm';
|
|
4
|
+
import { getApi } from '../kaspa/api.js';
|
|
5
|
+
import { getWallet } from '../kaspa/wallet.js';
|
|
6
|
+
const { sompiToKaspaString } = kaspa;
|
|
7
|
+
export async function getTransaction(params) {
|
|
8
|
+
if (!params.txId) {
|
|
9
|
+
throw new Error('Transaction ID (txId) is required');
|
|
10
|
+
}
|
|
11
|
+
const api = getApi(getWallet().getNetworkId());
|
|
12
|
+
try {
|
|
13
|
+
const tx = await api.getTransaction(params.txId);
|
|
14
|
+
return {
|
|
15
|
+
txId: tx.transaction_id,
|
|
16
|
+
accepted: tx.is_accepted,
|
|
17
|
+
blockHash: tx.block_hash?.[0],
|
|
18
|
+
blockTime: tx.block_time,
|
|
19
|
+
inputs: tx.inputs.map((input) => ({
|
|
20
|
+
transactionId: input.previous_outpoint_hash,
|
|
21
|
+
index: input.previous_outpoint_index,
|
|
22
|
+
})),
|
|
23
|
+
outputs: tx.outputs.map((output, idx) => ({
|
|
24
|
+
index: idx,
|
|
25
|
+
amount: sompiToKaspaString(BigInt(output.amount)),
|
|
26
|
+
address: output.script_public_key_address,
|
|
27
|
+
})),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
if (error instanceof Error && error.message.includes('404')) {
|
|
32
|
+
throw new Error(`Transaction not found: ${params.txId}`);
|
|
33
|
+
}
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// ABOUTME: Unit tests for get-transaction MCP tool
|
|
2
|
+
// ABOUTME: Tests transaction retrieval and error handling
|
|
3
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
4
|
+
vi.mock('kaspa-wasm', () => ({
|
|
5
|
+
sompiToKaspaString: (sompi) => (Number(sompi) / 100_000_000).toString(),
|
|
6
|
+
}));
|
|
7
|
+
vi.mock('../kaspa/wallet.js', () => ({
|
|
8
|
+
getWallet: vi.fn(),
|
|
9
|
+
}));
|
|
10
|
+
vi.mock('../kaspa/api.js', () => ({
|
|
11
|
+
getApi: vi.fn(),
|
|
12
|
+
}));
|
|
13
|
+
import { getTransaction } from './get-transaction.js';
|
|
14
|
+
import { getWallet } from '../kaspa/wallet.js';
|
|
15
|
+
import { getApi } from '../kaspa/api.js';
|
|
16
|
+
describe('getTransaction', () => {
|
|
17
|
+
const mockWallet = {
|
|
18
|
+
getNetworkId: vi.fn(),
|
|
19
|
+
};
|
|
20
|
+
const mockApi = {
|
|
21
|
+
getTransaction: vi.fn(),
|
|
22
|
+
};
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
vi.mocked(getWallet).mockReturnValue(mockWallet);
|
|
25
|
+
vi.mocked(getApi).mockReturnValue(mockApi);
|
|
26
|
+
mockWallet.getNetworkId.mockReset();
|
|
27
|
+
mockApi.getTransaction.mockReset();
|
|
28
|
+
});
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
vi.restoreAllMocks();
|
|
31
|
+
});
|
|
32
|
+
it('returns transaction details for accepted tx', async () => {
|
|
33
|
+
mockWallet.getNetworkId.mockReturnValue('mainnet');
|
|
34
|
+
mockApi.getTransaction.mockResolvedValue({
|
|
35
|
+
transaction_id: 'abc123',
|
|
36
|
+
is_accepted: true,
|
|
37
|
+
block_hash: ['blockhash1'],
|
|
38
|
+
block_time: 1234567890,
|
|
39
|
+
inputs: [
|
|
40
|
+
{ previous_outpoint_hash: 'input_tx_1', previous_outpoint_index: 0 },
|
|
41
|
+
],
|
|
42
|
+
outputs: [
|
|
43
|
+
{ amount: '10000000000', script_public_key_address: 'kaspa:qprecipient' },
|
|
44
|
+
{ amount: '5000000000', script_public_key_address: 'kaspa:qpchange' },
|
|
45
|
+
],
|
|
46
|
+
});
|
|
47
|
+
const result = await getTransaction({ txId: 'abc123' });
|
|
48
|
+
expect(result).toEqual({
|
|
49
|
+
txId: 'abc123',
|
|
50
|
+
accepted: true,
|
|
51
|
+
blockHash: 'blockhash1',
|
|
52
|
+
blockTime: 1234567890,
|
|
53
|
+
inputs: [{ transactionId: 'input_tx_1', index: 0 }],
|
|
54
|
+
outputs: [
|
|
55
|
+
{ index: 0, amount: '100', address: 'kaspa:qprecipient' },
|
|
56
|
+
{ index: 1, amount: '50', address: 'kaspa:qpchange' },
|
|
57
|
+
],
|
|
58
|
+
});
|
|
59
|
+
expect(getApi).toHaveBeenCalledWith('mainnet');
|
|
60
|
+
expect(mockApi.getTransaction).toHaveBeenCalledWith('abc123');
|
|
61
|
+
});
|
|
62
|
+
it('returns transaction details for pending tx', async () => {
|
|
63
|
+
mockWallet.getNetworkId.mockReturnValue('testnet-10');
|
|
64
|
+
mockApi.getTransaction.mockResolvedValue({
|
|
65
|
+
transaction_id: 'def456',
|
|
66
|
+
is_accepted: false,
|
|
67
|
+
block_hash: null,
|
|
68
|
+
block_time: undefined,
|
|
69
|
+
inputs: [],
|
|
70
|
+
outputs: [],
|
|
71
|
+
});
|
|
72
|
+
const result = await getTransaction({ txId: 'def456' });
|
|
73
|
+
expect(result).toEqual({
|
|
74
|
+
txId: 'def456',
|
|
75
|
+
accepted: false,
|
|
76
|
+
blockHash: undefined,
|
|
77
|
+
blockTime: undefined,
|
|
78
|
+
inputs: [],
|
|
79
|
+
outputs: [],
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
it('throws error when txId is not provided', async () => {
|
|
83
|
+
await expect(getTransaction({ txId: '' })).rejects.toThrow('Transaction ID (txId) is required');
|
|
84
|
+
});
|
|
85
|
+
it('throws error when transaction is not found (404)', async () => {
|
|
86
|
+
mockWallet.getNetworkId.mockReturnValue('mainnet');
|
|
87
|
+
mockApi.getTransaction.mockRejectedValue(new Error('API error 404: Not found'));
|
|
88
|
+
await expect(getTransaction({ txId: 'notfound' })).rejects.toThrow('Transaction not found: notfound');
|
|
89
|
+
});
|
|
90
|
+
it('rethrows other errors', async () => {
|
|
91
|
+
mockWallet.getNetworkId.mockReturnValue('mainnet');
|
|
92
|
+
mockApi.getTransaction.mockRejectedValue(new Error('Network error'));
|
|
93
|
+
await expect(getTransaction({ txId: 'abc123' })).rejects.toThrow('Network error');
|
|
94
|
+
});
|
|
95
|
+
it('handles block_hash being undefined', async () => {
|
|
96
|
+
mockWallet.getNetworkId.mockReturnValue('mainnet');
|
|
97
|
+
mockApi.getTransaction.mockResolvedValue({
|
|
98
|
+
transaction_id: 'ghi789',
|
|
99
|
+
is_accepted: true,
|
|
100
|
+
block_hash: undefined,
|
|
101
|
+
inputs: [],
|
|
102
|
+
outputs: [],
|
|
103
|
+
});
|
|
104
|
+
const result = await getTransaction({ txId: 'ghi789' });
|
|
105
|
+
expect(result.blockHash).toBeUndefined();
|
|
106
|
+
});
|
|
107
|
+
it('handles empty block_hash array', async () => {
|
|
108
|
+
mockWallet.getNetworkId.mockReturnValue('mainnet');
|
|
109
|
+
mockApi.getTransaction.mockResolvedValue({
|
|
110
|
+
transaction_id: 'jkl012',
|
|
111
|
+
is_accepted: true,
|
|
112
|
+
block_hash: [],
|
|
113
|
+
inputs: [],
|
|
114
|
+
outputs: [],
|
|
115
|
+
});
|
|
116
|
+
const result = await getTransaction({ txId: 'jkl012' });
|
|
117
|
+
expect(result.blockHash).toBeUndefined();
|
|
118
|
+
});
|
|
119
|
+
});
|