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:', () => {