Skip to content
Merged
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
146 changes: 93 additions & 53 deletions modules/express/src/typedRoutes/api/v2/sendmany.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import { BitgoExpressError } from '../../schemas/error';
* Request parameters for sending to multiple recipients (v2)
*/
export const SendManyRequestParams = {
/** The coin identifier (e.g., 'btc', 'tbtc', 'eth', 'teth') */
/** A cryptocurrency or token ticker symbol. */
coin: t.string,
/** The ID of the wallet */
/** The wallet ID. */
id: t.string,
} as const;

Expand Down Expand Up @@ -112,82 +112,122 @@ export const TokenEnablement = t.intersection([
* for building, signing, and sending transactions to multiple recipients.
*/
export const SendManyRequestBody = {
/** Array of recipients with addresses and amounts */
/** A list of recipient addresses and amounts. Must be present but empty for CPFP transactions. */
recipients: optional(t.array(Recipient)),

/** The wallet passphrase to decrypt the user key */
/** Passphrase to decrypt the user key on the wallet. Required if External Signer is not used to sign the transactions. */
walletPassphrase: optional(t.string),

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

/** The private key (prv) in string form */
/** The un-encrypted user private key in string form. If the key is a JSON object it must be stringified. Required if `walletPassphrase` is not available or encrypted private key is not stored by BitGo. */
prv: optional(t.string),

/** Estimate fees to aim for first confirmation within this number of blocks */
/**
* (BTC only) The number of blocks required to confirm a transaction. You can use `numBlocks` to estimate the fee
* rate by targeting confirmation within a given number of blocks. If both `feeRate` and `numBlocks` are absent,
* the transaction defaults to 2 blocks for confirmation.
*
* Note: The `maxFeeRate` limits the fee rate generated by `numBlocks`.
*/
numBlocks: optional(t.number),

/** The desired fee rate for the transaction in base units per kilobyte (e.g., satoshis/kB) */
/**
* Custom fee rate (in base units) per kilobyte (or virtual kilobyte). For example, satoshis per kvByte.
*
* If the `feeRate` is less than the minimum required network fee, then the minimum fee applies. For example,
* 1000 sat/kvByte, a flat 1000 microAlgos, or a flat 10 drops of xrp. For XRP, the actual fee is usually
* 4.5 times the open ledger fee.
*
* Note: The `feeRate` overrides the `maxFeeRate` and `minFeeRate`.
*/
feeRate: optional(t.number),

/** Fee multiplier (multiplies the estimated fee by this factor) */
/**
* (UTXO only) Custom multiplier to the `feeRate`. The resulting fee rate is limited by the `maxFeeRate`.
* For replace-by-fee (RBF) transactions (that include `rbfTxIds`), the `feeMultiplier` must be greater than 1,
* since it's an absolute fee multiplier to the transaction being replaced.
*
* Note: The `maxFeeRate` limits the fee rate generated by `feeMultiplier`.
*/
feeMultiplier: optional(t.number),

/** The maximum limit for a fee rate in base units per kilobyte */
/**
* (BTC only) The maximum fee rate (in base units) per kilobyte (or virtual kilobyte). For example, satoshis per kvByte.
* The `maxFeeRate` limits the fee rate generated by both `feeMultiplier` and `numBlocks`.
*
* Note: The `feeRate` overrides the `maxFeeRate`.
*/
maxFeeRate: optional(t.number),

/** Minimum number of confirmations needed for an unspent to be included (defaults to 1) */
/** The unspent selection for the transaction will only consider unspents with at least this many confirmations to be used as inputs. Does not apply to change outputs unless used in combination with `enforceMinConfirmsForChange`. */
minConfirms: optional(t.number),

/** If true, minConfirms also applies to change outputs */
/** Defaults to false. When set to true, will enforce minConfirms for change outputs. */
enforceMinConfirmsForChange: optional(t.boolean),

/** Target number of unspents to maintain in the wallet */
/**
* Defaults to 1000. Specifies the minimum count of good-sized unspents to maintain in the wallet.
* Change splitting ceases when the wallet has `targetWalletUnspents` good-sized unspents.
*
* Note: Wallets that continuously send a high count of transactions will automatically split large change amounts
* into multiple good-sized change outputs while they have fewer than `targetWalletUnspents` good-sized unspents
* in their unspent pool. Breaking up large unspents helps to reduce the amount of unconfirmed funds in flight in
* future transactions, and helps to avoid long chains of unconfirmed transactions. This is especially useful for
* newly funded wallets or recently refilled send-only wallets.
*/
targetWalletUnspents: optional(t.number),

/** Message to attach to the transaction */
message: optional(t.string),

/** Minimum value of unspents to use (in base units) */
/** Ignore unspents smaller than this amount of base units (e.g. satoshis). For doge, only string is allowed. */
minValue: optional(t.union([t.number, t.string])),

/** Maximum value of unspents to use (in base units) */
/** Ignore unspents larger than this amount of base units (e.g. satoshis). For doge, only string is allowed. */
maxValue: optional(t.union([t.number, t.string])),

/** Custom sequence ID for the transaction */
/**
* A `sequenceId` is a unique and arbitrary wallet identifier applied to transfers and transactions at creation.
* It is optional but highly recommended. With a `sequenceId` you can easily reference transfers and transactions—
* for example, to safely retry sending. Because the system only confirms one send request per `sequenceId`
* (and fails all subsequent attempts), you can retry sending without the risk of double spending.
* The `sequenceId` is only visible to users on the wallet and is not shared publicly.
*/
sequenceId: optional(t.string),

/** Absolute max ledger the transaction should be accepted in (for XRP) */
/** (XRP only) Absolute max ledger the transaction should be accepted in, whereafter it will be rejected. */
lastLedgerSequence: optional(t.number),

/** Relative ledger height (in relation to the current ledger) that the transaction should be accepted in */
/** (XRP only) Relative ledger height (in relation to the current ledger) that the transaction should be accepted in, whereafter it will be rejected. */
ledgerSequenceDelta: optional(t.number),

/** Custom gas price to be used for sending the transaction (for account-based coins) */
/** Custom gas price to be used for sending the transaction. Only for ETH and ERC20 tokens. */
gasPrice: optional(t.number),

/** Set to true to disable automatic change splitting for purposes of unspent management */
/** Defaults to false. Set `true` to disable automatic change splitting. Also see: `targetWalletUnspents` */
noSplitChange: optional(t.boolean),

/** Array of specific unspent IDs to use in the transaction */
/** Used to explicitly specify the unspents to be used in the input set in the transaction. Each unspent should be in the form `prevTxId:nOutput`. */
unspents: optional(t.array(t.string)),

/** Comment to attach to the transaction */
/** Optional metadata (only persisted in BitGo) to be applied to the transaction. Use this to add transaction-specific information such as the transaction's purpose or another identifier that you want to reference later. The value is shown in the UI in the transfer listing page. (length ≤ 256) */
comment: optional(t.string),

/** One-time password for 2FA */
/** Two factor auth code to enable sending the transaction. Not necessary if using a long lived access token within the spending limit. */
otp: optional(t.string),

/** Specifies the destination of the change output */
/** Specifies a custom destination address for the transaction's change output(s) (length ≤ 500) */
changeAddress: optional(t.string),

/** If true, allows using an external change address */
allowExternalChangeAddress: optional(t.boolean),

/** Send this transaction using coin-specific instant sending method (if available) */
/** (DASH only) Specifies whether or not to use Dash's "InstantSend" feature when sending a transaction. */
instant: optional(t.boolean),

/** Memo to use in transaction (supported by Stellar, XRP, etc.) */
/** Extra transaction information for CSPR, EOS, HBAR, RUNE, STX, TON, XLM, and XRP. Required for XLM transactions. Note: For XRP this is the destination tag (DT). For CSPR this is the transfer ID. */
memo: optional(MemoParams),

/** Transfer ID for tracking purposes */
Expand All @@ -196,28 +236,40 @@ export const SendManyRequestBody = {
/** EIP-1559 fee parameters for Ethereum transactions */
eip1559: optional(EIP1559Params),

/** Gas limit for the transaction (for account-based coins) */
/** Custom gas limit to be used for sending the transaction. Only for ETH and ERC20 tokens. */
gasLimit: optional(t.number),

/** Token name for token transfers */
/** Token name, defined in the BitGoJS Statics package. */
tokenName: optional(t.string),

/** Type of transaction (e.g., 'trustline' for Stellar) */
/**
* Required for transactions from MPC wallets. "acceleration" speeds up transactions with a certain nonce by
* adjusting the gas setting. "accountSet" is for XRP AccountSet transactions. "consolidate" combines multiple
* UTXO inputs into fewer outputs. "enabletoken" is for SOL. "fanout" splits UTXO inputs into many smaller outputs
* (UTXO coins only). "stakingLock" and "stakingUnlock" are for Stacks delegations. "transfer" is for native-asset
* transfers. "trustline" is for Stellar trustline transactions.
* Possible types include: [acceleration, accountSet, consolidate, enabletoken, fanout, stakingLock, stakingUnlock,
* transfer, transfertoken, trustline].
*
* For AVAX, possible types include: `addValidator`, `export`, and `import`.
* For XRP, possible types include: `payment` and `accountSet`. The default is `payment`.
* For STX, type is required.
*/
type: optional(t.string),

/** Custodian transaction ID (for institutional custody integrations) */
custodianTransactionId: optional(t.string),

/** If true, enables hop transactions for exchanges */
/** (ETH, AVAXC and POLYGON) Set to true if funds to destination need to come from single sig address. */
hop: optional(t.boolean),

/** Address type for the transaction (e.g., 'p2sh', 'p2wsh') */
/** @deprecated Use `changeAddressType` instead. The type of address to create for change. One of `p2sh`, `p2shP2wsh`, `p2wsh`, or `p2tr`. */
addressType: optional(t.string),

/** Change address type (e.g., 'p2sh', 'p2wsh') */
/** The address type for the change address. One of `p2sh`, `p2shP2wsh`, `p2wsh`, `p2tr` or `p2trMusig2`. */
changeAddressType: optional(t.string),

/** Transaction format (legacy or psbt) */
/** [UTXO only] Format of the returned transaction hex serialization. `legacy` for serialized transaction in custom bitcoinjs-lib format. `psbt` for BIP174 serialized transaction. Defaults to `legacy`. */
txFormat: optional(t.union([t.literal('legacy'), t.literal('psbt'), t.literal('psbt-lite')])),

/** If set to false, sweep all funds including required minimums (e.g., DOT requires 1 DOT minimum) */
Expand All @@ -229,7 +281,7 @@ export const SendManyRequestBody = {
/** NFT ID (for NFT transfers) */
nftId: optional(t.string),

/** Transaction nonce (for account-based coins) */
/** (DOT only) A nonce ID is a number used to protect private communications by preventing replay attacks. This is an advanced option where users can manually input a new nonce value in order to correct or fill in a missing nonce ID value. */
nonce: optional(t.string),

/** If true, only preview the transaction without sending */
Expand All @@ -238,7 +290,7 @@ export const SendManyRequestBody = {
/** Receive address (for specific coins like ADA) */
receiveAddress: optional(t.string),

/** Messages to be signed with specific addresses */
/** [UTXO only] An array of messages that you sign with the wallet keys using the BIP322 format. If passed, the `recipients` array must be empty. */
messages: optional(
t.array(
t.type({
Expand All @@ -260,13 +312,13 @@ export const SendManyRequestBody = {
/** Non-participation flag (for governance/staking protocols like Algorand) */
nonParticipation: optional(t.boolean),

/** Valid from block height */
/** Optional block this transaction is valid from. */
validFromBlock: optional(t.number),

/** Valid to block height */
/** Optional block this transaction is valid until. */
validToBlock: optional(t.number),

/** Reservation parameters for unspent management */
/** Optional parameter for UTXO coins to automatically reserve the unspents that are used in the build. Useful for Cold wallets. If using, must set `expireTime`. */
reservation: optional(
t.partial({
expireTime: t.string,
Expand Down Expand Up @@ -729,26 +781,14 @@ export const SendManyResponse = t.intersection([
]);

/**
* Send to multiple recipients (v2)
* Send coins or tokens to one or more recipients. You can use this endpoint to schedule outgoing transactions in bulk, lowering your aggregate amount of blockchain fees.
*
* This endpoint sends funds to multiple recipients by:
* 1. Building a transaction with the specified recipients and parameters
* 2. Signing the transaction with the user's key (decrypted with walletPassphrase or xprv)
* 3. Requesting a signature from BitGo's key
* 4. Sending the fully-signed transaction to the blockchain network
* Works with both multisignature and MPC wallets. Also supports external-signer mode.
*
* The v2 API supports:
* - Multiple recipients in a single transaction
* - Full control over transaction fees (feeRate, maxFeeRate, numBlocks)
* - UTXO selection (minValue, maxValue, unspents array)
* - Instant transactions (if supported by the coin)
* - TSS wallets with txRequest flow
* - Account-based and UTXO-based coins
* - Token transfers
* - Advanced features like memo fields, hop transactions, EIP-1559 fees
* Works with most BitGo-supported assets, but currently unavailable for: ALGO, ARBETH, AVAXC, CELO, CELO:CUSD, CSPR, DOT, EOS, HTETH:BGERCH, NEAR, OPETH, STX, TON, TRX, TRX:USDC, XLM, XRP, XTZ
*
* @operationId express.v2.wallet.sendmany
* @tag express
* @tag Express
*/
export const PostSendMany = httpRoute({
path: '/api/v2/{coin}/wallet/{id}/sendmany',
Expand Down
Loading