Skip to content

Commit 0b2d684

Browse files
rajangarg047claude
andcommitted
feat(sdk-coin-sol): derive SPL token (ATA) deposit addresses
Extend SOL deriveAddress to support SPL token deposit addresses. When a tokenName (e.g. sol:usdc) is supplied, derive the native owner address as before, then return the owner's Associated Token Account (ATA) for that token's mint — using the same getAssociatedTokenAccountAddress helper (statics mint + programId lookup, token-2022 aware) that the rest of the SOL coin uses, so the derived address matches what BitGo assigns as the token receive address. - sdk-core: add tokenName to DeriveAddressOptions. - express: accept tokenName on the address/derive request body. - tests: exact ATA for sol:usdc at a known index, parity with getAssociatedTokenAccountAddress, and unknown-token error. WCN-1054 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 0adbf08 commit 0b2d684

4 files changed

Lines changed: 50 additions & 1 deletion

File tree

modules/express/src/typedRoutes/api/v2/deriveAddress.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ export const DeriveAddressBody = {
3939
format: optional(CreateAddressFormat),
4040
/** Wallet version, to disambiguate derivation strategy (e.g. EVM forwarder vs MPC) */
4141
walletVersion: optional(t.number),
42+
/**
43+
* Token name (e.g. `sol:usdc`) to derive a token deposit address instead of the native one.
44+
* For Solana this returns the wallet's Associated Token Account (ATA) for the token's mint.
45+
*/
46+
tokenName: optional(t.string),
4247
/**
4348
* Wallet base address (the wallet contract address for EVM wallets). Required to derive
4449
* per-index forwarder receive addresses for legacy multisig EVM wallets (versions 1/2/4).

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

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -728,7 +728,8 @@ export class Sol extends BaseCoin {
728728
* @returns the derived address, the index used, and the HD derivation path
729729
*/
730730
async deriveAddress(params: DeriveAddressOptions): Promise<DeriveAddressResult> {
731-
const { address, derivationPath } = await deriveMPCWalletAddress(
731+
// Derive the native owner (wallet) address from the commonKeychain.
732+
const { address: ownerAddress, derivationPath } = await deriveMPCWalletAddress(
732733
{
733734
// extractCommonKeychain validates the commonKeychain is present at runtime
734735
keychains: (params.keychains ?? []) as TssVerifyAddressOptions['keychains'],
@@ -740,6 +741,19 @@ export class Sol extends BaseCoin {
740741
(publicKey) => this.getAddressFromPublicKey(publicKey)
741742
);
742743

744+
// No token requested: return the native SOL receive address.
745+
if (!params.tokenName) {
746+
return { address: ownerAddress, index: params.index, derivationPath };
747+
}
748+
749+
// Token requested: the deposit address is the owner's Associated Token Account (ATA) for the
750+
// token's mint, derived the same way SOL token addresses are produced elsewhere in this coin.
751+
const token = getSolTokenFromTokenName(params.tokenName);
752+
if (!token || token.tokenAddress === undefined || token.programId === undefined) {
753+
throw new Error(`unknown or unsupported SOL token: ${params.tokenName}`);
754+
}
755+
const address = await getAssociatedTokenAccountAddress(token.tokenAddress, ownerAddress, true, token.programId);
756+
743757
return { address, index: params.index, derivationPath };
744758
}
745759

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4189,6 +4189,29 @@ describe('SOL:', function () {
41894189
it('should throw if keychains are missing', async function () {
41904190
await assert.rejects(async () => await basecoin.deriveAddress({ keychains: [], index: 0 }), /keychains/);
41914191
});
4192+
4193+
it('should derive the SPL token (ATA) address when tokenName is given', async function () {
4194+
// owner = native address at index 1 (7YAesf…); ATA for sol:usdc:
4195+
const result = await basecoin.deriveAddress({ keychains, index: 1, tokenName: 'sol:usdc' });
4196+
result.address.should.equal('FG1XMJdXBQ5uYaNoKposABg6erxsvzbwC283W2ipnjQB');
4197+
result.address.should.not.equal(address); // not the native address
4198+
result.index.should.equal(1);
4199+
});
4200+
4201+
it('the derived token ATA matches getAssociatedTokenAccountAddress for the owner+mint', async function () {
4202+
const native = await basecoin.deriveAddress({ keychains, index: 3 });
4203+
const token = await basecoin.deriveAddress({ keychains, index: 3, tokenName: 'sol:usdc' });
4204+
const mint = (coins.get('sol:usdc') as unknown as { tokenAddress: string }).tokenAddress;
4205+
const expectedAta = await getAssociatedTokenAccountAddress(mint, native.address, true);
4206+
token.address.should.equal(expectedAta);
4207+
});
4208+
4209+
it('should throw for an unknown token name', async function () {
4210+
await assert.rejects(
4211+
async () => await basecoin.deriveAddress({ keychains, index: 1, tokenName: 'sol:not-a-real-token' }),
4212+
/unknown or unsupported SOL token/
4213+
);
4214+
});
41924215
});
41934216

41944217
describe('getAddressFromPublicKey', () => {

modules/sdk-core/src/bitgo/baseCoin/iBaseCoin.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,13 @@ export interface DeriveAddressOptions
244244
keychains?: ({ pub: string } | { commonKeychain: string })[];
245245
/** Wallet version, used to disambiguate derivation strategy for some coin families. */
246246
walletVersion?: number;
247+
/**
248+
* Token name (e.g. `sol:usdc`, `tsol:usdt`) to derive a token deposit address instead of the
249+
* native receive address. For Solana this resolves to the SPL mint and returns the wallet's
250+
* Associated Token Account (ATA) for that mint. Ignored by coins/tokens that reuse the native
251+
* address (e.g. ERC-20 on EVM).
252+
*/
253+
tokenName?: string;
247254
/**
248255
* Wallet base address (the wallet contract address for EVM wallets). Required to derive
249256
* per-index forwarder (CREATE2) receive addresses for legacy multisig EVM wallets.

0 commit comments

Comments
 (0)