Skip to content

Commit f5b8621

Browse files
feat: move wallet sweep API to typed routes
Ticket: WP-5432
1 parent 5fccfe7 commit f5b8621

File tree

4 files changed

+119
-2
lines changed

4 files changed

+119
-2
lines changed

modules/express/src/clientRoutes.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -833,7 +833,7 @@ async function handleV2FanOutUnspents(req: express.Request) {
833833
* handle wallet sweep
834834
* @param req
835835
*/
836-
async function handleV2Sweep(req: express.Request) {
836+
async function handleV2Sweep(req: ExpressApiRouteRequest<'express.v2.wallet.sweep', 'post'>) {
837837
const bitgo = req.bitgo;
838838
const coin = bitgo.coin(req.params.coin);
839839
const wallet = await coin.wallets().get({ id: req.params.id });
@@ -1687,7 +1687,7 @@ export function setupAPIRoutes(app: express.Application, config: Config): void {
16871687
promiseWrapper(handleV2FanOutUnspents)
16881688
);
16891689

1690-
app.post('/api/v2/:coin/wallet/:id/sweep', parseBody, prepareBitGo(config), promiseWrapper(handleV2Sweep));
1690+
router.post('express.v2.wallet.sweep', [prepareBitGo(config), typedPromiseWrapper(handleV2Sweep)]);
16911691

16921692
// CPFP
16931693
app.post(

modules/express/src/typedRoutes/api/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { PostSignTransaction } from './v1/signTransaction';
1616
import { PostKeychainLocal } from './v2/keychainLocal';
1717
import { PostLightningInitWallet } from './v2/lightningInitWallet';
1818
import { PostVerifyCoinAddress } from './v2/verifyAddress';
19+
import { PostSweep } from './v2/sweep';
1920

2021
export const ExpressApi = apiSpec({
2122
'express.ping': {
@@ -60,6 +61,9 @@ export const ExpressApi = apiSpec({
6061
'express.calculateminerfeeinfo': {
6162
post: PostCalculateMinerFeeInfo,
6263
},
64+
'express.v2.wallet.sweep': {
65+
post: PostSweep,
66+
},
6367
});
6468

6569
export type ExpressApi = typeof ExpressApi;
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import * as t from 'io-ts';
2+
import { httpRoute, httpRequest, optional } from '@api-ts/io-ts-http';
3+
import { BitgoExpressError } from '../../schemas/error';
4+
5+
/**
6+
* Path parameters for wallet sweep operation.
7+
* @property coin - Ticker or identifier of the coin (e.g. 'btc', 'eth').
8+
* @property id - Wallet ID
9+
*/
10+
export const SweepRequestParams = {
11+
/** Coin ticker / chain identifier */
12+
coin: t.string,
13+
/** Wallet ID */
14+
id: t.string,
15+
};
16+
17+
/**
18+
* Request body for wallet sweep operation.
19+
*
20+
* @property address - The address to sweep funds to
21+
* @property walletPassphrase - Wallet passphrase for signing
22+
* @property xprv - Extended private key (alternative to walletPassphrase)
23+
* @property otp - One-time password for 2FA
24+
* @property feeRate - Fee rate in satoshis per kilobyte
25+
* @property maxFeeRate - Maximum fee rate in satoshis per kilobyte
26+
* @property feeTxConfirmTarget - Number of blocks to target for confirmation
27+
* @property allowPartialSweep - Whether to allow partial sweep if full sweep fails
28+
*/
29+
export const SweepRequestBody = {
30+
/** Address to sweep funds to */
31+
address: optional(t.string),
32+
/** Wallet passphrase for signing the transaction */
33+
walletPassphrase: optional(t.string),
34+
/** Extended private key (alternative to walletPassphrase) */
35+
xprv: optional(t.string),
36+
/** One-time password for 2FA */
37+
otp: optional(t.string),
38+
/** Fee rate in satoshis per kilobyte */
39+
feeRate: optional(t.number),
40+
/** Maximum fee rate in satoshis per kilobyte */
41+
maxFeeRate: optional(t.number),
42+
/** Number of blocks to target for confirmation */
43+
feeTxConfirmTarget: optional(t.number),
44+
/** Whether to allow partial sweep if full sweep fails */
45+
allowPartialSweep: optional(t.boolean),
46+
};
47+
48+
/**
49+
* Sweep wallet
50+
*
51+
* Sweep all funds from a wallet to a specified address.
52+
* This operation consolidates all unspent outputs in the wallet and sends them to the target address.
53+
*
54+
* @operationId express.v2.wallet.sweep
55+
*/
56+
export const PostSweep = httpRoute({
57+
path: '/api/v2/{coin}/wallet/{id}/sweep',
58+
method: 'POST',
59+
request: httpRequest({
60+
params: SweepRequestParams,
61+
body: SweepRequestBody,
62+
}),
63+
response: {
64+
/** Successful sweep transaction */
65+
200: t.unknown, // The actual response varies by coin type
66+
/** Invalid request parameters */
67+
400: BitgoExpressError,
68+
/** Wallet not found */
69+
404: BitgoExpressError,
70+
},
71+
});
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { PostSweep, SweepRequestParams, SweepRequestBody } from '../../../src/typedRoutes/api/v2/sweep';
2+
import * as t from 'io-ts';
3+
import { isRight } from 'fp-ts/Either';
4+
5+
describe('Sweep Typed Route', () => {
6+
it('should have correct path', () => {
7+
expect(PostSweep.path).toBe('/api/v2/{coin}/wallet/{id}/sweep');
8+
});
9+
10+
it('should have POST method', () => {
11+
expect(PostSweep.method).toBe('POST');
12+
});
13+
14+
it('should validate params correctly', () => {
15+
const validParams = { coin: 'btc', id: 'wallet123' };
16+
const paramsCodec = t.type(SweepRequestParams);
17+
const result = paramsCodec.decode(validParams);
18+
expect(isRight(result)).toBe(true);
19+
});
20+
21+
it('should validate body correctly', () => {
22+
const validBody = {
23+
address: '1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa',
24+
walletPassphrase: 'test-passphrase',
25+
feeRate: 10000,
26+
maxFeeRate: 50000,
27+
allowPartialSweep: true,
28+
};
29+
30+
const bodyCodec = t.partial(SweepRequestBody);
31+
const result = bodyCodec.decode(validBody);
32+
expect(isRight(result)).toBe(true);
33+
});
34+
35+
it('should allow optional body fields', () => {
36+
const minimalBody = {};
37+
38+
const bodyCodec = t.partial(SweepRequestBody);
39+
const result = bodyCodec.decode(minimalBody);
40+
expect(isRight(result)).toBe(true);
41+
});
42+
});

0 commit comments

Comments
 (0)