add electrum wallet rpc client
This commit is contained in:
@@ -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'
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user