Skip to content

Commit b5427ad

Browse files
committed
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
1 parent fb76dcc commit b5427ad

2 files changed

Lines changed: 90 additions & 3 deletions

File tree

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

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1674,11 +1674,17 @@ export class Sol extends BaseCoin {
16741674
}
16751675

16761676
const bitgoKey = params.bitgoKey.replace(/\s/g, '');
1677-
const MPC = await EDDSAMethods.getInitializedMpcInstance();
1677+
const userKey = params.userKey?.replace(/\s/g, '') ?? '';
1678+
1679+
const isMpcV2 = params.walletPassphrase
1680+
? !(await EDDSAUtils.isEddsaMpcV1SigningMaterial(userKey, params.walletPassphrase, this.bitgo))
1681+
: false;
16781682

16791683
const index = params.index || 0;
16801684
const currPath = params.seed ? getDerivationPath(params.seed) + `/${index}` : `m/${index}`;
1681-
const accountId = MPC.deriveUnhardened(bitgoKey, currPath).slice(0, 64);
1685+
const accountId = isMpcV2
1686+
? deriveUnhardenedMps(bitgoKey, currPath).slice(0, 64)
1687+
: (await EDDSAMethods.getInitializedMpcInstance()).deriveUnhardened(bitgoKey, currPath).slice(0, 64);
16821688
const bs58EncodedPublicKey = new SolKeyPair({ pub: accountId }).getAddress();
16831689

16841690
const blockhash = await this.getBlockhash(params.apiKey);
@@ -1700,7 +1706,8 @@ export class Sol extends BaseCoin {
17001706
const recoverNestedTxn = await this.signAndGenerateBroadcastableTransaction(
17011707
params,
17021708
txBuilder,
1703-
bs58EncodedPublicKey
1709+
bs58EncodedPublicKey,
1710+
isMpcV2
17041711
);
17051712

17061713
const serializedTxn = (await recoverNestedTxn).serializedTx;

modules/sdk-coin-sol/test/unit/sol.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3255,6 +3255,86 @@ describe('SOL:', function () {
32553255
should.equal(addSignatureSpy.firstCall.args[0].pub, mpcV2WalletAddress);
32563256
});
32573257
});
3258+
3259+
describe('recoverNestedAta', () => {
3260+
let nestedAtaMpcV2Params: SolRecoveryOptions;
3261+
3262+
before(function () {
3263+
nestedAtaMpcV2Params = {
3264+
userKey: mpcV2UserKey,
3265+
backupKey: mpcV2BackupKey,
3266+
bitgoKey: mpcV2CommonKeyChain,
3267+
recoveryDestination: testData.closeATAkeys.destinationPubKey,
3268+
walletPassphrase: testData.keys.walletPassword,
3269+
nestedAtaAddress: 'FGuZSBhtreqSUsE86xokyjKz2i8VBtJzy6uMXXKyGHug',
3270+
ownerAtaAddress: 'Zfm98ZpVafydhFTYcsY6bHgubhB4cFgWFvbdEJxYhTA',
3271+
tokenMintAddress: 'ZBCNpuD7YMXzTHB2fhGkGi78MNsHGLRXUhRewNRm9RU',
3272+
};
3273+
});
3274+
3275+
beforeEach(() => {
3276+
mpcV2SandBox.stub(Sol.prototype, 'broadcastTransaction' as keyof Sol).resolves({
3277+
txId: testData.SolResponses.broadcastTransactionResponse.body.result,
3278+
});
3279+
});
3280+
3281+
it('should recover nested ATA with MPCv2 keycard using MPS-derived wallet address', async function () {
3282+
const getTSSSignatureSpy = mpcV2SandBox.spy(EDDSAMethods, 'getTSSSignature');
3283+
3284+
const result = await basecoin.recoverNestedAta(nestedAtaMpcV2Params);
3285+
3286+
result.should.not.be.empty();
3287+
should.equal(result.txId, testData.SolResponses.broadcastTransactionResponse.body.result);
3288+
should.equal(
3289+
callBack.getCalls().find((call) => call.args[0]?.payload?.method === 'getLatestBlockhash') !== undefined,
3290+
true
3291+
);
3292+
mpcV2SandBox.assert.notCalled(getTSSSignatureSpy);
3293+
});
3294+
3295+
it('should recover nested ATA with MPCv1 keycard using MPC.deriveUnhardened (regression)', async function () {
3296+
const getTSSSignatureSpy = mpcV2SandBox.spy(EDDSAMethods, 'getTSSSignature');
3297+
3298+
const result = await basecoin.recoverNestedAta({
3299+
userKey: testData.closeATAkeys.userKey,
3300+
backupKey: testData.closeATAkeys.backupKey,
3301+
bitgoKey: testData.closeATAkeys.bitgoKey,
3302+
recoveryDestination: testData.closeATAkeys.destinationPubKey,
3303+
walletPassphrase: testData.closeATAkeys.walletPassword,
3304+
nestedAtaAddress: 'FGuZSBhtreqSUsE86xokyjKz2i8VBtJzy6uMXXKyGHug',
3305+
ownerAtaAddress: 'Zfm98ZpVafydhFTYcsY6bHgubhB4cFgWFvbdEJxYhTA',
3306+
tokenMintAddress: 'ZBCNpuD7YMXzTHB2fhGkGi78MNsHGLRXUhRewNRm9RU',
3307+
});
3308+
3309+
result.should.not.be.empty();
3310+
should.equal(result.txId, testData.SolResponses.broadcastTransactionResponse.body.result);
3311+
mpcV2SandBox.assert.calledOnce(getTSSSignatureSpy);
3312+
});
3313+
3314+
it('should throw when MPCv2 recoverNestedAta bitgoKey does not match keycard commonKeyChain', async function () {
3315+
await basecoin
3316+
.recoverNestedAta({ ...nestedAtaMpcV2Params, bitgoKey: mismatchedBitgoKey })
3317+
.should.be.rejectedWith('EdDSA MPCv2 recovery: commonKeyChain from keycard does not match bitgoKey');
3318+
});
3319+
3320+
it('should throw when nestedAtaAddress is missing for recoverNestedAta MPCv2', async function () {
3321+
await basecoin
3322+
.recoverNestedAta({
3323+
...nestedAtaMpcV2Params,
3324+
nestedAtaAddress: undefined,
3325+
})
3326+
.should.be.rejectedWith('invalid nestedAtaAddress');
3327+
});
3328+
3329+
it('should throw when ownerAtaAddress is missing for recoverNestedAta MPCv2', async function () {
3330+
await basecoin
3331+
.recoverNestedAta({
3332+
...nestedAtaMpcV2Params,
3333+
ownerAtaAddress: undefined,
3334+
})
3335+
.should.be.rejectedWith('invalid ownerAtaAddress');
3336+
});
3337+
});
32583338
});
32593339

32603340
describe('Build Consolidation Recoveries:', () => {

0 commit comments

Comments
 (0)