Skip to content

Commit e2660bc

Browse files
Marzooqabitgobot
authored andcommitted
feat(sdk-coin-near): add MPCv2 signed hot recovery
Add MPCv2 detection and signing to Near.recover() alongside the existing MPCv1 path. What changed: - Import getEddsaSigningMaterial and signEddsaMpcV2RecoveryTx from @bitgo/sdk-core in near.ts - Add isMpcv2SigningMaterial() private method that decrypts the user keycard once and returns true when the plaintext is CBOR (MPCv2) - Refactor signRecoveryTransaction() to accept an isMpcV2 boolean; when true it calls signEddsaMpcV2RecoveryTx (MPS DSG) instead of the legacy EDDSAMethods.getTSSSignature path - Call isMpcv2SigningMaterial() once at the top of recover() and thread the isMpcV2 flag into both the native NEAR and NEP141 FT token paths - Add three new unit tests: native MPCv2 signed recovery, NEP141 FT token MPCv2 signed recovery, and bitgoKey/commonKeyChain mismatch Why: NEAR wallets provisioned with the new Silence Labs (MPCv2) key material cannot be recovered with the Zengo-era getTSSSignature path because the keycard format is different (CBOR base64 vs JSON uShare/yShare). This adds the same MPCv2 detection+signing path that was introduced for SOL in WCI-398, enabling hot recovery for MPCv2 NEAR wallets without any new caller-visible parameters. Ticket: WCI-1223 Session-Id: 8de2a998-4754-4499-82b5-49167b8d9fd6 Task-Id: 7e0eb924-0c33-4cc6-9402-c71587bb14a5
1 parent 91c613d commit e2660bc

2 files changed

Lines changed: 321 additions & 43 deletions

File tree

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

Lines changed: 70 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
EDDSAMethods,
1919
EDDSAMethodTypes,
2020
Environments,
21+
getEddsaSigningMaterial,
2122
KeyPair,
2223
MPCAlgorithm,
2324
MPCRecoveryOptions,
@@ -32,6 +33,7 @@ import {
3233
ParseTransactionOptions as BaseParseTransactionOptions,
3334
PublicKey,
3435
RecoveryTxRequest,
36+
signEddsaMpcV2RecoveryTx,
3537
SignedTransaction,
3638
SignTransactionOptions as BaseSignTransactionOptions,
3739
TokenEnablementConfig,
@@ -365,6 +367,7 @@ export class Near extends BaseCoin {
365367
}
366368
const bitgoKey = params.bitgoKey.replace(/\s/g, '');
367369
const isUnsignedSweep = !params.userKey && !params.backupKey && !params.walletPassphrase;
370+
const isMpcV2 = await this.isMpcv2SigningMaterial(params.userKey, params.backupKey, params.walletPassphrase);
368371
const MPC = await EDDSAMethods.getInitializedMpcInstance();
369372
const { storageAmountPerByte, transferCost, receiptConfig } = await this.getProtocolConfig();
370373
let isStorageDepositEnabled = false;
@@ -440,7 +443,8 @@ export class Near extends BaseCoin {
440443
bitgoKey,
441444
isStorageDepositEnabled,
442445
availableTokenBalance,
443-
isUnsignedSweep
446+
isUnsignedSweep,
447+
isMpcV2
444448
);
445449
}
446450

