diff --git a/modules/sdk-api/package.json b/modules/sdk-api/package.json index e6a937276f..0dd6c53ae1 100644 --- a/modules/sdk-api/package.json +++ b/modules/sdk-api/package.json @@ -42,6 +42,7 @@ "dependencies": { "@bitgo/argon2": "^1.1.0", "@bitgo/sdk-core": "^36.41.0", + "@bitgo/statics": "^58.36.0", "@bitgo/sdk-hmac": "^1.9.0", "@bitgo/sjcl": "^1.1.0", "@bitgo/unspents": "^0.51.3", diff --git a/modules/sdk-api/src/bitgoAPI.ts b/modules/sdk-api/src/bitgoAPI.ts index 19d481893b..488a4d5770 100644 --- a/modules/sdk-api/src/bitgoAPI.ts +++ b/modules/sdk-api/src/bitgoAPI.ts @@ -22,6 +22,7 @@ import { makeRandomKey, sanitizeLegacyPath, } from '@bitgo/sdk-core'; +import { BaseCoin as StaticsBaseCoin } from '@bitgo/statics'; import * as sdkHmac from '@bitgo/sdk-hmac'; import { DefaultHmacAuthStrategy, type IHmacAuthStrategy } from '@bitgo/sdk-hmac'; import * as utxolib from '@bitgo/utxo-lib'; @@ -1582,6 +1583,10 @@ export class BitGoAPI implements BitGoBase { GlobalCoinFactory.register(name, coin); } + public registerWithBaseCoin(coin: CoinConstructor, baseCoin: Readonly): void { + GlobalCoinFactory.registerToken(baseCoin, coin); + } + /** * Get bitcoin market data * diff --git a/modules/sdk-coin-eth/src/register.ts b/modules/sdk-coin-eth/src/register.ts index 7420142e10..050468d717 100644 --- a/modules/sdk-coin-eth/src/register.ts +++ b/modules/sdk-coin-eth/src/register.ts @@ -21,7 +21,16 @@ export const register = (sdk: BitGoBase): void => { }; export const registerWithCoinMap = (sdk: BitGoBase, coinMap: CoinMap): void => { - Erc20Token.createTokenConstructors(getFormattedErc20Tokens(coinMap)).forEach(({ name, coinConstructor }) => { + register(sdk); + + // Registration for dynamic ERC20 tokens that are not hardcoded in the SDK, but are present in the coin map generated using AMS. + const formattedTokens = getFormattedErc20Tokens(coinMap); + Erc20Token.createTokenConstructors(formattedTokens).forEach(({ name, coinConstructor }) => { + // Register constructor for both type names and contract addresses sdk.register(name, coinConstructor); }); + // Add new tokens to the global coin map so they're available for lookup + formattedTokens.forEach((token) => { + sdk.registerWithBaseCoin(Erc20Token.createTokenConstructor(token), coinMap.get(token.type)); + }); }; diff --git a/modules/sdk-coin-eth/test/unit/register.ts b/modules/sdk-coin-eth/test/unit/register.ts new file mode 100644 index 0000000000..3e6f1cf54e --- /dev/null +++ b/modules/sdk-coin-eth/test/unit/register.ts @@ -0,0 +1,81 @@ +import sinon from 'sinon'; +import assert from 'assert'; +import { BitGoAPI } from '@bitgo/sdk-api'; +import { coins, Erc20Coin } from '@bitgo/statics'; +import { register, registerWithCoinMap } from '../../src/register'; +import { Erc20Token } from '../../src/erc20Token'; +import { Erc721Token } from '../../src/erc721Token'; + +describe('ETH Register', function () { + let bitgo: BitGoAPI; + let registerSpy: sinon.SinonSpy; + let registerWithBaseCoinSpy: sinon.SinonSpy; + + beforeEach(function () { + bitgo = new BitGoAPI({ env: 'test' }); + registerSpy = sinon.spy(bitgo, 'register'); + registerWithBaseCoinSpy = sinon.spy(bitgo, 'registerWithBaseCoin'); + }); + + afterEach(function () { + registerSpy.restore(); + registerWithBaseCoinSpy.restore(); + }); + + describe('register', function () { + it('should register base coins and token constructors', function () { + register(bitgo); + + const registeredNames = registerSpy.getCalls().map((call) => call.args[0]); + + // Base coins should be registered + assert.ok(registeredNames.includes('eth')); + assert.ok(registeredNames.includes('gteth')); + assert.ok(registeredNames.includes('teth')); + assert.ok(registeredNames.includes('hteth')); + + // ERC20 and ERC721 tokens should be registered + const erc20Count = Erc20Token.createTokenConstructors().length; + const erc721Count = Erc721Token.createTokenConstructors().length; + assert.strictEqual(registerSpy.callCount, 4 + erc20Count + erc721Count); + }); + }); + + describe('registerWithCoinMap', function () { + it('should call register internally for base coins and tokens', function () { + registerWithCoinMap(bitgo, coins); + + const registeredNames = registerSpy.getCalls().map((call) => call.args[0]); + + // Base coins should be registered via register() + assert.ok(registeredNames.includes('eth')); + assert.ok(registeredNames.includes('gteth')); + assert.ok(registeredNames.includes('teth')); + assert.ok(registeredNames.includes('hteth')); + }); + + it('should register dynamic ERC20 tokens via registerWithBaseCoin', function () { + registerWithCoinMap(bitgo, coins); + + // registerWithBaseCoin should have been called for dynamic tokens + assert.ok(registerWithBaseCoinSpy.callCount > 0); + + // Each call should pass a valid baseCoin from the coinMap + for (let i = 0; i < registerWithBaseCoinSpy.callCount; i++) { + const call = registerWithBaseCoinSpy.getCall(i); + const baseCoin = call.args[1]; + assert.ok(coins.has(baseCoin.name), `${baseCoin.name} should exist in the coin map`); + } + }); + + it('should not call registerWithBaseCoin when coin map has no ERC20 tokens', function () { + // Create a coin map with only base coins (no ERC20 tokens) + const limitedCoinMap = coins.filter((coin) => !(coin instanceof Erc20Coin)); + + registerWithCoinMap(bitgo, limitedCoinMap); + + // registerWithBaseCoin should not be called since no ERC20 tokens are in the map + assert.strictEqual(registerWithBaseCoinSpy.callCount, 0); + }); + }); +}); diff --git a/modules/sdk-core/src/bitgo/bitgoBase.ts b/modules/sdk-core/src/bitgo/bitgoBase.ts index ac03322b92..399ee6e5c4 100644 --- a/modules/sdk-core/src/bitgo/bitgoBase.ts +++ b/modules/sdk-core/src/bitgo/bitgoBase.ts @@ -6,6 +6,7 @@ import { GetSharingKeyOptions, IRequestTracer, } from '../api'; +import { BaseCoin as StaticsBaseCoin } from '@bitgo/statics'; import { IBaseCoin } from './baseCoin'; import { CoinConstructor } from './coinFactory'; import { EnvironmentName } from './environments'; @@ -35,4 +36,5 @@ export interface BitGoBase { setRequestTracer(reqTracer: IRequestTracer): void; url(path: string, version?: number): string; register(name: string, coin: CoinConstructor): void; + registerWithBaseCoin(coin: CoinConstructor, baseCoin: Readonly): void; }