diff --git a/modules/bitgo/src/v2/coinFactory.ts b/modules/bitgo/src/v2/coinFactory.ts index 109c9e339b..0594157217 100644 --- a/modules/bitgo/src/v2/coinFactory.ts +++ b/modules/bitgo/src/v2/coinFactory.ts @@ -516,6 +516,12 @@ export function registerCoinConstructors(coinFactory: CoinFactory, coinMap: Coin } ); + CantonToken.createTokenConstructors([...tokens.bitcoin.canton.tokens, ...tokens.testnet.canton.tokens]).forEach( + ({ name, coinConstructor }) => { + coinFactory.register(name, coinConstructor); + } + ); + HbarToken.createTokenConstructors([...tokens.bitcoin.hbar.tokens, ...tokens.testnet.hbar.tokens]).forEach( ({ name, coinConstructor }) => { coinFactory.register(name, coinConstructor); diff --git a/modules/sdk-coin-canton/src/register.ts b/modules/sdk-coin-canton/src/register.ts index fbc63c8f44..d8816e783d 100644 --- a/modules/sdk-coin-canton/src/register.ts +++ b/modules/sdk-coin-canton/src/register.ts @@ -1,4 +1,5 @@ -import { BitGoBase } from '@bitgo/sdk-core'; +import { BitGoBase, GlobalCoinFactory } from '@bitgo/sdk-core'; +import { type CoinMap, getFormattedCantonTokens } from '@bitgo/statics'; import { Canton } from './canton'; import { Tcanton } from './tcanton'; import { CantonToken } from './cantonToken'; @@ -10,3 +11,15 @@ export const register = (sdk: BitGoBase): void => { sdk.register(name, coinConstructor); }); }; + +export const registerWithCoinMap = (sdk: BitGoBase, coinMap: CoinMap): void => { + sdk.register('canton', Canton.createInstance); + sdk.register('tcanton', Tcanton.createInstance); + // Registration for Canton tokens from the coin map (includes both hardcoded and dynamic tokens from AMS). + CantonToken.createTokenConstructors(getFormattedCantonTokens(coinMap)).forEach(({ name, coinConstructor }) => { + sdk.register(name, coinConstructor); + if (coinMap.has(name)) { + GlobalCoinFactory.registerToken(coinMap.get(name), coinConstructor); + } + }); +}; diff --git a/modules/sdk-coin-canton/test/unit/register.ts b/modules/sdk-coin-canton/test/unit/register.ts new file mode 100644 index 0000000000..5a258dd28c --- /dev/null +++ b/modules/sdk-coin-canton/test/unit/register.ts @@ -0,0 +1,75 @@ +import sinon from 'sinon'; +import assert from 'assert'; +import { BitGoAPI } from '@bitgo/sdk-api'; +import { GlobalCoinFactory } from '@bitgo/sdk-core'; +import { coins, CantonToken } from '@bitgo/statics'; +import { register, registerWithCoinMap } from '../../src/register'; +import { CantonToken as SdkCantonToken } from '../../src/cantonToken'; + +describe('Canton Register', function () { + let bitgo: BitGoAPI; + let registerSpy: sinon.SinonSpy; + let registerTokenSpy: sinon.SinonSpy; + + beforeEach(function () { + bitgo = new BitGoAPI({ env: 'test' }); + registerSpy = sinon.spy(bitgo, 'register'); + registerTokenSpy = sinon.spy(GlobalCoinFactory, 'registerToken'); + }); + + afterEach(function () { + registerSpy.restore(); + registerTokenSpy.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('canton')); + assert.ok(registeredNames.includes('tcanton')); + + // Canton tokens from statics should be registered + const cantonTokenCount = SdkCantonToken.createTokenConstructors().length; + assert.strictEqual(registerSpy.callCount, 2 + cantonTokenCount); + }); + }); + + 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 + assert.ok(registeredNames.includes('canton')); + assert.ok(registeredNames.includes('tcanton')); + }); + + it('should add dynamic Canton tokens to the global coin map', function () { + registerWithCoinMap(bitgo, coins); + + // registerToken should have been called for Canton tokens in the coin map + assert.ok(registerTokenSpy.callCount > 0); + + // Each call should pass a valid coin from the coinMap + for (let i = 0; i < registerTokenSpy.callCount; i++) { + const call = registerTokenSpy.getCall(i); + const staticsCoin = call.args[0]; + assert.ok(coins.has(staticsCoin.name), `${staticsCoin.name} should exist in the coin map`); + } + }); + + it('should not add tokens to the global coin map when coin map has no Canton tokens', function () { + const limitedCoinMap = coins.filter((coin) => !(coin instanceof CantonToken)); + + registerWithCoinMap(bitgo, limitedCoinMap); + + // registerToken should not be called since no Canton tokens are in the map + assert.strictEqual(registerTokenSpy.callCount, 0); + }); + }); +}); diff --git a/modules/statics/src/coins.ts b/modules/statics/src/coins.ts index 713db77d13..b6aed9d175 100644 --- a/modules/statics/src/coins.ts +++ b/modules/statics/src/coins.ts @@ -6,6 +6,7 @@ import { avaxErc20, beraErc20, bscToken, + cantonToken, celoToken, cosmosToken, eosToken, @@ -97,6 +98,7 @@ export function createToken(token: AmsTokenConfig): Readonly | undefin mon: erc20Token, xdc: erc20Token, bsc: bscToken, + canton: cantonToken, celo: celoToken, cosmos: cosmosToken, eth: erc20, @@ -369,6 +371,13 @@ export function createToken(token: AmsTokenConfig): Readonly | undefin token.contractAddress, // contractAddress ...commonArgs.slice(4) // asset, features, prefix, suffix, network, primaryKeyCurve ); + case 'canton': + return initializer( + ...commonArgs.slice(0, 4), // id, name, fullName, decimalPlaces + token.baseUrl, // baseUrl + token.contractAddress, // contractAddress + ...commonArgs.slice(4) // asset, features, prefix, suffix, network, primaryKeyCurve + ); default: return undefined; } diff --git a/modules/statics/src/networkFeatureMapForTokens.ts b/modules/statics/src/networkFeatureMapForTokens.ts index a006d1c8a1..9ea4ad163c 100644 --- a/modules/statics/src/networkFeatureMapForTokens.ts +++ b/modules/statics/src/networkFeatureMapForTokens.ts @@ -4,6 +4,7 @@ import { ADA_TOKEN_FEATURES, APT_FEATURES, BSC_TOKEN_FEATURES, + CANTON_TOKEN_FEATURES, EVM_FEATURES, POLYGON_TOKEN_FEATURES, POLYX_TOKEN_FEATURES, @@ -51,6 +52,7 @@ export const networkFeatureMapForTokens: Partial +export const getFormattedCantonTokens = (customCoinMap = coins) => customCoinMap.reduce((acc: CantonTokenConfig[], coin) => { if (coin instanceof CantonToken) { acc.push(getCantonTokenConfig(coin)); diff --git a/modules/statics/test/unit/resources/amsTokenConfig.ts b/modules/statics/test/unit/resources/amsTokenConfig.ts index a1d5121d57..7a1d4d437c 100644 --- a/modules/statics/test/unit/resources/amsTokenConfig.ts +++ b/modules/statics/test/unit/resources/amsTokenConfig.ts @@ -1209,4 +1209,27 @@ export const reducedTokenConfigForAllChains = { excludedFeatures: [], }, ], + 'tcanton:faketoken': [ + { + id: '9d4e8c1a-2b3f-4a5d-8e6f-7c8d9a0b1c2d', + fullName: 'Canton fake token', + name: 'tcanton:faketoken', + prefix: '', + suffix: 'TCANTON:FAKETOKEN', + baseUnit: 'canton', + kind: 'crypto', + family: 'canton', + isToken: true, + decimalPlaces: 10, + asset: 'tcanton:faketoken', + primaryKeyCurve: 'ed25519', + baseUrl: 'https://api.utilities.digitalasset-dev.com/api/token-standard/v0/registrars/', + contractAddress: 'fake-registrar::1220abcdef0123456789abcdef0123456789abcdef0123456789abcdef012345:FAKETOKEN', + network: { + name: 'CantonTestnet', + }, + additionalFeatures: [], + excludedFeatures: [], + }, + ], };