Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions modules/express/src/typedRoutes/api/v2/deriveAddress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ export const DeriveAddressBody = {
format: optional(CreateAddressFormat),
/** Wallet version, to disambiguate derivation strategy (e.g. EVM forwarder vs MPC) */
walletVersion: optional(t.number),
/**
* Token name (e.g. `sol:usdc`) to derive a token deposit address instead of the native one.
* For Solana this returns the wallet's Associated Token Account (ATA) for the token's mint.
*/
tokenName: optional(t.string),
/**
* Wallet base address (the wallet contract address for EVM wallets). Required to derive
* per-index forwarder receive addresses for legacy multisig EVM wallets (versions 1/2/4).
Expand Down
16 changes: 15 additions & 1 deletion modules/sdk-coin-sol/src/sol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -728,7 +728,8 @@ export class Sol extends BaseCoin {
* @returns the derived address, the index used, and the HD derivation path
*/
async deriveAddress(params: DeriveAddressOptions): Promise<DeriveAddressResult> {
const { address, derivationPath } = await deriveMPCWalletAddress(
// Derive the native owner (wallet) address from the commonKeychain.
const { address: ownerAddress, derivationPath } = await deriveMPCWalletAddress(
{
// extractCommonKeychain validates the commonKeychain is present at runtime
keychains: (params.keychains ?? []) as TssVerifyAddressOptions['keychains'],
Expand All @@ -740,6 +741,19 @@ export class Sol extends BaseCoin {
(publicKey) => this.getAddressFromPublicKey(publicKey)
);

// No token requested: return the native SOL receive address.
if (!params.tokenName) {
return { address: ownerAddress, index: params.index, derivationPath };
}

// Token requested: the deposit address is the owner's Associated Token Account (ATA) for the
// token's mint, derived the same way SOL token addresses are produced elsewhere in this coin.
const token = getSolTokenFromTokenName(params.tokenName);
if (!token || token.tokenAddress === undefined || token.programId === undefined) {
throw new Error(`unknown or unsupported SOL token: ${params.tokenName}`);
}
const address = await getAssociatedTokenAccountAddress(token.tokenAddress, ownerAddress, true, token.programId);

return { address, index: params.index, derivationPath };
}

Expand Down
23 changes: 23 additions & 0 deletions modules/sdk-coin-sol/test/unit/sol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4189,6 +4189,29 @@ describe('SOL:', function () {
it('should throw if keychains are missing', async function () {
await assert.rejects(async () => await basecoin.deriveAddress({ keychains: [], index: 0 }), /keychains/);
});

it('should derive the SPL token (ATA) address when tokenName is given', async function () {
// owner = native address at index 1 (7YAesf…); ATA for sol:usdc:
const result = await basecoin.deriveAddress({ keychains, index: 1, tokenName: 'sol:usdc' });
result.address.should.equal('FG1XMJdXBQ5uYaNoKposABg6erxsvzbwC283W2ipnjQB');
result.address.should.not.equal(address); // not the native address
result.index.should.equal(1);
});

it('the derived token ATA matches getAssociatedTokenAccountAddress for the owner+mint', async function () {
const native = await basecoin.deriveAddress({ keychains, index: 3 });
const token = await basecoin.deriveAddress({ keychains, index: 3, tokenName: 'sol:usdc' });
const mint = (coins.get('sol:usdc') as unknown as { tokenAddress: string }).tokenAddress;
const expectedAta = await getAssociatedTokenAccountAddress(mint, native.address, true);
token.address.should.equal(expectedAta);
});

it('should throw for an unknown token name', async function () {
await assert.rejects(
async () => await basecoin.deriveAddress({ keychains, index: 1, tokenName: 'sol:not-a-real-token' }),
/unknown or unsupported SOL token/
);
});
});

describe('getAddressFromPublicKey', () => {
Expand Down
7 changes: 7 additions & 0 deletions modules/sdk-core/src/bitgo/baseCoin/iBaseCoin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,13 @@ export interface DeriveAddressOptions
keychains?: ({ pub: string } | { commonKeychain: string })[];
/** Wallet version, used to disambiguate derivation strategy for some coin families. */
walletVersion?: number;
/**
* Token name (e.g. `sol:usdc`, `tsol:usdt`) to derive a token deposit address instead of the
* native receive address. For Solana this resolves to the SPL mint and returns the wallet's
* Associated Token Account (ATA) for that mint. Ignored by coins/tokens that reuse the native
* address (e.g. ERC-20 on EVM).
*/
tokenName?: string;
/**
* Wallet base address (the wallet contract address for EVM wallets). Required to derive
* per-index forwarder (CREATE2) receive addresses for legacy multisig EVM wallets.
Expand Down
Loading