add bitcoin wallet admin status endpoint
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
import { ServiceUnavailableException } from '@nestjs/common';
|
||||
import type { ConfigService } from '@nestjs/config';
|
||||
import { BitcoinWalletSyncStatus } from '../types/BitcoinWalletSyncStatus';
|
||||
import type { ElectrumWalletRpcClient } from './ElectrumWalletRpcClient';
|
||||
import { BitcoinWalletAdminService } from './BitcoinWalletAdminService';
|
||||
|
||||
describe('BitcoinWalletAdminService', () => {
|
||||
let service: BitcoinWalletAdminService;
|
||||
let walletRpcClient: {
|
||||
getVersion: jest.Mock;
|
||||
isSynchronized: jest.Mock;
|
||||
getInfo: jest.Mock;
|
||||
getBalance: jest.Mock;
|
||||
};
|
||||
let configService: {
|
||||
get: jest.Mock;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
walletRpcClient = {
|
||||
getVersion: jest.fn().mockResolvedValue('4.8.1'),
|
||||
isSynchronized: jest.fn().mockResolvedValue(true),
|
||||
getInfo: jest.fn().mockResolvedValue({
|
||||
blockchain_height: 900_000,
|
||||
server_height: 900_000
|
||||
}),
|
||||
getBalance: jest.fn().mockResolvedValue({
|
||||
balanceAtomic: '150000',
|
||||
confirmedBalanceAtomic: '150000'
|
||||
})
|
||||
};
|
||||
|
||||
configService = {
|
||||
get: jest.fn().mockReturnValue({
|
||||
network: 'testnet'
|
||||
})
|
||||
};
|
||||
|
||||
service = new BitcoinWalletAdminService(
|
||||
walletRpcClient as unknown as ElectrumWalletRpcClient,
|
||||
configService as unknown as ConfigService
|
||||
);
|
||||
});
|
||||
|
||||
it('returns wallet status when RPC calls succeed', async () => {
|
||||
const status = await service.getStatus();
|
||||
|
||||
expect(status).toEqual(
|
||||
expect.objectContaining({
|
||||
network: 'testnet',
|
||||
rpcVersion: '4.8.1',
|
||||
blockHeight: 900_000,
|
||||
serverHeight: 900_000,
|
||||
syncStatus: BitcoinWalletSyncStatus.Synced,
|
||||
balanceBtc: '0.00150000',
|
||||
confirmedBalanceBtc: '0.00150000'
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('reports syncing when the wallet is not synchronized', async () => {
|
||||
walletRpcClient.isSynchronized.mockResolvedValue(false);
|
||||
|
||||
const status = await service.getStatus();
|
||||
|
||||
expect(status.syncStatus).toBe(BitcoinWalletSyncStatus.Syncing);
|
||||
});
|
||||
|
||||
it('throws when RPC calls fail', async () => {
|
||||
walletRpcClient.getBalance.mockRejectedValue(new Error('rpc down'));
|
||||
|
||||
await expect(service.getStatus()).rejects.toBeInstanceOf(ServiceUnavailableException);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
import { Injectable, ServiceUnavailableException } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { convertBtcAtomicToBtc } from '../../../utils/bitcoin/convertBtcAtomicToBtc';
|
||||
import type { Config } from '../../../types/Config';
|
||||
import { BitcoinWalletSyncStatus } from '../types/BitcoinWalletSyncStatus';
|
||||
import type { BitcoinWalletStatusView } from '../types/BitcoinWalletStatusView';
|
||||
import { ElectrumWalletRpcClient } from './ElectrumWalletRpcClient';
|
||||
|
||||
@Injectable()
|
||||
export class BitcoinWalletAdminService {
|
||||
constructor(
|
||||
private readonly walletRpcClient: ElectrumWalletRpcClient,
|
||||
private readonly configService: ConfigService
|
||||
) {}
|
||||
|
||||
async getStatus(): Promise<BitcoinWalletStatusView> {
|
||||
const { network } = this.configService.get('electrumWallet') as Config['electrumWallet'];
|
||||
|
||||
try {
|
||||
const [rpcVersion, isSynchronized, info, { balanceAtomic, confirmedBalanceAtomic }] = await Promise.all([
|
||||
this.walletRpcClient.getVersion(),
|
||||
this.walletRpcClient.isSynchronized(),
|
||||
this.walletRpcClient.getInfo(),
|
||||
this.walletRpcClient.getBalance()
|
||||
]);
|
||||
|
||||
const blockHeight = info.blockchain_height ?? null;
|
||||
const serverHeight = info.server_height ?? null;
|
||||
|
||||
return {
|
||||
network,
|
||||
rpcVersion,
|
||||
blockHeight,
|
||||
serverHeight,
|
||||
syncStatus: this.resolveSyncStatus(isSynchronized, blockHeight, serverHeight),
|
||||
balanceBtc: convertBtcAtomicToBtc(balanceAtomic),
|
||||
confirmedBalanceBtc: convertBtcAtomicToBtc(confirmedBalanceAtomic)
|
||||
};
|
||||
} catch {
|
||||
throw new ServiceUnavailableException(
|
||||
'Could not load wallet status. The Bitcoin wallet may be busy or unavailable.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private resolveSyncStatus(
|
||||
isSynchronized: boolean,
|
||||
blockHeight: number | null,
|
||||
serverHeight: number | null
|
||||
): BitcoinWalletSyncStatus {
|
||||
if (isSynchronized) {
|
||||
return BitcoinWalletSyncStatus.Synced;
|
||||
}
|
||||
|
||||
if (blockHeight === null || serverHeight === null) {
|
||||
return BitcoinWalletSyncStatus.Unknown;
|
||||
}
|
||||
|
||||
return BitcoinWalletSyncStatus.Syncing;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user