MPC Error Recovery
Overview
When withdrawing from an MPC wallet, you may encounter a server-side error during the MPC signing protocol. Depending on the status of the transaction request, you can recover the transaction and successfully complete and broadcast it. Configuring error handling in your server-side application helps prevent nonce holes from occurring and minimizes manual interventions should signing errors occur.
BitGo enables a systematic approach to transaction recovery using a unique sequenceId per transaction request.
Prerequisites
- Get Started.
- Ensure your
.env
files is properly configured and all dependencies are installed (see Install SDK). - Ensure BitGo Express is running (see Install BitGo Express).
- Ensure your
- Initiate a withdrawal with a unique
sequenceId
(see Withdraw Overview).
1. Check Status
Use the sequenceId
to check the status of a failed transaction request.
Endpoint: Get Transaction Requests by Wallet
1 2 3 4 5 6 7 8 9
export COIN="<ASSET_ID>" export WALLET_ID="<YOUR_WALLET_ID>" export SEQUENCE_ID="<YOUR_SEQUENCE_ID>" export ACCESS_TOKEN="<YOUR_ACCESS_TOKEN>" curl -X GET \ "https://app.bitgo-test.com/api/v2/$COIN/wallet/$WALLET_ID/txrequests?sequenceId=$SEQUENCE_ID" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $ACCESS_TOKEN"
Step Result
If the sequenceId
isn't registered with BitGo, you can safely retry the original withdrawal.
However, if the as sequenceId
is registered with BitGo and the state
response field is pendingDelivery
, then continue following the steps on this page to rebuild, re-sign, and send the transaction request.
The following is an abbreviated response:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
{ "txRequests": [ { "intent": { "selfSend": true, "intentType": "payment", "recipients": [ { "address": { "address": "string", "option": {} }, "data": "string" } ], "sequenceId": "string", "comment": "string", "nonce": "string", "receiveAddress": "string", }, "apiVersion": "full", "txRequestId": "123e4567-e89b-12d3-a456-426614174000", "idempotencyKey": "string", "walletId": "59cd72485007a239fb00282ed480da1f", "walletType": "hot", "version": 0, "enterpriseId": "59cd72485007a239fb00282ed480da1f", "state": "pendingDelivery", // must be pendingDelivery "date": "2025-04-24T00:00:00.000Z", "createdDate": "2025-04-24T00:00:00.000Z", "userId": "string", "initiatedBy": "string", "pendingApprovalId": "string", "policiesChecked": true, } ] }
2. Rebuild Transaction Request
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276
// The following libraries are required: import dotenv from "dotenv"; dotenv.config(); import * as z from "zod"; import axios from "axios"; import cuid from "cuid"; import readline from "readline"; // The following environment variables and utility functions are required: const env = z .object({ ACCESS_TOKEN: z.string().min(1), ENV: z.union([z.literal("test"), z.literal("prod")]), WALLET_PASSPHRASE: z.string().min(1), WALLET_ID: z.string(), ENTERPRISE_ID: z.string(), BITGO_EXPRESS_URL: z.string().default("http://localhost:3080"), }) .parse(process.env); const headers = { Authorization: `Bearer ${env.ACCESS_TOKEN}`, }; const api = axios.create({ headers, baseURL: env.BITGO_EXPRESS_URL, }); async function doWithRetry( fn: () => Promise<void>, maxAttempts: number = 3, attemptCount: number = 1 ) { try { return await fn(); } catch (e) { console.log(`Attempt ${attemptCount} failed with error, Retrying...`, e); if (attemptCount === maxAttempts) { console.log("Max attempts reached. Exiting..."); throw e; } return await doWithRetry(fn, maxAttempts, attemptCount + 1); } } /** * * Send a transaction via express * @param walletId Wallet ID * @param coin wallet coin * @param sequenceId Unique identifier for this transaction * @param destination Destination address * @param amount Amount to send (default: 1 base unit (lamport, satoshi, etc.)) * @returns {Promise<void>} */ async function sendWithdrawal( enterpriseId: string, walletId: string, walletPassphrase: string, coin: string, sequenceId: string, destination: string, amount: number = 1 ) { console.log(`Sending a ${amount} base units of ${coin} to ${destination}`); const sendManyUrl = `/api/v2/${coin}/wallet/${walletId}/sendmany`; const sendManyData = { sequenceId: sequenceId, apiVersion: "full", recipients: [ { address: destination, amount: amount.toString(), }, ], type: "transfer", walletPassphrase: walletPassphrase, enterprise: enterpriseId, }; // This may throw an error! // For this example, we force a failure in the local express instance try { await api.post(sendManyUrl, sendManyData); } catch (e: any) { console.log( "Got error while sending transaction: ", e.response?.data?.error ); throw e; } } /** * Rebuilds, signs, and broadcasts an existing transaction. * @param walletId Wallet ID * @param txRequestId ID of the transaction request that needs to be retried * @param walletPassphrase Wallet passphrase * @returns {Promise<void>} */ async function retryWithdrawal( walletId: string, walletPassphrase: string, coin: string, txRequestId: string ) { if (!txRequestId) { throw new Error("Transaction ID (txRequestId) is required for retry mode"); } console.log(`Retrying signature for transaction with ID: ${txRequestId}`); // Rebuild transaction & Reset signing steps const rebuildUrl = `/api/v2/wallet/${walletId}/txrequests/${txRequestId}/signatureshares`; await api.delete(rebuildUrl); console.log("Transaction rebuilt successfully"); // Sign const signUrl = `/api/v2/${coin}/wallet/${walletId}/signtxtss`; const signData = { walletPassphrase: walletPassphrase, txPrebuild: { txRequestId: txRequestId, }, }; const signResponse = await api.post(signUrl, signData); console.log("Transaction signed successfully:", signResponse.data); } /** * Implements a generic retry mechanism for any withdrawal errors. * Any errors get safely retried 3 times. * Failures beyond 3 retries are logged and the script will exit. * @param walletId * @param walletPassphrase * @param coin * @param destination * @param amount * @param sequenceId * @returns */ async function handleWithdrawalError( walletId: string, walletPassphrase: string, coin: string, destination: string, amount: number, sequenceId: string ): Promise<void> { await new Promise((resolve) => { rl.question( "Sending transaction failed! When ready press Enter to start the retry process...", () => { rl.close(); resolve(null); } ); }); const listTxRequestResponse = await api.get( `/api/v2/wallet/${walletId}/txrequests`, { params: { sequenceIds: [sequenceId], latest: true } } ); const txRequests = await listTxRequestResponse.data.txRequests; if (txRequests.length > 1) { console.error( `Found ${txRequests.length} transaction requests with sequence ID: ${sequenceId}` ); console.error("This is unexpected, and should be reported as a bug."); return; } const sequenceIdExists = txRequests.length >= 0; if (!sequenceIdExists) { console.log( "sequenceId is not used, transaction can safely be re-tried with the same sequenceId." ); return await doWithRetry(async () => { console.log("Retrying transaction with the same sequenceId..."); return await processWithdrawal( env.WALLET_ID, destination, amount, sequenceId ); }); } else { const txRequest = txRequests[0]; const { txRequestId, state } = txRequest; console.log(`Found txRequest ${txRequestId} sequenceId: ${sequenceId}`); if (["delivered", "canceled", "rejected", "failed"].includes(state)) { console.log( `Transaction ${txRequestId} is in terminal state ${state}. Exiting...` ); return; } else if (state === "pendingApproval") { console.log(`Transaction ${txRequestId} is pending approval. Exiting...`); return; } return await doWithRetry(async () => { console.log(`Rebuilding and re-signing transaction...`); return await retryWithdrawal( walletId, walletPassphrase, coin, txRequestId ); }); } } async function processWithdrawal( walletId: string, destination: string, amount: number, sequenceId: string ): Promise<void> { const getWalletResponse = await api.get(`/api/v2/wallet/${walletId}`, { params: { allTokens: false, unspentCount: false, expandAdvancedWhitelist: false, includeStakingBalances: false, includeBalance: false, }, }); const { label, coin } = getWalletResponse.data; console.log(`Starting script for wallet: ${env.WALLET_ID} ${label} ${coin}`); try { await sendWithdrawal( env.ENTERPRISE_ID, env.WALLET_ID, env.WALLET_PASSPHRASE, coin, sequenceId, destination, amount ); console.log(`Transaction sent successfully, no retries required.`); } catch (e) { console.log("Got error while sending transaction, attempting to recover"); await handleWithdrawalError( env.WALLET_ID, env.WALLET_PASSPHRASE, coin, destination, amount, sequenceId ); } finally { console.log("Script finished."); } } // The following is the `main` function that handles command line arguments and calls the `processWithdrawal` function: async function main() { // Parse command line arguments const args = process.argv.slice(2); if (!args.length || args.length < 2 || args[0] == "help") { console.log( "Usage: ts-node index.ts <destination> <amount> <sequenceId>" ); console.log("Example: ts-node index.ts 2N4v... 1 12345"); console.log("Example: ts-node index.ts help"); process.exit(0); } const destination = args[0]; const amount = args[1] ? parseInt(args[1], 10) : 1; const sequenceId = args[2] || cuid(); await processWithdrawal(env.WALLET_ID, destination, amount, sequenceId); } main() .catch((err) => console.error(err)) .finally(() => process.exit(0));
Next Steps
Confirm the status of your transaction request by calling the Get Transaction Requests by Wallet endpoint.