From 448229ac81fddd6e557d09df7f8ebc5d9e738c7c Mon Sep 17 00:00:00 2001 From: rajangarg047 Date: Fri, 12 Jun 2026 15:45:39 -0400 Subject: [PATCH] feat(sdk-coin-sol): implement deriveAddress for SOL Override BaseCoin.deriveAddress on the Sol coin to locally derive a receive address from the wallet's commonKeychain + index, reusing the shared deriveMPCWalletAddress (ed25519) helper. This is the inverse of isWalletAddress and shares its exact derivation path, so derive and verify can never diverge. Offline and key-material-free (public keys only). Supports the SMC prefix path via derivedFromParentWithSeed. Adds unit coverage including a derive->verify round-trip and an SMC-seed case. WCN-917 Co-Authored-By: Claude Opus 4.8 (1M context) --- modules/sdk-coin-sol/src/sol.ts | 27 +++++++++++++++++ modules/sdk-coin-sol/test/unit/sol.ts | 43 +++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/modules/sdk-coin-sol/src/sol.ts b/modules/sdk-coin-sol/src/sol.ts index 9f2aaf29fa..c16cd651fc 100644 --- a/modules/sdk-coin-sol/src/sol.ts +++ b/modules/sdk-coin-sol/src/sol.ts @@ -54,6 +54,9 @@ import { VerifyTransactionOptions, TssVerifyAddressOptions, verifyEddsaTssWalletAddress, + deriveMPCWalletAddress, + DeriveAddressOptions, + DeriveAddressResult, UnexpectedAddressError, } from '@bitgo/sdk-core'; import { auditEddsaPrivateKey, getDerivationPath } from '@bitgo/sdk-lib-mpc'; @@ -714,6 +717,30 @@ export class Sol extends BaseCoin { return true; } + /** + * Locally derive a SOL wallet receive address from a derivation path, using the wallet's + * commonKeychain only (no private keys, no network access). This is the inverse of + * {@link isWalletAddress}: it *produces* the address via the same EdDSA TSS derivation that + * isWalletAddress checks against, so the two can never diverge. + * @param params keychains (commonKeychain), derivation index, and optional SMC seed + * @returns the derived address, the index used, and the HD derivation path + */ + async deriveAddress(params: DeriveAddressOptions): Promise { + const { address, derivationPath } = await deriveMPCWalletAddress( + { + // extractCommonKeychain validates the commonKeychain is present at runtime + keychains: (params.keychains ?? []) as TssVerifyAddressOptions['keychains'], + index: params.index, + derivedFromParentWithSeed: params.derivedFromParentWithSeed, + multisigTypeVersion: params.multisigTypeVersion, + keyCurve: 'ed25519', + }, + (publicKey) => this.getAddressFromPublicKey(publicKey) + ); + + return { address, index: params.index, derivationPath }; + } + /** * Converts a Solana public key to an address * @param publicKey Hex-encoded public key (64 hex characters = 32 bytes) diff --git a/modules/sdk-coin-sol/test/unit/sol.ts b/modules/sdk-coin-sol/test/unit/sol.ts index 040d99ad76..3f244285b7 100644 --- a/modules/sdk-coin-sol/test/unit/sol.ts +++ b/modules/sdk-coin-sol/test/unit/sol.ts @@ -3842,6 +3842,49 @@ describe('SOL:', function () { }); }); + describe('deriveAddress', () => { + // Same vector as the isWalletAddress tests above: commonKeychain @ index 1 -> address. + const address = '7YAesfwPk41VChUgr65bm8FEep7ymWqLSW5rpYB5zZPY'; + const commonKeychain = + '8ea32ecacfc83effbd2e2790ee44fa7c59b4d86c29a12f09fb613d8195f93f4e21875cad3b98adada40c040c54c3569467df41a020881a6184096378701862bd'; + const keychains = [{ id: '1', type: 'tss' as const, commonKeychain }]; + + it('should derive the expected address for a given commonKeychain and index', async function () { + const result = await basecoin.deriveAddress({ keychains, index: 1 }); + result.address.should.equal(address); + result.index.should.equal(1); + assert.strictEqual(result.derivationPath, 'm/1'); + }); + + it('should derive a different address for a different index', async function () { + const result = await basecoin.deriveAddress({ keychains, index: 2 }); + result.address.should.not.equal(address); + }); + + it('round-trips with isWalletAddress (derived address verifies as true)', async function () { + const derived = await basecoin.deriveAddress({ keychains, index: 5 }); + const verified = await basecoin.isWalletAddress({ keychains, address: derived.address, index: 5 }); + verified.should.equal(true); + }); + + it('should use the SMC prefix path when derivedFromParentWithSeed is set', async function () { + const derivedFromParentWithSeed = 'smc-test-seed-123'; + const derived = await basecoin.deriveAddress({ keychains, index: 1, derivedFromParentWithSeed }); + // round-trips through the SMC verify path + const verified = await basecoin.isWalletAddress({ + keychains, + address: derived.address, + index: 1, + derivedFromParentWithSeed, + }); + verified.should.equal(true); + }); + + it('should throw if keychains are missing', async function () { + await assert.rejects(async () => await basecoin.deriveAddress({ keychains: [], index: 0 }), /keychains/); + }); + }); + describe('getAddressFromPublicKey', () => { it('should convert public key to base58 address', function () { const publicKey = '61220a9394802b1d1df37b35f7a3197970f48081092cee011fc98f7b71b2bd43';