Skip to content

fix(statics): fix unstETH support #6630

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 6, 2025
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
13 changes: 12 additions & 1 deletion modules/bitgo/src/v2/coinFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ import {
Eos,
EosToken,
Erc20Token,
Erc721Token,
Etc,
Eth,
Ethw,
Expand Down Expand Up @@ -399,6 +400,12 @@ export function registerCoinConstructors(coinFactory: CoinFactory, coinMap: Coin
}
);

Erc721Token.createTokenConstructors([...tokens.bitcoin.eth.nfts, ...tokens.testnet.eth.nfts]).forEach(
({ name, coinConstructor }) => {
coinFactory.register(name, coinConstructor);
}
);

StellarToken.createTokenConstructors([...tokens.bitcoin.xlm.tokens, ...tokens.testnet.xlm.tokens]).forEach(
({ name, coinConstructor }) => {
coinFactory.register(name, coinConstructor);
Expand Down Expand Up @@ -862,7 +869,11 @@ export function getTokenConstructor(tokenConfig: TokenConfig): CoinConstructor |
switch (tokenConfig.coin) {
case 'eth':
case 'hteth':
return Erc20Token.createTokenConstructor(tokenConfig as Erc20TokenConfig);
if (tokenConfig.type.includes('erc721')) {
Copy link
Preview

Copilot AI Aug 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using string inclusion check for token type detection is fragile. Consider using a more robust method like checking a specific token standard property or using startsWith('erc721:') to be more precise.

Suggested change
if (tokenConfig.type.includes('erc721')) {
if (tokenConfig.type.startsWith('erc721')) {

Copilot uses AI. Check for mistakes.

return Erc721Token.createTokenConstructor(tokenConfig as EthLikeTokenConfig);
} else {
return Erc20Token.createTokenConstructor(tokenConfig as Erc20TokenConfig);
}
case 'xlm':
case 'txlm':
return StellarToken.createTokenConstructor(tokenConfig as StellarTokenConfig);
Expand Down
4 changes: 2 additions & 2 deletions modules/bitgo/src/v2/coins/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import { Doge, Tdoge } from '@bitgo/sdk-coin-doge';
import { Dot, Tdot } from '@bitgo/sdk-coin-dot';
import { Eos, EosToken, Teos } from '@bitgo/sdk-coin-eos';
import { Etc, Tetc } from '@bitgo/sdk-coin-etc';
import { Erc20Token, Eth, Gteth, Hteth, Teth } from '@bitgo/sdk-coin-eth';
import { Erc20Token, Erc721Token, Eth, Gteth, Hteth, Teth } from '@bitgo/sdk-coin-eth';
import { EvmCoin } from '@bitgo/sdk-coin-evm';
import { Flr, Tflr } from '@bitgo/sdk-coin-flr';
import { Ethw } from '@bitgo/sdk-coin-ethw';
Expand Down Expand Up @@ -104,7 +104,7 @@ export { Doge, Tdoge };
export { Dot, Tdot };
export { Bcha, Tbcha };
export { Eos, EosToken, Teos };
export { Erc20Token, Eth, Gteth, Hteth, Teth };
export { Erc20Token, Erc721Token, Eth, Gteth, Hteth, Teth };
export { Ethw };
export { EthLikeCoin, TethLikeCoin };
export { Etc, Tetc };
Expand Down
1 change: 1 addition & 0 deletions modules/bitgo/test/browser/browser.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ describe('Coins', () => {
AbstractUtxoCoin: 1,
AbstractLightningCoin: 1,
Erc20Token: 1,
Erc721Token: 1,
EthLikeCoin: 1,
TethLikeCoin: 1,
OfcToken: 1,
Expand Down
21 changes: 21 additions & 0 deletions modules/bitgo/test/v2/resources/amsTokenConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,25 @@ export const reducedAmsTokenConfig = {
contractAddress: '0x89a959b9184b4f8c8633646d5dfd049d2ebc983a',
},
],
'terc721:unsteth': [
{
id: '49ff49ea-3355-4717-bbb0-5e8f5cae2201',
fullName: 'Test Lido: stETH Withdrawal NFT',
name: 'terc721:unsteth',
prefix: '',
suffix: '',
baseUnit: 'wei',
kind: 'crypto',
family: 'eth',
isToken: true,
additionalFeatures: [],
excludedFeatures: [],
decimalPlaces: 0,
asset: 'terc721:unsteth',
network: {
name: 'Hoodi',
},
contractAddress: '0xfe56573178f1bcdf53f01a6e9977670dcbbd9186',
},
],
};
64 changes: 55 additions & 9 deletions modules/bitgo/test/v2/unit/ams/ams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,20 +25,19 @@ describe('Asset metadata service', () => {
});

it('should not fetch coin from custom coin factory when useAms is false', async () => {
const bitgo = TestBitGo.decorate(BitGo, { env: 'mock', microservicesUri, useAms: false } as any);
bitgo.initializeTestVars();
bitgo.initCoinFactory(reducedAmsTokenConfig);
const bitgoNoAms = TestBitGo.decorate(BitGo, { env: 'mock', microservicesUri, useAms: false } as any);
bitgoNoAms.initializeTestVars();
bitgoNoAms.initCoinFactory(reducedAmsTokenConfig);
(() => {
bitgo.coin('hteth:faketoken');
bitgoNoAms.coin('hteth:faketoken');
}).should.throw(
'Coin or token type hteth:faketoken not supported or not compiled. Please be sure that you are using the latest version of BitGoJS. If using @bitgo/sdk-api, please confirm you have registered hteth:faketoken first.'
);
});

it('should be able to register a token in the coin factory', () => {
const tokenName = 'hteth:faketoken';
const amsToken = reducedAmsTokenConfig[tokenName][0];
bitgo.registerToken(amsToken);
bitgo.registerToken(tokenName);
const coin = bitgo.coin(tokenName);
should.exist(coin);
coin.type.should.equal(tokenName);
Expand All @@ -47,6 +46,53 @@ describe('Asset metadata service', () => {
coin.tokenContractAddress.should.equal('0x89a959b9184b4f8c8633646d5dfd049d2ebc983a');
});

describe('ERC721 NFTs', () => {
it('should create a custom coin factory from ams response', async () => {
bitgo.initCoinFactory(reducedAmsTokenConfig);
const coin = bitgo.coin('erc721:unsteth');
should.exist(coin);
coin.type.should.equal('erc721:unsteth');
coin.name.should.equal('Lido: stETH Withdrawal NFT');
coin.decimalPlaces.should.equal(0);
coin.tokenContractAddress.should.equal('0x889edc2edab5f40e902b864ad4d7ade8e412f9b1');
});

it('should be able to register an nft in the coin factory', () => {
const nftName = 'terc721:unsteth';
bitgo.registerToken(nftName);
const coin = bitgo.coin(nftName);
should.exist(coin);
coin.type.should.equal(nftName);
coin.name.should.equal('Test Lido: stETH Withdrawal NFT');
coin.decimalPlaces.should.equal(0);
coin.tokenContractAddress.should.equal('0xfe56573178f1bcdf53f01a6e9977670dcbbd9186');
});

it('should fetch all assets from AMS and initialize the coin factory', async () => {
const bitgo = TestBitGo.decorate(BitGo, { env: 'mock', microservicesUri, useAms: true } as BitGoOptions);
bitgo.initializeTestVars();

// Setup nocks
nock(microservicesUri).get('/api/v1/assets/list/testnet').reply(200, reducedAmsTokenConfig);

await bitgo.registerAllTokens();
const coin = bitgo.coin('terc721:unsteth');
should.exist(coin);
});

it('should fetch nft from default coin factory when useAms is false', () => {
const bitgoNoAms = TestBitGo.decorate(BitGo, { env: 'mock', microservicesUri, useAms: false } as BitGoOptions);
bitgoNoAms.initializeTestVars();
bitgoNoAms.initCoinFactory(reducedAmsTokenConfig);
const coin: any = bitgoNoAms.coin('erc721:unsteth');
should.exist(coin);
coin.type.should.equal('erc721:unsteth');
coin.name.should.equal('Lido: stETH Withdrawal NFT');
coin.decimalPlaces.should.equal(0);
coin.tokenContractAddress.should.equal('0x889edc2edab5f40e902b864ad4d7ade8e412f9b1');
});
});

it('should fetch all assets from AMS and initialize the coin factory', async () => {
const bitgo = TestBitGo.decorate(BitGo, { env: 'mock', microservicesUri, useAms: true } as BitGoOptions);
bitgo.initializeTestVars();
Expand All @@ -61,10 +107,10 @@ describe('Asset metadata service', () => {

describe('registerToken', () => {
it('should throw an error when useAms is false', async () => {
const bitgo = TestBitGo.decorate(BitGo, { env: 'mock', microservicesUri, useAms: false } as BitGoOptions);
bitgo.initializeTestVars();
const bitgoNoAms = TestBitGo.decorate(BitGo, { env: 'mock', microservicesUri, useAms: false } as BitGoOptions);
bitgoNoAms.initializeTestVars();

await bitgo
await bitgoNoAms
.registerToken('hteth:faketoken')
.should.be.rejectedWith('registerToken is only supported when useAms is set to true');
});
Expand Down
Loading