Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions modules/express/src/clientRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -830,10 +830,10 @@ async function handleV2FanOutUnspents(req: ExpressApiRouteRequest<'express.v2.wa
* handle wallet sweep
* @param req
*/
async function handleV2Sweep(req: express.Request) {
async function handleV2Sweep(req: ExpressApiRouteRequest<'express.v2.wallet.sweep', 'post'>) {
const bitgo = req.bitgo;
const coin = bitgo.coin(req.params.coin);
const wallet = await coin.wallets().get({ id: req.params.id });
const coin = bitgo.coin(req.decoded.coin);
const wallet = await coin.wallets().get({ id: req.decoded.id });
return wallet.sweep(createSendParams(req));
}

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

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

// CPFP
app.post(
Expand Down
9 changes: 9 additions & 0 deletions modules/express/src/typedRoutes/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import { PostLightningWalletWithdraw } from './v2/lightningWithdraw';
import { PutV2PendingApproval } from './v2/pendingApproval';
import { PostConsolidateAccount } from './v2/consolidateAccount';
import { PostCanonicalAddress } from './v2/canonicalAddress';
import { PostWalletSweep } from './v2/walletSweep';

// Too large types can cause the following error
//
Expand Down Expand Up @@ -290,6 +291,12 @@ export const ExpressV2CanonicalAddressApiSpec = apiSpec({
},
});

export const ExpressV2WalletSweepApiSpec = apiSpec({
'express.v2.wallet.sweep': {
post: PostWalletSweep,
},
});

export type ExpressApi = typeof ExpressPingApiSpec &
typeof ExpressPingExpressApiSpec &
typeof ExpressLoginApiSpec &
Expand Down Expand Up @@ -324,6 +331,7 @@ export type ExpressApi = typeof ExpressPingApiSpec &
typeof ExpressExternalSigningApiSpec &
typeof ExpressWalletSigningApiSpec &
typeof ExpressV2CanonicalAddressApiSpec &
typeof ExpressV2WalletSweepApiSpec &
typeof ExpressWalletManagementApiSpec;

export const ExpressApi: ExpressApi = {
Expand Down Expand Up @@ -361,6 +369,7 @@ export const ExpressApi: ExpressApi = {
...ExpressExternalSigningApiSpec,
...ExpressWalletSigningApiSpec,
...ExpressV2CanonicalAddressApiSpec,
...ExpressV2WalletSweepApiSpec,
...ExpressWalletManagementApiSpec,
};

Expand Down
102 changes: 102 additions & 0 deletions modules/express/src/typedRoutes/api/v2/walletSweep.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import * as t from 'io-ts';
import { httpRoute, httpRequest, optional } from '@api-ts/io-ts-http';
import { BitgoExpressError } from '../../schemas/error';
import { SendManyResponse } from './sendmany';

/**
* Request path parameters for sweeping a wallet
*/
export const WalletSweepParams = {
/** The coin type */
coin: t.string,
/** The wallet ID */
id: t.string,
} as const;

/**
* Request body for sweeping all funds from a wallet
*
* The sweep operation sends all available funds from the wallet to a specified address.
* For UTXO coins, it uses the native /sweepWallet endpoint.
* For account-based coins, it calculates the maximum spendable amount and uses sendMany.
*/
export const WalletSweepBody = {
/** The destination address to send all funds to - REQUIRED */
address: t.string,

/** The wallet passphrase to decrypt the user key */
walletPassphrase: optional(t.string),

/** The extended private key (alternative to walletPassphrase) */
xprv: optional(t.string),

/** One-time password for 2FA */
otp: optional(t.string),

/** The desired fee rate for the transaction in satoshis/kB (UTXO coins) */
feeRate: optional(t.number),

/** Upper limit for fee rate in satoshis/kB (UTXO coins) */
maxFeeRate: optional(t.number),

/** Estimate fees to aim for confirmation within this number of blocks (UTXO coins) */
feeTxConfirmTarget: optional(t.number),

/** Allows sweeping 200 unspents when wallet has more than that (UTXO coins) */
allowPartialSweep: optional(t.boolean),

/** Transaction format: 'legacy', 'psbt', or 'psbt-lite' (UTXO coins) */
txFormat: optional(t.union([t.literal('legacy'), t.literal('psbt'), t.literal('psbt-lite')])),
} as const;

/**
* Sweep all funds from a wallet to a specified address
*
* This endpoint sweeps (sends) all available funds from a wallet to a single destination address.
*
* **Behavior by coin type:**
* - **UTXO coins (BTC, LTC, etc.)**: Uses the native /sweepWallet endpoint that:
* - Collects all unspents in the wallet
* - Builds a transaction sending everything (minus fees) to the destination
* - Signs and broadcasts the transaction
* - Validates that all funds go to the specified destination address
*
* - **Account-based coins (ETH, etc.)**:
* - Checks for unconfirmed funds (fails if any exist)
* - Queries the maximumSpendable amount
* - Creates a sendMany transaction with that amount to the destination
*
* **Implementation Note:**
* Both execution paths (UTXO and account-based) ultimately call the same underlying
* transaction sending mechanisms as sendMany, resulting in identical response structures.
*
* **Authentication:**
* - Requires either `walletPassphrase` (to decrypt the encrypted user key) or `xprv` (raw private key)
* - Optional `otp` for 2FA
*
* **Fee control (UTXO coins):**
* - `feeRate`: Desired fee rate in satoshis/kB
* - `maxFeeRate`: Upper limit for fee rate
* - `feeTxConfirmTarget`: Target number of blocks for confirmation
*
* **Special options:**
* - `allowPartialSweep`: For UTXO wallets with >200 unspents, allows sweeping just 200
* - `txFormat`: Choose between 'legacy', 'psbt', or 'psbt-lite' format
*
* @tag express
* @operationId express.v2.wallet.sweep
*/
export const PostWalletSweep = httpRoute({
path: '/api/v2/{coin}/wallet/{id}/sweep',
method: 'POST',
request: httpRequest({
params: WalletSweepParams,
body: WalletSweepBody,
}),
response: {
/** Successfully swept funds - same structure as sendMany */
200: SendManyResponse,
/** Invalid request or sweep operation fails */
400: BitgoExpressError,
},
});
Loading