From bc52daa83fe92619e076fa010c7838795f91d329 Mon Sep 17 00:00:00 2001 From: "vibhavgopalkrishna145@bitgo.com" Date: Thu, 25 Jun 2026 07:19:38 +0000 Subject: [PATCH] feat(sdk-coin-sol): add MPCv2 support to recoverNestedAta Update recoverNestedAta to detect MPCv2 keycards via isEddsaMpcV1SigningMaterial and derive the wallet root address using deriveUnhardenedMps instead of MPC.deriveUnhardened. Pass the isMpcV2 flag to signAndGenerateBroadcastableTransaction so the RecoverNested instruction signer and the signing path are both correct for MPCv2 wallets. Add unit tests for MPCv2 keycard routing, MPCv1 regression, mismatched bitgoKey, and missing address validation. Ticket: WCI-495 Session-Id: 342e5b05-4d0f-487f-8709-7f2d871de396 Task-Id: 69cf21d5-0fa2-4660-83d9-7a4f6b417906 --- modules/sdk-coin-sol/src/sol.ts | 13 ++++- modules/sdk-coin-sol/test/unit/sol.ts | 80 +++++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 3 deletions(-) diff --git a/modules/sdk-coin-sol/src/sol.ts b/modules/sdk-coin-sol/src/sol.ts index b7892bf30f..7cf3014b95 100644 --- a/modules/sdk-coin-sol/src/sol.ts +++ b/modules/sdk-coin-sol/src/sol.ts @@ -1700,11 +1700,17 @@ export class Sol extends BaseCoin { } const bitgoKey = params.bitgoKey.replace(/\s/g, ''); - const MPC = await EDDSAMethods.getInitializedMpcInstance(); + const userKey = params.userKey?.replace(/\s/g, '') ?? ''; + + const isMpcV2 = params.walletPassphrase + ? !(await EDDSAUtils.isEddsaMpcV1SigningMaterial(userKey, params.walletPassphrase, this.bitgo)) + : false; const index = params.index || 0; const currPath = params.seed ? getDerivationPath(params.seed) + `/${index}` : `m/${index}`; - const accountId = MPC.deriveUnhardened(bitgoKey, currPath).slice(0, 64); + const accountId = isMpcV2 + ? deriveUnhardenedMps(bitgoKey, currPath).slice(0, 64) + : (await EDDSAMethods.getInitializedMpcInstance()).deriveUnhardened(bitgoKey, currPath).slice(0, 64); const bs58EncodedPublicKey = new SolKeyPair({ pub: accountId }).getAddress(); const blockhash = await this.getBlockhash(params.apiKey); @@ -1726,7 +1732,8 @@ export class Sol extends BaseCoin { const recoverNestedTxn = await this.signAndGenerateBroadcastableTransaction( params, txBuilder, - bs58EncodedPublicKey + bs58EncodedPublicKey, + isMpcV2 ); const serializedTxn = (await recoverNestedTxn).serializedTx; diff --git a/modules/sdk-coin-sol/test/unit/sol.ts b/modules/sdk-coin-sol/test/unit/sol.ts index 2269541bf2..61d53648e6 100644 --- a/modules/sdk-coin-sol/test/unit/sol.ts +++ b/modules/sdk-coin-sol/test/unit/sol.ts @@ -3449,6 +3449,86 @@ describe('SOL:', function () { should.equal(addSignatureSpy.firstCall.args[0].pub, mpcV2WalletAddress); }); }); + + describe('recoverNestedAta', () => { + let nestedAtaMpcV2Params: SolRecoveryOptions; + + before(function () { + nestedAtaMpcV2Params = { + userKey: mpcV2UserKey, + backupKey: mpcV2BackupKey, + bitgoKey: mpcV2CommonKeyChain, + recoveryDestination: testData.closeATAkeys.destinationPubKey, + walletPassphrase: testData.keys.walletPassword, + nestedAtaAddress: 'FGuZSBhtreqSUsE86xokyjKz2i8VBtJzy6uMXXKyGHug', + ownerAtaAddress: 'Zfm98ZpVafydhFTYcsY6bHgubhB4cFgWFvbdEJxYhTA', + tokenMintAddress: 'ZBCNpuD7YMXzTHB2fhGkGi78MNsHGLRXUhRewNRm9RU', + }; + }); + + beforeEach(() => { + mpcV2SandBox.stub(Sol.prototype, 'broadcastTransaction' as keyof Sol).resolves({ + txId: testData.SolResponses.broadcastTransactionResponse.body.result, + }); + }); + + it('should recover nested ATA with MPCv2 keycard using MPS-derived wallet address', async function () { + const getTSSSignatureSpy = mpcV2SandBox.spy(EDDSAMethods, 'getTSSSignature'); + + const result = await basecoin.recoverNestedAta(nestedAtaMpcV2Params); + + result.should.not.be.empty(); + should.equal(result.txId, testData.SolResponses.broadcastTransactionResponse.body.result); + should.equal( + callBack.getCalls().find((call) => call.args[0]?.payload?.method === 'getLatestBlockhash') !== undefined, + true + ); + mpcV2SandBox.assert.notCalled(getTSSSignatureSpy); + }); + + it('should recover nested ATA with MPCv1 keycard using MPC.deriveUnhardened (regression)', async function () { + const getTSSSignatureSpy = mpcV2SandBox.spy(EDDSAMethods, 'getTSSSignature'); + + const result = await basecoin.recoverNestedAta({ + userKey: testData.closeATAkeys.userKey, + backupKey: testData.closeATAkeys.backupKey, + bitgoKey: testData.closeATAkeys.bitgoKey, + recoveryDestination: testData.closeATAkeys.destinationPubKey, + walletPassphrase: testData.closeATAkeys.walletPassword, + nestedAtaAddress: 'FGuZSBhtreqSUsE86xokyjKz2i8VBtJzy6uMXXKyGHug', + ownerAtaAddress: 'Zfm98ZpVafydhFTYcsY6bHgubhB4cFgWFvbdEJxYhTA', + tokenMintAddress: 'ZBCNpuD7YMXzTHB2fhGkGi78MNsHGLRXUhRewNRm9RU', + }); + + result.should.not.be.empty(); + should.equal(result.txId, testData.SolResponses.broadcastTransactionResponse.body.result); + mpcV2SandBox.assert.calledOnce(getTSSSignatureSpy); + }); + + it('should throw when MPCv2 recoverNestedAta bitgoKey does not match keycard commonKeyChain', async function () { + await basecoin + .recoverNestedAta({ ...nestedAtaMpcV2Params, bitgoKey: mismatchedBitgoKey }) + .should.be.rejectedWith('EdDSA MPCv2 recovery: commonKeyChain from keycard does not match bitgoKey'); + }); + + it('should throw when nestedAtaAddress is missing for recoverNestedAta MPCv2', async function () { + await basecoin + .recoverNestedAta({ + ...nestedAtaMpcV2Params, + nestedAtaAddress: undefined, + }) + .should.be.rejectedWith('invalid nestedAtaAddress'); + }); + + it('should throw when ownerAtaAddress is missing for recoverNestedAta MPCv2', async function () { + await basecoin + .recoverNestedAta({ + ...nestedAtaMpcV2Params, + ownerAtaAddress: undefined, + }) + .should.be.rejectedWith('invalid ownerAtaAddress'); + }); + }); }); describe('Build Consolidation Recoveries:', () => {