Skip to content

Commit a65c5fd

Browse files
authored
Merge pull request #7593 from BitGo/WP-5432-wallet-sweep-type-codec
feat(express): migrated wallet sweep to type route
2 parents 5944b2e + 111ebe6 commit a65c5fd

File tree

4 files changed

+1276
-4
lines changed

4 files changed

+1276
-4
lines changed

modules/express/src/clientRoutes.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -830,10 +830,10 @@ async function handleV2FanOutUnspents(req: ExpressApiRouteRequest<'express.v2.wa
830830
* handle wallet sweep
831831
* @param req
832832
*/
833-
async function handleV2Sweep(req: express.Request) {
833+
async function handleV2Sweep(req: ExpressApiRouteRequest<'express.v2.wallet.sweep', 'post'>) {
834834
const bitgo = req.bitgo;
835-
const coin = bitgo.coin(req.params.coin);
836-
const wallet = await coin.wallets().get({ id: req.params.id });
835+
const coin = bitgo.coin(req.decoded.coin);
836+
const wallet = await coin.wallets().get({ id: req.decoded.id });
837837
return wallet.sweep(createSendParams(req));
838838
}
839839

@@ -1667,7 +1667,7 @@ export function setupAPIRoutes(app: express.Application, config: Config): void {
16671667
]);
16681668
router.post('express.v2.wallet.fanoutunspents', [prepareBitGo(config), typedPromiseWrapper(handleV2FanOutUnspents)]);
16691669

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

16721672
// CPFP
16731673
app.post(

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import { PostLightningWalletWithdraw } from './v2/lightningWithdraw';
4646
import { PutV2PendingApproval } from './v2/pendingApproval';
4747
import { PostConsolidateAccount } from './v2/consolidateAccount';
4848
import { PostCanonicalAddress } from './v2/canonicalAddress';
49+
import { PostWalletSweep } from './v2/walletSweep';
4950

5051
// Too large types can cause the following error
5152
//
@@ -290,6 +291,12 @@ export const ExpressV2CanonicalAddressApiSpec = apiSpec({
290291
},
291292
});
292293

294+
export const ExpressV2WalletSweepApiSpec = apiSpec({
295+
'express.v2.wallet.sweep': {
296+
post: PostWalletSweep,
297+
},
298+
});
299+
293300
export type ExpressApi = typeof ExpressPingApiSpec &
294301
typeof ExpressPingExpressApiSpec &
295302
typeof ExpressLoginApiSpec &
@@ -324,6 +331,7 @@ export type ExpressApi = typeof ExpressPingApiSpec &
324331
typeof ExpressExternalSigningApiSpec &
325332
typeof ExpressWalletSigningApiSpec &
326333
typeof ExpressV2CanonicalAddressApiSpec &
334+
typeof ExpressV2WalletSweepApiSpec &
327335
typeof ExpressWalletManagementApiSpec;
328336

329337
export const ExpressApi: ExpressApi = {
@@ -361,6 +369,7 @@ export const ExpressApi: ExpressApi = {
361369
...ExpressExternalSigningApiSpec,
362370
...ExpressWalletSigningApiSpec,
363371
...ExpressV2CanonicalAddressApiSpec,
372+
...ExpressV2WalletSweepApiSpec,
364373
...ExpressWalletManagementApiSpec,
365374
};
366375

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
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+
import { SendManyResponse } from './sendmany';
5+
6+
/**
7+
* Request path parameters for sweeping a wallet
8+
*/
9+
export const WalletSweepParams = {
10+
/** The coin type */
11+
coin: t.string,
12+
/** The wallet ID */
13+
id: t.string,
14+
} as const;
15+
16+
/**
17+
* Request body for sweeping all funds from a wallet
18+
*
19+
* The sweep operation sends all available funds from the wallet to a specified address.
20+
* For UTXO coins, it uses the native /sweepWallet endpoint.
21+
* For account-based coins, it calculates the maximum spendable amount and uses sendMany.
22+
*/
23+
export const WalletSweepBody = {
24+
/** The destination address to send all funds to - REQUIRED */
25+
address: t.string,
26+
27+
/** The wallet passphrase to decrypt the user key */
28+
walletPassphrase: optional(t.string),
29+
30+
/** The extended private key (alternative to walletPassphrase) */
31+
xprv: optional(t.string),
32+
33+
/** One-time password for 2FA */
34+
otp: optional(t.string),
35+
36+
/** The desired fee rate for the transaction in satoshis/kB (UTXO coins) */
37+
feeRate: optional(t.number),
38+
39+
/** Upper limit for fee rate in satoshis/kB (UTXO coins) */
40+
maxFeeRate: optional(t.number),
41+
42+
/** Estimate fees to aim for confirmation within this number of blocks (UTXO coins) */
43+
feeTxConfirmTarget: optional(t.number),
44+
45+
/** Allows sweeping 200 unspents when wallet has more than that (UTXO coins) */
46+
allowPartialSweep: optional(t.boolean),
47+
48+
/** Transaction format: 'legacy', 'psbt', or 'psbt-lite' (UTXO coins) */
49+
txFormat: optional(t.union([t.literal('legacy'), t.literal('psbt'), t.literal('psbt-lite')])),
50+
} as const;
51+
52+
/**
53+
* Sweep all funds from a wallet to a specified address
54+
*
55+
* This endpoint sweeps (sends) all available funds from a wallet to a single destination address.
56+
*
57+
* **Behavior by coin type:**
58+
* - **UTXO coins (BTC, LTC, etc.)**: Uses the native /sweepWallet endpoint that:
59+
* - Collects all unspents in the wallet
60+
* - Builds a transaction sending everything (minus fees) to the destination
61+
* - Signs and broadcasts the transaction
62+
* - Validates that all funds go to the specified destination address
63+
*
64+
* - **Account-based coins (ETH, etc.)**:
65+
* - Checks for unconfirmed funds (fails if any exist)
66+
* - Queries the maximumSpendable amount
67+
* - Creates a sendMany transaction with that amount to the destination
68+
*
69+
* **Implementation Note:**
70+
* Both execution paths (UTXO and account-based) ultimately call the same underlying
71+
* transaction sending mechanisms as sendMany, resulting in identical response structures.
72+
*
73+
* **Authentication:**
74+
* - Requires either `walletPassphrase` (to decrypt the encrypted user key) or `xprv` (raw private key)
75+
* - Optional `otp` for 2FA
76+
*
77+
* **Fee control (UTXO coins):**
78+
* - `feeRate`: Desired fee rate in satoshis/kB
79+
* - `maxFeeRate`: Upper limit for fee rate
80+
* - `feeTxConfirmTarget`: Target number of blocks for confirmation
81+
*
82+
* **Special options:**
83+
* - `allowPartialSweep`: For UTXO wallets with >200 unspents, allows sweeping just 200
84+
* - `txFormat`: Choose between 'legacy', 'psbt', or 'psbt-lite' format
85+
*
86+
* @tag express
87+
* @operationId express.v2.wallet.sweep
88+
*/
89+
export const PostWalletSweep = httpRoute({
90+
path: '/api/v2/{coin}/wallet/{id}/sweep',
91+
method: 'POST',
92+
request: httpRequest({
93+
params: WalletSweepParams,
94+
body: WalletSweepBody,
95+
}),
96+
response: {
97+
/** Successfully swept funds - same structure as sendMany */
98+
200: SendManyResponse,
99+
/** Invalid request or sweep operation fails */
100+
400: BitgoExpressError,
101+
},
102+
});

0 commit comments

Comments
 (0)