Slave/btc integration #4
@@ -18,8 +18,8 @@ export class MoneroWalletController {
|
||||
|
||||
@Post('/withdraw')
|
||||
@Throttle(throttleProfiles.walletWithdraw)
|
||||
withdraw(@Body() { destinationAddress, password }: MoneroWalletWithdrawDto) {
|
||||
return this.walletAdminService.withdrawAll(destinationAddress, password);
|
||||
withdraw(@Body() { destinationAddress, priority, password }: MoneroWalletWithdrawDto) {
|
||||
return this.walletAdminService.withdrawAll(destinationAddress, priority, password);
|
||||
}
|
||||
|
||||
@Post('/reveal-seed')
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Transform } from 'class-transformer';
|
||||
import { IsNotEmpty, IsString } from 'class-validator';
|
||||
import { IsEnum, IsNotEmpty, IsString } from 'class-validator';
|
||||
import { MoneroWithdrawPriority } from '../../../types/moneroWallet/MoneroWithdrawPriority';
|
||||
import { IsMoneroStandardAddress } from '../../../validation/decorators/isMoneroStandardAddress';
|
||||
|
||||
export class MoneroWalletWithdrawDto {
|
||||
@@ -9,6 +10,10 @@ export class MoneroWalletWithdrawDto {
|
||||
@IsMoneroStandardAddress()
|
||||
destinationAddress: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsEnum(MoneroWithdrawPriority)
|
||||
priority: MoneroWithdrawPriority;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
password: string;
|
||||
|
||||
@@ -2,6 +2,7 @@ import { BadRequestException, ServiceUnavailableException } from '@nestjs/common
|
||||
import type { ConfigService } from '@nestjs/config';
|
||||
import axios from 'axios';
|
||||
import type { AuthService } from '../../auth/services/AuthService';
|
||||
import { MoneroWithdrawPriority } from '../../../types/moneroWallet/MoneroWithdrawPriority';
|
||||
import { WalletSyncStatus } from '../../../types/wallet/WalletSyncStatus';
|
||||
import type { MoneroWalletRpcClient } from './MoneroWalletRpcClient';
|
||||
import { MoneroWalletAdminService } from './MoneroWalletAdminService';
|
||||
@@ -99,9 +100,9 @@ describe('MoneroWalletAdminService', () => {
|
||||
unlockedBalanceAtomic: '0'
|
||||
});
|
||||
|
||||
await expect(service.withdrawAll('4DestinationAddressExample', 'password')).rejects.toThrow(
|
||||
new BadRequestException('No unlocked balance to withdraw.')
|
||||
);
|
||||
await expect(
|
||||
service.withdrawAll('4DestinationAddressExample', MoneroWithdrawPriority.Normal, 'password')
|
||||
).rejects.toThrow(new BadRequestException('No unlocked balance to withdraw.'));
|
||||
|
||||
expect(authService.verifyPassword).toHaveBeenCalledWith('password');
|
||||
expect(walletRpcClient.sweepAll).not.toHaveBeenCalled();
|
||||
@@ -110,15 +111,22 @@ describe('MoneroWalletAdminService', () => {
|
||||
it('rejects withdrawals while the wallet is still syncing', async () => {
|
||||
walletRpcClient.getHeight.mockResolvedValue(2_999_000);
|
||||
|
||||
await expect(service.withdrawAll('4DestinationAddressExample', 'password')).rejects.toThrow(
|
||||
new BadRequestException('Wallet is still syncing. Try again after sync completes.')
|
||||
);
|
||||
await expect(
|
||||
service.withdrawAll('4DestinationAddressExample', MoneroWithdrawPriority.Normal, 'password')
|
||||
).rejects.toThrow(new BadRequestException('Wallet is still syncing. Try again after sync completes.'));
|
||||
});
|
||||
|
||||
it('sweeps unlocked funds when the wallet is synced', async () => {
|
||||
const result = await service.withdrawAll('4DestinationAddressExample', 'password');
|
||||
const result = await service.withdrawAll(
|
||||
'4DestinationAddressExample',
|
||||
MoneroWithdrawPriority.Fast,
|
||||
'password'
|
||||
);
|
||||
|
||||
expect(walletRpcClient.sweepAll).toHaveBeenCalledWith('4DestinationAddressExample');
|
||||
expect(walletRpcClient.sweepAll).toHaveBeenCalledWith(
|
||||
'4DestinationAddressExample',
|
||||
MoneroWithdrawPriority.Fast
|
||||
);
|
||||
expect(result).toEqual({
|
||||
txHashes: ['tx-hash-1'],
|
||||
amountXmr: '1.00000000'
|
||||
@@ -163,7 +171,9 @@ describe('MoneroWalletAdminService', () => {
|
||||
it('throws when sweep all fails after prechecks pass', async () => {
|
||||
walletRpcClient.sweepAll.mockRejectedValue(new Error('sweep failed'));
|
||||
|
||||
await expect(service.withdrawAll('4DestinationAddressExample', 'password')).rejects.toThrow(
|
||||
await expect(
|
||||
service.withdrawAll('4DestinationAddressExample', MoneroWithdrawPriority.Normal, 'password')
|
||||
).rejects.toThrow(
|
||||
new ServiceUnavailableException(
|
||||
'Withdrawal failed. Funds may be unspendable dust, still locked, or the wallet may be out of sync. Refresh status and try again.'
|
||||
)
|
||||
|
||||
@@ -8,6 +8,7 @@ import type { MoneroDaemonGetInfoResult } from '../types/MoneroDaemonGetInfoResu
|
||||
import type { MoneroWalletRevealSeedResult } from '../types/MoneroWalletRevealSeedResult';
|
||||
import type { MoneroWalletStatusView } from '../types/MoneroWalletStatusView';
|
||||
import { WalletSyncStatus } from '../../../types/wallet/WalletSyncStatus';
|
||||
import type { MoneroWithdrawPriority } from '../../../types/moneroWallet/MoneroWithdrawPriority';
|
||||
import type { MoneroWalletWithdrawResult } from '../types/MoneroWalletWithdrawResult';
|
||||
import { MoneroWalletRpcClient } from './MoneroWalletRpcClient';
|
||||
|
||||
@@ -49,7 +50,11 @@ export class MoneroWalletAdminService {
|
||||
}
|
||||
}
|
||||
|
||||
async withdrawAll(destinationAddress: string, password: string): Promise<MoneroWalletWithdrawResult> {
|
||||
async withdrawAll(
|
||||
destinationAddress: string,
|
||||
priority: MoneroWithdrawPriority,
|
||||
password: string
|
||||
): Promise<MoneroWalletWithdrawResult> {
|
||||
this.authService.verifyPassword(password);
|
||||
|
||||
await this.walletRpcClient.tryRefresh();
|
||||
@@ -59,11 +64,15 @@ export class MoneroWalletAdminService {
|
||||
let daemonHeight: number | null;
|
||||
|
||||
try {
|
||||
[{ unlockedBalanceAtomic }, walletHeight, daemonHeight] = await Promise.all([
|
||||
const [balanceResult, walletHeightResult, daemonHeightResult] = await Promise.all([
|
||||
this.walletRpcClient.getBalance(),
|
||||
this.walletRpcClient.getHeight(),
|
||||
this.fetchDaemonHeight()
|
||||
]);
|
||||
|
||||
unlockedBalanceAtomic = balanceResult.unlockedBalanceAtomic;
|
||||
walletHeight = walletHeightResult;
|
||||
daemonHeight = daemonHeightResult;
|
||||
} catch {
|
||||
throw new ServiceUnavailableException('Could not reach the Monero wallet. Try again in a moment.');
|
||||
}
|
||||
@@ -80,7 +89,10 @@ export class MoneroWalletAdminService {
|
||||
let amountAtomic: string;
|
||||
|
||||
try {
|
||||
({ txHashes, amountAtomic } = await this.walletRpcClient.sweepAll(destinationAddress));
|
||||
const sweepResult = await this.walletRpcClient.sweepAll(destinationAddress, priority);
|
||||
|
||||
txHashes = sweepResult.txHashes;
|
||||
amountAtomic = sweepResult.amountAtomic;
|
||||
} catch {
|
||||
throw new ServiceUnavailableException(
|
||||
'Withdrawal failed. Funds may be unspendable dust, still locked, or the wallet may be out of sync. Refresh status and try again.'
|
||||
|
||||
@@ -12,6 +12,7 @@ import type { MoneroWalletRpcGetBalanceResult } from '../types/MoneroWalletRpcGe
|
||||
import type { MoneroWalletRpcGetHeightResult } from '../types/MoneroWalletRpcGetHeightResult';
|
||||
import type { MoneroWalletRpcGetVersionResult } from '../types/MoneroWalletRpcGetVersionResult';
|
||||
import type { MoneroWalletRpcQueryKeyResult } from '../types/MoneroWalletRpcQueryKeyResult';
|
||||
import type { MoneroWithdrawPriority } from '../../../types/moneroWallet/MoneroWithdrawPriority';
|
||||
import type { MoneroWalletRpcSweepAllResult } from '../types/MoneroWalletRpcSweepAllResult';
|
||||
import type { MoneroWalletRpcDigestChallenge } from '../types/MoneroWalletRpcDigestChallenge';
|
||||
import type { MoneroWalletRpcResponse } from '../types/MoneroWalletRpcResponse';
|
||||
@@ -222,14 +223,17 @@ export class MoneroWalletRpcClient {
|
||||
};
|
||||
}
|
||||
|
||||
async sweepAll(destinationAddress: string): Promise<{ txHashes: string[]; amountAtomic: string }> {
|
||||
async sweepAll(
|
||||
destinationAddress: string,
|
||||
priority: MoneroWithdrawPriority
|
||||
): Promise<{ txHashes: string[]; amountAtomic: string }> {
|
||||
const result = await this.call<MoneroWalletRpcSweepAllResult>(
|
||||
'sweep_all',
|
||||
{
|
||||
address: destinationAddress,
|
||||
account_index: this.accountIndex,
|
||||
subaddr_indices_all: true,
|
||||
priority: 1
|
||||
priority
|
||||
},
|
||||
{ timeoutMs: 120_000 }
|
||||
);
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Maps to monero-wallet-rpc `sweep_all` / `transfer` priority (0–4).
|
||||
*
|
||||
* Absolute fee multipliers (fee algorithm 4, current mainnet) are 1, 5, 25, 1000 for priorities 1–4.
|
||||
* Official GUI labels are scaled relative to Normal (priority 2): 0.2×, 1×, 5×, 200×.
|
||||
*
|
||||
* @see https://www.getmonero.org/resources/developer-guides/wallet-rpc.html#sweep_all
|
||||
*/
|
||||
export enum MoneroWithdrawPriority {
|
||||
Automatic = 0,
|
||||
Slow = 1,
|
||||
Normal = 2,
|
||||
Fast = 3,
|
||||
Fastest = 4
|
||||
}
|
||||
Reference in New Issue
Block a user