Skip to content

Commit 6b64045

Browse files
feat(sdk-api): add registerToken method to BitGoAPI
Adds a `registerToken(coinConfig, coinConstructor)` method to `BitGoAPI` that delegates to `GlobalCoinFactory.registerToken`. Enables runtime registration of AMS-sourced tokens without requiring statics library updates. Method is idempotent and does not affect existing statics coins. Closes CSHLD-89
1 parent 01a3479 commit 6b64045

File tree

2 files changed

+92
-0
lines changed

2 files changed

+92
-0
lines changed

modules/sdk-api/src/bitgoAPI.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
BitGoBase,
66
BitGoRequest,
77
CoinConstructor,
8+
CoinFactory,
89
common,
910
DecryptKeysOptions,
1011
DecryptOptions,
@@ -1559,6 +1560,21 @@ export class BitGoAPI implements BitGoBase {
15591560
GlobalCoinFactory.register(name, coin);
15601561
}
15611562

1563+
/**
1564+
* Registers a token into GlobalCoinFactory's coin map and constructor map.
1565+
* Enables runtime registration of tokens sourced from AMS (not in statics).
1566+
* Idempotent: re-registering the same token does not throw.
1567+
*
1568+
* @param coinConfig - The static coin/token config object
1569+
* @param coinConstructor - The constructor function for the token class
1570+
*/
1571+
public registerToken(
1572+
coinConfig: Parameters<CoinFactory['registerToken']>[0],
1573+
coinConstructor: CoinConstructor
1574+
): void {
1575+
GlobalCoinFactory.registerToken(coinConfig, coinConstructor);
1576+
}
1577+
15621578
/**
15631579
* Get bitcoin market data
15641580
*

modules/sdk-api/test/unit/bitgoAPI.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { ProxyAgent } from 'proxy-agent';
44
import * as sinon from 'sinon';
55
import nock from 'nock';
66
import type { IHmacAuthStrategy } from '@bitgo/sdk-hmac';
7+
import { GlobalCoinFactory, CoinConstructor } from '@bitgo/sdk-core';
78

89
describe('Constructor', function () {
910
describe('cookiesPropagationEnabled argument', function () {
@@ -794,4 +795,79 @@ describe('Constructor', function () {
794795
nock.cleanAll();
795796
});
796797
});
798+
799+
describe('registerToken', function () {
800+
let bitgo: BitGoAPI;
801+
let sandbox: sinon.SinonSandbox;
802+
803+
beforeEach(function () {
804+
bitgo = new BitGoAPI({ env: 'custom', customRootURI: 'https://app.example.local' });
805+
sandbox = sinon.createSandbox();
806+
});
807+
808+
afterEach(function () {
809+
sandbox.restore();
810+
});
811+
812+
it('should call GlobalCoinFactory.registerToken and allow sdk.coin() to resolve the registered token', function () {
813+
const mockCoinInstance = { type: 'test-ams-dynamic-token' } as any;
814+
const mockConstructor = sandbox.stub().returns(mockCoinInstance) as unknown as CoinConstructor;
815+
const mockCoinConfig = { name: 'test-ams-dynamic-token', id: 'test-ams-dynamic-token-id' } as any;
816+
817+
sandbox.stub(GlobalCoinFactory, 'registerToken');
818+
sandbox.stub(GlobalCoinFactory, 'getInstance').callsFake((_bitgo, name) => {
819+
if (name === 'test-ams-dynamic-token') {
820+
return mockConstructor(_bitgo, mockCoinConfig);
821+
}
822+
throw new Error(`Unsupported coin: ${name}`);
823+
});
824+
825+
bitgo.registerToken(mockCoinConfig, mockConstructor);
826+
827+
const coin = bitgo.coin('test-ams-dynamic-token');
828+
coin.should.equal(mockCoinInstance);
829+
(GlobalCoinFactory.registerToken as sinon.SinonStub)
830+
.calledOnceWith(mockCoinConfig, mockConstructor)
831+
.should.be.true();
832+
});
833+
834+
it('should be idempotent — calling registerToken twice for same token does not throw', function () {
835+
const mockCoinConfig = { name: 'test-ams-idempotent-token', id: 'test-ams-idempotent-token-id' } as any;
836+
const mockConstructor = sandbox.stub() as unknown as CoinConstructor;
837+
sandbox.stub(GlobalCoinFactory, 'registerToken');
838+
839+
(() => {
840+
bitgo.registerToken(mockCoinConfig, mockConstructor);
841+
bitgo.registerToken(mockCoinConfig, mockConstructor);
842+
}).should.not.throw();
843+
844+
(GlobalCoinFactory.registerToken as sinon.SinonStub).callCount.should.equal(2);
845+
});
846+
847+
it('should not affect existing statics coins after registerToken calls', function () {
848+
const newCoinConfig = { name: 'test-ams-new-token', id: 'test-ams-new-token-id' } as any;
849+
const newConstructor = sandbox.stub() as unknown as CoinConstructor;
850+
const existingCoinInstance = { type: 'eth' } as any;
851+
const existingConstructor = sandbox.stub().returns(existingCoinInstance) as unknown as CoinConstructor;
852+
853+
const registerTokenStub = sandbox.stub(GlobalCoinFactory, 'registerToken');
854+
sandbox.stub(GlobalCoinFactory, 'getInstance').callsFake((_bitgo, name) => {
855+
if (name === 'eth') {
856+
return existingConstructor(_bitgo, undefined);
857+
}
858+
throw new Error(`Unsupported coin: ${name}`);
859+
});
860+
861+
// Register a new AMS token
862+
bitgo.registerToken(newCoinConfig, newConstructor);
863+
864+
// Existing statics coin should still resolve correctly
865+
const ethCoin = bitgo.coin('eth');
866+
ethCoin.should.equal(existingCoinInstance);
867+
868+
// registerToken was called only once (for the new token, not for eth)
869+
registerTokenStub.calledOnce.should.be.true();
870+
registerTokenStub.calledWith(newCoinConfig, newConstructor).should.be.true();
871+
});
872+
});
797873
});

0 commit comments

Comments
 (0)