Skip to content

Commit ee14577

Browse files
committed
feat(sdk-coin-dot): add MPCv2 support to recoverConsolidations
Detect signing material once in recoverConsolidations() and thread it into each recover() iteration via an optional precomputedMaterial param, avoiding per-address keycard decryption. Drops DotRecoveryOptions and multisigTypeVersion field in favour of the EddsaSigningMaterial union. Removes non-null assertions and as-any casts throughout. Ticket: WCI-1236
1 parent 725a325 commit ee14577

2 files changed

Lines changed: 350 additions & 40 deletions

File tree

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

Lines changed: 58 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ import {
3030
AuditDecryptedKeyParams,
3131
verifyEddsaTssWalletAddress,
3232
TxIntentMismatchRecipientError,
33+
getEddsaSigningMaterial as sharedGetEddsaSigningMaterial,
34+
signEddsaMpcV2RecoveryTx,
35+
EddsaSigningMaterial,
36+
decryptKeychainPrivateKey,
3337
} from '@bitgo/sdk-core';
3438
import { BaseCoin as StaticsBaseCoin, coins, PolkadotSpecNameType } from '@bitgo/statics';
3539
import {
@@ -347,7 +351,7 @@ export class Dot extends BaseCoin {
347351
* @returns {MPCTx} the serialized transaction hex string and index
348352
* of the address being swept
349353
*/
350-
async recover(params: MPCRecoveryOptions): Promise<MPCTx | MPCSweepTxs> {
354+
async recover(params: MPCRecoveryOptions, precomputedMaterial?: EddsaSigningMaterial): Promise<MPCTx | MPCSweepTxs> {
351355
if (!params.bitgoKey) {
352356
throw new Error('missing bitgoKey');
353357
}
@@ -403,43 +407,43 @@ export class Dot extends BaseCoin {
403407
throw new Error('missing wallet passphrase');
404408
}
405409

406-
// Clean up whitespace from entered values
407410
const userKey = params.userKey.replace(/\s/g, '');
408411
const backupKey = params.backupKey.replace(/\s/g, '');
409-
410-
// Decrypt private keys from KeyCard values
411-
let userPrv;
412-
try {
413-
userPrv = await this.bitgo.decrypt({
414-
input: userKey,
415-
password: params.walletPassphrase,
416-
});
417-
} catch (e) {
418-
throw new Error(`Error decrypting user keychain: ${e.message}`);
419-
}
420-
/** TODO BG-52419 Implement Codec for parsing */
421-
const userSigningMaterial = JSON.parse(userPrv) as EDDSAMethodTypes.UserSigningMaterial;
422-
423-
let backupPrv;
424-
try {
425-
backupPrv = await this.bitgo.decrypt({
426-
input: backupKey,
427-
password: params.walletPassphrase,
412+
const dotKeyPair = new DotKeyPair({ pub: accountId });
413+
const signingMaterial =
414+
precomputedMaterial ?? (await this.getEddsaSigningMaterial(userKey, params.walletPassphrase));
415+
416+
if (signingMaterial.version === 'v2') {
417+
const signature = await this.signDotMpcV2Recovery({
418+
message: unsignedTransaction.signablePayload,
419+
userKey: signingMaterial.encryptedUserKey,
420+
backupKey,
421+
walletPassphrase: params.walletPassphrase,
422+
bitgoKey,
423+
derivationPath: currPath,
424+
bitgo: this.bitgo,
428425
});
429-
} catch (e) {
430-
throw new Error(`Error decrypting backup keychain: ${e.message}`);
426+
txnBuilder.addSignature({ pub: dotKeyPair.getKeys().pub }, signature);
427+
} else {
428+
/** TODO BG-52419 Implement Codec for parsing */
429+
const userSigningMaterial = JSON.parse(signingMaterial.userPrv) as EDDSAMethodTypes.UserSigningMaterial;
430+
const backupPrv = await decryptKeychainPrivateKey(
431+
this.bitgo,
432+
{ encryptedPrv: backupKey },
433+
params.walletPassphrase
434+
);
435+
if (!backupPrv) {
436+
throw new Error('Error decrypting backup keychain: invalid password or corrupted key');
437+
}
438+
const backupSigningMaterial = JSON.parse(backupPrv) as EDDSAMethodTypes.BackupSigningMaterial;
439+
const signatureHex = await EDDSAMethods.getTSSSignature(
440+
userSigningMaterial,
441+
backupSigningMaterial,
442+
currPath,
443+
unsignedTransaction
444+
);
445+
txnBuilder.addSignature({ pub: dotKeyPair.getKeys().pub }, signatureHex);
431446
}
432-
const backupSigningMaterial = JSON.parse(backupPrv) as EDDSAMethodTypes.BackupSigningMaterial;
433-
434-
// add signature
435-
const signatureHex = await EDDSAMethods.getTSSSignature(
436-
userSigningMaterial,
437-
backupSigningMaterial,
438-
currPath,
439-
unsignedTransaction
440-
);
441-
const dotKeyPair = new DotKeyPair({ pub: accountId });
442-
txnBuilder.addSignature({ pub: dotKeyPair.getKeys().pub }, signatureHex);
443447
const signedTransaction = await txnBuilder.build();
444448
serializedTx = signedTransaction.toBroadcastFormat();
445449
} else {
@@ -506,16 +510,24 @@ export class Dot extends BaseCoin {
506510
}
507511

508512
const bitgoKey = params.bitgoKey.replace(/\s/g, '');
509-
const MPC = await EDDSAMethods.getInitializedMpcInstance();
513+
const userKey = params.userKey?.replace(/\s/g, '') ?? '';
514+
515+
// Detect signing material once to avoid re-decrypting the keycard on every loop iteration.
516+
const signingMaterial = params.walletPassphrase
517+
? await this.getEddsaSigningMaterial(userKey, params.walletPassphrase)
518+
: undefined;
519+
510520
const baseIndex = 0;
511521
const basePath = params.seed ? getDerivationPath(params.seed) + `/${baseIndex}` : `m/${baseIndex}`;
512-
const accountId = MPC.deriveUnhardened(bitgoKey, basePath).slice(0, 64);
522+
const accountId = (await EDDSAMethods.getInitializedMpcInstance())
523+
.deriveUnhardened(bitgoKey, basePath)
524+
.slice(0, 64);
513525
const baseAddress = this.getAddressFromPublicKey(accountId);
514526

515527
const consolidationTransactions: any[] = [];
516528
let lastScanIndex = startIdx;
517529
for (let i = startIdx; i < endIdx; i++) {
518-
const recoverParams = {
530+
const recoverParams: MPCRecoveryOptions = {
519531
userKey: params.userKey,
520532
backupKey: params.backupKey,
521533
bitgoKey: params.bitgoKey,
@@ -527,7 +539,7 @@ export class Dot extends BaseCoin {
527539

528540
let recoveryTransaction;
529541
try {
530-
recoveryTransaction = await this.recover(recoverParams);
542+
recoveryTransaction = await this.recover(recoverParams, signingMaterial);
531543
} catch (e) {
532544
if (e.message === 'Did not find address with funds to recover') {
533545
lastScanIndex = i;
@@ -742,6 +754,14 @@ export class Dot extends BaseCoin {
742754
return new TransactionBuilderFactory(coins.get(this.getChain()));
743755
}
744756

757+
protected async getEddsaSigningMaterial(userKey: string, walletPassphrase: string): Promise<EddsaSigningMaterial> {
758+
return sharedGetEddsaSigningMaterial(userKey.replace(/\s/g, ''), walletPassphrase, this.bitgo);
759+
}
760+
761+
protected async signDotMpcV2Recovery(params: Parameters<typeof signEddsaMpcV2RecoveryTx>[0]): Promise<Buffer> {
762+
return signEddsaMpcV2RecoveryTx(params);
763+
}
764+
745765
/** @inheritDoc */
746766
auditDecryptedKey({ publicKey, prv, multiSigType }: AuditDecryptedKeyParams) {
747767
if (multiSigType !== 'tss') {

0 commit comments

Comments
 (0)