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
13 changes: 4 additions & 9 deletions modules/express/src/clientRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -812,10 +812,10 @@ export async function handleV2ConsolidateAccount(req: express.Request) {
* handle wallet fanout unspents
* @param req
*/
async function handleV2FanOutUnspents(req: express.Request) {
async function handleV2FanOutUnspents(req: ExpressApiRouteRequest<'express.v2.wallet.fanoutunspents', '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.fanoutUnspents(createSendParams(req));
}

Expand Down Expand Up @@ -1660,12 +1660,7 @@ export function setupAPIRoutes(app: express.Application, config: Config): void {
prepareBitGo(config),
promiseWrapper(handleV2ConsolidateUnspents)
);
app.post(
'/api/v2/:coin/wallet/:id/fanoutunspents',
parseBody,
prepareBitGo(config),
promiseWrapper(handleV2FanOutUnspents)
);
router.post('express.v2.wallet.fanoutunspents', [prepareBitGo(config), typedPromiseWrapper(handleV2FanOutUnspents)]);

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

Expand Down
10 changes: 7 additions & 3 deletions modules/express/src/typedRoutes/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { PostWalletSignTx } from './v2/walletSignTx';
import { PostWalletTxSignTSS } from './v2/walletTxSignTSS';
import { PostShareWallet } from './v2/shareWallet';
import { PutExpressWalletUpdate } from './v2/expressWalletUpdate';
import { PostFanoutUnspents } from './v2/fanoutUnspents';

// Too large types can cause the following error
//
Expand Down Expand Up @@ -135,10 +136,13 @@ export const ExpressV1WalletConsolidateUnspentsApiSpec = apiSpec({
},
});

export const ExpressV1WalletFanoutUnspentsApiSpec = apiSpec({
export const ExpressWalletFanoutUnspentsApiSpec = apiSpec({
'express.v1.wallet.fanoutunspents': {
put: PutFanoutUnspents,
},
'express.v2.wallet.fanoutunspents': {
post: PostFanoutUnspents,
},
});

export const ExpressV2WalletCreateAddressApiSpec = apiSpec({
Expand Down Expand Up @@ -217,7 +221,7 @@ export type ExpressApi = typeof ExpressPingApiSpec &
typeof ExpressV1KeychainLocalApiSpec &
typeof ExpressV1PendingApprovalConstructTxApiSpec &
typeof ExpressV1WalletConsolidateUnspentsApiSpec &
typeof ExpressV1WalletFanoutUnspentsApiSpec &
typeof ExpressWalletFanoutUnspentsApiSpec &
typeof ExpressV2WalletCreateAddressApiSpec &
typeof ExpressKeychainLocalApiSpec &
typeof ExpressKeychainChangePasswordApiSpec &
Expand All @@ -243,7 +247,7 @@ export const ExpressApi: ExpressApi = {
...ExpressV1KeychainLocalApiSpec,
...ExpressV1PendingApprovalConstructTxApiSpec,
...ExpressV1WalletConsolidateUnspentsApiSpec,
...ExpressV1WalletFanoutUnspentsApiSpec,
...ExpressWalletFanoutUnspentsApiSpec,
...ExpressV2WalletCreateAddressApiSpec,
...ExpressKeychainLocalApiSpec,
...ExpressKeychainChangePasswordApiSpec,
Expand Down
119 changes: 119 additions & 0 deletions modules/express/src/typedRoutes/api/v2/fanoutUnspents.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import * as t from 'io-ts';
import { httpRoute, httpRequest, optional } from '@api-ts/io-ts-http';
import { BitgoExpressError } from '../../schemas/error';

/**
* Request parameters for fanning out unspents in a wallet (v2)
*/
export const FanoutUnspentsRequestParams = {
/** The coin identifier (e.g., 'btc', 'tbtc') */
coin: t.string,
/** The ID of the wallet */
id: t.string,
} as const;

/**
* Request body for fanning out unspents in a wallet (v2)
*
* This endpoint supports the full set of parameters available in the BitGo SDK
* for advanced UTXO management. The fanout operation takes existing unspents and
* creates a larger number of equally-sized outputs for improved transaction parallelization.
*/
export const FanoutUnspentsRequestBody = {
/** The wallet passphrase to decrypt the user key */
walletPassphrase: optional(t.string),
/** The extended private key (alternative to walletPassphrase) */
xprv: optional(t.string),
/** The number of new unspents to create */
numUnspentsToMake: optional(t.number),
/** Minimum value of unspents to use (in base units) */
minValue: optional(t.union([t.number, t.string])),
/** Maximum value of unspents to use (in base units) */
maxValue: optional(t.union([t.number, t.string])),
/** Minimum block height of unspents to use */
minHeight: optional(t.number),
/** Minimum number of confirmations needed for an unspent to be included (defaults to 1) */
minConfirms: optional(t.number),
/** If true, minConfirms also applies to change outputs */
enforceMinConfirmsForChange: optional(t.boolean),
/** Maximum number of inputs to use in the transaction */
maxNumInputsToUse: optional(t.number),
/** Array of specific unspent IDs to use */
unspents: optional(t.array(t.string)),
/** The desired fee rate for the transaction in satoshis/kB */
feeRate: optional(t.number),
/** The maximum limit for a fee rate in satoshis/kB */
maxFeeRate: optional(t.number),
/** The maximum proportion of value you're willing to lose to fees (as a decimal, e.g., 0.1 for 10%) */
maxFeePercentage: optional(t.number),
/** Estimate fees to aim for first confirmation within this number of blocks */
feeTxConfirmTarget: optional(t.number),
/** Comment to attach to the transaction */
comment: optional(t.string),
/** One-time password for 2FA */
otp: optional(t.string),
/** Target address for the fanout outputs */
targetAddress: optional(t.string),
} as const;

/**
* Response for fanning out unspents in a wallet (v2)
*
* Returns transaction details after the fanout operation is built, signed, and sent.
*/
export const FanoutUnspentsResponse = t.type({
/** The status of the transaction ('accepted', 'signed', 'pendingApproval', or 'otp') */
status: t.string,
/** The transaction hex/serialized transaction */
tx: t.string,
/** The transaction hash/ID */
hash: optional(t.string),
/** Alternative field for transaction ID (some responses use this instead of hash) */
txid: optional(t.string),
/** The fee amount in base units (satoshis for BTC) */
fee: optional(t.number),
/** The fee rate in base units per kilobyte (satoshis/kB for BTC) */
feeRate: optional(t.number),
/** Whether the transaction is instant */
instant: optional(t.boolean),
/** The instant transaction ID (if applicable) */
instantId: optional(t.string),
/** Travel rule information */
travelInfos: optional(t.unknown),
/** BitGo fee information (if applicable) */
bitgoFee: optional(t.unknown),
/** Travel rule result (if applicable) */
travelResult: optional(t.unknown),
});

/**
* Fan out unspents in a wallet (v2)
*
* This endpoint fans out unspents in a wallet by creating a transaction that spends from
* one or more inputs to create multiple equal-sized outputs. This is useful for increasing
* the number of UTXOs in a wallet, which can improve transaction parallelization and allow
* for concurrent spending operations.
*
* The v2 API differs from v1 by:
* - Requiring a coin parameter in the path
* - Supporting the full set of SDK parameters for advanced UTXO management
* - Using numUnspentsToMake instead of target (though both refer to output count)
* - Supporting additional parameters like maxNumInputsToUse, unspents array, fee controls
*
* @operationId express.v2.wallet.fanoutunspents
* @tag express
*/
export const PostFanoutUnspents = httpRoute({
path: '/api/v2/{coin}/wallet/{id}/fanoutunspents',
method: 'POST',
request: httpRequest({
params: FanoutUnspentsRequestParams,
body: FanoutUnspentsRequestBody,
}),
response: {
/** Successfully fanned out unspents */
200: FanoutUnspentsResponse,
/** Invalid request or fan out operation fails */
400: BitgoExpressError,
},
});
Loading