Skip to content

Commit 73e24f9

Browse files
authored
Merge pull request #9497 from BitGo/feat/sdk-coin-ada/WCI-1232-mpcv2-recover-consolidations
feat(sdk-coin-ada): add MPCv2 support to recoverConsolidations
2 parents 37281ad + 9b72678 commit 73e24f9

2 files changed

Lines changed: 387 additions & 44 deletions

File tree

modules/sdk-coin-ada/src/ada.ts

Lines changed: 64 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ import {
3333
AuditDecryptedKeyParams,
3434
extractCommonKeychain,
3535
TssVerifyAddressOptions,
36+
getEddsaSigningMaterial as sharedGetEddsaSigningMaterial,
37+
signEddsaMpcV2RecoveryTx,
38+
EddsaSigningMaterial,
39+
decryptKeychainPrivateKey,
3640
} from '@bitgo/sdk-core';
3741
import { KeyPair as AdaKeyPair, Transaction, TransactionBuilderFactory, Utils } from './lib';
3842
import type { Asset } from './lib/transaction';
@@ -370,7 +374,7 @@ export class Ada extends BaseCoin {
370374
* @returns {MPCTx | MPCSweepTxs} array of the serialized transaction hex strings and indices
371375
* of the addresses being swept
372376
*/
373-
async recover(params: MPCRecoveryOptions): Promise<MPCTx | MPCSweepTxs> {
377+
async recover(params: MPCRecoveryOptions, precomputedMaterial?: EddsaSigningMaterial): Promise<MPCTx | MPCSweepTxs> {
374378
if (!params.bitgoKey) {
375379
throw new Error('missing bitgoKey');
376380
}
@@ -447,53 +451,50 @@ export class Ada extends BaseCoin {
447451

448452
let serializedTx = unsignedTransaction.toBroadcastFormat();
449453
if (!isUnsignedSweep) {
450-
if (!params.userKey) {
451-
throw new Error('missing userKey');
452-
}
453-
if (!params.backupKey) {
454-
throw new Error('missing backupKey');
455-
}
456-
if (!params.walletPassphrase) {
457-
throw new Error('missing wallet passphrase');
458-
}
454+
assert(params.userKey, 'missing userKey');
455+
assert(params.backupKey, 'missing backupKey');
456+
assert(params.walletPassphrase, 'missing wallet passphrase');
459457

460458
// Clean up whitespace from entered values
461459
const userKey = params.userKey.replace(/\s/g, '');
462460
const backupKey = params.backupKey.replace(/\s/g, '');
463-
464-
// Decrypt private keys from KeyCard values
465-
let userPrv;
466-
try {
467-
userPrv = await this.bitgo.decrypt({
468-
input: userKey,
469-
password: params.walletPassphrase,
470-
});
471-
} catch (e) {
472-
throw new Error(`Error decrypting user keychain: ${e.message}`);
473-
}
474-
/** TODO BG-52419 Implement Codec for parsing */
475-
const userSigningMaterial = JSON.parse(userPrv) as EDDSAMethodTypes.UserSigningMaterial;
476-
477-
let backupPrv;
478-
try {
479-
backupPrv = await this.bitgo.decrypt({
480-
input: backupKey,
481-
password: params.walletPassphrase,
461+
const adaKeyPair = new AdaKeyPair({ pub: accountId });
462+
const signingMaterial =
463+
precomputedMaterial ?? (await this.getEddsaSigningMaterial(userKey, params.walletPassphrase));
464+
465+
if (signingMaterial.version === 'v2') {
466+
const signature = await this.signAdaMpcV2Recovery({
467+
message: unsignedTransaction.signablePayload,
468+
userKey: signingMaterial.encryptedUserKey,
469+
backupKey,
470+
walletPassphrase: params.walletPassphrase,
471+
bitgoKey,
472+
derivationPath: currPath,
473+
bitgo: this.bitgo,
482474
});
483-
} catch (e) {
484-
throw new Error(`Error decrypting backup keychain: ${e.message}`);
475+
txBuilder.addSignature({ pub: adaKeyPair.getKeys().pub }, signature);
476+
} else {
477+
/** TODO BG-52419 Implement Codec for parsing */
478+
const userSigningMaterial = JSON.parse(signingMaterial.userPrv) as EDDSAMethodTypes.UserSigningMaterial;
479+
const backupPrv = await decryptKeychainPrivateKey(
480+
this.bitgo,
481+
{ encryptedPrv: backupKey },
482+
params.walletPassphrase
483+
);
484+
if (!backupPrv) {
485+
throw new Error('Error decrypting backup keychain: invalid password or corrupted key');
486+
}
487+
const backupSigningMaterial = JSON.parse(backupPrv) as EDDSAMethodTypes.BackupSigningMaterial;
488+
489+
// add signature
490+
const signatureHex = await EDDSAMethods.getTSSSignature(
491+
userSigningMaterial,
492+
backupSigningMaterial,
493+
currPath,
494+
unsignedTransaction
495+
);
496+
txBuilder.addSignature({ pub: adaKeyPair.getKeys().pub }, signatureHex);
485497
}
486-
const backupSigningMaterial = JSON.parse(backupPrv) as EDDSAMethodTypes.BackupSigningMaterial;
487-
488-
// add signature
489-
const signatureHex = await EDDSAMethods.getTSSSignature(
490-
userSigningMaterial,
491-
backupSigningMaterial,
492-
currPath,
493-
unsignedTransaction
494-
);
495-
const adaKeyPair = new AdaKeyPair({ pub: accountId });
496-
txBuilder.addSignature({ pub: adaKeyPair.getKeys().pub }, signatureHex);
497498
const signedTransaction = await txBuilder.build();
498499
serializedTx = signedTransaction.toBroadcastFormat();
499500
} else {
@@ -574,6 +575,14 @@ export class Ada extends BaseCoin {
574575
seed: params.seed,
575576
};
576577
const { address: baseAddress } = await this.getAdaAddressAndAccountId(addressParams);
578+
579+
// Detect signing material once to avoid re-decrypting the keycard on every loop iteration.
580+
let signingMaterial: EddsaSigningMaterial | undefined;
581+
if (params.walletPassphrase) {
582+
assert(params.userKey, 'missing userKey');
583+
signingMaterial = await this.getEddsaSigningMaterial(params.userKey, params.walletPassphrase);
584+
}
585+
577586
const consolidationTransactions: any[] = [];
578587
let lastScanIndex = startIdx;
579588
for (let i = startIdx; i < endIdx; i++) {
@@ -589,7 +598,7 @@ export class Ada extends BaseCoin {
589598

590599
let recoveryTransaction;
591600
try {
592-
recoveryTransaction = await this.recover(recoverParams);
601+
recoveryTransaction = await this.recover(recoverParams, signingMaterial);
593602
} catch (e) {
594603
if (
595604
e.message === 'Did not find address with funds to recover.' ||
@@ -690,6 +699,18 @@ export class Ada extends BaseCoin {
690699
return new TransactionBuilderFactory(coins.get(this.getBaseChain()));
691700
}
692701

702+
/**
703+
* Detects whether a keycard's decrypted plaintext is MPCv1 JSON or MPCv2 CBOR.
704+
* Unsigned sweeps (no walletPassphrase) have no keycard to inspect and default to MPCv1.
705+
*/
706+
protected async getEddsaSigningMaterial(userKey: string, walletPassphrase: string): Promise<EddsaSigningMaterial> {
707+
return sharedGetEddsaSigningMaterial(userKey.replace(/\s/g, ''), walletPassphrase, this.bitgo);
708+
}
709+
710+
protected async signAdaMpcV2Recovery(params: Parameters<typeof signEddsaMpcV2RecoveryTx>[0]): Promise<Buffer> {
711+
return signEddsaMpcV2RecoveryTx(params);
712+
}
713+
693714
/** inherited doc */
694715
setCoinSpecificFieldsInIntent(intent: PopulatedIntent, params: PrebuildTransactionWithIntentOptions): void {
695716
intent.unspents = params.unspents;

0 commit comments

Comments
 (0)