Skip to content
Merged
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
1 change: 0 additions & 1 deletion modules/sdk-coin-sol/src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ export { MessageBuilderFactory } from './messages';
export { explainSolTransaction, ExplainTransactionWasmOptions } from './explainTransactionWasm';
export {
MintExtensionReadResult,
assertExtensionCompatibility,
extensionTypeNames,
mapModeledExtensions,
parseMintExtensions,
Expand Down
21 changes: 7 additions & 14 deletions modules/sdk-coin-sol/src/lib/tokenExtensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,6 @@ const EXTENSION_NAME_MAP: Readonly<Record<string, SolTokenExtensionType>> = {
ScaledUiAmountConfig: SolTokenExtensionType.ScaledUiAmount,
};

const CONFIDENTIAL_TRANSFER_NAMES = ['ConfidentialTransferMint', 'ConfidentialTransferFeeConfig'];

/** Human-readable names of every extension type present on the mint. */
export function extensionTypeNames(mintInfo: Mint): string[] {
if (mintInfo.tlvData.length === 0) {
Expand All @@ -58,17 +56,13 @@ export function mapModeledExtensions(detectedTypeNames: readonly string[]): SolT
return modeled;
}

/**
* Enforce the protocol-level incompatibility: Transfer Hook and Confidential
* Transfer cannot coexist on the same mint. Pure — unit-testable without chain data.
*/
export function assertExtensionCompatibility(detectedTypeNames: readonly string[]): void {
const hasHook = detectedTypeNames.includes('TransferHook');
const hasConfidential = detectedTypeNames.some((n) => CONFIDENTIAL_TRANSFER_NAMES.includes(n));
if (hasHook && hasConfidential) {
throw new Error('Mint declares both Transfer Hook and Confidential Transfer, which cannot coexist');
}
}
// No SDK-level extension-combination assert: the on-chain program already
// enforces its own invalid-combination rules at extension init
// (`check_for_invalid_mint_extension_combinations` in token-2022), and it does
// NOT forbid TransferHook + ConfidentialTransferMint. A previous assert here
// rejected that legal pair and blocked onboarding of real mints. Custody
// policy on which combinations BitGo will serve belongs to consumers
// (statics `getUnsupportedSolTokenExtensions`, the AMS onboarding gate).

function toBase58(key: PublicKey | null): string | undefined {
return key ? key.toBase58() : undefined;
Expand All @@ -80,7 +74,6 @@ function toBase58(key: PublicKey | null): string | undefined {
*/
export function parseMintExtensions(mintInfo: Mint): MintExtensionReadResult {
const detectedTypeNames = extensionTypeNames(mintInfo);
assertExtensionCompatibility(detectedTypeNames);

const extensions: SolTokenExtensions = { detected: mapModeledExtensions(detectedTypeNames) };
const authorities: NonNullable<SolTokenExtensions['authorities']> = {
Expand Down
41 changes: 34 additions & 7 deletions modules/sdk-coin-sol/test/unit/tokenExtensions.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import 'should';
import { SolTokenExtensionType } from '@bitgo/statics';
import { assertExtensionCompatibility, mapModeledExtensions } from '../../src/lib/tokenExtensions';
import { ExtensionType, type Mint } from '@solana/spl-token';
import { PublicKey } from '@solana/web3.js';
import { mapModeledExtensions, parseMintExtensions } from '../../src/lib/tokenExtensions';

describe('Sol Token-2022 mint extension parsing', function () {
describe('mapModeledExtensions', function () {
Expand All @@ -22,13 +24,38 @@ describe('Sol Token-2022 mint extension parsing', function () {
});
});

describe('assertExtensionCompatibility', function () {
it('throws when Transfer Hook and Confidential Transfer coexist', function () {
(() => assertExtensionCompatibility(['TransferHook', 'ConfidentialTransferMint'])).should.throw(/cannot coexist/);
});
describe('parseMintExtensions', function () {
function tlvEntry(type: ExtensionType, value: Buffer): Buffer {
const head = Buffer.alloc(4);
head.writeUInt16LE(type, 0);
head.writeUInt16LE(value.length, 2);
return Buffer.concat([head, value]);
}

function fakeMint(tlvData: Buffer): Mint {
return {
address: PublicKey.default,
mintAuthority: null,
supply: BigInt(0),
decimals: 6,
isInitialized: true,
freezeAuthority: null,
tlvData,
};
}

it('allows Transfer Hook without Confidential Transfer', function () {
(() => assertExtensionCompatibility(['TransferHook', 'TransferFeeConfig'])).should.not.throw();
it('parses a mint declaring both Transfer Hook and Confidential Transfer', function () {
// Legal on-chain: token-2022's check_for_invalid_mint_extension_combinations
// does not forbid this pair. Regression test — an SDK-level assert used to
// reject it and block onboarding of real mints.
const tlvData = Buffer.concat([
tlvEntry(ExtensionType.ConfidentialTransferMint, Buffer.alloc(0)),
tlvEntry(ExtensionType.TransferHook, Buffer.alloc(64)), // authority (32) + programId (32)
]);
const result = parseMintExtensions(fakeMint(tlvData));
result.detectedTypeNames.should.eql(['ConfidentialTransferMint', 'TransferHook']);
result.extensions.detected.should.eql([SolTokenExtensionType.TransferHook]);
result.extensions.transferHookProgramId?.should.equal('11111111111111111111111111111111');
});
});
});
Loading