|
8 | 8 | * --coin tbtc \ |
9 | 9 | * --walletId <walletId> \ |
10 | 10 | * --passphrase <walletPassphrase> \ |
11 | | - * --accessToken <bearerToken> \ |
| 11 | + * --accessToken <sessionToken> \ |
12 | 12 | * [--otp <code>] \ |
13 | 13 | * [--boxD <ciphertext>] \ |
14 | 14 | * [--boxA <ciphertext>] \ |
15 | 15 | * [--boxB <ciphertext>] \ |
16 | 16 | * [--passcodeEncryptionCode <code>] \ |
17 | 17 | * [--dry-run] |
18 | 18 | * |
19 | | - * --accessToken: Short-lived BitGo access token. Generate this using the following guide |
20 | | - * https://developers.bitgo.com/docs/get-started-access-tokens#1-create-short-lived-access-token |
| 19 | + * --accessToken: A session token obtained by logging in via the BitGo API (POST /api/v2/user/login). |
| 20 | + * Note: UI-generated OAuth access tokens lack the scope for the passcoderecovery endpoint. |
| 21 | + * Use a session token from login instead. |
21 | 22 | * --boxD: Box D from the original keycard. Required if the wallet passphrase has been changed since the |
22 | 23 | * wallet was created (i.e. the current passphrase is not the original one used at creation time). |
23 | 24 | * --boxA: Box A from the original keycard. Required for MPCv2 wallets — reducedEncryptedPrv is never stored |
@@ -155,11 +156,11 @@ function parseArgs() { |
155 | 156 | .option('coin', { type: 'string', demandOption: true, description: 'Coin ticker (e.g. tbtc, teth)' }) |
156 | 157 | .option('walletId', { type: 'string', demandOption: true, description: 'Wallet ID' }) |
157 | 158 | .option('passphrase', { type: 'string', demandOption: true, description: 'Current wallet passphrase' }) |
158 | | - .option('accessToken', { type: 'string', demandOption: true, description: 'Short-lived BitGo access token' }) |
| 159 | + .option('accessToken', { type: 'string', demandOption: true, description: 'Session token from POST /api/v2/user/login' }) |
159 | 160 | .option('otp', { type: 'string', description: 'OTP for session unlock' }) |
160 | | - .option('boxD', { type: 'string', description: 'Box D ciphertext from the original keycard' }) |
161 | | - .option('boxA', { type: 'string', description: 'Box A ciphertext from the original keycard (MPCv2)' }) |
162 | | - .option('boxB', { type: 'string', description: 'Box B ciphertext from the original keycard' }) |
| 161 | + .option('boxD', { type: 'string', description: 'Box D ciphertext from the original keycard', coerce: (v: string) => v?.replace(/\s/g, '') }) |
| 162 | + .option('boxA', { type: 'string', description: 'Box A ciphertext from the original keycard (MPCv2)', coerce: (v: string) => v?.replace(/\s/g, '') }) |
| 163 | + .option('boxB', { type: 'string', description: 'Box B ciphertext from the original keycard', coerce: (v: string) => v?.replace(/\s/g, '') }) |
163 | 164 | .option('passcodeEncryptionCode', { |
164 | 165 | type: 'string', |
165 | 166 | description: 'Passcode encryption code (fetched automatically if omitted)', |
@@ -204,16 +205,33 @@ async function main() { |
204 | 205 | if (dryRun) console.log('[dry-run] No changes will be persisted.'); |
205 | 206 |
|
206 | 207 | const bitgo = new BitGo({ env }); |
| 208 | + |
207 | 209 | bitgo.authenticateWithAccessToken({ accessToken }); |
208 | 210 |
|
209 | 211 | if (!dryRun) { |
210 | | - await bitgo.unlock({ otp: otp ?? '0000000', duration: 600 }); |
211 | | - console.log('Session unlocked.'); |
| 212 | + try { |
| 213 | + await bitgo.unlock({ otp: otp ?? '0000000', duration: 600 }); |
| 214 | + console.log('Session unlocked.'); |
| 215 | + } catch (err: unknown) { |
| 216 | + const msg = err instanceof Error ? err.message : String(err); |
| 217 | + if (msg.includes('already unlocked longer')) { |
| 218 | + console.log('Session already unlocked (longer duration) — proceeding.'); |
| 219 | + } else { |
| 220 | + throw err; |
| 221 | + } |
| 222 | + } |
212 | 223 | } |
213 | 224 |
|
214 | 225 | const wallet = await bitgo.coin(coin).wallets().get({ id: walletId }); |
215 | 226 | console.log(`Wallet: ${wallet.label()} (${walletId})`); |
216 | 227 |
|
| 228 | + if (wallet.multisigTypeVersion() === 'MPCv2' && (!boxA || !boxB)) { |
| 229 | + throw new Error( |
| 230 | + 'This is an MPCv2 wallet. --boxA and --boxB are required to re-encrypt the reducedEncryptedPrv ' + |
| 231 | + 'key shares stored on the keycard. Without them the new keycard will be missing Box A and Box B.' |
| 232 | + ); |
| 233 | + } |
| 234 | + |
217 | 235 | // Fetch passcodeEncryptionCode — needed to decrypt Box D (when provided) and to generate Box D |
218 | 236 | // in the new keycard. Always fetched when boxD is provided; otherwise skipped in dry-run. |
219 | 237 | const needsPec = boxD || !dryRun; |
|
0 commit comments