diff --git a/masterBitgoExpress.json b/masterBitgoExpress.json index 0e07fc97..eada3628 100644 --- a/masterBitgoExpress.json +++ b/masterBitgoExpress.json @@ -125,6 +125,46 @@ } } }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "422": { + "description": "Unprocessable Entity", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, "500": { "description": "Internal Server Error", "content": { @@ -134,12 +174,24 @@ } } } + }, + "501": { + "description": "Not Implemented", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } } } } }, "/api/{coin}/wallet/{walletId}/consolidate": { "post": { + "summary": "Build, sign, and send a consolidation transaction for an account-based asset all in 1 call.", + "description": "For account-based assets, consolidating the balances in the receive addresses to the base address maximizes the spendable balance of a wallet.", "parameters": [ { "name": "walletId", @@ -164,31 +216,36 @@ "schema": { "type": "object", "properties": { - "pubkey": { - "type": "string" - }, "source": { "type": "string", "enum": [ "user", "backup" - ] + ], + "description": "The key to use for signing the transaction" + }, + "pubkey": { + "type": "string", + "description": "Public key of the key used for signing multisig transactions" }, "consolidateAddresses": { "type": "array", "items": { - "type": "string" + "type": "string", + "description": "Optional: restrict the consolidation to the specified receive addresses. If not provided, will consolidate the funds from all receive addresses up to 500 addresses." } }, + "commonKeychain": { + "type": "string", + "description": "For TSS wallets, this is the common keychain of the wallet" + }, "apiVersion": { "type": "string", "enum": [ "full", "lite" - ] - }, - "commonKeychain": { - "type": "string" + ], + "description": "The Trasaction Request API version to use for MPC EdDSA Hot Wallets. Defaults based on the wallet type and asset curve." } }, "required": [ @@ -217,6 +274,36 @@ } } }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "422": { + "description": "Unprocessable Entity", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, "500": { "description": "Internal Server Error", "content": { @@ -226,12 +313,24 @@ } } } + }, + "501": { + "description": "Not Implemented", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } } } } }, "/api/{coin}/wallet/{walletId}/consolidateunspents": { "post": { + "summary": "Build and send a transaction to consolidate unspents in a wallet.", + "description": "Consolidating unspents is only for UTXO-based assets.", "parameters": [ { "name": "walletId", @@ -257,29 +356,36 @@ "type": "object", "properties": { "pubkey": { - "type": "string" + "type": "string", + "description": "Public key of the key used for signing multisig transactions" }, "source": { "type": "string", "enum": [ "user", "backup" - ] + ], + "description": "The key to use for signing the transaction" }, "feeRate": { - "type": "number" + "type": "number", + "description": "Custom fee rate (in base units) per kilobyte" }, "maxFeeRate": { - "type": "number" + "type": "number", + "description": "Maximum fee rate (in base units) per kilobyte" }, "maxFeePercentage": { - "type": "number" + "type": "number", + "description": "Maximum fee percentage" }, "feeTxConfirmTarget": { - "type": "number" + "type": "number", + "description": "Fee transaction confirmation target" }, "bulk": { - "type": "boolean" + "type": "boolean", + "description": "Enable bulk processing" }, "minValue": { "oneOf": [ @@ -289,7 +395,8 @@ { "type": "number" } - ] + ], + "description": "Minimum value for unspents" }, "maxValue": { "oneOf": [ @@ -299,25 +406,32 @@ { "type": "number" } - ] + ], + "description": "Maximum value for unspents" }, "minHeight": { - "type": "number" + "type": "number", + "description": "Minimum block height" }, "minConfirms": { - "type": "number" + "type": "number", + "description": "Minimum confirmations required" }, "enforceMinConfirmsForChange": { - "type": "boolean" + "type": "boolean", + "description": "Enforce minimum confirmations for change outputs" }, "limit": { - "type": "number" + "type": "number", + "description": "Limit the number of unspents to process" }, "numUnspentsToMake": { - "type": "number" + "type": "number", + "description": "Number of unspents to make" }, "targetAddress": { - "type": "string" + "type": "string", + "description": "Target address for consolidation" }, "txFormat": { "type": "string", @@ -325,7 +439,8 @@ "legacy", "psbt", "psbt-lite" - ] + ], + "description": "Transaction format" } }, "required": [ @@ -369,6 +484,36 @@ } } }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "422": { + "description": "Unprocessable Entity", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, "500": { "description": "Internal Server Error", "content": { @@ -378,6 +523,16 @@ } } } + }, + "501": { + "description": "Not Implemented", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } } } } @@ -408,8 +563,13 @@ "schema": { "type": "object", "properties": { - "pubkey": { - "type": "string" + "source": { + "type": "string", + "enum": [ + "user", + "backup" + ], + "description": "The key to use for signing the transaction" }, "type": { "type": "string", @@ -419,49 +579,87 @@ "acceleration", "accountSet", "enabletoken", - "stakingLock", - "stakingUnlock", "transfertoken", "trustline" - ] + ], + "description": "Required for transactions from MPC wallets." }, - "commonKeychain": { - "type": "string" + "recipients": { + "type": "array", + "items": { + "type": "object", + "properties": { + "address": { + "type": "string", + "description": "Destination address", + "example": "2MvrwRYBAuRtPTiZ5MyKg42Ke55W3fZJfZS", + "maxLength": 250 + }, + "amount": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + } + ], + "description": "The amount in base units (e.g. satoshis) to send. For doge, only string is allowed.", + "example": "\"2000000\"", + "pattern": "^-?\\d+$" + } + }, + "required": [ + "address", + "amount" + ], + "description": "List of recipient addresses and amounts to send" + } }, - "source": { + "pubkey": { "type": "string", - "enum": [ - "user", - "backup" - ] + "description": "Public key of the key used for signing multisig transactions i.e if source is user, this is the user's public key if source is backup, this is the backup key's public key" }, - "recipients": { - "type": "array", - "items": {} + "commonKeychain": { + "type": "string", + "description": "For TSS wallets, this is the common keychain of the wallet, it remains the same whether source is user or backup" }, "numBlocks": { - "type": "number" + "type": "number", + "description": "(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`.", + "minimum": 2, + "maximum": 1000 }, "feeRate": { - "type": "number" + "type": "number", + "description": "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`." }, "feeMultiplier": { - "type": "number" + "type": "number", + "description": "(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`." }, "maxFeeRate": { - "type": "number" + "type": "number", + "description": "(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`." }, "minConfirms": { - "type": "number" + "type": "number", + "description": "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`." }, "enforceMinConfirmsForChange": { - "type": "boolean" + "type": "boolean", + "default": false, + "description": "When set to true, will enforce minConfirms for change outputs. Defaults to false." }, "targetWalletUnspents": { - "type": "number" + "type": "number", + "default": 1000, + "description": "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." }, "message": { - "type": "string" + "type": "string", + "description": "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.", + "maxLength": 256 }, "minValue": { "oneOf": [ @@ -471,7 +669,8 @@ { "type": "string" } - ] + ], + "description": "Ignore unspents smaller than this amount of base units (e.g. satoshis). For doge, only string is allowed." }, "maxValue": { "oneOf": [ @@ -481,56 +680,75 @@ { "type": "string" } - ] + ], + "description": "Ignore unspents larger than this amount of base units (e.g. satoshis). For doge, only string is allowed." }, "sequenceId": { - "type": "string" + "type": "string", + "description": "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." }, "lastLedgerSequence": { - "type": "number" + "type": "number", + "description": "(XRP only) Absolute max ledger the transaction should be accepted in, whereafter it will be rejected" }, "ledgerSequenceDelta": { - "type": "number" + "type": "number", + "description": "(XRP only) Relative ledger height (in relation to the current ledger) that the transaction should be accepted in, whereafter it will be rejected" }, "noSplitChange": { - "type": "boolean" + "type": "boolean", + "default": false, + "description": "Set `true` to disable automatic change splitting. Also see: `targetWalletUnspents`" }, "unspents": { "type": "array", "items": { - "type": "string" + "type": "string", + "description": "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`." } }, "comment": { - "type": "string" + "type": "string", + "description": "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.", + "maxLength": 256 }, "otp": { - "type": "string" + "type": "string", + "description": "Two factor auth code to enable sending the transaction. Not necessary if using a long lived access token within the spending limit." }, "changeAddress": { - "type": "string" + "type": "string", + "description": "Specifies a custom destination address for the transaction's change output(s)", + "maxLength": 250 }, "allowExternalChangeAddress": { - "type": "boolean" + "type": "boolean", + "description": "Flag for allowing external change addresses" }, "instant": { - "type": "boolean" + "type": "boolean", + "description": "(DASH only) Specifies whether or not to use Dash's \"InstantSend\" feature when sending a transaction." }, "memo": { - "type": "string" + "type": "string", + "description": "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." }, "transferId": { - "type": "number" + "type": "number", + "description": "Transfer ID for the transaction" }, "eip1559": {}, "gasLimit": { - "type": "number" + "type": "number", + "description": "Custom gas limit to be used for sending the transaction. Only for ETH and ERC20 tokens." }, "custodianTransactionId": { - "type": "string" + "type": "string", + "description": "Custodian transaction ID" }, "nonce": { - "type": "string" + "type": "string", + "description": "(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." } }, "required": [ @@ -569,6 +787,26 @@ } } }, + "409": { + "description": "Conflict", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "422": { + "description": "Unprocessable Entity", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, "500": { "description": "Internal Server Error", "content": { @@ -578,12 +816,24 @@ } } } + }, + "501": { + "description": "Not Implemented", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } } } } }, "/api/{coin}/wallet/{walletId}/txrequest/{txRequestId}/signAndSend": { "post": { + "summary": "Sign a TxRequest and Broadcast it (MPC wallets only)", + "description": "This is usually needed after resolving a pending approval for a MPC wallet", "parameters": [ { "name": "walletId", @@ -621,14 +871,17 @@ "enum": [ "user", "backup" - ] + ], + "description": "The key to use for signing the transaction" }, "commonKeychain": { - "type": "string" + "type": "string", + "description": "Common keychain of the wallet during wallet creation" } }, "required": [ - "source" + "source", + "commonKeychain" ] } } @@ -643,6 +896,46 @@ } } }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "422": { + "description": "Unprocessable Entity", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, "500": { "description": "Internal Server Error", "content": { @@ -652,12 +945,24 @@ } } } + }, + "501": { + "description": "Not Implemented", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } } } } }, "/api/{coin}/wallet/generate": { "post": { + "summary": "Generates a new onPrem self-managed cold wallet.", + "description": "The wallet creation process involves several steps that happen automatically:\n1. User Keychain Creation: Creates the user keychain in the advanced wallet manager and encrypts it with the respective KMS.\n2. Backup Keychain Creation: Creates the backup keychain in the advanced wallet manager and encrypts it with the respective KMS.\n3. Keychain Upload: Uploads the user/backup public keys to BitGo.\n4. BitGo Key Creation: Creates the BitGo key on the BitGo service.\n5. Wallet Creation: Creates the wallet on BitGo with the 3 keys.", "parameters": [ { "name": "coin", @@ -675,23 +980,41 @@ "type": "object", "properties": { "label": { - "type": "string" + "type": "string", + "description": "A human-readable label for the wallet This will be displayed in the BitGo dashboard and API responses", + "example": "My Wallet" }, "multisigType": { "type": "string", "enum": [ "onchain", "tss" - ] + ], + "description": "The type of multisig wallet to create - onchain: Traditional multisig wallets using on-chain scripts - tss: Threshold Signature Scheme wallets using MPC protocols If absent, BitGo uses the default wallet type for the asset", + "example": "tss" }, "enterprise": { - "type": "string" + "type": "string", + "description": "Enterprise ID - Required for Ethereum wallets Ethereum wallets can only be created under an enterprise Each enterprise has a fee address which will be used to pay for transaction fees Your enterprise ID can be seen by clicking on the \"Manage Organization\" link on the enterprise dropdown", + "example": "59cd72485007a239fb00282ed480da1f", + "pattern": "^[0-9a-f]{32}$" }, "disableTransactionNotifications": { - "type": "boolean" + "type": "boolean", + "description": "Flag for disabling wallet transaction notifications When true, BitGo will not send email/SMS notifications for wallet transactions", + "example": false }, "isDistributedCustody": { - "type": "boolean" + "type": "boolean", + "description": "True, if the wallet type is a distributed-custodial If passed, you must also pass the 'enterprise' parameter Distributed custody allows multiple parties to share control of the wallet", + "example": false + }, + "walletVersion": { + "type": "number", + "description": "Specify the wallet creation contract version used when creating an Ethereum wallet contract - 0: Old wallet creation (legacy) - 1: New wallet creation, only deployed upon receiving funds - 2: Same functionality as v1 but with NFT support - 3: MPC wallets", + "example": 1, + "minimum": 0, + "maximum": 3 } }, "required": [ @@ -701,19 +1024,75 @@ ] } } - } - }, - "responses": { - "200": { - "description": "OK", + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "wallet": { + "$ref": "#/components/schemas/WalletType" + }, + "userKeychain": { + "$ref": "#/components/schemas/UserKeychainType" + }, + "backupKeychain": { + "$ref": "#/components/schemas/UserKeychainType" + }, + "bitgoKeychain": { + "$ref": "#/components/schemas/BitgoKeychainType" + }, + "responseType": { + "type": "string" + } + }, + "required": [ + "wallet", + "userKeychain", + "backupKeychain", + "bitgoKeychain", + "responseType" + ] + } + } + } + }, + "400": { + "description": "Bad Request", "content": { "application/json": { - "schema": {} + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } } } }, - "400": { - "description": "Bad Request", + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "422": { + "description": "Unprocessable Entity", "content": { "application/json": { "schema": { @@ -731,6 +1110,16 @@ } } } + }, + "501": { + "description": "Not Implemented", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } } } } @@ -754,7 +1143,7 @@ "application/json": { "schema": { "type": "object", - "description": "Request type for the wallet recovery endpoint. Used to recover funds from both standard multisig wallets and TSS wallets.", + "description": "Request type for the wallet recovery endpoint. Used to recover funds from both standard multisig and TSS wallets.", "properties": { "isTssRecovery": { "type": "boolean", @@ -1027,6 +1416,36 @@ } } }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, "422": { "description": "Unprocessable Entity", "content": { @@ -1046,6 +1465,16 @@ } } } + }, + "501": { + "description": "Not Implemented", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } } } } @@ -1164,6 +1593,46 @@ } } }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "422": { + "description": "Unprocessable Entity", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, "500": { "description": "Internal Server Error", "content": { @@ -1173,6 +1642,16 @@ } } } + }, + "501": { + "description": "Not Implemented", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } } } } @@ -1347,15 +1826,427 @@ "details": { "type": "string", "description": "Error details" - }, - "stack": { - "type": "string" } }, "required": [ "error", "details" ] + }, + "WalletType": { + "title": "WalletType", + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Wallet ID", + "example": "59cd72485007a239fb00282ed480da1f", + "pattern": "^[0-9a-f]{32}$" + }, + "users": { + "type": "array", + "items": { + "type": "object", + "properties": { + "user": { + "type": "string" + }, + "permissions": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "user", + "permissions" + ], + "description": "Ids of users with access to the wallet" + } + }, + "coin": { + "type": "string", + "description": "Name of the blockchain the wallet is on", + "example": "tbtc4" + }, + "label": { + "type": "string", + "description": "Name the user assigned to the wallet", + "example": "My TBTC4 Wallet" + }, + "m": { + "type": "number", + "description": "Number of signatures required for the wallet to send", + "example": 2 + }, + "n": { + "type": "number", + "description": "Number of signers on the wallet", + "example": 3 + }, + "keys": { + "type": "array", + "example": [ + "59cd72485007a239fb00282ed480da1f" + ], + "items": { + "type": "string", + "description": "Ids of wallet keys" + } + }, + "keySignatures": { + "type": "object", + "additionalProperties": {}, + "description": "Signatures for the backup and BitGo public keys signed by the user key" + }, + "enterprise": { + "type": "string" + }, + "organization": { + "type": "string" + }, + "bitgoOrg": { + "type": "string" + }, + "tags": { + "type": "array", + "example": [ + "59cd72485007a239fb00282ed480da1f" + ], + "items": { + "type": "string", + "description": "Tags set on the wallet" + } + }, + "disableTransactionNotifications": { + "type": "boolean", + "description": "Flag for disabling wallet transaction notifications", + "example": false + }, + "freeze": { + "type": "object", + "additionalProperties": {}, + "description": "Freeze state (used to stop the wallet from spending)", + "example": {} + }, + "deleted": { + "type": "boolean", + "description": "Flag which indicates the wallet has been deleted", + "example": false + }, + "approvalsRequired": { + "type": "number", + "description": "Number of admin approvals required for an action to fire", + "example": 1 + }, + "isCold": { + "type": "boolean", + "description": "Flag for identifying cold wallets", + "example": false + }, + "coinSpecific": { + "type": "object", + "additionalProperties": {}, + "description": "Coin-specific data" + }, + "admin": { + "type": "object", + "additionalProperties": {}, + "description": "Admin data (wallet policies)", + "example": {} + }, + "pendingApprovals": { + "type": "array", + "example": [], + "items": { + "type": "object", + "additionalProperties": {}, + "description": "Pending approvals on the wallet" + } + }, + "allowBackupKeySigning": { + "type": "boolean", + "description": "Flag for allowing signing with backup key", + "example": false + }, + "clientFlags": { + "type": "array", + "items": { + "type": "string" + } + }, + "recoverable": { + "type": "boolean", + "description": "Flag indicating whether this wallet's user key is recoverable with the passphrase held by the user." + }, + "startDate": { + "type": "string", + "description": "Time when this wallet was created" + }, + "hasLargeNumberOfAddresses": { + "type": "boolean", + "description": "Flag indicating that this wallet is large (more than 100,000 addresses). If this is set, some APIs may omit properties which are expensive to calculate for wallets with many addresses (for example, the total address counts returned by the List Addresses API)." + }, + "config": { + "type": "object", + "additionalProperties": {}, + "description": "Custom configuration options for this wallet" + }, + "balanceString": { + "type": "string", + "description": "Wallet balance as string", + "example": "0" + }, + "confirmedBalanceString": { + "type": "string", + "description": "Confirmed wallet balance as string", + "example": "0" + }, + "spendableBalanceString": { + "type": "string", + "description": "Spendable wallet balance as string", + "example": "0" + }, + "receiveAddress": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "address": { + "type": "string" + }, + "chain": { + "type": "number" + }, + "index": { + "type": "number" + }, + "coin": { + "type": "string" + }, + "wallet": { + "type": "string" + }, + "coinSpecific": { + "type": "object", + "additionalProperties": {} + } + }, + "required": [ + "id", + "address", + "chain", + "index", + "coin", + "wallet", + "coinSpecific" + ] + }, + "balance": { + "type": "number", + "description": "Wallet balance as number", + "example": 0 + }, + "rbfBalance": { + "type": "number" + }, + "rbfBalanceString": { + "type": "string" + }, + "reservedBalanceString": { + "type": "string" + }, + "lockedBalanceString": { + "type": "string" + }, + "stakedBalanceString": { + "type": "string" + }, + "unspentCount": { + "type": "number" + }, + "pendingChainInitialization": { + "type": "boolean" + }, + "pendingEcdsaTssInitialization": { + "type": "boolean" + }, + "multisigType": { + "type": "string" + }, + "multisigTypeVersion": { + "type": "string" + }, + "type": { + "type": "string" + }, + "subType": { + "type": "string" + }, + "creator": { + "type": "string" + }, + "walletFullyCreated": { + "type": "boolean" + } + }, + "required": [ + "id", + "users", + "coin", + "label", + "m", + "n", + "keys", + "keySignatures", + "enterprise", + "organization", + "bitgoOrg", + "tags", + "disableTransactionNotifications", + "freeze", + "deleted", + "approvalsRequired", + "isCold", + "coinSpecific", + "admin", + "pendingApprovals", + "allowBackupKeySigning", + "clientFlags", + "recoverable", + "startDate", + "hasLargeNumberOfAddresses", + "config", + "balanceString", + "confirmedBalanceString", + "spendableBalanceString", + "receiveAddress" + ] + }, + "UserKeychainType": { + "title": "UserKeychainType", + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Keychain ID", + "example": "59cd72485007a239fb00282ed480da1f", + "pattern": "^[0-9a-f]{32}$" + }, + "source": { + "type": "string", + "description": "Party that created the key", + "example": "user" + }, + "type": { + "type": "string", + "description": "Keychain type (e.g. \"independent\" for onchain, \"tss\" for MPC)" + }, + "pub": { + "type": "string", + "description": "Public part of a key pair (onchain wallets)", + "example": "xpub661MyMwAqRbcGMVhmc7wqQRYMtcX9LAvSj1pjB213y5TsrkV2uuzJjWnjBrT1FUeNWGPjaVm5p7o6jdNcQJrV1cy3a1R8NQ9m7LuYKA8RpH" + }, + "ethAddress": { + "type": "string", + "description": "Ethereum address corresponding to this keychain (onchain wallets)", + "example": "0xf5b7cca8621691f9dde304cb7128b6bb3d409363" + }, + "coin": { + "type": "string", + "description": "Asset ticker for this keychain (onchain wallets)" + }, + "commonKeychain": { + "type": "string", + "description": "Common keychain string (TSS wallets)" + } + }, + "required": [ + "id", + "source", + "type" + ] + }, + "BitgoKeychainType": { + "title": "BitgoKeychainType", + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Keychain ID", + "example": "59cd72485007a239fb00282ed480da1f", + "pattern": "^[0-9a-f]{32}$" + }, + "source": { + "type": "string", + "description": "Party that created the key", + "example": "bitgo" + }, + "type": { + "type": "string", + "description": "Keychain type (e.g. \"independent\" for onchain, \"tss\" for MPC)" + }, + "isBitGo": { + "type": "boolean", + "description": "Flag for identifying keychain as created by BitGo", + "example": true + }, + "isTrust": { + "type": "boolean", + "description": "Flag for identifying keychain as trust keychain", + "example": false + }, + "hsmType": { + "type": "string", + "description": "HSM type used for the BitGo key", + "example": "institutional" + }, + "pub": { + "type": "string", + "description": "Public part of a key pair (onchain wallets)" + }, + "ethAddress": { + "type": "string", + "description": "Ethereum address corresponding to this keychain (onchain wallets)" + }, + "commonKeychain": { + "type": "string", + "description": "Common keychain string (TSS wallets)" + }, + "verifiedVssProof": { + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "string" + } + ], + "description": "Whether VSS proof was verified (TSS wallets)" + }, + "keyShares": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": {}, + "description": "TSS key share metadata (TSS wallets)" + } + }, + "walletHSMGPGPublicKeySigs": { + "type": "string", + "description": "Wallet HSM GPG public key signatures (TSS wallets)" + } + }, + "required": [ + "id", + "source", + "type", + "isBitGo", + "isTrust", + "hsmType" + ] } } } diff --git a/src/__tests__/api/master/eddsa.test.ts b/src/__tests__/api/master/eddsa.test.ts index bffea605..947832a4 100644 --- a/src/__tests__/api/master/eddsa.test.ts +++ b/src/__tests__/api/master/eddsa.test.ts @@ -16,7 +16,6 @@ import { AdvancedWalletManagerClient as AdvancedWalletManagerClient } from '../. import { handleEddsaSigning } from '../../../api/master/handlers/eddsa'; import { readKey } from 'openpgp'; -// TODO: Re-enable once using EDDSA Custom signing fns describe('Eddsa Signing Handler', () => { let bitgo: BitGoBase; let wallet: Wallet; diff --git a/src/__tests__/api/master/generateWallet.test.ts b/src/__tests__/api/master/generateWallet.test.ts index d43b9728..0f916148 100644 --- a/src/__tests__/api/master/generateWallet.test.ts +++ b/src/__tests__/api/master/generateWallet.test.ts @@ -114,22 +114,74 @@ describe('POST /api/:coin/wallet/generate', () => { keyType: 'independent', enterprise: 'test_enterprise', }) - .reply(200, { id: 'bitgo-key-id', pub: 'xpub_bitgo' }); + .reply(200, { + id: 'bitgo-key-id', + pub: 'xpub_bitgo', + source: 'bitgo', + type: 'independent', + isBitGo: true, + isTrust: false, + hsmType: 'institutional', + }); const bitgoAddWalletNock = nock(bitgoApiUrl) .post(`/api/v2/${coin}/wallet/add`, { label: 'test_wallet', + enterprise: 'test_enterprise', + multisigType: 'onchain', + coin: coin, m: 2, n: 3, keys: ['user-key-id', 'backup-key-id', 'bitgo-key-id'], type: 'cold', subType: 'onPrem', - multisigType: 'onchain', - enterprise: 'test_enterprise', }) .matchHeader('any', () => true) .reply(200, { id: 'new-wallet-id', + users: [ + { + user: 'user-id', + permissions: ['admin', 'spend', 'view'], + }, + ], + coin: coin, + label: 'test_wallet', + m: 2, + n: 3, + keys: ['user-key-id', 'backup-key-id', 'bitgo-key-id'], + keySignatures: {}, + enterprise: 'test_enterprise', + organization: 'org-id', + bitgoOrg: 'BitGo Inc', + tags: ['new-wallet-id', 'test_enterprise'], + disableTransactionNotifications: false, + freeze: {}, + deleted: false, + approvalsRequired: 1, + isCold: true, + coinSpecific: {}, + admin: {}, + pendingApprovals: [], + allowBackupKeySigning: false, + clientFlags: [], + recoverable: false, + startDate: '2025-01-01T00:00:00.000Z', + hasLargeNumberOfAddresses: false, + config: {}, + balanceString: '0', + confirmedBalanceString: '0', + spendableBalanceString: '0', + receiveAddress: { + id: 'addr-id', + address: 'tb1qexampleaddress000000000000000000000', + chain: 20, + index: 0, + coin: coin, + wallet: 'new-wallet-id', + coinSpecific: {}, + }, + // optional-ish fields used in assertions multisigType: 'onchain', type: 'cold', subType: 'onPrem', @@ -416,31 +468,33 @@ describe('POST /api/:coin/wallet/generate', () => { const addWalletNock = nock(bitgoApiUrl) .post(`/api/v2/${eddsaCoin}/wallet/add`, { label: 'test_wallet', + enterprise: 'test_enterprise', + multisigType: 'tss', + coin: eddsaCoin, m: 2, n: 3, keys: ['id', 'id', 'id'], type: 'cold', subType: 'onPrem', - multisigType: 'tss', - enterprise: 'test_enterprise', }) .reply(200, { - id: '685cb53debcd0bcb5ab4fe80d2b74be2', - users: [['Object']], - coin: 'tsol', - label: 'OnPrem eddsa sendMany test 2025-06-26T02:49:18.622Z', + id: 'wallet-id', + users: [ + { + user: 'user-id', + permissions: ['admin', 'spend', 'view'], + }, + ], + coin: eddsaCoin, + label: 'test_wallet', m: 2, n: 3, - keys: [ - '685cb5393d57687bdf0a464594ca9e36', - '685cb53a3d57687bdf0a4657b5f1f364', - '685cb536f21050339163a75dd04d41bf', - ], + keys: ['id', 'id', 'id'], keySignatures: {}, - enterprise: '6750c2d327511bc4e5f83ccfcfe1b3eb', - organization: '6750c2e027511bc4e5f83d251248fc14', - bitgoOrg: 'BitGo Trust', - tags: ['685cb53debcd0bcb5ab4fe80d2b74be2', '6750c2d327511bc4e5f83ccfcfe1b3eb'], + enterprise: 'test_enterprise', + organization: 'org-id', + bitgoOrg: 'BitGo Inc', + tags: ['wallet-id', 'test_enterprise'], disableTransactionNotifications: false, freeze: {}, deleted: false, @@ -454,37 +508,30 @@ describe('POST /api/:coin/wallet/generate', () => { nonceExpiresAt: '2025-06-25T23:00:12.019Z', trustedTokens: [], }, - admin: { - policy: ['Object'], - }, + admin: {}, + pendingApprovals: [], + allowBackupKeySigning: false, clientFlags: [], walletFlags: [], - allowBackupKeySigning: false, recoverable: false, - startDate: '2025-06-26T02:49:33.000Z', - type: 'cold', - buildDefaults: {}, - customChangeKeySignatures: {}, + startDate: '2025-01-01T00:00:00.000Z', hasLargeNumberOfAddresses: false, - multisigType: 'tss', - hasReceiveTransferPolicy: false, - creator: '63f512adc61d7100088e99bf1deece73', - subType: 'onPrem', config: {}, - pendingChainInitialization: true, balanceString: '0', confirmedBalanceString: '0', spendableBalanceString: '0', - reservedBalanceString: '0', receiveAddress: { - id: '685cb53eebcd0bcb5ab4fe8ed214d5b9', - address: '74AUHib3F6Fq5eVm2ywP5ik9iQjviwAfZXWnGM9JHhJ4', + id: 'addr-id', + address: '93AHaUAExampleRootAddress', chain: 0, index: 0, - coin: 'tsol', - wallet: '685cb53debcd0bcb5ab4fe80d2b74be2', - coinSpecific: ['Object'], + coin: eddsaCoin, + wallet: 'wallet-id', + coinSpecific: {}, }, + multisigType: 'tss', + type: 'cold', + subType: 'onPrem', }); const response = await agent @@ -1051,7 +1098,7 @@ describe('POST /api/:coin/wallet/generate', () => { type: 'tss', isMPCv2: true, }) - .reply(200, { id: 'user-key-id' }); + .reply(200, { id: 'user-key-id', source: 'user', type: 'tss' }); const bitgoAddBackupKeyNock = nock(bitgoApiUrl) .post(`/api/v2/${ecdsaCoin}/key`, { @@ -1060,7 +1107,7 @@ describe('POST /api/:coin/wallet/generate', () => { type: 'tss', isMPCv2: true, }) - .reply(200, { id: 'backup-key-id' }); + .reply(200, { id: 'backup-key-id', source: 'backup', type: 'tss' }); const bitgoAddBitGoKeyNock = nock(bitgoApiUrl) .post(`/api/v2/${ecdsaCoin}/key`, { @@ -1069,21 +1116,72 @@ describe('POST /api/:coin/wallet/generate', () => { type: 'tss', isMPCv2: true, }) - .reply(200, { id: 'bitgo-key-id' }); + .reply(200, { + id: 'bitgo-key-id', + source: 'bitgo', + type: 'tss', + commonKeychain: 'commonKeychain', + isBitGo: true, + isTrust: false, + hsmType: 'institutional', + }); const bitgoAddWalletNock = nock(bitgoApiUrl) .post(`/api/v2/${ecdsaCoin}/wallet/add`, { label: 'test-wallet', // ? + enterprise: 'test-enterprise', + multisigType: 'tss', + coin: ecdsaCoin, m: 2, n: 3, keys: ['user-key-id', 'backup-key-id', 'bitgo-key-id'], type: 'cold', subType: 'onPrem', - multisigType: 'tss', - enterprise: 'test-enterprise', }) .reply(200, { id: 'new-wallet-id', + users: [ + { + user: 'user-id', + permissions: ['admin', 'spend', 'view'], + }, + ], + coin: ecdsaCoin, + label: 'test-wallet', + m: 2, + n: 3, + keys: ['user-key-id', 'backup-key-id', 'bitgo-key-id'], + keySignatures: {}, + enterprise: 'test-enterprise', + organization: 'org-id', + bitgoOrg: 'BitGo Inc', + tags: ['new-wallet-id', 'test-enterprise'], + disableTransactionNotifications: false, + freeze: {}, + deleted: false, + approvalsRequired: 1, + isCold: true, + coinSpecific: {}, + admin: {}, + pendingApprovals: [], + allowBackupKeySigning: false, + clientFlags: [], + recoverable: false, + startDate: '2025-01-01T00:00:00.000Z', + hasLargeNumberOfAddresses: false, + config: {}, + balanceString: '0', + confirmedBalanceString: '0', + spendableBalanceString: '0', + receiveAddress: { + id: 'addr-id', + address: '0xexample', + chain: 0, + index: 0, + coin: ecdsaCoin, + wallet: 'new-wallet-id', + coinSpecific: {}, + }, multisigType: 'tss', type: 'cold', subType: 'onPrem', diff --git a/src/__tests__/api/secured/postIndependentKey.test.ts b/src/__tests__/api/secured/postIndependentKey.test.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/src/__tests__/masterBitgoExpress/generateWallet.test.ts b/src/__tests__/masterBitgoExpress/generateWallet.test.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/src/api/master/handlers/generateWallet.ts b/src/api/master/handlers/generateWallet.ts index 32ba9f30..4fc220e6 100644 --- a/src/api/master/handlers/generateWallet.ts +++ b/src/api/master/handlers/generateWallet.ts @@ -48,6 +48,7 @@ async function handleGenerateOnPremOnChainWallet( // Create wallet parameters with type assertion to allow 'onprem' subtype const walletParams = { + ...req.decoded, label: label, m: 2, n: 3, @@ -157,6 +158,7 @@ async function handleGenerateOnPremMpcWallet( const { label, enterprise } = req.decoded; const walletParams: SupplementGenerateWalletOptions = { + ...req.decoded, label: label, m: 2, n: 3, diff --git a/src/api/master/handlers/recoveryWallet.ts b/src/api/master/handlers/recoveryWallet.ts index 54b45d4d..3187778b 100644 --- a/src/api/master/handlers/recoveryWallet.ts +++ b/src/api/master/handlers/recoveryWallet.ts @@ -21,13 +21,8 @@ import { } from '../../../shared/recoveryUtils'; import { AdvancedWalletManagerClient } from '../clients/advancedWalletManagerClient'; -import { - CoinSpecificParams, - CoinSpecificParamsUnion, - MasterApiSpecRouteRequest, - ScriptType2Of3, - SolanaRecoveryOptions, -} from '../routers/masterBitGoExpressApiSpec'; +import { MasterApiSpecRouteRequest, ScriptType2Of3 } from '../routers/masterBitGoExpressApiSpec'; +import { CoinSpecificParams, CoinSpecificParamsUnion } from '../routers/recoveryRoute'; import { recoverEddsaWallets } from './recoverEddsaWallets'; import { EnvironmentName, MasterExpressConfig } from '../../../shared/types'; import { recoverEcdsaMpcV2Params, recoverEcdsaMPCv2Wallets } from './recoverEcdsaWallets'; @@ -154,7 +149,7 @@ async function handleEddsaRecovery( }; let unsignedSweepPrebuildTx: Awaited>; if (sdkCoin.getFamily() === CoinFamily.SOL) { - const solanaParams = params.coinSpecificParams as SolanaRecoveryOptions; + const solanaParams = params.coinSpecificParams as SolRecoveryOptions; const solanaRecoveryOptions: SolRecoveryOptions = { ...options }; solanaRecoveryOptions.recoveryDestinationAtaAddress = solanaParams.recoveryDestinationAtaAddress; diff --git a/src/api/master/routers/accelerateRoute.ts b/src/api/master/routers/accelerateRoute.ts new file mode 100644 index 00000000..96ac3bb7 --- /dev/null +++ b/src/api/master/routers/accelerateRoute.ts @@ -0,0 +1,107 @@ +import { httpRequest, HttpResponse, httpRoute, optional } from '@api-ts/io-ts-http'; +import * as t from 'io-ts'; +import { ErrorResponses } from '../../../shared/errors'; + +/** + * Request type for the transaction acceleration endpoint. + * Used to accelerate unconfirmed transactions on UTXO-based blockchains using CPFP or RBF. + * + * @endpoint POST /api/{coin}/wallet/{walletId}/accelerate + * @description Speeds up unconfirmed transactions by creating a child transaction (CPFP) or replacing the original transaction (RBF) + */ +export const AccelerateRequest = { + /** + * Public key used for signing the acceleration transaction. + * @example "xpub661MyMwAqRbcGCNnmzqt3u5KhxmXBHiC78cwAyUMaKJXpFDfHpJwNap6qpG1Kz2SPexKXy3akhPQz7GDYWpHNWkLxRLj6bDxQSf74aTAP9y" + */ + pubkey: t.string, + + /** + * The key to use for signing the transaction. + * @example "user" + */ + source: t.union([t.literal('user'), t.literal('backup')]), + + /** + * Transaction IDs to accelerate using Child-Pays-For-Parent (CPFP). + * CPFP creates a new transaction that spends an output from the original transaction. + * @example ["abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234"] + */ + cpfpTxIds: optional(t.array(t.string)), + + /** + * Fee rate in satoshis per byte for the CPFP transaction. + * Higher fee rates result in faster confirmations but higher transaction costs. + * @example 50 // 50 satoshis per byte + */ + cpfpFeeRate: optional(t.number), + + /** + * Maximum fee in satoshis for the acceleration transaction. + * Helps prevent overpaying for transaction acceleration. + * @example 100000 // 0.001 BTC + */ + maxFee: optional(t.number), + + /** + * Transaction IDs to accelerate using Replace-By-Fee (RBF). + * RBF creates a new transaction that replaces the original transaction. + * The original transaction must have been created with RBF enabled. + * @example ["abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234"] + */ + rbfTxIds: optional(t.array(t.string)), + + /** + * Fee multiplier for RBF transactions. + * The new fee will be the original fee multiplied by this value. + * @example 1.5 // Increase fee by 50% + */ + feeMultiplier: optional(t.number), +}; + +/** + * Response type for the transaction acceleration endpoint. + * + * @endpoint POST /api/{coin}/wallet/{walletId}/accelerate + * @description Sign an acceleration transaction and send to BitGo to sign and broadcast + */ +const AccelerateResponse: HttpResponse = { + /** + * Successful acceleration response. + * @returns The signed transaction and its ID + * @example { "txid": "abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234", "tx": "01000000000101edd7a5d948a6c79f273ce686a6a8f2e96ed8c2583b5e77b866aa2a1b3426fbed0100000000ffffffff02102700000000000017a914192f23283c2a9e6c5d11562db0eb5d4eb47f460287b9bc2c000000000017a9145c139b242ab3701f321d2399d3a11b028b3b361e870247304402206ac9477fece38d96688c6c3719cb27396c0563ead0567457e7e884b406b6da8802201992d1cfa1b55a67ce8acb482e9957812487d2555f5f54fb0286ecd3095d78e4012103c92564575197c4d6e3d9792280e7548b3ba52a432101c62de2186c4e2fa7fc580000000000" } + */ + 200: t.type({ + /** + * The transaction ID (hash) of the acceleration transaction. + * This can be used to track the transaction on a block explorer. + * @example "abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234" + */ + txid: t.string, + /** + * The full signed transaction in hexadecimal format. + * This transaction can be broadcast to the network. + * @example "01000000000101edd7a5d948a6c79f273ce686a6a8f2e96ed8c2583b5e77b866aa2a1b3426fbed0100000000ffffffff02102700000000000017a914192f23283c2a9e6c5d11562db0eb5d4eb47f460287b9bc2c000000000017a9145c139b242ab3701f321d2399d3a11b028b3b361e870247304402206ac9477fece38d96688c6c3719cb27396c0563ead0567457e7e884b406b6da8802201992d1cfa1b55a67ce8acb482e9957812487d2555f5f54fb0286ecd3095d78e4012103c92564575197c4d6e3d9792280e7548b3ba52a432101c62de2186c4e2fa7fc580000000000" + */ + tx: t.string, + }), + ...ErrorResponses, +}; + +/** + * Accelerate unconfirmed transactions on UTXO-based blockchains. + * Supports Child-Pays-For-Parent (CPFP) and Replace-By-Fee (RBF) acceleration methods. + */ +export const AccelerateRoute = httpRoute({ + method: 'POST', + path: '/api/{coin}/wallet/{walletId}/accelerate', + request: httpRequest({ + params: { + walletId: t.string, + coin: t.string, + }, + body: AccelerateRequest, + }), + response: AccelerateResponse, + description: 'Accelerate transaction', +}); diff --git a/src/api/master/routers/consolidateRoute.ts b/src/api/master/routers/consolidateRoute.ts new file mode 100644 index 00000000..8091c807 --- /dev/null +++ b/src/api/master/routers/consolidateRoute.ts @@ -0,0 +1,53 @@ +import { httpRequest, HttpResponse, httpRoute, optional } from '@api-ts/io-ts-http'; +import * as t from 'io-ts'; +import { ErrorResponses } from '../../../shared/errors'; + +export const ConsolidateRequest = { + /** + * The key to use for signing the transaction + */ + source: t.union([t.literal('user'), t.literal('backup')]), + /** + * Public key of the key used for signing multisig transactions + */ + pubkey: t.union([t.undefined, t.string]), + /** + * Optional: restrict the consolidation to the specified receive addresses. If not provided, will consolidate the + * funds from all receive addresses up to 500 addresses. + */ + consolidateAddresses: optional(t.array(t.string)), + + /** + * For TSS wallets, this is the common keychain of the wallet + */ + commonKeychain: t.union([t.undefined, t.string]), + + /** + * The Trasaction Request API version to use for MPC EdDSA Hot Wallets. + * Defaults based on the wallet type and asset curve. + */ + apiVersion: t.union([t.undefined, t.literal('full'), t.literal('lite')]), +}; + +export const ConsolidateResponse: HttpResponse = { + 200: t.any, + ...ErrorResponses, +}; + +/** + * Build, sign, and send a consolidation transaction for an account-based asset all in 1 call. + * For account-based assets, consolidating the balances in the receive addresses to the base address maximizes the spendable balance of a wallet. + */ +export const ConsolidateRoute = httpRoute({ + method: 'POST', + path: '/api/{coin}/wallet/{walletId}/consolidate', + request: httpRequest({ + params: { + walletId: t.string, + coin: t.string, + }, + body: ConsolidateRequest, + }), + response: ConsolidateResponse, + description: 'Consolidate addresses', +}); diff --git a/src/api/master/routers/consolidateUnspentsRoute.ts b/src/api/master/routers/consolidateUnspentsRoute.ts new file mode 100644 index 00000000..92bf3134 --- /dev/null +++ b/src/api/master/routers/consolidateUnspentsRoute.ts @@ -0,0 +1,95 @@ +import { httpRequest, HttpResponse, httpRoute, optional } from '@api-ts/io-ts-http'; +import * as t from 'io-ts'; +import { ErrorResponses } from '../../../shared/errors'; + +export const ConsolidateUnspentsRequest = { + /** + * Public key of the key used for signing multisig transactions + */ + pubkey: t.string, + /** + * The key to use for signing the transaction + */ + source: t.union([t.literal('user'), t.literal('backup')]), + /** + * Custom fee rate (in base units) per kilobyte + */ + feeRate: optional(t.number), + /** + * Maximum fee rate (in base units) per kilobyte + */ + maxFeeRate: optional(t.number), + /** + * Maximum fee percentage + */ + maxFeePercentage: optional(t.number), + /** + * Fee transaction confirmation target + */ + feeTxConfirmTarget: optional(t.number), + /** + * Enable bulk processing + */ + bulk: optional(t.boolean), + /** + * Minimum value for unspents + */ + minValue: optional(t.union([t.string, t.number])), + /** + * Maximum value for unspents + */ + maxValue: optional(t.union([t.string, t.number])), + /** + * Minimum block height + */ + minHeight: optional(t.number), + /** + * Minimum confirmations required + */ + minConfirms: optional(t.number), + /** + * Enforce minimum confirmations for change outputs + */ + enforceMinConfirmsForChange: optional(t.boolean), + /** + * Limit the number of unspents to process + */ + limit: optional(t.number), + /** + * Number of unspents to make + */ + numUnspentsToMake: optional(t.number), + /** + * Target address for consolidation + */ + targetAddress: optional(t.string), + /** + * Transaction format + */ + txFormat: optional(t.union([t.literal('legacy'), t.literal('psbt'), t.literal('psbt-lite')])), +}; + +export const ConsolidateUnspentsResponse: HttpResponse = { + 200: t.type({ + tx: t.string, + txid: t.string, + }), + ...ErrorResponses, +}; + +/** + * Build and send a transaction to consolidate unspents in a wallet. + * Consolidating unspents is only for UTXO-based assets. + */ +export const ConsolidateUnspentsRoute = httpRoute({ + method: 'POST', + path: '/api/{coin}/wallet/{walletId}/consolidateunspents', + request: httpRequest({ + params: { + walletId: t.string, + coin: t.string, + }, + body: ConsolidateUnspentsRequest, + }), + response: ConsolidateUnspentsResponse, +}); diff --git a/src/api/master/routers/generateWalletRoute.ts b/src/api/master/routers/generateWalletRoute.ts new file mode 100644 index 00000000..295b9bf6 --- /dev/null +++ b/src/api/master/routers/generateWalletRoute.ts @@ -0,0 +1,356 @@ +import { httpRequest, HttpResponse, httpRoute, optional } from '@api-ts/io-ts-http'; + +import * as t from 'io-ts'; +import { ErrorResponses } from '../../../shared/errors'; + +const WalletType = t.intersection([ + t.type({ + /** + * Wallet ID + * @example "59cd72485007a239fb00282ed480da1f" + * @pattern ^[0-9a-f]{32}$ + */ + id: t.string, + /** + * Ids of users with access to the wallet + */ + users: t.array( + t.type({ + user: t.string, + permissions: t.array(t.string), + }), + ), + /** + * Name of the blockchain the wallet is on + * @example "tbtc4" + */ + coin: t.string, + /** + * Name the user assigned to the wallet + * @example "My TBTC4 Wallet" + */ + label: t.string, + /** + * Number of signatures required for the wallet to send + * @example 2 + */ + m: t.number, + /** + * Number of signers on the wallet + * @example 3 + */ + n: t.number, + /** + * Ids of wallet keys + * @example ["59cd72485007a239fb00282ed480da1f"] + */ + keys: t.array(t.string), + /** + * Signatures for the backup and BitGo public keys signed by the user key + */ + keySignatures: t.record(t.string, t.unknown), + enterprise: t.string, + organization: t.string, + bitgoOrg: t.string, + /** + * Tags set on the wallet + * @example ["59cd72485007a239fb00282ed480da1f"] + */ + tags: t.array(t.string), + /** + * Flag for disabling wallet transaction notifications + * @example false + */ + disableTransactionNotifications: t.boolean, + /** + * Freeze state (used to stop the wallet from spending) + * @example {} + */ + freeze: t.record(t.string, t.unknown), + /** + * Flag which indicates the wallet has been deleted + * @example false + */ + deleted: t.boolean, + /** + * Number of admin approvals required for an action to fire + * @example 1 + */ + approvalsRequired: t.number, + /** + * Flag for identifying cold wallets + * @example false + */ + isCold: t.boolean, + /** + * Coin-specific data + */ + coinSpecific: t.record(t.string, t.unknown), + /** + * Admin data (wallet policies) + * @example {} + */ + admin: t.record(t.string, t.unknown), + /** + * Pending approvals on the wallet + * @example [] + */ + pendingApprovals: t.array(t.record(t.string, t.unknown)), + /** + * Flag for allowing signing with backup key + * @example false + */ + allowBackupKeySigning: t.boolean, + clientFlags: t.array(t.string), + /** + * Flag indicating whether this wallet's user key is recoverable with the passphrase held by the user. + */ + recoverable: t.boolean, + /** + * Time when this wallet was created + */ + startDate: t.string, + /** + * Flag indicating that this wallet is large (more than 100,000 addresses). If this is set, some APIs may omit + * properties which are expensive to calculate for wallets with many addresses (for example, the total address + * counts returned by the List Addresses API). + */ + hasLargeNumberOfAddresses: t.boolean, + /** + * Custom configuration options for this wallet + */ + config: t.record(t.string, t.unknown), + /** + * Wallet balance as string + * @example "0" + */ + balanceString: t.string, + /** + * Confirmed wallet balance as string + * @example "0" + */ + confirmedBalanceString: t.string, + /** + * Spendable wallet balance as string + * @example "0" + */ + spendableBalanceString: t.string, + receiveAddress: t.type({ + id: t.string, + address: t.string, + chain: t.number, + index: t.number, + coin: t.string, + wallet: t.string, + coinSpecific: t.record(t.string, t.unknown), + }), + }), + t.partial({ + /** + * Wallet balance as number + * @example 0 + */ + balance: t.number, + rbfBalance: t.number, + rbfBalanceString: t.string, + reservedBalanceString: t.string, + lockedBalanceString: t.string, + stakedBalanceString: t.string, + unspentCount: t.number, + pendingChainInitialization: t.boolean, + pendingEcdsaTssInitialization: t.boolean, + multisigType: t.string, + multisigTypeVersion: t.string, + type: t.string, + subType: t.string, + creator: t.string, + walletFullyCreated: t.boolean, + }), +]); + +const UserKeychainType = t.intersection([ + t.type({ + /** + * Keychain ID + * @example "59cd72485007a239fb00282ed480da1f" + * @pattern ^[0-9a-f]{32}$ + */ + id: t.string, + /** + * Party that created the key + * @example "user" + */ + source: t.string, + /** + * Keychain type (e.g. "independent" for onchain, "tss" for MPC) + */ + type: t.string, + }), + t.partial({ + /** + * Public part of a key pair (onchain wallets) + * @example "xpub661MyMwAqRbcGMVhmc7wqQRYMtcX9LAvSj1pjB213y5TsrkV2uuzJjWnjBrT1FUeNWGPjaVm5p7o6jdNcQJrV1cy3a1R8NQ9m7LuYKA8RpH" + */ + pub: t.string, + /** + * Ethereum address corresponding to this keychain (onchain wallets) + * @example "0xf5b7cca8621691f9dde304cb7128b6bb3d409363" + */ + ethAddress: t.string, + /** + * Asset ticker for this keychain (onchain wallets) + */ + coin: t.string, + /** + * Common keychain string (TSS wallets) + */ + commonKeychain: t.string, + }), +]); + +const BitgoKeychainType = t.intersection([ + t.type({ + /** + * Keychain ID + * @example "59cd72485007a239fb00282ed480da1f" + * @pattern ^[0-9a-f]{32}$ + */ + id: t.string, + /** + * Party that created the key + * @example "bitgo" + */ + source: t.string, + /** + * Keychain type (e.g. "independent" for onchain, "tss" for MPC) + */ + type: t.string, + /** + * Flag for identifying keychain as created by BitGo + * @example true + */ + isBitGo: t.boolean, + /** + * Flag for identifying keychain as trust keychain + * @example false + */ + isTrust: t.boolean, + /** + * HSM type used for the BitGo key + * @example "institutional" + */ + hsmType: t.string, + }), + t.partial({ + /** + * Public part of a key pair (onchain wallets) + */ + pub: t.string, + /** + * Ethereum address corresponding to this keychain (onchain wallets) + */ + ethAddress: t.string, + /** + * Common keychain string (TSS wallets) + */ + commonKeychain: t.string, + /** + * Whether VSS proof was verified (TSS wallets) + */ + verifiedVssProof: t.union([t.boolean, t.string]), + /** + * TSS key share metadata (TSS wallets) + */ + keyShares: t.array(t.record(t.string, t.unknown)), + /** + * Wallet HSM GPG public key signatures (TSS wallets) + */ + walletHSMGPGPublicKeySigs: t.string, + }), +]); + +const GenerateWalletResponse: HttpResponse = { + 200: t.type({ + wallet: WalletType, + userKeychain: UserKeychainType, + backupKeychain: UserKeychainType, + bitgoKeychain: BitgoKeychainType, + responseType: t.string, + }), + ...ErrorResponses, +}; + +const GenerateWalletRequest = { + /** + * A human-readable label for the wallet + * This will be displayed in the BitGo dashboard and API responses + * @example "My Wallet" + */ + label: t.string, + + /** + * The type of multisig wallet to create + * - onchain: Traditional multisig wallets using on-chain scripts + * - tss: Threshold Signature Scheme wallets using MPC protocols + * If absent, BitGo uses the default wallet type for the asset + * @example "tss" + */ + multisigType: t.union([t.literal('onchain'), t.literal('tss')]), + + /** + * Enterprise ID - Required for Ethereum wallets + * Ethereum wallets can only be created under an enterprise + * Each enterprise has a fee address which will be used to pay for transaction fees + * Your enterprise ID can be seen by clicking on the "Manage Organization" link on the enterprise dropdown + * @example "59cd72485007a239fb00282ed480da1f" + * @pattern ^[0-9a-f]{32}$ + */ + enterprise: t.string, + + /** + * Flag for disabling wallet transaction notifications + * When true, BitGo will not send email/SMS notifications for wallet transactions + * @example false + */ + disableTransactionNotifications: optional(t.boolean), + + /** + * True, if the wallet type is a distributed-custodial + * If passed, you must also pass the 'enterprise' parameter + * Distributed custody allows multiple parties to share control of the wallet + * @example false + */ + isDistributedCustody: optional(t.boolean), + + /** + * Specify the wallet creation contract version used when creating an Ethereum wallet contract + * - 0: Old wallet creation (legacy) + * - 1: New wallet creation, only deployed upon receiving funds + * - 2: Same functionality as v1 but with NFT support + * - 3: MPC wallets + * @example 1 + * @minimum 0 + * @maximum 3 + */ + walletVersion: optional(t.number), +}; + +/** + * Generates a new onPrem self-managed cold wallet. + * The wallet creation process involves several steps that happen automatically: + * 1. User Keychain Creation: Creates the user keychain in the advanced wallet manager and encrypts it with the respective KMS. + * 2. Backup Keychain Creation: Creates the backup keychain in the advanced wallet manager and encrypts it with the respective KMS. + * 3. Keychain Upload: Uploads the user/backup public keys to BitGo. + * 4. BitGo Key Creation: Creates the BitGo key on the BitGo service. + * 5. Wallet Creation: Creates the wallet on BitGo with the 3 keys. + */ +export const WalletGenerateRoute = httpRoute({ + method: 'POST', + path: '/api/{coin}/wallet/generate', + request: httpRequest({ + params: { coin: t.string }, + body: GenerateWalletRequest, + }), + response: GenerateWalletResponse, + description: 'Generate a new wallet', +}); diff --git a/src/api/master/routers/masterBitGoExpressApiSpec.ts b/src/api/master/routers/masterBitGoExpressApiSpec.ts index 0c979482..08a54bc9 100644 --- a/src/api/master/routers/masterBitGoExpressApiSpec.ts +++ b/src/api/master/routers/masterBitGoExpressApiSpec.ts @@ -1,11 +1,4 @@ -import { - apiSpec, - httpRequest, - HttpResponse, - httpRoute, - Method as HttpMethod, - optional, -} from '@api-ts/io-ts-http'; +import { apiSpec, Method as HttpMethod } from '@api-ts/io-ts-http'; import { Response } from '@api-ts/response'; import { createRouter, @@ -13,7 +6,6 @@ import { type WrappedRouter, } from '@api-ts/typed-express-router'; import express from 'express'; -import * as t from 'io-ts'; import { MasterExpressConfig } from '../../../shared/types'; import * as utxolib from '@bitgo-beta/utxo-lib'; import { prepareBitGo, responseHandler } from '../../../shared/middleware'; @@ -27,720 +19,33 @@ import { handleAccelerate } from '../handlers/handleAccelerate'; import { handleConsolidateUnspents } from '../handlers/handleConsolidateUnspents'; import { handleSignAndSendTxRequest } from '../handlers/handleSignAndSendTxRequest'; import { handleRecoveryConsolidationsOnPrem } from '../handlers/recoveryConsolidationsWallet'; -import { ErrorResponses } from '../../../shared/errors'; +import { WalletGenerateRoute } from './generateWalletRoute'; +import { AccelerateRoute } from './accelerateRoute'; +import { RecoveryRoute } from './recoveryRoute'; +import { RecoveryConsolidationsRoute } from './recoveryConsolidationsRoute'; +import { SendManyRoute } from './sendManyRoute'; +import { ConsolidateRoute } from './consolidateRoute'; +import { ConsolidateUnspentsRoute } from './consolidateUnspentsRoute'; +import { SignAndSendMpcRoute } from './signAndSendMpcRoute'; export type ScriptType2Of3 = utxolib.bitgo.outputScripts.ScriptType2Of3; -/** - * Recovery parameter types used by the wallet recovery endpoints - */ -export const RecoveryParamTypes = { - /** - * UTXO-specific recovery parameters for Bitcoin & Bitcoin-like cryptocurrencies. - * Used for recovering funds from standard multisig wallets on UTXO chains. - * Required when recovering BTC, BCH, LTC, DASH, ZEC, etc. - */ - utxoRecoveryOptions: t.partial({ - /** - * Array of address types to ignore during recovery. - * Useful when you want to exclude specific address types from the recovery process. - * @example ["p2sh-p2wsh", "p2wsh"] - */ - ignoreAddressTypes: t.array(t.string), - /** - * Derivation path for the user key. - * Specifies the HD path to derive the correct user key for signing. - * @example "m/0/0/0/0" - * @default "m/0" - */ - userKeyPath: t.string, - /** - * Fee rate for the recovery transaction in satoshis per byte. - * Higher fee rates result in faster confirmations but higher transaction costs. - * @example 20 // 20 satoshis per byte - */ - feeRate: t.number, - /** - * Number of addresses to scan for funds. - * Higher values will scan more addresses but take longer to complete. - * @example 20 // scan 20 addresses - */ - scan: optional(t.number), - }), - - /** - * EVM-specific recovery parameters for Ethereum and EVM-compatible chains. - * Used for recovering funds from standard multisig wallets on Ethereum and EVM-compatible chains. - * Required when recovering ETH, MATIC, BSC, AVAX C-Chain, etc. - */ - ethLikeRecoveryOptions: t.partial({ - /** - * Gas price in wei for the recovery transaction (for legacy transactions). - * Higher gas prices result in faster confirmations but higher transaction costs. - * @example 50000000000 // 50 Gwei - */ - gasPrice: t.number, - - /** - * Gas limit for the recovery transaction. - * Must be enough to cover the contract execution costs. - * @example 500000 - */ - gasLimit: t.number, - - /** - * EIP-1559 gas parameters for modern Ethereum transactions. - * Required for EIP-1559 compatible networks (Ethereum post-London fork). - */ - eip1559: t.type({ - /** - * Maximum priority fee per gas in wei (tip for miners/validators). - * @example 2000000000 // 2 Gwei - */ - maxPriorityFeePerGas: t.number, - - /** - * Maximum fee per gas in wei (base fee + priority fee). - * @example 50000000000 // 50 Gwei - */ - maxFeePerGas: t.number, - }), - - /** - * Replay protection options for the transaction. - * Required to prevent transaction replay attacks across different chains. - */ - replayProtectionOptions: t.type({ - /** - * Chain ID or name. - * @example 1 // Ethereum Mainnet - * @example "goerli" // Goerli Testnet - */ - chain: t.union([t.string, t.number]), - - /** - * Hardfork name to determine the transaction format. - * @example "london" // Post-London fork (EIP-1559) - * @example "istanbul" // Pre-London fork - * @default "london" - */ - hardfork: t.string, - }), - - /** - * Number of addresses to scan for funds. - * Higher values will scan more addresses but take longer to complete. - * @example 20 // scan 20 addresses - * @default 20 - */ - scan: optional(t.number), - }), - - /** - * Solana-specific recovery parameters. - */ - solanaRecoveryOptions: t.partial({ - /** - * Durable nonce configuration for transaction durability. - * Optional but recommended for recovery operations. - * Refer to https://github.com/BitGo/wallet-recovery-wizard/blob/master/DURABLE_NONCE.md on durable nonce creation. - */ - durableNonce: optional( - t.type({ - /** - * The public key of the durable nonce account. - */ - publicKey: t.string, - /** - * The secret key of the durable nonce account. - */ - secretKey: t.string, - }), - ), - /** - * The token contract address for token recovery. - * Required when recovering tokens. - */ - tokenContractAddress: t.string, - /** - * The close associated token account address. - * Required for token recovery. - */ - closeAtaAddress: t.string, - /** - * The recovery destination's associated token account address. - * Required for token recovery. - */ - recoveryDestinationAtaAddress: t.string, - /** - * The program ID for the token. - * Required for token recovery. - */ - programId: t.string, - }), - - // ECDSA ETH-like recovery specific parameters - ecdsaEthLikeRecoverySpecificParams: t.type({ - walletContractAddress: t.string, - bitgoDestinationAddress: t.string, - apiKey: t.string, - }), - - // ECDSA Cosmos-like recovery specific parameters - ecdsaCosmosLikeRecoverySpecificParams: t.type({ - rootAddress: t.string, - }), -}; - -export type EvmRecoveryOptions = typeof RecoveryParamTypes.ethLikeRecoveryOptions._A; -export type UtxoRecoveryOptions = typeof RecoveryParamTypes.utxoRecoveryOptions._A; -export type SolanaRecoveryOptions = typeof RecoveryParamTypes.solanaRecoveryOptions._A; -export type EcdsaEthLikeRecoverySpecificParams = - typeof RecoveryParamTypes.ecdsaEthLikeRecoverySpecificParams._A; -export type EcdsaCosmosLikeRecoverySpecificParams = - typeof RecoveryParamTypes.ecdsaCosmosLikeRecoverySpecificParams._A; - -// Combined coin specific parameters -const CoinSpecificParams = t.partial({ - utxoRecoveryOptions: RecoveryParamTypes.utxoRecoveryOptions, - evmRecoveryOptions: RecoveryParamTypes.ethLikeRecoveryOptions, - solanaRecoveryOptions: RecoveryParamTypes.solanaRecoveryOptions, - ecdsaEthLikeRecoverySpecificParams: RecoveryParamTypes.ecdsaEthLikeRecoverySpecificParams, - ecdsaCosmosLikeRecoverySpecificParams: RecoveryParamTypes.ecdsaCosmosLikeRecoverySpecificParams, -}); - -export type CoinSpecificParams = t.TypeOf; -export type CoinSpecificParamsUnion = - | EvmRecoveryOptions - | UtxoRecoveryOptions - | SolanaRecoveryOptions - | EcdsaEthLikeRecoverySpecificParams - | EcdsaCosmosLikeRecoverySpecificParams; - // Middleware functions export function parseBody(req: express.Request, res: express.Response, next: express.NextFunction) { req.headers['content-type'] = req.headers['content-type'] || 'application/json'; return express.json({ limit: '20mb' })(req, res, next); } -// Response type for /generate endpoint -const GenerateWalletResponse: HttpResponse = { - // TODO: Get type from public types repo - 200: t.any, - ...ErrorResponses, -}; - -// Request type for /generate endpoint -const GenerateWalletRequest = { - label: t.string, - multisigType: t.union([t.literal('onchain'), t.literal('tss')]), - enterprise: t.string, - disableTransactionNotifications: t.union([t.undefined, t.boolean]), - isDistributedCustody: t.union([t.undefined, t.boolean]), -}; - -export const SendManyRequest = { - pubkey: t.union([t.undefined, t.string]), - // Required for MPC - type: t.union([ - t.undefined, - t.literal('transfer'), - t.literal('fillNonce'), - t.literal('acceleration'), - t.literal('accountSet'), - t.literal('enabletoken'), - t.literal('stakingLock'), - t.literal('stakingUnlock'), - t.literal('transfertoken'), - t.literal('trustline'), - ]), - commonKeychain: t.union([t.undefined, t.string]), - source: t.union([t.literal('user'), t.literal('backup')]), - recipients: t.union([t.undefined, t.array(t.any)]), - numBlocks: t.union([t.undefined, t.number]), - feeRate: t.union([t.undefined, t.number]), - feeMultiplier: t.union([t.undefined, t.number]), - maxFeeRate: t.union([t.undefined, t.number]), - minConfirms: t.union([t.undefined, t.number]), - enforceMinConfirmsForChange: t.union([t.undefined, t.boolean]), - targetWalletUnspents: t.union([t.undefined, t.number]), - message: t.union([t.undefined, t.string]), - minValue: t.union([t.undefined, t.union([t.number, t.string])]), - maxValue: t.union([t.undefined, t.union([t.number, t.string])]), - sequenceId: t.union([t.undefined, t.string]), - lastLedgerSequence: t.union([t.undefined, t.number]), - ledgerSequenceDelta: t.union([t.undefined, t.number]), - noSplitChange: t.union([t.undefined, t.boolean]), - unspents: t.union([t.undefined, t.array(t.string)]), - comment: t.union([t.undefined, t.string]), - otp: t.union([t.undefined, t.string]), - changeAddress: t.union([t.undefined, t.string]), - allowExternalChangeAddress: t.union([t.undefined, t.boolean]), - instant: t.union([t.undefined, t.boolean]), - memo: t.union([t.undefined, t.string]), - transferId: t.union([t.undefined, t.number]), - eip1559: t.union([t.undefined, t.any]), - gasLimit: t.union([t.undefined, t.number]), - custodianTransactionId: t.union([t.undefined, t.string]), - nonce: t.union([t.undefined, t.string]), -}; - -export const SendManyResponse: HttpResponse = { - // TODO: Get type from public types repo / Wallet Platform - 200: t.any, - ...ErrorResponses, -}; - -// Request type for /consolidate endpoint -export const ConsolidateRequest = { - pubkey: t.union([t.undefined, t.string]), - source: t.union([t.literal('user'), t.literal('backup')]), - consolidateAddresses: t.union([t.undefined, t.array(t.string)]), - apiVersion: t.union([t.undefined, t.literal('full'), t.literal('lite')]), - commonKeychain: t.union([t.undefined, t.string]), -}; - -// Response type for /consolidate endpoint -const ConsolidateResponse: HttpResponse = { - 200: t.any, - ...ErrorResponses, -}; - -/** - * Request type for the transaction acceleration endpoint. - * Used to accelerate unconfirmed transactions on UTXO-based blockchains using CPFP or RBF. - * - * @endpoint POST /api/{coin}/wallet/{walletId}/accelerate - * @description Speeds up unconfirmed transactions by creating a child transaction (CPFP) or replacing the original transaction (RBF) - */ -export const AccelerateRequest = { - /** - * Public key used for signing the acceleration transaction. - * @example "xpub661MyMwAqRbcGCNnmzqt3u5KhxmXBHiC78cwAyUMaKJXpFDfHpJwNap6qpG1Kz2SPexKXy3akhPQz7GDYWpHNWkLxRLj6bDxQSf74aTAP9y" - */ - pubkey: t.string, - - /** - * The key to use for signing the transaction. - * @example "user" - */ - source: t.union([t.literal('user'), t.literal('backup')]), - - /** - * Transaction IDs to accelerate using Child-Pays-For-Parent (CPFP). - * CPFP creates a new transaction that spends an output from the original transaction. - * @example ["abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234"] - */ - cpfpTxIds: optional(t.array(t.string)), - - /** - * Fee rate in satoshis per byte for the CPFP transaction. - * Higher fee rates result in faster confirmations but higher transaction costs. - * @example 50 // 50 satoshis per byte - */ - cpfpFeeRate: optional(t.number), - - /** - * Maximum fee in satoshis for the acceleration transaction. - * Helps prevent overpaying for transaction acceleration. - * @example 100000 // 0.001 BTC - */ - maxFee: optional(t.number), - - /** - * Transaction IDs to accelerate using Replace-By-Fee (RBF). - * RBF creates a new transaction that replaces the original transaction. - * The original transaction must have been created with RBF enabled. - * @example ["abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234"] - */ - rbfTxIds: optional(t.array(t.string)), - - /** - * Fee multiplier for RBF transactions. - * The new fee will be the original fee multiplied by this value. - * @example 1.5 // Increase fee by 50% - */ - feeMultiplier: optional(t.number), -}; - -/** - * Response type for the transaction acceleration endpoint. - * - * @endpoint POST /api/{coin}/wallet/{walletId}/accelerate - * @description Sign an acceleration transaction and send to BitGo to sign and broadcast - */ -const AccelerateResponse: HttpResponse = { - /** - * Successful acceleration response. - * @returns The signed transaction and its ID - * @example { "txid": "abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234", "tx": "01000000000101edd7a5d948a6c79f273ce686a6a8f2e96ed8c2583b5e77b866aa2a1b3426fbed0100000000ffffffff02102700000000000017a914192f23283c2a9e6c5d11562db0eb5d4eb47f460287b9bc2c000000000017a9145c139b242ab3701f321d2399d3a11b028b3b361e870247304402206ac9477fece38d96688c6c3719cb27396c0563ead0567457e7e884b406b6da8802201992d1cfa1b55a67ce8acb482e9957812487d2555f5f54fb0286ecd3095d78e4012103c92564575197c4d6e3d9792280e7548b3ba52a432101c62de2186c4e2fa7fc580000000000" } - */ - 200: t.type({ - /** - * The transaction ID (hash) of the acceleration transaction. - * This can be used to track the transaction on a block explorer. - * @example "abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234" - */ - txid: t.string, - /** - * The full signed transaction in hexadecimal format. - * This transaction can be broadcast to the network. - * @example "01000000000101edd7a5d948a6c79f273ce686a6a8f2e96ed8c2583b5e77b866aa2a1b3426fbed0100000000ffffffff02102700000000000017a914192f23283c2a9e6c5d11562db0eb5d4eb47f460287b9bc2c000000000017a9145c139b242ab3701f321d2399d3a11b028b3b361e870247304402206ac9477fece38d96688c6c3719cb27396c0563ead0567457e7e884b406b6da8802201992d1cfa1b55a67ce8acb482e9957812487d2555f5f54fb0286ecd3095d78e4012103c92564575197c4d6e3d9792280e7548b3ba52a432101c62de2186c4e2fa7fc580000000000" - */ - tx: t.string, - }), - ...ErrorResponses, -}; - -/** - * Response type for the wallet recovery endpoint. - * - * @endpoint POST /api/{coin}/wallet/recovery - * @description Returns the signed recovery transaction that can be broadcast to the network - */ -const RecoveryWalletResponse: HttpResponse = { - /** - * Successful recovery response. - * @returns The signed transaction in hex format - * @example { "txHex": "01000000000101edd7a5d948a6c79f273ce686a6a8f2e96ed8c2583b5e77b866aa2a1b3426fbed0100000000ffffffff02102700000000000017a914192f23283c2a9e6c5d11562db0eb5d4eb47f460287b9bc2c000000000017a9145c139b242ab3701f321d2399d3a11b028b3b361e870247304402206ac9477fece38d96688c6c3719cb27396c0563ead0567457e7e884b406b6da8802201992d1cfa1b55a67ce8acb482e9957812487d2555f5f54fb0286ecd3095d78e4012103c92564575197c4d6e3d9792280e7548b3ba52a432101c62de2186c4e2fa7fc580000000000" } - */ - 200: t.type({ - /** - * The full signed transaction in hexadecimal format. - * This transaction can be broadcast to the network to complete the recovery. - */ - txHex: t.string, - }), - ...ErrorResponses, -}; - -/** - * Request type for the wallet recovery endpoint. - * Used to recover funds from both standard multisig wallets and TSS wallets. - * - * @endpoint POST /api/{coin}/wallet/recovery - * @description Recover funds from a wallet by building a transaction with user and backup keys - */ -const RecoveryWalletRequest = { - /** - * Set to true to perform a TSS (Threshold Signature Scheme) recovery. - * @example true - */ - isTssRecovery: t.union([t.undefined, t.boolean]), - /** - * Parameters specific to TSS recovery. - * Required when isTssRecovery is true. - */ - tssRecoveryParams: optional( - t.type({ - /** - * The common keychain string used for TSS wallets. - * Required for TSS recovery. - * @example "0280ec751d3b165a48811b2cc90f90dcf323f33e8bcaadc0341e1e010adcdcf7005afde80dd286d65b6be947af0424dd1e9f7611f3d20e02a4fc84ad8c8b74c1a5" - */ - commonKeychain: t.string, - }), - ), - /** - * Parameters specific to standard multisig recovery. - * Required when isTssRecovery is false (default). - */ - multiSigRecoveryParams: optional( - t.type({ - /** - * The user's public key. - * @example "xpub661MyMwAqRbcGCNnmzqt3u5KhxmXBHiC78cwAyUMaKJXpFDfHpJwNap6qpG1Kz2SPexKXy3akhPQz7GDYWpHNWkLxRLj6bDxQSf74aTAP9y" - */ - userPub: t.string, - /** - * The backup public key. - * @example "xpub661MyMwAqRbcGCNnmzqt3u5KhxmXBHiC78cwAyUMaKJXpFDfHpJwNap6qpG1Kz2SPexKXy3akhPQz7GDYWpHNWkLxRLj6bDxQSf74aTAP9y" - */ - backupPub: t.string, - /** - * The BitGo public key. - * Required for UTXO coins, optional for others. - * @example "xpub661MyMwAqRbcGCNnmzqt3u5KhxmXBHiC78cwAyUMaKJXpFDfHpJwNap6qpG1Kz2SPexKXy3akhPQz7GDYWpHNWkLxRLj6bDxQSf74aTAP9y" - */ - bitgoPub: t.string, - /** - * The wallet contract address. - * Required for ETH-like recoveries. - * @example "0x1234567890123456789012345678901234567890" - */ - walletContractAddress: t.string, - }), - ), - /** - * The address where recovered funds will be sent. - * Must be a valid address for the coin being recovered. - * @example "2N8ryDAob6Qn8uCsWvkkQDhyeCQTqybGUFe" // For BTC - * @example "0x1234567890123456789012345678901234567890" // For ETH - * @example "9zvKDB8o96QvToQierXtwSfqK9NqaHw7uvmxWsmSrxns" // For SOL - */ - recoveryDestinationAddress: t.string, - /** - * API Key for a block chain explorer. - * Required for some coins (BTC, ETH) to build a recovery transaction without BitGo. - */ - apiKey: optional(t.string), - /** - * Coin-specific recovery options. - * Different parameters are required based on the coin family: - * - For UTXO coins (BTC, etc): provide utxoRecoveryOptions. - * - For EVM chains (ETH, etc): provide evmRecoveryOptions. - * - For Solana: provide solanaRecoveryOptions. - */ - coinSpecificParams: optional(CoinSpecificParams), -}; - -/** - * Request type for wallet recovery consolidations endpoint. - * Used to consolidate and recover funds from multiple addresses in a wallet, via signing with user and backup keys. - * - * @endpoint POST /api/{coin}/wallet/recoveryconsolidations - * @description Consolidates and recovers funds from multiple addresses in a wallet - */ -const RecoveryConsolidationsWalletRequest = { - /** - * The user's public key for standard multisig wallets. - * Required for onchain multisig recovery consolidations. - * @example "xpub661MyMwAqRbcGCNnmzqt3u5KhxmXBHiC78cwAyUMaKJXpFDfHpJwNap6qpG1Kz2SPexKXy3akhPQz7GDYWpHNWkLxRLj6bDxQSf74aTAP9y" - */ - userPub: optional(t.string), - - /** - * The backup public key for standard multisig wallets. - * Required for onchain multisig recovery consolidations. - * @example "xpub661MyMwAqRbcGCNnmzqt3u5KhxmXBHiC78cwAyUMaKJXpFDfHpJwNap6qpG1Kz2SPexKXy3akhPQz7GDYWpHNWkLxRLj6bDxQSf74aTAP9y" - */ - backupPub: optional(t.string), - - /** - * The BitGo public key for standard multisig wallets. - * Required for onchain UTXO multisig recovery consolidations. - * @example "xpub661MyMwAqRbcGCNnmzqt3u5KhxmXBHiC78cwAyUMaKJXpFDfHpJwNap6qpG1Kz2SPexKXy3akhPQz7GDYWpHNWkLxRLj6bDxQSf74aTAP9y" - */ - bitgoPub: optional(t.string), - - /** - * The type of wallet to recover - * - onchain: Traditional multisig wallets. - * - tss: Threshold Signature Scheme wallets. - * @example "onchain" - */ - multisigType: t.union([t.literal('onchain'), t.literal('tss')]), - - /** - * The common keychain for TSS wallets. - * Required when multisigType is 'tss'. - * @example "0280ec751d3b165a48811b2cc90f90dcf323f33e8bcaadc0341e1e010adcdcf7005afde80dd286d65b6be947af0424dd1e9f7611f3d20e02a4fc84ad8c8b74c1a5" - */ - commonKeychain: optional(t.string), - - /** - * The token contract address for token recovery (e.g., ERC20 tokens on Ethereum or SPL tokens on Solana). - * Required when recovering specific tokens instead of the native coin. - * @example "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" // USDC on Ethereum - * @example "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" // USDC on Solana - */ - tokenContractAddress: optional(t.string), - - /** - * The starting index to scan for addresses to consolidate. - * Useful for limiting the scan range for better performance. - * @example 0 - */ - startingScanIndex: optional(t.number), - - /** - * The ending index to scan for addresses to consolidate. - * Useful for limiting the scan range for better performance. - * @example 100 - * @default 20 - */ - endingScanIndex: optional(t.number), - - /** - * API key for blockchain explorer services. - * Required for some coins to build recovery transactions. - * @example "v2x8d5e46cf15a7b9b7xc60685d4f56xd8bd5f5cdcef3c1e9d4399c955d587179b" - */ - apiKey: optional(t.string), - - /** - * Durable nonces configuration for Solana transactions. - * Provides transaction durability for Solana recovery operations. - * Refer to https://github.com/BitGo/wallet-recovery-wizard/blob/master/DURABLE_NONCE.md on durable nonce creation. - */ - durableNonces: optional( - t.type({ - /** - * The secret key of the durable nonce account. - * @example "3XNrU5JSPs2VnZCLnWK8GDzB6Pqoy3tYNMJJVesKBXnGqRxwdXDg2QKgv7E9a6QbAiKnLHSxysKWgXDKNdfXZCQM" - */ - secretKey: t.string, - - /** - * Array of public keys associated with the durable nonce. - * @example ["BurablNonc1234567890123456789012345678901", "BurablNonc1234567890123456789012345678902"] - */ - publicKeys: t.array(t.string), - }), - ), -}; - -/** - * Response type for the wallet recovery consolidations endpoint - * - * @endpoint POST /api/{coin}/wallet/recoveryconsolidations - * @description Returns the signed consolidation transactions - */ -const RecoveryConsolidationsWalletResponse: HttpResponse = { - /** - * Successful consolidation response. - * Returns an array of consolidation transactions and recovery details. - * The exact structure depends on the coin and recovery type. - */ - 200: t.any, // Complex response structure varies by coin and recovery type - ...ErrorResponses, -}; - -export const ConsolidateUnspentsRequest = { - pubkey: t.string, - source: t.union([t.literal('user'), t.literal('backup')]), - feeRate: t.union([t.undefined, t.number]), - maxFeeRate: t.union([t.undefined, t.number]), - maxFeePercentage: t.union([t.undefined, t.number]), - feeTxConfirmTarget: t.union([t.undefined, t.number]), - bulk: t.union([t.undefined, t.boolean]), - minValue: t.union([t.undefined, t.union([t.string, t.number])]), - maxValue: t.union([t.undefined, t.union([t.string, t.number])]), - minHeight: t.union([t.undefined, t.number]), - minConfirms: t.union([t.undefined, t.number]), - enforceMinConfirmsForChange: t.union([t.undefined, t.boolean]), - limit: t.union([t.undefined, t.number]), - numUnspentsToMake: t.union([t.undefined, t.number]), - targetAddress: t.union([t.undefined, t.string]), - txFormat: t.union([t.undefined, t.literal('legacy'), t.literal('psbt'), t.literal('psbt-lite')]), -}; - -const ConsolidateUnspentsResponse: HttpResponse = { - 200: t.type({ - tx: t.string, - txid: t.string, - }), - ...ErrorResponses, -}; - -const SignMpcRequest = { - source: t.union([t.literal('user'), t.literal('backup')]), - commonKeychain: t.union([t.undefined, t.string]), -}; - -const SignMpcResponse: HttpResponse = { - 200: t.any, - ...ErrorResponses, -}; - -/** - * Accelerate unconfirmed transactions on UTXO-based blockchains. - * Supports Child-Pays-For-Parent (CPFP) and Replace-By-Fee (RBF) acceleration methods. - */ -const AccelerateRoute = httpRoute({ - method: 'POST', - path: '/api/{coin}/wallet/{walletId}/accelerate', - request: httpRequest({ - params: { - walletId: t.string, - coin: t.string, - }, - body: AccelerateRequest, - }), - response: AccelerateResponse, - description: 'Accelerate transaction', -}); - -/** - * Consolidate funds from multiple addresses in a wallet and sign with user & backup keys in a recovery situation. - * Used for both standard multisig wallets and TSS wallets to consolidate funds from various addresses. - */ -const RecoveryConsolidationsRoute = httpRoute({ - method: 'POST', - path: '/api/{coin}/wallet/recoveryconsolidations', - request: httpRequest({ - params: { - coin: t.string, - }, - body: RecoveryConsolidationsWalletRequest, - }), - response: RecoveryConsolidationsWalletResponse, - description: 'Consolidate and recover an existing wallet', -}); - -/** - * Recover funds from an existing wallet using user and backup keys. - * This endpoint allows for both standard multisig and TSS wallet recovery. - */ -const RecoveryRoute = httpRoute({ - method: 'POST', - path: '/api/{coin}/wallet/recovery', - request: httpRequest({ - params: { - coin: t.string, - }, - body: RecoveryWalletRequest, - }), - response: RecoveryWalletResponse, - description: 'Recover an existing wallet', -}); - // API Specification export const MasterBitGoExpressApiSpec = apiSpec({ 'v1.wallet.generate': { - post: httpRoute({ - method: 'POST' as const, - path: '/api/{coin}/wallet/generate', - request: httpRequest({ - params: { - coin: t.string, - }, - body: GenerateWalletRequest, - }), - response: GenerateWalletResponse, - description: 'Generate a new wallet', - }), + post: WalletGenerateRoute, }, 'v1.wallet.sendMany': { - post: httpRoute({ - method: 'POST', - path: '/api/{coin}/wallet/{walletId}/sendMany', - request: httpRequest({ - params: { - walletId: t.string, - coin: t.string, - }, - body: SendManyRequest, - }), - response: SendManyResponse, - description: 'Send many transactions', - }), + post: SendManyRoute, }, 'v1.wallet.txrequest.signAndSend': { - post: httpRoute({ - method: 'POST', - path: '/api/{coin}/wallet/{walletId}/txrequest/{txRequestId}/signAndSend', - request: httpRequest({ - params: { - walletId: t.string, - coin: t.string, - txRequestId: t.string, - }, - body: SignMpcRequest, - }), - response: SignMpcResponse, - description: 'Sign MPC with TxRequest', - }), + post: SignAndSendMpcRoute, }, 'v1.wallet.recovery': { post: RecoveryRoute, @@ -749,37 +54,13 @@ export const MasterBitGoExpressApiSpec = apiSpec({ post: RecoveryConsolidationsRoute, }, 'v1.wallet.consolidate': { - post: httpRoute({ - method: 'POST', - path: '/api/{coin}/wallet/{walletId}/consolidate', - request: httpRequest({ - params: { - walletId: t.string, - coin: t.string, - }, - body: ConsolidateRequest, - }), - response: ConsolidateResponse, - description: 'Consolidate addresses', - }), + post: ConsolidateRoute, }, 'v1.wallet.accelerate': { post: AccelerateRoute, }, 'v1.wallet.consolidateunspents': { - post: httpRoute({ - method: 'POST', - path: '/api/{coin}/wallet/{walletId}/consolidateunspents', - request: httpRequest({ - params: { - walletId: t.string, - coin: t.string, - }, - body: ConsolidateUnspentsRequest, - }), - response: ConsolidateUnspentsResponse, - description: 'Consolidate unspents', - }), + post: ConsolidateUnspentsRoute, }, }); diff --git a/src/api/master/routers/recoveryConsolidationsRoute.ts b/src/api/master/routers/recoveryConsolidationsRoute.ts new file mode 100644 index 00000000..4132faab --- /dev/null +++ b/src/api/master/routers/recoveryConsolidationsRoute.ts @@ -0,0 +1,132 @@ +import { httpRequest, HttpResponse, httpRoute, optional } from '@api-ts/io-ts-http'; +import * as t from 'io-ts'; +import { ErrorResponses } from '../../../shared/errors'; + +/** + * Request type for wallet recovery consolidations endpoint. + * Used to consolidate and recover funds from multiple addresses in a wallet, via signing with user and backup keys. + * + * @endpoint POST /api/{coin}/wallet/recoveryconsolidations + * @description Consolidates and recovers funds from multiple addresses in a wallet + */ +const RecoveryConsolidationsWalletRequest = { + /** + * The user's public key for standard multisig wallets. + * Required for onchain multisig recovery consolidations. + * @example "xpub661MyMwAqRbcGCNnmzqt3u5KhxmXBHiC78cwAyUMaKJXpFDfHpJwNap6qpG1Kz2SPexKXy3akhPQz7GDYWpHNWkLxRLj6bDxQSf74aTAP9y" + */ + userPub: optional(t.string), + + /** + * The backup public key for standard multisig wallets. + * Required for onchain multisig recovery consolidations. + * @example "xpub661MyMwAqRbcGCNnmzqt3u5KhxmXBHiC78cwAyUMaKJXpFDfHpJwNap6qpG1Kz2SPexKXy3akhPQz7GDYWpHNWkLxRLj6bDxQSf74aTAP9y" + */ + backupPub: optional(t.string), + + /** + * The BitGo public key for standard multisig wallets. + * Required for onchain UTXO multisig recovery consolidations. + * @example "xpub661MyMwAqRbcGCNnmzqt3u5KhxmXBHiC78cwAyUMaKJXpFDfHpJwNap6qpG1Kz2SPexKXy3akhPQz7GDYWpHNWkLxRLj6bDxQSf74aTAP9y" + */ + bitgoPub: optional(t.string), + + /** + * The type of wallet to recover + * - onchain: Traditional multisig wallets. + * - tss: Threshold Signature Scheme wallets. + * @example "onchain" + */ + multisigType: t.union([t.literal('onchain'), t.literal('tss')]), + + /** + * The common keychain for TSS wallets. + * Required when multisigType is 'tss'. + * @example "0280ec751d3b165a48811b2cc90f90dcf323f33e8bcaadc0341e1e010adcdcf7005afde80dd286d65b6be947af0424dd1e9f7611f3d20e02a4fc84ad8c8b74c1a5" + */ + commonKeychain: optional(t.string), + + /** + * The token contract address for token recovery (e.g., ERC20 tokens on Ethereum or SPL tokens on Solana). + * Required when recovering specific tokens instead of the native coin. + * @example "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" // USDC on Ethereum + * @example "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" // USDC on Solana + */ + tokenContractAddress: optional(t.string), + + /** + * The starting index to scan for addresses to consolidate. + * Useful for limiting the scan range for better performance. + * @example 0 + */ + startingScanIndex: optional(t.number), + + /** + * The ending index to scan for addresses to consolidate. + * Useful for limiting the scan range for better performance. + * @example 100 + * @default 20 + */ + endingScanIndex: optional(t.number), + + /** + * API key for blockchain explorer services. + * Required for some coins to build recovery transactions. + * @example "v2x8d5e46cf15a7b9b7xc60685d4f56xd8bd5f5cdcef3c1e9d4399c955d587179b" + */ + apiKey: optional(t.string), + + /** + * Durable nonces configuration for Solana transactions. + * Provides transaction durability for Solana recovery operations. + * Refer to https://github.com/BitGo/wallet-recovery-wizard/blob/master/DURABLE_NONCE.md on durable nonce creation. + */ + durableNonces: optional( + t.type({ + /** + * The secret key of the durable nonce account. + * @example "3XNrU5JSPs2VnZCLnWK8GDzB6Pqoy3tYNMJJVesKBXnGqRxwdXDg2QKgv7E9a6QbAiKnLHSxysKWgXDKNdfXZCQM" + */ + secretKey: t.string, + + /** + * Array of public keys associated with the durable nonce. + * @example ["BurablNonc1234567890123456789012345678901", "BurablNonc1234567890123456789012345678902"] + */ + publicKeys: t.array(t.string), + }), + ), +}; + +/** + * Response type for the wallet recovery consolidations endpoint + * + * @endpoint POST /api/{coin}/wallet/recoveryconsolidations + * @description Returns the signed consolidation transactions + */ +const RecoveryConsolidationsWalletResponse: HttpResponse = { + /** + * Successful consolidation response. + * Returns an array of consolidation transactions and recovery details. + * The exact structure depends on the coin and recovery type. + */ + 200: t.any, // Complex response structure varies by coin and recovery type + ...ErrorResponses, +}; + +/** + * Consolidate funds from multiple addresses in a wallet and sign with user & backup keys in a recovery situation. + * Used for both standard multisig wallets and TSS wallets to consolidate funds from various addresses. + */ +export const RecoveryConsolidationsRoute = httpRoute({ + method: 'POST', + path: '/api/{coin}/wallet/recoveryconsolidations', + request: httpRequest({ + params: { + coin: t.string, + }, + body: RecoveryConsolidationsWalletRequest, + }), + response: RecoveryConsolidationsWalletResponse, + description: 'Consolidate and recover an existing wallet', +}); diff --git a/src/api/master/routers/recoveryRoute.ts b/src/api/master/routers/recoveryRoute.ts new file mode 100644 index 00000000..75596f96 --- /dev/null +++ b/src/api/master/routers/recoveryRoute.ts @@ -0,0 +1,308 @@ +import { httpRequest, HttpResponse, httpRoute, optional } from '@api-ts/io-ts-http'; +import * as t from 'io-ts'; +import { ErrorResponses } from '../../../shared/errors'; + +/** + * Recovery parameter types used by the wallet recovery endpoints + */ +export const RecoveryParamTypes = { + /** + * UTXO-specific recovery parameters for Bitcoin & Bitcoin-like cryptocurrencies. + * Used for recovering funds from standard multisig wallets on UTXO chains. + * Required when recovering BTC, BCH, LTC, DASH, ZEC, etc. + */ + utxoRecoveryOptions: t.partial({ + /** + * Array of address types to ignore during recovery. + * Useful when you want to exclude specific address types from the recovery process. + * @example ["p2sh-p2wsh", "p2wsh"] + */ + ignoreAddressTypes: t.array(t.string), + /** + * Derivation path for the user key. + * Specifies the HD path to derive the correct user key for signing. + * @example "m/0/0/0/0" + * @default "m/0" + */ + userKeyPath: t.string, + /** + * Fee rate for the recovery transaction in satoshis per byte. + * Higher fee rates result in faster confirmations but higher transaction costs. + * @example 20 // 20 satoshis per byte + */ + feeRate: t.number, + /** + * Number of addresses to scan for funds. + * Higher values will scan more addresses but take longer to complete. + * @example 20 // scan 20 addresses + */ + scan: optional(t.number), + }), + + /** + * EVM-specific recovery parameters for Ethereum and EVM-compatible chains. + * Used for recovering funds from standard multisig wallets on Ethereum and EVM-compatible chains. + * Required when recovering ETH, MATIC, BSC, AVAX C-Chain, etc. + */ + ethLikeRecoveryOptions: t.partial({ + /** + * Gas price in wei for the recovery transaction (for legacy transactions). + * Higher gas prices result in faster confirmations but higher transaction costs. + * @example 50000000000 // 50 Gwei + */ + gasPrice: t.number, + + /** + * Gas limit for the recovery transaction. + * Must be enough to cover the contract execution costs. + * @example 500000 + */ + gasLimit: t.number, + + /** + * EIP-1559 gas parameters for modern Ethereum transactions. + * Required for EIP-1559 compatible networks (Ethereum post-London fork). + */ + eip1559: t.type({ + /** + * Maximum priority fee per gas in wei (tip for miners/validators). + * @example 2000000000 // 2 Gwei + */ + maxPriorityFeePerGas: t.number, + + /** + * Maximum fee per gas in wei (base fee + priority fee). + * @example 50000000000 // 50 Gwei + */ + maxFeePerGas: t.number, + }), + + /** + * Replay protection options for the transaction. + * Required to prevent transaction replay attacks across different chains. + */ + replayProtectionOptions: t.type({ + /** + * Chain ID or name. + * @example 1 // Ethereum Mainnet + * @example "goerli" // Goerli Testnet + */ + chain: t.union([t.string, t.number]), + + /** + * Hardfork name to determine the transaction format. + * @example "london" // Post-London fork (EIP-1559) + * @example "istanbul" // Pre-London fork + * @default "london" + */ + hardfork: t.string, + }), + + /** + * Number of addresses to scan for funds. + * Higher values will scan more addresses but take longer to complete. + * @example 20 // scan 20 addresses + * @default 20 + */ + scan: optional(t.number), + }), + + /** + * Solana-specific recovery parameters. + */ + solanaRecoveryOptions: t.partial({ + /** + * Durable nonce configuration for transaction durability. + * Optional but recommended for recovery operations. + * Refer to https://github.com/BitGo/wallet-recovery-wizard/blob/master/DURABLE_NONCE.md on durable nonce creation. + */ + durableNonce: optional( + t.type({ + /** + * The public key of the durable nonce account. + */ + publicKey: t.string, + /** + * The secret key of the durable nonce account. + */ + secretKey: t.string, + }), + ), + /** + * The token contract address for token recovery. + * Required when recovering tokens. + */ + tokenContractAddress: t.string, + /** + * The close associated token account address. + * Required for token recovery. + */ + closeAtaAddress: t.string, + /** + * The recovery destination's associated token account address. + * Required for token recovery. + */ + recoveryDestinationAtaAddress: t.string, + /** + * The program ID for the token. + * Required for token recovery. + */ + programId: t.string, + }), + + // ECDSA ETH-like recovery specific parameters + ecdsaEthLikeRecoverySpecificParams: t.type({ + walletContractAddress: t.string, + bitgoDestinationAddress: t.string, + apiKey: t.string, + }), + + // ECDSA Cosmos-like recovery specific parameters + ecdsaCosmosLikeRecoverySpecificParams: t.type({ + rootAddress: t.string, + }), +}; + +export type EvmRecoveryOptions = typeof RecoveryParamTypes.ethLikeRecoveryOptions._A; +export type UtxoRecoveryOptions = typeof RecoveryParamTypes.utxoRecoveryOptions._A; +export type SolanaRecoveryOptions = typeof RecoveryParamTypes.solanaRecoveryOptions._A; +export type EcdsaEthLikeRecoverySpecificParams = + typeof RecoveryParamTypes.ecdsaEthLikeRecoverySpecificParams._A; +export type EcdsaCosmosLikeRecoverySpecificParams = + typeof RecoveryParamTypes.ecdsaCosmosLikeRecoverySpecificParams._A; + +// Combined coin specific parameters +const CoinSpecificParams = t.partial({ + utxoRecoveryOptions: RecoveryParamTypes.utxoRecoveryOptions, + evmRecoveryOptions: RecoveryParamTypes.ethLikeRecoveryOptions, + solanaRecoveryOptions: RecoveryParamTypes.solanaRecoveryOptions, + ecdsaEthLikeRecoverySpecificParams: RecoveryParamTypes.ecdsaEthLikeRecoverySpecificParams, + ecdsaCosmosLikeRecoverySpecificParams: RecoveryParamTypes.ecdsaCosmosLikeRecoverySpecificParams, +}); + +export type CoinSpecificParams = t.TypeOf; +export type CoinSpecificParamsUnion = + | EvmRecoveryOptions + | UtxoRecoveryOptions + | SolanaRecoveryOptions + | EcdsaEthLikeRecoverySpecificParams + | EcdsaCosmosLikeRecoverySpecificParams; + +/** + * Response type for the wallet recovery endpoint. + * + * @endpoint POST /api/{coin}/wallet/recovery + * @description Returns the signed recovery transaction that can be broadcast to the network + */ +const RecoveryWalletResponse: HttpResponse = { + /** + * Successful recovery response. + * @returns The signed transaction in hex format + * @example { "txHex": "01000000000101edd7a5d948a6c79f273ce686a6a8f2e96ed8c2583b5e77b866aa2a1b3426fbed0100000000ffffffff02102700000000000017a914192f23283c2a9e6c5d11562db0eb5d4eb47f460287b9bc2c000000000017a9145c139b242ab3701f321d2399d3a11b028b3b361e870247304402206ac9477fece38d96688c6c3719cb27396c0563ead0567457e7e884b406b6da8802201992d1cfa1b55a67ce8acb482e9957812487d2555f5f54fb0286ecd3095d78e4012103c92564575197c4d6e3d9792280e7548b3ba52a432101c62de2186c4e2fa7fc580000000000" } + */ + 200: t.type({ + /** + * The full signed transaction in hexadecimal format. + * This transaction can be broadcast to the network to complete the recovery. + */ + txHex: t.string, + }), + ...ErrorResponses, +}; + +/** + * Request type for the wallet recovery endpoint. + * Used to recover funds from both standard multisig and TSS wallets. + * + * @endpoint POST /api/{coin}/wallet/recovery + * @description Recover funds from a wallet by building a transaction with user and backup keys + */ +const RecoveryWalletRequest = { + /** + * Set to true to perform a TSS (Threshold Signature Scheme) recovery. + * @example true + */ + isTssRecovery: t.union([t.undefined, t.boolean]), + /** + * Parameters specific to TSS recovery. + * Required when isTssRecovery is true. + */ + tssRecoveryParams: optional( + t.type({ + /** + * The common keychain string used for TSS wallets. + * Required for TSS recovery. + * @example "0280ec751d3b165a48811b2cc90f90dcf323f33e8bcaadc0341e1e010adcdcf7005afde80dd286d65b6be947af0424dd1e9f7611f3d20e02a4fc84ad8c8b74c1a5" + */ + commonKeychain: t.string, + }), + ), + /** + * Parameters specific to standard multisig recovery. + * Required when isTssRecovery is false (default). + */ + multiSigRecoveryParams: optional( + t.type({ + /** + * The user's public key. + * @example "xpub661MyMwAqRbcGCNnmzqt3u5KhxmXBHiC78cwAyUMaKJXpFDfHpJwNap6qpG1Kz2SPexKXy3akhPQz7GDYWpHNWkLxRLj6bDxQSf74aTAP9y" + */ + userPub: t.string, + /** + * The backup public key. + * @example "xpub661MyMwAqRbcGCNnmzqt3u5KhxmXBHiC78cwAyUMaKJXpFDfHpJwNap6qpG1Kz2SPexKXy3akhPQz7GDYWpHNWkLxRLj6bDxQSf74aTAP9y" + */ + backupPub: t.string, + /** + * The BitGo public key. + * Required for UTXO coins, optional for others. + * @example "xpub661MyMwAqRbcGCNnmzqt3u5KhxmXBHiC78cwAyUMaKJXpFDfHpJwNap6qpG1Kz2SPexKXy3akhPQz7GDYWpHNWkLxRLj6bDxQSf74aTAP9y" + */ + bitgoPub: t.string, + /** + * The wallet contract address. + * Required for ETH-like recoveries. + * @example "0x1234567890123456789012345678901234567890" + */ + walletContractAddress: t.string, + }), + ), + /** + * The address where recovered funds will be sent. + * Must be a valid address for the coin being recovered. + * @example "2N8ryDAob6Qn8uCsWvkkQDhyeCQTqybGUFe" // For BTC + * @example "0x1234567890123456789012345678901234567890" // For ETH + * @example "9zvKDB8o96QvToQierXtwSfqK9NqaHw7uvmxWsmSrxns" // For SOL + */ + recoveryDestinationAddress: t.string, + /** + * API Key for a block chain explorer. + * Required for some coins (BTC, ETH) to build a recovery transaction without BitGo. + */ + apiKey: optional(t.string), + /** + * Coin-specific recovery options. + * Different parameters are required based on the coin family: + * - For UTXO coins (BTC, etc): provide utxoRecoveryOptions. + * - For EVM chains (ETH, etc): provide evmRecoveryOptions. + * - For Solana: provide solanaRecoveryOptions. + */ + coinSpecificParams: optional(CoinSpecificParams), +}; + +/** + * Recover funds from an existing wallet using user and backup keys. + * This endpoint allows for both standard multisig and TSS wallet recovery. + */ +export const RecoveryRoute = httpRoute({ + method: 'POST', + path: '/api/{coin}/wallet/recovery', + request: httpRequest({ + params: { + coin: t.string, + }, + body: RecoveryWalletRequest, + }), + response: RecoveryWalletResponse, + description: 'Recover an existing wallet', +}); diff --git a/src/api/master/routers/sendManyRoute.ts b/src/api/master/routers/sendManyRoute.ts new file mode 100644 index 00000000..4edbed68 --- /dev/null +++ b/src/api/master/routers/sendManyRoute.ts @@ -0,0 +1,222 @@ +import { httpRequest, HttpResponse, httpRoute, optional } from '@api-ts/io-ts-http'; +import * as t from 'io-ts'; +import { ErrorResponses } from '../../../shared/errors'; + +export const SendManyRequest = { + /** + * The key to use for signing the transaction + */ + source: t.union([t.literal('user'), t.literal('backup')]), + /** + * Required for transactions from MPC wallets. + */ + type: t.union([ + t.undefined, + t.literal('transfer'), + t.literal('fillNonce'), + t.literal('acceleration'), + t.literal('accountSet'), + t.literal('enabletoken'), + t.literal('transfertoken'), + t.literal('trustline'), + ]), + /** + * List of recipient addresses and amounts to send + */ + recipients: optional( + t.array( + t.type({ + /** + * Destination address + * @maxLength 250 + * @example "2MvrwRYBAuRtPTiZ5MyKg42Ke55W3fZJfZS" + */ + address: t.string, + /** + * The amount in base units (e.g. satoshis) to send. For doge, only string is allowed. + * @example "2000000" + * @pattern ^-?\d+$ + */ + amount: t.union([t.string, t.number]), + }), + ), + ), + /** + * Public key of the key used for signing multisig transactions + * i.e if source is user, this is the user's public key + * if source is backup, this is the backup key's public key + */ + pubkey: optional(t.string), + + /** + * For TSS wallets, this is the common keychain of the wallet, + * it remains the same whether source is user or backup + */ + commonKeychain: optional(t.string), + + /** + * (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`. + * @minimum 2 + * @maximum 1000 + */ + numBlocks: optional(t.number), + + /** + * 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), + + /** + * (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), + + /** + * (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), + + /** + * 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), + + /** + * When set to true, will enforce minConfirms for change outputs. Defaults to false. + * @default false + */ + enforceMinConfirmsForChange: optional(t.boolean), + + /** + * 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. + * @default 1000 + */ + targetWalletUnspents: optional(t.number), + + /** + * 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. + * @maxLength 256 + */ + message: optional(t.string), + + /** + * 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])), + + /** + * 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])), + + /** + * 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), + + /** + * (XRP only) Absolute max ledger the transaction should be accepted in, whereafter it will be rejected + */ + lastLedgerSequence: optional(t.number), + + /** + * (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), + + /** + * Set `true` to disable automatic change splitting. + * Also see: `targetWalletUnspents` + * @default false + */ + noSplitChange: optional(t.boolean), + + /** + * 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)), + + /** + * 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. + * @maxLength 256 + */ + comment: optional(t.string), + + /** + * 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 a custom destination address for the transaction's change output(s) + * @maxLength 250 + */ + changeAddress: optional(t.string), + + /** + * Flag for allowing external change addresses + */ + allowExternalChangeAddress: optional(t.boolean), + + /** + * (DASH only) Specifies whether or not to use Dash's "InstantSend" feature when sending a transaction. + */ + instant: optional(t.boolean), + + /** + * 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(t.string), + + /** + * Transfer ID for the transaction + */ + transferId: optional(t.number), + + /** + * EIP-1559 gas parameters for modern Ethereum transactions + */ + eip1559: optional(t.any), + + /** + * Custom gas limit to be used for sending the transaction. Only for ETH and ERC20 tokens. + */ + gasLimit: optional(t.number), + + /** + * Custodian transaction ID + */ + custodianTransactionId: optional(t.string), + + /** + * (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), +}; + +export const SendManyResponse: HttpResponse = { + 200: t.any, + ...ErrorResponses, +}; + +export const SendManyRoute = httpRoute({ + method: 'POST', + path: '/api/{coin}/wallet/{walletId}/sendMany', + request: httpRequest({ + params: { + walletId: t.string, + coin: t.string, + }, + body: SendManyRequest, + }), + response: SendManyResponse, + description: 'Send many transactions', +}); diff --git a/src/api/master/routers/signAndSendMpcRoute.ts b/src/api/master/routers/signAndSendMpcRoute.ts new file mode 100644 index 00000000..5d5120ed --- /dev/null +++ b/src/api/master/routers/signAndSendMpcRoute.ts @@ -0,0 +1,38 @@ +import { httpRequest, HttpResponse, httpRoute } from '@api-ts/io-ts-http'; +import * as t from 'io-ts'; +import { ErrorResponses } from '../../../shared/errors'; + +export const SignMpcRequest = { + /** + * The key to use for signing the transaction + */ + source: t.union([t.literal('user'), t.literal('backup')]), + /** + * Common keychain of the wallet during wallet creation + */ + commonKeychain: t.string, +}; + +export const SignMpcResponse: HttpResponse = { + 200: t.any, + ...ErrorResponses, +}; + +/** + * Sign a TxRequest and Broadcast it (MPC wallets only) + * This is usually needed after resolving a pending approval for a MPC wallet + */ +export const SignAndSendMpcRoute = httpRoute({ + method: 'POST', + path: '/api/{coin}/wallet/{walletId}/txrequest/{txRequestId}/signAndSend', + request: httpRequest({ + params: { + walletId: t.string, + coin: t.string, + txRequestId: t.string, + }, + body: SignMpcRequest, + }), + response: SignMpcResponse, + description: 'Sign a TxRequest and Broadcast it', +});