@@ -25,6 +25,8 @@ import {
2525 UnexpectedAddressError ,
2626 verifyEddsaTssWalletAddress ,
2727 VerifyTransactionOptions ,
28+ EDDSAUtils ,
29+ decryptKeychainPrivateKey ,
2830} from '@bitgo/sdk-core' ;
2931import { CoinFamily , BaseCoin as StaticsBaseCoin } from '@bitgo/statics' ;
3032import { KeyPair as SubstrateKeyPair , Transaction } from './lib' ;
@@ -38,6 +40,12 @@ import { ApiPromise } from '@polkadot/api';
3840
3941export const DEFAULT_SCAN_FACTOR = 20 ;
4042
43+ /**
44+ * Discriminated union carrying keycard version and decrypted V1 user key (to avoid re-decryption).
45+ * V1 keycards are JSON; V2 keycards are CBOR-encoded reduced key shares.
46+ */
47+ type SubstrateSigningMaterial = { version : 'v1' ; userPrv : string } | { version : 'v2' ; encryptedUserKey : string } ;
48+
4149export class SubstrateCoin extends BaseCoin {
4250 protected readonly _staticsCoin : Readonly < StaticsBaseCoin > ;
4351 readonly MAX_VALIDITY_DURATION = 2400 ;
@@ -356,42 +364,17 @@ export class SubstrateCoin extends BaseCoin {
356364 throw new Error ( 'missing wallet passphrase' ) ;
357365 }
358366
359- const userKey = params . userKey . replace ( / \s / g, '' ) ;
360- const backupKey = params . backupKey . replace ( / \s / g, '' ) ;
361-
362- // Decrypt private keys from KeyCard values
363- let userPrv ;
364- try {
365- userPrv = await this . bitgo . decrypt ( {
366- input : userKey ,
367- password : params . walletPassphrase ,
368- } ) ;
369- } catch ( e ) {
370- throw new Error ( `Error decrypting user keychain: ${ e . message } ` ) ;
371- }
372- const userSigningMaterial = JSON . parse ( userPrv ) as EDDSAMethodTypes . UserSigningMaterial ;
373-
374- let backupPrv ;
375- try {
376- backupPrv = await this . bitgo . decrypt ( {
377- input : backupKey ,
378- password : params . walletPassphrase ,
379- } ) ;
380- } catch ( e ) {
381- throw new Error ( `Error decrypting backup keychain: ${ e . message } ` ) ;
382- }
383- const backupSigningMaterial = JSON . parse ( backupPrv ) as EDDSAMethodTypes . BackupSigningMaterial ;
384-
385- // add signature
386- const signatureHex = await EDDSAMethods . getTSSSignature (
387- userSigningMaterial ,
388- backupSigningMaterial ,
367+ const signingMaterial = await this . isMpcV2Keycard ( params . userKey ! , params . walletPassphrase ! ) ;
368+ await this . addSubstrateRecoverySignature (
369+ txBuilder ,
370+ signingMaterial ,
371+ params . backupKey ! . replace ( / \s / g, '' ) ,
372+ params . walletPassphrase ! ,
373+ unsignedTransaction ,
389374 currPath ,
390- unsignedTransaction
375+ bitgoKey ,
376+ accountId
391377 ) ;
392-
393- const substrateKeyPair = new SubstrateKeyPair ( { pub : accountId } ) ;
394- txBuilder . addSignature ( { pub : substrateKeyPair . getKeys ( ) . pub } , signatureHex ) ;
395378 const signedTransaction = await txBuilder . build ( ) ;
396379 serializedTx = signedTransaction . toBroadcastFormat ( ) ;
397380 } else {
@@ -526,6 +509,110 @@ export class SubstrateCoin extends BaseCoin {
526509 return { transactions : consolidationTransactions , lastScanIndex } ;
527510 }
528511
512+ /**
513+ * Decrypts an encrypted keychain value, wrapping errors with a descriptive message.
514+ */
515+ private async decryptKeychain ( encryptedKey : string , passphrase : string , label : string ) : Promise < string > {
516+ const prv = await decryptKeychainPrivateKey ( this . bitgo , { encryptedPrv : encryptedKey } , passphrase ) ;
517+ if ( ! prv ) {
518+ throw new Error ( `Error decrypting ${ label } keychain: invalid password or corrupted key` ) ;
519+ }
520+ return prv ;
521+ }
522+
523+ /**
524+ * Probes the key format and returns a discriminated union so callers avoid a second decrypt.
525+ * V1 keycards are JSON; V2 keycards are CBOR-encoded reduced key shares.
526+ */
527+ protected async isMpcV2Keycard ( userKey : string , walletPassphrase : string ) : Promise < SubstrateSigningMaterial > {
528+ const normalized = userKey . replace ( / \s / g, '' ) ;
529+ let isV1 : boolean ;
530+ try {
531+ isV1 = await EDDSAUtils . isEddsaMpcV1SigningMaterial ( normalized , walletPassphrase , this . bitgo ) ;
532+ } catch ( e ) {
533+ throw new Error ( `Error decrypting user keychain: ${ e instanceof Error ? e . message : String ( e ) } ` ) ;
534+ }
535+ if ( isV1 ) {
536+ const userPrv = await this . decryptKeychain ( normalized , walletPassphrase , 'user' ) ;
537+ return { version : 'v1' , userPrv } ;
538+ }
539+ return { version : 'v2' , encryptedUserKey : normalized } ;
540+ }
541+
542+ // Protected so tests can stub via instance overrides without adding new test dependencies.
543+ protected async getEddsaMpcV2RecoveryKeyShares (
544+ encryptedUserKey : string ,
545+ encryptedBackupKey : string ,
546+ walletPassphrase : string
547+ ) : ReturnType < typeof EDDSAUtils . getEddsaMpcV2RecoveryKeySharesFromReducedKey > {
548+ return EDDSAUtils . getEddsaMpcV2RecoveryKeySharesFromReducedKey (
549+ encryptedUserKey ,
550+ encryptedBackupKey ,
551+ walletPassphrase ,
552+ this . bitgo
553+ ) ;
554+ }
555+
556+ // Protected so tests can stub via instance overrides without adding new test dependencies.
557+ protected async signEddsaMpcV2Recovery (
558+ signablePayload : Buffer ,
559+ currPath : string ,
560+ ...args : Parameters < typeof EDDSAUtils . signRecoveryEddsaMPCv2 > extends [ Buffer , string , ...infer R ] ? R : never
561+ ) : Promise < Buffer > {
562+ return EDDSAUtils . signRecoveryEddsaMPCv2 ( signablePayload , currPath , ...args ) ;
563+ }
564+
565+ /**
566+ * Adds an MPCv1 or MPCv2 signature to a Substrate transaction builder.
567+ * MPCv2 signatures are prefixed with ED25519_MULTI_SIGNATURE_PREFIX (Ed25519 discriminant
568+ * in the Substrate MultiSignature enum).
569+ */
570+ protected async addSubstrateRecoverySignature (
571+ txBuilder : NativeTransferBuilder ,
572+ signingMaterial : SubstrateSigningMaterial ,
573+ backupKey : string ,
574+ walletPassphrase : string ,
575+ unsignedTransaction : Transaction ,
576+ currPath : string ,
577+ bitgoKey : string ,
578+ accountId : string
579+ ) : Promise < void > {
580+ const ED25519_MULTI_SIGNATURE_PREFIX = 0x00 ;
581+ const substrateKeyPair = new SubstrateKeyPair ( { pub : accountId } ) ;
582+
583+ if ( signingMaterial . version === 'v2' ) {
584+ const { userKeyShare, backupKeyShare, commonKeyChain } = await this . getEddsaMpcV2RecoveryKeyShares (
585+ signingMaterial . encryptedUserKey ,
586+ backupKey ,
587+ walletPassphrase
588+ ) ;
589+ if ( commonKeyChain . toLowerCase ( ) !== bitgoKey . toLowerCase ( ) ) {
590+ throw new Error ( 'EdDSA MPCv2 recovery: commonKeyChain from keycard does not match bitgoKey' ) ;
591+ }
592+ const rawSig = await this . signEddsaMpcV2Recovery (
593+ unsignedTransaction . signablePayload ,
594+ currPath ,
595+ userKeyShare ,
596+ backupKeyShare ,
597+ commonKeyChain
598+ ) ;
599+ const substrateSig = Buffer . concat ( [ Buffer . from ( [ ED25519_MULTI_SIGNATURE_PREFIX ] ) , rawSig ] ) ;
600+ txBuilder . addSignature ( { pub : substrateKeyPair . getKeys ( ) . pub } , substrateSig ) ;
601+ } else {
602+ const userSigningMaterial = JSON . parse ( signingMaterial . userPrv ) as EDDSAMethodTypes . UserSigningMaterial ;
603+ const backupPrv = await this . decryptKeychain ( backupKey , walletPassphrase , 'backup' ) ;
604+ const backupSigningMaterial = JSON . parse ( backupPrv ) as EDDSAMethodTypes . BackupSigningMaterial ;
605+
606+ const signatureHex = await EDDSAMethods . getTSSSignature (
607+ userSigningMaterial ,
608+ backupSigningMaterial ,
609+ currPath ,
610+ unsignedTransaction
611+ ) ;
612+ txBuilder . addSignature ( { pub : substrateKeyPair . getKeys ( ) . pub } , signatureHex ) ;
613+ }
614+ }
615+
529616 /** inherited doc */
530617 async createBroadcastableSweepTransaction ( params : MPCSweepRecoveryOptions ) : Promise < MPCTxs > {
531618 const req = params . signatureShares ;
0 commit comments