diff --git a/modules/sdk-coin-sol/src/lib/index.ts b/modules/sdk-coin-sol/src/lib/index.ts index ea7a7343e8..d1b0543f73 100644 --- a/modules/sdk-coin-sol/src/lib/index.ts +++ b/modules/sdk-coin-sol/src/lib/index.ts @@ -24,7 +24,6 @@ export { MessageBuilderFactory } from './messages'; export { explainSolTransaction, ExplainTransactionWasmOptions } from './explainTransactionWasm'; export { MintExtensionReadResult, - assertExtensionCompatibility, extensionTypeNames, mapModeledExtensions, parseMintExtensions, diff --git a/modules/sdk-coin-sol/src/lib/tokenExtensions.ts b/modules/sdk-coin-sol/src/lib/tokenExtensions.ts index eb1e1f1955..424896e6d3 100644 --- a/modules/sdk-coin-sol/src/lib/tokenExtensions.ts +++ b/modules/sdk-coin-sol/src/lib/tokenExtensions.ts @@ -36,8 +36,6 @@ const EXTENSION_NAME_MAP: Readonly> = { 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) { @@ -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; @@ -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 = { diff --git a/modules/sdk-coin-sol/test/unit/tokenExtensions.ts b/modules/sdk-coin-sol/test/unit/tokenExtensions.ts index 93d9b3df73..c045d2ac8b 100644 --- a/modules/sdk-coin-sol/test/unit/tokenExtensions.ts +++ b/modules/sdk-coin-sol/test/unit/tokenExtensions.ts @@ -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 () { @@ -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'); }); }); });