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
99 changes: 96 additions & 3 deletions modules/statics/src/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,85 @@ export enum ProgramID {
Token2022ProgramId = 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb',
}

/**
* Token-2022 (SPL Token Extensions) that BitGo has explicit custody handling for.
* This set is the onboarding safety gate: a mint whose
* TLV data declares any extension NOT represented here must be rejected at onboarding
* and escalated to engineering — no soft pass.
*/
export enum SolTokenExtensionType {
TransferFee = 'transferFee',
TransferHook = 'transferHook',
PermanentDelegate = 'permanentDelegate',
InterestBearing = 'interestBearing',
ScaledUiAmount = 'scaledUiAmount',
DefaultAccountState = 'defaultAccountState',
}

/** On-chain authority addresses relevant to Token-2022 extensions, captured at onboarding. */
export interface SolTokenAuthorities {
freezeAuthority?: string;
mintAuthority?: string;
transferFeeConfigAuthority?: string;
withdrawWithheldAuthority?: string;
permanentDelegate?: string;
rateAuthority?: string;
}

export interface SolTransferFeeConfig {
transferFeeBasisPoints: number;
/** Raw base units, stringified to avoid precision loss. */
maximumFee: string;
}

export interface SolDefaultAccountStateConfig {
/** True when the mint's Default Account State is Frozen. */
frozen: boolean;
/** True when a permissionless thaw program (Token ACL etc.) is associated */
permissionlessThaw: boolean;
}

export interface SolInterestBearingConfig {
/** Annual interest rate in basis points. */
rateBasisPoints: number;
/** 'continuous' (issuer mints frequently) or 'redeemOnMaturity' (accrued not transferable until settlement). */
settlementModel?: 'continuous' | 'redeemOnMaturity';
}
export interface SolScaledUiAmountConfig {
/** UiAmountMultiplier active at onboarding; time-series history is tracked off-chain from day one. */
initialMultiplier: string;
}
/**
* Per-mint Token-2022 extension configuration detected at onboarding.
* Absent => classic SPL / no modeled extensions.
*/
export interface SolTokenExtensions {
/** Every extension detected on the mint's TLV data. Onboarding hard-stops if any is not a SolTokenExtensionType. */
detected: SolTokenExtensionType[];
authorities?: SolTokenAuthorities;
/** Resolved human-readable issuer name for notifications and transaction history. */
issuerName?: string;
transferFee?: SolTransferFeeConfig;
/** Transfer Hook program id. The extra-account-meta list is resolved fresh at tx time — never stored. */
transferHookProgramId?: string;
interestBearing?: SolInterestBearingConfig;
scaledUiAmount?: SolScaledUiAmountConfig;
defaultAccountState?: SolDefaultAccountStateConfig;
}

/** Extensions BitGo can safely custody (the onboarding allowlist). */
export const SUPPORTED_SOL_TOKEN_EXTENSIONS: ReadonlySet<SolTokenExtensionType> = new Set(
Object.values(SolTokenExtensionType)
);

/**
* Returns the detected extensions BitGo has no handling code for.
* A non-empty result MUST hard-stop onboarding.
*/
export function getUnsupportedSolTokenExtensions(detected: readonly string[]): string[] {
return detected.filter((ext) => !SUPPORTED_SOL_TOKEN_EXTENSIONS.has(ext as SolTokenExtensionType));
}

