Skip to content

Commit f43e93f

Browse files
Marzooqabitgobot
authored andcommitted
feat(sdk-coin-near): 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 f43e93f

2 files changed

Lines changed: 315 additions & 43 deletions

File tree

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

Lines changed: 64 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,7 @@ 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(txBuilder, params, derivationPath, senderAddress, isMpcV2);
553558
return { serializedTx: serializedTx, scanIndex: idx };
554559
}
555560
}
@@ -631,12 +636,11 @@ export class Near extends BaseCoin {
631636
txBuilder: TransactionBuilder,
632637
params: MPCRecoveryOptions,
633638
derivationPath: string,
634-
senderAddress: string
639+
senderAddress: string,
640+
isMpcV2 = false
635641
): Promise<string> {
636642
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
643+
640644
if (!params.userKey) {
641645
throw new Error('missing userKey');
642646
}
@@ -647,49 +651,68 @@ export class Near extends BaseCoin {
647651
throw new Error('missing wallet passphrase');
648652
}
649653

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

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,
657+
let signatureHex: Buffer;
658+
if (isMpcV2) {
659+
signatureHex = await signEddsaMpcV2RecoveryTx({
660+
message: unsignedTransaction.signablePayload,
661+
userKey,
662+
backupKey,
663+
walletPassphrase: params.walletPassphrase,
664+
bitgoKey: params.bitgoKey.replace(/\s/g, ''),
665+
derivationPath,
666+
bitgo: this.bitgo,
660667
});
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;
668+
} else {
669+
let userPrv;
670+
try {
671+
userPrv = await this.bitgo.decrypt({
672+
input: userKey,
673+
password: params.walletPassphrase,
674+
});
675+
} catch (e) {
676+
throw new Error(`Error decrypting user keychain: ${e.message}`);
677+
}
678+
const userSigningMaterial = JSON.parse(userPrv) as EDDSAMethodTypes.UserSigningMaterial;
666679

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);
680+
let backupPrv;
681+
try {
682+
backupPrv = await this.bitgo.decrypt({
683+
input: backupKey,
684+
password: params.walletPassphrase,
685+
});
686+
} catch (e) {
687+
throw new Error(`Error decrypting backup keychain: ${e.message}`);
688+
}
689+
const backupSigningMaterial = JSON.parse(backupPrv) as EDDSAMethodTypes.BackupSigningMaterial;
690+
691+
signatureHex = await EDDSAMethods.getTSSSignature(
692+
userSigningMaterial,
693+
backupSigningMaterial,
694+
derivationPath,
695+
unsignedTransaction
696+
);
697+
}
688698

699+
txBuilder.addSignature({ pub: senderAddress } as PublicKey, signatureHex);
689700
const completedTransaction = await txBuilder.build();
690701
return completedTransaction.toBroadcastFormat();
691702
}
692703

704+
private async isMpcv2SigningMaterial(
705+
userKey?: string,
706+
backupKey?: string,
707+
walletPassphrase?: string
708+
): Promise<boolean> {
709+
if (!walletPassphrase) return false;
710+
if (!userKey) throw new Error('missing userKey');
711+
if (!backupKey) throw new Error('missing backupKey');
712+
const material = await getEddsaSigningMaterial(userKey.replace(/\s/g, ''), walletPassphrase, this.bitgo);
713+
return material.version === 'v2';
714+
}
715+
693716
async createBroadcastableSweepTransaction(params: MPCSweepRecoveryOptions): Promise<MPCTxs> {
694717
const req = params.signatureShares;
695718
const broadcastableTransactions: MPCTx[] = [];

0 commit comments

Comments
 (0)