add electrum wallet rpc client

This commit is contained in:
2026-09-04 11:32:12 +02:00
parent 1143b8873c
commit 8c0749875e
10 changed files with 449 additions and 0 deletions
@@ -0,0 +1,211 @@
import { ConfigService } from '@nestjs/config';
import axios from 'axios';
import type { ElectrumWalletRpcClientTest } from '../types/ElectrumWalletRpcClientTest';
import { ElectrumWalletRpcClient } from './ElectrumWalletRpcClient';
jest.mock('axios');
const mockedAxios = axios as jest.Mocked<typeof axios>;
describe('ElectrumWalletRpcClient', () => {
let client: ElectrumWalletRpcClient;
let clientTest: ElectrumWalletRpcClientTest;
beforeEach(() => {
client = new ElectrumWalletRpcClient({
get: jest.fn().mockReturnValue({
rpcUrl: 'http://electrum.test:7777',
username: 'electrum',
password: 'secret',
rpcTimeoutMs: 5000
})
} as unknown as ConfigService);
clientTest = client as unknown as ElectrumWalletRpcClientTest;
mockedAxios.post.mockReset();
});
describe('mapIncomingTransfer', () => {
it('maps confirmed transfers with confirmations derived from block height', () => {
expect(
clientTest.mapIncomingTransfer(
{
tx_hash: 'abc123',
height: 800_000
},
'50000',
800_002
)
).toEqual({
txHash: 'abc123',
amountAtomic: '50000',
confirmations: 3
});
});
it('returns zero confirmations for unconfirmed transfers', () => {
expect(
clientTest.mapIncomingTransfer(
{
tx_hash: 'abc123',
height: 0
},
'50000',
800_002
)
).toEqual({
txHash: 'abc123',
amountAtomic: '50000',
confirmations: 0
});
});
it('returns null for non-positive amounts', () => {
expect(
clientTest.mapIncomingTransfer(
{
tx_hash: 'abc123',
height: 800_000
},
'0',
800_002
)
).toBeNull();
});
});
describe('sumIncomingOutputValueAtomic', () => {
it('sums outputs paying to the target address', () => {
expect(
clientTest.sumIncomingOutputValueAtomic(
{
outputs: [
{ address: 'bc1qother', value_sats: 10_000 },
{ address: 'bc1qtest', value_sats: 50_000 },
{ address: 'bc1qtest', value_sats: 25_000 }
]
},
'bc1qtest'
)
).toBe('75000');
});
it('returns zero when no outputs match the address', () => {
expect(
clientTest.sumIncomingOutputValueAtomic(
{
outputs: [{ address: 'bc1qother', value_sats: 10_000 }]
},
'bc1qtest'
)
).toBe('0');
});
});
describe('getIncomingTransfers', () => {
it('resolves incoming amounts from transaction outputs', async () => {
mockedAxios.post
.mockResolvedValueOnce({
data: {
jsonrpc: '2.0',
id: 'nullcart',
result: [{ tx_hash: 'abc123', height: 800_000 }]
}
})
.mockResolvedValueOnce({
data: {
jsonrpc: '2.0',
id: 'nullcart',
result: '01000000'
}
})
.mockResolvedValueOnce({
data: {
jsonrpc: '2.0',
id: 'nullcart',
result: {
outputs: [
{ address: 'bc1qother', value_sats: 10_000 },
{ address: 'bc1qtest', value_sats: 50_000 }
]
}
}
});
await expect(client.getIncomingTransfers('bc1qtest', 800_002)).resolves.toEqual([
{
txHash: 'abc123',
amountAtomic: '50000',
confirmations: 3
}
]);
expect(mockedAxios.post).toHaveBeenNthCalledWith(
2,
'http://electrum.test:7777',
{
jsonrpc: '2.0',
id: 'nullcart',
method: 'gettransaction',
params: { txid: 'abc123' }
},
expect.any(Object)
);
expect(mockedAxios.post).toHaveBeenNthCalledWith(
3,
'http://electrum.test:7777',
{
jsonrpc: '2.0',
id: 'nullcart',
method: 'deserialize',
params: { tx: '01000000' }
},
expect.any(Object)
);
});
});
describe('createAddress', () => {
it('creates an address and sets a label when provided', async () => {
mockedAxios.post
.mockResolvedValueOnce({ data: { jsonrpc: '2.0', id: 'nullcart', result: 'bc1qtest' } })
.mockResolvedValueOnce({ data: { jsonrpc: '2.0', id: 'nullcart', result: true } });
await expect(client.createAddress('checkout - order-1')).resolves.toBe('bc1qtest');
expect(mockedAxios.post).toHaveBeenNthCalledWith(
2,
'http://electrum.test:7777',
{
jsonrpc: '2.0',
id: 'nullcart',
method: 'setlabel',
params: { key: 'bc1qtest', label: 'checkout - order-1' }
},
expect.objectContaining({
auth: { username: 'electrum', password: 'secret' }
})
);
});
});
describe('getBalance', () => {
it('returns confirmed and total balances in atomic units', async () => {
mockedAxios.post.mockResolvedValueOnce({
data: {
jsonrpc: '2.0',
id: 'nullcart',
result: {
confirmed: '0.00025',
unconfirmed: '0.0001'
}
}
});
await expect(client.getBalance()).resolves.toEqual({
balanceAtomic: '35000',
confirmedBalanceAtomic: '25000'
});
});
});
});