From 4969a364a579657ee1c40138741f6076ee756188 Mon Sep 17 00:00:00 2001 From: ArunBala-Bitgo Date: Tue, 23 Dec 2025 12:40:01 +0530 Subject: [PATCH] feat: fix config for erc20 tokens Ticket: WIN-8435 --- .../test/v2/unit/coins/ofcErc20Tokens.ts | 236 +++++++++++++++ modules/statics/src/base.ts | 2 +- modules/statics/src/coins/ofcErc20Coins.ts | 284 ++++++++++++++++-- 3 files changed, 495 insertions(+), 27 deletions(-) create mode 100644 modules/bitgo/test/v2/unit/coins/ofcErc20Tokens.ts diff --git a/modules/bitgo/test/v2/unit/coins/ofcErc20Tokens.ts b/modules/bitgo/test/v2/unit/coins/ofcErc20Tokens.ts new file mode 100644 index 0000000000..3100216f61 --- /dev/null +++ b/modules/bitgo/test/v2/unit/coins/ofcErc20Tokens.ts @@ -0,0 +1,236 @@ +import 'should'; +import { TestBitGo } from '@bitgo/sdk-test'; +import { BitGo } from '../../../../src/bitgo'; +import { coins, CoinFeature } from '@bitgo/statics'; + +describe('OFC ERC20 Tokens Configuration:', function () { + let bitgo; + + before(function () { + bitgo = TestBitGo.decorate(BitGo, { env: 'test' }); + bitgo.initializeTestVars(); + }); + + describe('validate addressCoin configuration for all OFC ERC20 tokens', function () { + it('should have addressCoin matching the first part of underlying asset', function () { + // Get all OFC ERC20 tokens (ofcerc20 and tofcerc20 instances) + // These are identified by having an addressCoin property + const ofcCoins = coins.filter((coin: any) => coin.family === 'ofc' && coin.addressCoin); + + const misconfigurations: string[] = []; + + ofcCoins.forEach((ofcCoin: any) => { + // Get the underlying asset + const asset = ofcCoin.asset; + + // Skip if no addressCoin property (not all OFC coins have it) + if (!ofcCoin.addressCoin) { + return; + } + + // Skip testnet tokens - they use testnet-specific addressCoins + // (e.g., gteth, hteth, tsol, tavaxc, tpolygon, tarbeth) + if (ofcCoin.network.type === 'testnet') { + return; + } + + // Determine expected addressCoin from the asset (mainnet only) + let expectedAddressCoin; + + if (asset.includes(':')) { + // For assets like 'baseeth:spec', 'xdc:usdc', 'mon:usdc', etc. + // The addressCoin should be the part before the colon + expectedAddressCoin = asset.split(':')[0]; + } else if (ofcCoin.name.includes(':')) { + // For tokens with ':' in the name like 'ofcbaseeth:spec' + // Extract the chain from the name + const nameParts = ofcCoin.name.replace(/^ofc/, '').split(':'); + if (nameParts.length > 1) { + expectedAddressCoin = nameParts[0]; + } else { + expectedAddressCoin = 'eth'; // Default to eth for standard ERC20 tokens + } + } else { + // For standard tokens without ':' in asset (e.g., 'USDC', 'LINK') + expectedAddressCoin = 'eth'; + } + + // Check addressCoin matches expected value + if (ofcCoin.addressCoin !== expectedAddressCoin) { + misconfigurations.push( + `Token ${ofcCoin.name} with asset ${asset} should have addressCoin='${expectedAddressCoin}' but has '${ofcCoin.addressCoin}'` + ); + } + }); + + // Report all misconfigurations at once + if (misconfigurations.length > 0) { + throw new Error( + `Found ${misconfigurations.length} addressCoin misconfigurations:\n` + misconfigurations.join('\n') + ); + } + }); + + it('should validate specific chain-specific tokens', function () { + // Test specific tokens by chain + const testCases = [ + // XDC Network tokens + { token: 'ofcxdc:usdc', addressCoin: 'xdc', chain: 'XDC' }, + { token: 'ofcxdc:lbt', addressCoin: 'xdc', chain: 'XDC' }, + { token: 'ofcxdc:gama', addressCoin: 'xdc', chain: 'XDC' }, + { token: 'ofcxdc:srx', addressCoin: 'xdc', chain: 'XDC' }, + { token: 'ofcxdc:weth', addressCoin: 'xdc', chain: 'XDC' }, + // Base Ethereum tokens + { token: 'ofcbaseeth:spec', addressCoin: 'baseeth', chain: 'Base' }, + { token: 'ofcbaseeth:soon', addressCoin: 'baseeth', chain: 'Base' }, + { token: 'ofcbaseeth:wave', addressCoin: 'baseeth', chain: 'Base' }, + { token: 'ofcbaseeth:tig', addressCoin: 'baseeth', chain: 'Base' }, + { token: 'ofcbaseeth:virtual', addressCoin: 'baseeth', chain: 'Base' }, + { token: 'ofcbaseeth:zora', addressCoin: 'baseeth', chain: 'Base' }, + { token: 'ofcbaseeth:toshi', addressCoin: 'baseeth', chain: 'Base' }, + { token: 'ofcbaseeth:creator', addressCoin: 'baseeth', chain: 'Base' }, + { token: 'ofcbaseeth:avnt', addressCoin: 'baseeth', chain: 'Base' }, + { token: 'ofcbaseeth:mira', addressCoin: 'baseeth', chain: 'Base' }, + { token: 'ofcbaseeth:towns', addressCoin: 'baseeth', chain: 'Base' }, + { token: 'ofcbaseeth:recall', addressCoin: 'baseeth', chain: 'Base' }, + { token: 'ofcbaseeth:brlv', addressCoin: 'baseeth', chain: 'Base' }, + { token: 'ofcbaseeth:wbrly', addressCoin: 'baseeth', chain: 'Base' }, + { token: 'ofcbaseeth:sapien', addressCoin: 'baseeth', chain: 'Base' }, + { token: 'ofcbaseeth:aixbt', addressCoin: 'baseeth', chain: 'Base' }, + { token: 'ofcbaseeth:brett', addressCoin: 'baseeth', chain: 'Base' }, + // MON Network tokens + { token: 'ofcmon:usdc', addressCoin: 'mon', chain: 'MON' }, + { token: 'ofcmon:wmon', addressCoin: 'mon', chain: 'MON' }, + // HYPE Network token + { token: 'ofchype:hwhype', addressCoin: 'hype', chain: 'HYPE' }, + // IP (Story Network) token + { token: 'ofcip:aria', addressCoin: 'ip', chain: 'Story' }, + ]; + + const errors: string[] = []; + + testCases.forEach(({ token, addressCoin, chain }) => { + const ofcCoin: any = coins.get(token); + if (!ofcCoin) { + errors.push(`${chain} token ${token} not found in statics`); + } else if (ofcCoin.addressCoin !== addressCoin) { + errors.push( + `${chain} token ${token} should have addressCoin='${addressCoin}' but has '${ofcCoin.addressCoin}'` + ); + } + }); + + if (errors.length > 0) { + throw new Error(`Found ${errors.length} configuration errors:\n` + errors.join('\n')); + } + }); + + it('should validate all tokens have addressCoin property', function () { + // Get all OFC ERC20 tokens (ofcerc20 and tofcerc20 instances) + // These should all have an addressCoin property + const ofcErc20Tokens = coins.filter( + (coin: any) => + coin.family === 'ofc' && coin.isToken === true && (coin.name.includes(':') || coin.asset.includes(':')) + ); + + const tokensWithoutAddressCoin: string[] = []; + + ofcErc20Tokens.forEach((ofcCoin: any) => { + if (!ofcCoin.addressCoin) { + tokensWithoutAddressCoin.push(`${ofcCoin.name} (asset: ${ofcCoin.asset})`); + } + }); + + // Report tokens without addressCoin (informational only, not a failure) + tokensWithoutAddressCoin.length.should.be.greaterThanOrEqual(0); + }); + }); + + describe('validate required custody features for all OFC ERC20 tokens', function () { + it('should have required custody features for ofcerc20 and tofcerc20 tokens', function () { + const requiredFeatures = [ + CoinFeature.ACCOUNT_MODEL, + CoinFeature.REQUIRES_BIG_NUMBER, + CoinFeature.CUSTODY, + CoinFeature.CUSTODY_BITGO_TRUST, + ]; + + // Get all OFC ERC20 tokens (ofcerc20 and tofcerc20 instances) + // These are identified by having an addressCoin property + const ofcCoins = coins.filter((coin: any) => coin.family === 'ofc' && coin.addressCoin); + + const missingFeatures: string[] = []; + + ofcCoins.forEach((ofcCoin) => { + requiredFeatures.forEach((feature) => { + if (!ofcCoin.features.includes(feature)) { + missingFeatures.push(`Token ${ofcCoin.name} is missing feature: ${feature}`); + } + }); + }); + + if (missingFeatures.length > 0) { + throw new Error(`Found ${missingFeatures.length} missing features:\n` + missingFeatures.join('\n')); + } + }); + }); + + describe('validate address validation for chain-specific tokens', function () { + it('should validate bg- format addresses for all OFC tokens', function () { + // Get sample OFC tokens from different chains + const testTokens = [ + 'ofcxdc:usdc', + 'ofcbaseeth:spec', + 'ofcmon:usdc', + 'ofchype:hwhype', + 'ofcip:aria', + 'ofceth', + 'ofcbtc', + ]; + + const errors: string[] = []; + + testTokens.forEach((tokenName) => { + const ofcCoin = bitgo.coin(tokenName); + if (ofcCoin) { + const validBgAddress = 'bg-5b2b80eafbdf94d5030bb23f9b56ad64'; + const invalidBgAddress = 'bg-5b2b80eafbdf94d5030bb23f9b56ad64nnn'; + + if (!ofcCoin.isValidAddress(validBgAddress)) { + errors.push(`${tokenName} should accept valid bg- address format`); + } + + if (ofcCoin.isValidAddress(invalidBgAddress)) { + errors.push(`${tokenName} should reject invalid bg- address format`); + } + } + }); + + if (errors.length > 0) { + throw new Error(`Found ${errors.length} address validation errors:\n` + errors.join('\n')); + } + }); + }); + + describe('validate all OFC tokens are properly registered', function () { + it('should be able to instantiate all OFC tokens', function () { + const ofcCoins = coins.filter((coin) => coin.family === 'ofc'); + const errors: string[] = []; + + ofcCoins.forEach((ofcCoin) => { + try { + const coin = bitgo.coin(ofcCoin.name); + if (!coin) { + errors.push(`Failed to instantiate ${ofcCoin.name}`); + } + } catch (e) { + errors.push(`Error instantiating ${ofcCoin.name}: ${e.message}`); + } + }); + + if (errors.length > 0) { + throw new Error(`Found ${errors.length} instantiation errors:\n` + errors.join('\n')); + } + }); + }); +}); diff --git a/modules/statics/src/base.ts b/modules/statics/src/base.ts index 6dee7a26f9..a3abde5d19 100644 --- a/modules/statics/src/base.ts +++ b/modules/statics/src/base.ts @@ -2918,7 +2918,7 @@ export enum UnderlyingAsset { 'baseeth:wbrly' = 'baseeth:wbrly', 'baseeth:recall' = 'baseeth:recall', 'baseeth:sapien' = 'baseeth:sapien', - 'baseeth:aixbt' = 'baseeht:aixbt', + 'baseeth:aixbt' = 'baseeth:aixbt', 'baseeth:brett' = 'baseeth:brett', // BaseETH testnet tokens diff --git a/modules/statics/src/coins/ofcErc20Coins.ts b/modules/statics/src/coins/ofcErc20Coins.ts index a5229d8ed0..240e6c3b20 100644 --- a/modules/statics/src/coins/ofcErc20Coins.ts +++ b/modules/statics/src/coins/ofcErc20Coins.ts @@ -4493,23 +4493,63 @@ export const tOfcErc20Coins = [ ofcerc20('3ad9b598-11bd-4dba-9a42-a74eae4c6b43', 'ofceth:omi', 'omi', 18, underlyingAssetForSymbol('eth:omi')), ofcerc20('bf7b99fe-d666-4db7-a775-c05e5bff98ce', 'ofceth:andy', 'andy', 18, underlyingAssetForSymbol('eth:andy')), - ofcerc20('d2b5f3e4-3c4e-4f1e-9f0a-1b2c3d4e5f6a', 'ofcbaseeth:spec', 'Spectral', 18, UnderlyingAsset['baseeth:spec']), + ofcerc20( + 'd2b5f3e4-3c4e-4f1e-9f0a-1b2c3d4e5f6a', + 'ofcbaseeth:spec', + 'Spectral', + 18, + UnderlyingAsset['baseeth:spec'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'baseeth' + ), ofcerc20( 'bc7be60b-7eb8-4512-9675-d804f540962a', 'ofcbaseeth:soon', 'Soon Token', 18, - UnderlyingAsset['baseeth:soon'] + UnderlyingAsset['baseeth:soon'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'baseeth' ), - ofcerc20('c2c6b14c-62e8-4dc1-9b8e-63e8fc5c7ab6', 'ofcbaseeth:wave', 'Waveform', 18, UnderlyingAsset['baseeth:wave']), + ofcerc20( + 'c2c6b14c-62e8-4dc1-9b8e-63e8fc5c7ab6', + 'ofcbaseeth:wave', + 'Waveform', + 18, + UnderlyingAsset['baseeth:wave'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'baseeth' + ), ofcerc20( 'd20cc76e-1384-4261-9d90-df2d6a87b3d0', 'ofchype:hwhype', 'Hyperwave HYPE', 18, - UnderlyingAsset['hype:hwhype'] + UnderlyingAsset['hype:hwhype'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'hype' ), ofcerc20( @@ -4517,7 +4557,14 @@ export const tOfcErc20Coins = [ 'ofcbaseeth:tig', 'The Innovation Game', 18, - UnderlyingAsset['baseeth:tig'] + UnderlyingAsset['baseeth:tig'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'baseeth' ), ofcerc20( @@ -4525,39 +4572,119 @@ export const tOfcErc20Coins = [ 'ofcbaseeth:virtual', 'Virtual Protocol', 18, - UnderlyingAsset['baseeth:virtual'] + UnderlyingAsset['baseeth:virtual'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'baseeth' ), - ofcerc20('a5e8f6e7-6f7a-4f4a-8f3c-4e5f6a7b8c9d', 'ofcbaseeth:zora', 'Zora', 18, UnderlyingAsset['baseeth:zora']), + ofcerc20( + 'a5e8f6e7-6f7a-4f4a-8f3c-4e5f6a7b8c9d', + 'ofcbaseeth:zora', + 'Zora', + 18, + UnderlyingAsset['baseeth:zora'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'baseeth' + ), - ofcerc20('b6f9f7e8-7a8b-4f5b-8f4d-5f6a7b8c9dae', 'ofcbaseeth:toshi', 'Toshi', 18, UnderlyingAsset['baseeth:toshi']), + ofcerc20( + 'b6f9f7e8-7a8b-4f5b-8f4d-5f6a7b8c9dae', + 'ofcbaseeth:toshi', + 'Toshi', + 18, + UnderlyingAsset['baseeth:toshi'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'baseeth' + ), ofcerc20( 'c7aaf8e9-8b9c-4f6c-8f5e-6a7b8c9daebf', 'ofcbaseeth:creator', 'CreatorDAO', 18, - UnderlyingAsset['baseeth:creator'] + UnderlyingAsset['baseeth:creator'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'baseeth' ), - ofcerc20('d8bbf9ea-9cad-4f7d-8f6f-7b8c9daebfca', 'ofcbaseeth:avnt', 'Avantis', 18, UnderlyingAsset['baseeth:avnt']), + ofcerc20( + 'd8bbf9ea-9cad-4f7d-8f6f-7b8c9daebfca', + 'ofcbaseeth:avnt', + 'Avantis', + 18, + UnderlyingAsset['baseeth:avnt'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'baseeth' + ), ofcerc20( 'e9ccfaeb-adbe-4f8e-8f7a-8c9daebfcadb', 'ofcbaseeth:mira', 'Mira Network', 18, - UnderlyingAsset['baseeth:mira'] + UnderlyingAsset['baseeth:mira'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'baseeth' ), - ofcerc20('faddfbec-becf-4f9f-8f8b-9daebfcadbec', 'ofcbaseeth:towns', 'Towns', 18, UnderlyingAsset['baseeth:towns']), + ofcerc20( + 'faddfbec-becf-4f9f-8f8b-9daebfcadbec', + 'ofcbaseeth:towns', + 'Towns', + 18, + UnderlyingAsset['baseeth:towns'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'baseeth' + ), ofcerc20( 'f6bebafa-7934-4ca2-9195-1f4543c2ce0c', 'ofcbaseeth:recall', 'Recall', 18, - UnderlyingAsset['baseeth:recall'] + UnderlyingAsset['baseeth:recall'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'baseeth' ), ofcerc20( @@ -4565,30 +4692,71 @@ export const tOfcErc20Coins = [ 'ofcbaseeth:brlv', 'BRL Velocity', 18, - UnderlyingAsset['baseeth:brlv'] + UnderlyingAsset['baseeth:brlv'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'baseeth' ), ofcerc20( '72d1eb99-3882-42db-abdd-c3a02f3829b4', 'ofcbaseeth:wbrly', 'Wrapped BRLY', 24, - UnderlyingAsset['baseeth:wbrly'] + UnderlyingAsset['baseeth:wbrly'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'baseeth' ), ofcerc20( 'bfda6989-f5d4-4cc4-a80f-6b88e8da5198', 'ofcbaseeth:sapien', 'Sapien', 18, - UnderlyingAsset['baseeth:sapien'] + UnderlyingAsset['baseeth:sapien'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'baseeth' ), ofcerc20( 'bdfff799-1623-4847-93c0-c1a040c13d3f', 'ofcbaseeth:aixbt', 'Aixbt by Virtuals', 18, - UnderlyingAsset['baseeth:aixbt'] + UnderlyingAsset['baseeth:aixbt'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'baseeth' + ), + ofcerc20( + '3ce0c7b4-7043-4309-8493-7809001ad410', + 'ofcbaseeth:brett', + 'Brett', + 18, + UnderlyingAsset['baseeth:brett'], + undefined, + undefined, + '', + undefined, + undefined, + true, + 'baseeth' ), - ofcerc20('3ce0c7b4-7043-4309-8493-7809001ad410', 'ofcbaseeth:brett', 'Brett', 18, UnderlyingAsset['baseeth:brett']), ofcerc20('abeefced-cfda-4afa-8f9c-aebfcadbecfd', 'ofceth:align', 'Aligned', 18, UnderlyingAsset['eth:align']), @@ -4640,14 +4808,26 @@ export const tOfcErc20Coins = [ 6, underlyingAssetForSymbol('mon:usdc'), undefined, - [CoinFeature.STABLECOIN] + undefined, + '', + undefined, + undefined, + true, + 'mon' ), ofcerc20( '7a8631a5-deed-43c5-92a0-13e3322429ba', 'ofcmon:wmon', 'Wrapped MON', 18, - underlyingAssetForSymbol('mon:wmon') + underlyingAssetForSymbol('mon:wmon'), + undefined, + undefined, + '', + undefined, + undefined, + true, + 'mon' ), // XDC Network tokens @@ -4658,33 +4838,85 @@ export const tOfcErc20Coins = [ 6, underlyingAssetForSymbol('xdc:usdc'), undefined, - [CoinFeature.STABLECOIN] + undefined, + '', + undefined, + undefined, + true, + 'xdc' ), ofcerc20( 'b4666353-81d0-491b-a554-bdd8e677be24', 'ofcxdc:lbt', 'Law Block Token', 18, - underlyingAssetForSymbol('xdc:lbt') + underlyingAssetForSymbol('xdc:lbt'), + undefined, + undefined, + '', + undefined, + undefined, + true, + 'xdc' ), ofcerc20( '086883c7-f7e9-458e-a0a1-ed3ec525f9c6', 'ofcxdc:gama', 'Gama Token', 18, - underlyingAssetForSymbol('xdc:gama') + underlyingAssetForSymbol('xdc:gama'), + undefined, + undefined, + '', + undefined, + undefined, + true, + 'xdc' + ), + ofcerc20( + '0c8b533c-1929-4de8-af36-9cf4b4409c0d', + 'ofcxdc:srx', + 'STORX', + 18, + underlyingAssetForSymbol('xdc:srx'), + undefined, + undefined, + '', + undefined, + undefined, + true, + 'xdc' ), - ofcerc20('0c8b533c-1929-4de8-af36-9cf4b4409c0d', 'ofcxdc:srx', 'STORX', 18, underlyingAssetForSymbol('xdc:srx')), ofcerc20( '3c7ec48a-ba51-47c9-9044-f29d9c0daf35', 'ofcxdc:weth', 'Wrapped Ether (XDC)', 18, - underlyingAssetForSymbol('xdc:weth') + underlyingAssetForSymbol('xdc:weth'), + undefined, + undefined, + '', + undefined, + undefined, + true, + 'xdc' ), // Story Network tokens - ofcerc20('452cc4f6-3c77-4193-a572-4b0d0f838c3c', 'ofcip:aria', 'Aria', 18, underlyingAssetForSymbol('ip:aria')), + ofcerc20( + '452cc4f6-3c77-4193-a572-4b0d0f838c3c', + 'ofcip:aria', + 'Aria', + 18, + underlyingAssetForSymbol('ip:aria'), + undefined, + undefined, + '', + undefined, + undefined, + true, + 'ip' + ), ]; function underlyingAssetForSymbol(underlyingAssetValue: string): UnderlyingAsset {