add bitcoin wallet admin status endpoint
This commit is contained in:
@@ -13,6 +13,7 @@ import {
|
|||||||
getOrderConfig,
|
getOrderConfig,
|
||||||
getInvoiceConfig,
|
getInvoiceConfig,
|
||||||
getMoneroWalletConfig,
|
getMoneroWalletConfig,
|
||||||
|
getElectrumWalletConfig,
|
||||||
getPostgresConfig,
|
getPostgresConfig,
|
||||||
getShopSettingsConfig,
|
getShopSettingsConfig,
|
||||||
getSimplexConfig
|
getSimplexConfig
|
||||||
@@ -22,6 +23,7 @@ import { AuthModule } from './modules/auth/AuthModule';
|
|||||||
import { EncryptionModule } from './modules/encryption/EncryptionModule';
|
import { EncryptionModule } from './modules/encryption/EncryptionModule';
|
||||||
import { HealthCheckModule } from './modules/healthCheck/HealthCheckModule';
|
import { HealthCheckModule } from './modules/healthCheck/HealthCheckModule';
|
||||||
import { MoneroWalletModule } from './modules/moneroWallet/MoneroWalletModule';
|
import { MoneroWalletModule } from './modules/moneroWallet/MoneroWalletModule';
|
||||||
|
import { BitcoinWalletModule } from './modules/bitcoinWallet/BitcoinWalletModule';
|
||||||
import { SimplexModule } from './modules/simplex/SimplexModule';
|
import { SimplexModule } from './modules/simplex/SimplexModule';
|
||||||
import { DiscountCodesModule } from './modules/discountCode/DiscountCodesModule';
|
import { DiscountCodesModule } from './modules/discountCode/DiscountCodesModule';
|
||||||
import { DataWipeModule } from './modules/dataWipe/DataWipeModule';
|
import { DataWipeModule } from './modules/dataWipe/DataWipeModule';
|
||||||
@@ -55,6 +57,7 @@ import { Config } from './types/Config';
|
|||||||
registerAs('order', getOrderConfig),
|
registerAs('order', getOrderConfig),
|
||||||
registerAs('invoice', getInvoiceConfig),
|
registerAs('invoice', getInvoiceConfig),
|
||||||
registerAs('moneroWallet', getMoneroWalletConfig),
|
registerAs('moneroWallet', getMoneroWalletConfig),
|
||||||
|
registerAs('electrumWallet', getElectrumWalletConfig),
|
||||||
registerAs('simplex', getSimplexConfig)
|
registerAs('simplex', getSimplexConfig)
|
||||||
]
|
]
|
||||||
}),
|
}),
|
||||||
@@ -91,6 +94,7 @@ import { Config } from './types/Config';
|
|||||||
PaymentModule,
|
PaymentModule,
|
||||||
DataWipeModule,
|
DataWipeModule,
|
||||||
MoneroWalletModule,
|
MoneroWalletModule,
|
||||||
|
BitcoinWalletModule,
|
||||||
StorefrontCoreModule,
|
StorefrontCoreModule,
|
||||||
StorefrontProductModule,
|
StorefrontProductModule,
|
||||||
StorefrontCartModule,
|
StorefrontCartModule,
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { BitcoinWalletController } from './controllers/BitcoinWalletController';
|
||||||
|
import { BitcoinWalletAdminService } from './services/BitcoinWalletAdminService';
|
||||||
|
import { ElectrumWalletRpcClient } from './services/ElectrumWalletRpcClient';
|
||||||
|
import { ElectrumWalletRpcConnectionService } from './services/ElectrumWalletRpcConnectionService';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [BitcoinWalletController],
|
||||||
|
providers: [ElectrumWalletRpcClient, ElectrumWalletRpcConnectionService, BitcoinWalletAdminService],
|
||||||
|
exports: [ElectrumWalletRpcClient]
|
||||||
|
})
|
||||||
|
export class BitcoinWalletModule {}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { Controller, Get, UseGuards } from '@nestjs/common';
|
||||||
|
import { JwtGuard } from '../../../guards/JwtGuard';
|
||||||
|
import { BitcoinWalletAdminService } from '../services/BitcoinWalletAdminService';
|
||||||
|
|
||||||
|
@Controller('bitcoin-wallet')
|
||||||
|
@UseGuards(JwtGuard)
|
||||||
|
export class BitcoinWalletController {
|
||||||
|
constructor(private readonly walletAdminService: BitcoinWalletAdminService) {}
|
||||||
|
|
||||||
|
@Get('/')
|
||||||
|
getStatus() {
|
||||||
|
return this.walletAdminService.getStatus();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { ElectrumNetwork } from '../../../types/ElectrumNetwork';
|
||||||
|
import { BitcoinWalletSyncStatus } from './BitcoinWalletSyncStatus';
|
||||||
|
|
||||||
|
export interface BitcoinWalletStatusView {
|
||||||
|
network: ElectrumNetwork;
|
||||||
|
rpcVersion: string;
|
||||||
|
blockHeight: number | null;
|
||||||
|
serverHeight: number | null;
|
||||||
|
syncStatus: BitcoinWalletSyncStatus;
|
||||||
|
balanceBtc: string;
|
||||||
|
confirmedBalanceBtc: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export enum BitcoinWalletSyncStatus {
|
||||||
|
Synced = 'synced',
|
||||||
|
Syncing = 'syncing',
|
||||||
|
Unknown = 'unknown'
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user