@@ -474,7 +478,7 @@ export class Near extends BaseCoin {
474478
const unsignedTransaction = (await txBuilder.build()) as Transaction;
475479
let serializedTx = unsignedTransaction.toBroadcastFormat();
476480
if (!isUnsignedSweep) {
477-
serializedTx = await this.signRecoveryTransaction(txBuilder, params, currPath, accountId);
481+
serializedTx = await this.signRecoveryTransaction(txBuilder, params, currPath, accountId, isMpcV2);
478482
} else {
479483
return this.buildUnsignedSweepTransaction(
480484
txBuilder,
@@ -514,7 +518,8 @@ export class Near extends BaseCoin {
514518
bitgoKey: string,
515519
isStorageDepositEnabled: boolean,
516520
availableTokenBalance: BigNumber,
517-
isUnsignedSweep: boolean
521+
isUnsignedSweep: boolean,
522+
isMpcV2 = false
518523
): Promise<MPCTx | MPCSweepTxs> {
519524
const factory = new TransactionBuilderFactory(token);
520525
const bs58EncodedPublicKey = nearAPI.utils.serialize.base_encode(new Uint8Array(Buffer.from(senderAddress, 'hex')));
@@ -549,7 +554,13 @@ export class Near extends BaseCoin {
549554
token
550555
);
551556
} else {
552-
const serializedTx = await this.signRecoveryTransaction(txBuilder, params, derivationPath, senderAddress);
557+
const serializedTx = await this.signRecoveryTransaction(
558+
txBuilder,
559+
params,
560+
derivationPath,
561+
senderAddress,
562+
isMpcV2
563+
);
553564
return { serializedTx: serializedTx, scanIndex: idx };
554565
}
555566
}
@@ -631,12 +642,11 @@ export class Near extends BaseCoin {
631642
txBuilder: TransactionBuilder,
632643
params: MPCRecoveryOptions,
633644
derivationPath: string,
634-
senderAddress: string
645+
senderAddress: string,
646+
isMpcV2 = false
635647
): Promise<string> {
636648
const unsignedTransaction = (await txBuilder.build()) as Transaction;
637-
// Sign the txn
638-
/* ***************** START **************************************/
639-
// TODO(BG-51092): This looks like a common part which can be extracted out too
649+
640650
if (!params.userKey) {
641651
throw new Error('missing userKey');
642652
}
@@ -647,49 +657,68 @@ export class Near extends BaseCoin {
647657
throw new Error('missing wallet passphrase');
648658
}
649659

650-
// Clean up whitespace from entered values
651660
const userKey = params.userKey.replace(/\s/g, '');
652661
const backupKey = params.backupKey.replace(/\s/g, '');
653662

654-
// Decrypt private keys from KeyCard values
655-
let userPrv;
656-
try {
657-
userPrv = await this.bitgo.decrypt({
658-
input: userKey,
659-
password: params.walletPassphrase,
663+
let signatureHex: Buffer;
664+
if (isMpcV2) {
665+
signatureHex = await signEddsaMpcV2RecoveryTx({
666+
message: unsignedTransaction.signablePayload,
667+
userKey,
668+
backupKey,
669+
walletPassphrase: params.walletPassphrase,
670+
bitgoKey: params.bitgoKey.replace(/\s/g, ''),
671+
derivationPath,
672+
bitgo: this.bitgo,
660673
});
661-
} catch (e) {
662-
throw new Error(`Error decrypting user keychain: ${e.message}`);
663-
}
664-
/** TODO BG-52419 Implement Codec for parsing */
665-
const userSigningMaterial = JSON.parse(userPrv) as EDDSAMethodTypes.UserSigningMaterial;
674+
} else {
675+
let userPrv;
676+
try {
677+
userPrv = await this.bitgo.decrypt({
678+
input: userKey,
679+
password: params.walletPassphrase,
680+
});
681+
} catch (e) {
682+
throw new Error(`Error decrypting user keychain: ${e.message}`);
683+
}
684+
const userSigningMaterial = JSON.parse(userPrv) as EDDSAMethodTypes.UserSigningMaterial;
666685

667-
let backupPrv;
668-
try {
669-
backupPrv = await this.bitgo.decrypt({
670-
input: backupKey,
671-
password: params.walletPassphrase,
672-
});
673-
} catch (e) {
674-
throw new Error(`Error decrypting backup keychain: ${e.message}`);
675-
}
676-
const backupSigningMaterial = JSON.parse(backupPrv) as EDDSAMethodTypes.BackupSigningMaterial;
677-
/* ********************** END ***********************************/
678-
679-
// add signature
680-
const signatureHex = await EDDSAMethods.getTSSSignature(
681-
userSigningMaterial,
682-
backupSigningMaterial,
683-
derivationPath,
684-
unsignedTransaction
685-
);
686-
const publicKeyObj = { pub: senderAddress };
687-
txBuilder.addSignature(publicKeyObj as PublicKey, signatureHex);
686+
let backupPrv;
687+
try {
688+
backupPrv = await this.bitgo.decrypt({
689+
input: backupKey,
690+
password: params.walletPassphrase,
691+
});
692+
} catch (e) {
693+
throw new Error(`Error decrypting backup keychain: ${e.message}`);
694+
}
695+
const backupSigningMaterial = JSON.parse(backupPrv) as EDDSAMethodTypes.BackupSigningMaterial;
688696

697+
signatureHex = await EDDSAMethods.getTSSSignature(
698+
userSigningMaterial,
699+
backupSigningMaterial,
700+
derivationPath,
701+
unsignedTransaction
702+
);
703+
}
704+
705+
txBuilder.addSignature({ pub: senderAddress } as PublicKey, signatureHex);
689706
const completedTransaction = await txBuilder.build();
690707
return completedTransaction.toBroadcastFormat();
691708
}
692709

710+
private async isMpcv2SigningMaterial(
711+
userKey?: string,
712+
backupKey?: string,
713+
walletPassphrase?: string
714+
): Promise<boolean> {
715+
if (!walletPassphrase) return false;
716+
if (!userKey) throw new Error('missing userKey');
717+
if (!backupKey) throw new Error('missing backupKey');
718+
const material = await getEddsaSigningMaterial(userKey.replace(/\s/g, ''), walletPassphrase, this.bitgo);
719+
return material.version === 'v2';
720+
}
721+
693722
async createBroadcastableSweepTransaction(params: MPCSweepRecoveryOptions): Promise<MPCTxs> {
694723
const req = params.signatureShares;
695724
const broadcastableTransactions: MPCTx[] = [];

0 commit comments

Comments
 (0)