export interface AccountConstructorOptions {
id: string;
fullName: string;
Expand Down Expand Up @@ -128,6 +207,7 @@ export interface SolCoinConstructorOptions extends AccountConstructorOptions {
tokenAddress: string;
contractAddress: string;
programId: string;
tokenExtensions?: SolTokenExtensions;
}

export interface XrpCoinConstructorOptions extends AccountConstructorOptions {
Expand Down Expand Up @@ -453,6 +533,7 @@ export class SolCoin extends AccountCoinToken {
public tokenAddress: string;
public contractAddress: string;
public programId: string;
public tokenExtensions?: SolTokenExtensions;
constructor(options: SolCoinConstructorOptions) {
super({
...options,
Expand All @@ -461,6 +542,12 @@ export class SolCoin extends AccountCoinToken {
this.tokenAddress = options.contractAddress;
this.contractAddress = options.contractAddress;
this.programId = options.programId;
this.tokenExtensions = options.tokenExtensions;
}

/** True if this token was onboarded with the given Token-2022 extension. */
public hasExtension(type: SolTokenExtensionType): boolean {
return this.tokenExtensions?.detected.includes(type) ?? false;
}
}

Expand Down Expand Up @@ -2086,7 +2173,8 @@ export function solToken(
prefix = '',
suffix: string = name.toUpperCase(),
network: AccountNetwork = Networks.main.sol,
primaryKeyCurve: KeyCurve = KeyCurve.Ed25519
primaryKeyCurve: KeyCurve = KeyCurve.Ed25519,
tokenExtensions?: SolTokenExtensions
) {
return Object.freeze(
new SolCoin({
Expand All @@ -2105,6 +2193,7 @@ export function solToken(
isToken: true,
primaryKeyCurve,
baseUnit: BaseUnit.SOL,
tokenExtensions,
})
);
}
Expand Down Expand Up @@ -2135,7 +2224,9 @@ export function tsolToken(
programId = ProgramID.TokenProgramId,
prefix = '',
suffix: string = name.toUpperCase(),
network: AccountNetwork = Networks.test.sol
network: AccountNetwork = Networks.test.sol,
primaryKeyCurve: KeyCurve = KeyCurve.Ed25519,
tokenExtensions?: SolTokenExtensions
) {
return solToken(
id,
Expand All @@ -2149,7 +2240,9 @@ export function tsolToken(
programId,
prefix,
suffix,
network
network,
primaryKeyCurve,
tokenExtensions
);
}

Expand Down
9 changes: 9 additions & 0 deletions modules/statics/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,15 @@ export {
JettonToken,
CantonToken,
Erc7984Coin,
SolTokenExtensionType,
SolTokenExtensions,
SolTokenAuthorities,
SolTransferFeeConfig,
SolDefaultAccountStateConfig,
SolInterestBearingConfig,
SolScaledUiAmountConfig,
SUPPORTED_SOL_TOKEN_EXTENSIONS,
getUnsupportedSolTokenExtensions,
} from './account';
export { CoinMap } from './map';
export {
Expand Down
39 changes: 38 additions & 1 deletion modules/statics/test/unit/coins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
getFormattedTokenConfigForCoin,
getFormattedTokens,
HederaToken,
KeyCurve,
Networks,
NetworkType,
registerNetwork,
Expand All @@ -41,7 +42,15 @@ import {
trimmedDynamicBaseChainConfig,
} from './resources/amsTokenConfig';
import { EthLikeErc20Token } from '../../../sdk-coin-evm/src';
import { ProgramID, taptNFTCollection, terc20 } from '../../src/account';
import {
AccountCoin,
getUnsupportedSolTokenExtensions,
ProgramID,
solToken,
SolTokenExtensionType,
taptNFTCollection,
terc20,
} from '../../src/account';
import { allCoinsAndTokens } from '../../src/allCoinsAndTokens';

interface DuplicateCoinObject {
Expand Down Expand Up @@ -1098,6 +1107,34 @@ describe('Token contract address field defaults', () => {
validProgramIds.should.containEql((coin as SolCoin).programId);
});
});

it('flags extensions with no BitGo handling code (onboarding safety gate)', () => {
getUnsupportedSolTokenExtensions([SolTokenExtensionType.TransferFee]).should.eql([]);
getUnsupportedSolTokenExtensions(['confidentialTransfer', SolTokenExtensionType.TransferHook]).should.eql([
'confidentialTransfer',
]);
});

it('exposes detected extensions via SolCoin.hasExtension', () => {
const token = solToken(
'a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d',
'sol:testext',
'Test Extension Token',
6,
'Ea5SjE2Y6yvCeW5dYTn7PYMuW5ikXkvbGdcmSnXeaLjS',
'Ea5SjE2Y6yvCeW5dYTn7PYMuW5ikXkvbGdcmSnXeaLjS',
UnderlyingAsset.SOL,
[...AccountCoin.DEFAULT_FEATURES, CoinFeature.REQUIRES_RESERVE],
ProgramID.Token2022ProgramId,
'',
'TESTEXT',
Networks.main.sol,
KeyCurve.Ed25519,
{ detected: [SolTokenExtensionType.ScaledUiAmount], scaledUiAmount: { initialMultiplier: '1' } }
);
token.hasExtension(SolTokenExtensionType.ScaledUiAmount).should.be.true();
token.hasExtension(SolTokenExtensionType.TransferFee).should.be.false();
});
});
describe('XRP tokens', function () {
it('have `contractAddress` === `issuerAddress::currencyCode`', () => {
Expand Down
Loading