diff --git a/modules/bitgo/src/v2/coinFactory.ts b/modules/bitgo/src/v2/coinFactory.ts index c249573309..9ea1ab63f8 100644 --- a/modules/bitgo/src/v2/coinFactory.ts +++ b/modules/bitgo/src/v2/coinFactory.ts @@ -9,6 +9,7 @@ import { Near, TNear, Nep141Token } from '@bitgo/sdk-coin-near'; import { SolToken } from '@bitgo/sdk-coin-sol'; import { TrxToken } from '@bitgo/sdk-coin-trx'; import { CoinFactory, CoinConstructor } from '@bitgo/sdk-core'; +import { EthLikeErc20Token } from '@bitgo/sdk-coin-evm'; import { CoinMap, coins, @@ -532,6 +533,20 @@ export function registerCoinConstructors(coinFactory: CoinFactory, coinMap: Coin VetToken.createTokenConstructors().forEach(({ name, coinConstructor }) => coinFactory.register(name, coinConstructor) ); + + // Generic ERC20 token registration for coins with SUPPORTS_ERC20 feature + coins + .filter((coin) => coin.features.includes(CoinFeature.SUPPORTS_ERC20) && !coin.isToken) + .forEach((coin) => { + const coinNames = { + Mainnet: `${coin.name}`, + Testnet: `t${coin.name}`, + }; + + EthLikeErc20Token.createTokenConstructors(coinNames).forEach(({ name, coinConstructor }) => { + coinFactory.register(name, coinConstructor); + }); + }); } export function getCoinConstructor(coinName: string): CoinConstructor | undefined { diff --git a/modules/bitgo/src/v2/coins/index.ts b/modules/bitgo/src/v2/coins/index.ts index 8ea3bc6b6d..0fe7dd5d07 100644 --- a/modules/bitgo/src/v2/coins/index.ts +++ b/modules/bitgo/src/v2/coins/index.ts @@ -31,7 +31,7 @@ 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, Erc721Token, Eth, Gteth, Hteth, Teth } from '@bitgo/sdk-coin-eth'; -import { EvmCoin } from '@bitgo/sdk-coin-evm'; +import { EvmCoin, EthLikeErc20Token } from '@bitgo/sdk-coin-evm'; import { Flr, Tflr } from '@bitgo/sdk-coin-flr'; import { Ethw } from '@bitgo/sdk-coin-ethw'; import { EthLikeCoin, TethLikeCoin } from '@bitgo/sdk-coin-ethlike'; @@ -108,7 +108,7 @@ export { Erc20Token, Erc721Token, Eth, Gteth, Hteth, Teth }; export { Ethw }; export { EthLikeCoin, TethLikeCoin }; export { Etc, Tetc }; -export { EvmCoin }; +export { EvmCoin, EthLikeErc20Token }; export { Flr, Tflr }; export { Hash, Thash }; export { Hbar, Thbar }; diff --git a/modules/bitgo/test/browser/browser.spec.ts b/modules/bitgo/test/browser/browser.spec.ts index 75d6fb1409..7dac37a9bb 100644 --- a/modules/bitgo/test/browser/browser.spec.ts +++ b/modules/bitgo/test/browser/browser.spec.ts @@ -52,6 +52,7 @@ describe('Coins', () => { CosmosToken: 1, CosmosSharedCoin: 1, VetToken: 1, + EthLikeErc20Token: 1, }; Object.keys(BitGoJS.Coin) .filter((coinName) => !excludedKeys[coinName]) diff --git a/modules/sdk-coin-evm/src/ethLikeErc20Token.ts b/modules/sdk-coin-evm/src/ethLikeErc20Token.ts new file mode 100644 index 0000000000..c9805cdfbc --- /dev/null +++ b/modules/sdk-coin-evm/src/ethLikeErc20Token.ts @@ -0,0 +1,55 @@ +/** + * @prettier + */ +import { coins, EthLikeTokenConfig } from '@bitgo/statics'; +import { BitGoBase, CoinConstructor, common, MPCAlgorithm, NamedCoinConstructor } from '@bitgo/sdk-core'; +import { CoinNames, EthLikeToken, recoveryBlockchainExplorerQuery } from '@bitgo/abstract-eth'; +import { TransactionBuilder } from './lib'; +import assert from 'assert'; + +export class EthLikeErc20Token extends EthLikeToken { + public readonly tokenConfig: EthLikeTokenConfig; + private readonly coinNames: CoinNames; + + constructor(bitgo: BitGoBase, tokenConfig: EthLikeTokenConfig, coinNames: CoinNames) { + super(bitgo, tokenConfig, coinNames); + this.coinNames = coinNames; + } + + static createTokenConstructor(config: EthLikeTokenConfig, coinNames: CoinNames): CoinConstructor { + return (bitgo: BitGoBase) => new this(bitgo, config, coinNames); + } + + static createTokenConstructors(coinNames: CoinNames): NamedCoinConstructor[] { + return super.createTokenConstructors(coinNames); + } + + protected getTransactionBuilder(): TransactionBuilder { + return new TransactionBuilder(coins.get(this.getBaseChain())); + } + + getMPCAlgorithm(): MPCAlgorithm { + return 'ecdsa'; + } + + supportsTss(): boolean { + return true; + } + + async recoveryBlockchainExplorerQuery(query: Record): Promise> { + const family = this.getFamily(); + const evmConfig = common.Environments[this.bitgo.getEnv()].evm; + assert( + evmConfig && this.getFamily() in evmConfig, + `env config is missing for ${this.getFamily()} in ${this.bitgo.getEnv()}` + ); + const explorerUrl = evmConfig[family].baseUrl; + const apiToken = evmConfig[family].apiToken; + return await recoveryBlockchainExplorerQuery(query, explorerUrl as string, apiToken); + } + + //TODO: implement a way to return the coin family name or coin name instead of standard ERC20 Token. + getFullName(): string { + return 'ERC20 Token'; + } +} diff --git a/modules/sdk-coin-evm/src/index.ts b/modules/sdk-coin-evm/src/index.ts index 94e4f48f17..46b9b7c4f5 100644 --- a/modules/sdk-coin-evm/src/index.ts +++ b/modules/sdk-coin-evm/src/index.ts @@ -1,3 +1,4 @@ export * from './evmCoin'; export * from './lib'; export * from './register'; +export * from './ethLikeErc20Token'; diff --git a/modules/sdk-coin-evm/src/register.ts b/modules/sdk-coin-evm/src/register.ts index c310d3c3bf..7fde6c3810 100644 --- a/modules/sdk-coin-evm/src/register.ts +++ b/modules/sdk-coin-evm/src/register.ts @@ -1,6 +1,7 @@ import { BitGoBase } from '@bitgo/sdk-core'; import { CoinFeature, coins, NetworkType } from '@bitgo/statics'; import { EvmCoin } from './evmCoin'; +import { EthLikeErc20Token } from './ethLikeErc20Token'; export const registerAll = (sdk: BitGoBase): void => { coins @@ -14,12 +15,25 @@ export const registerAll = (sdk: BitGoBase): void => { }; export const register = (coinFamily: string, sdk: BitGoBase): void => { - if (coins.get(coinFamily).features.includes(CoinFeature.SHARED_EVM_SDK)) { - coins - .filter((coin) => coin.family === coinFamily && !coin.isToken) - .forEach((coin) => { + const coinFeatures = coins.get(coinFamily).features; + coins + .filter((coin) => coin.family === coinFamily && !coin.isToken) + .forEach((coin) => { + // Handle SHARED_EVM_SDK registration + if (coinFeatures.includes(CoinFeature.SHARED_EVM_SDK)) { sdk.register(coin.name, EvmCoin.createInstance); - }); - //TODO: add token registration after EVM Token Optimisation is implemented - } + } + + // Handle SUPPORTS_ERC20 registration + if (coinFeatures.includes(CoinFeature.SUPPORTS_ERC20)) { + const coinNames = { + Mainnet: `${coin.name}`, + Testnet: `${coin.name}`, + }; + + EthLikeErc20Token.createTokenConstructors(coinNames).forEach(({ name, coinConstructor }) => { + sdk.register(name, coinConstructor); + }); + } + }); }; diff --git a/modules/statics/src/account.ts b/modules/statics/src/account.ts index f3f482c167..5fa2d54903 100644 --- a/modules/statics/src/account.ts +++ b/modules/statics/src/account.ts @@ -431,6 +431,12 @@ export class AdaCoin extends AccountCoinToken { } } +export class EthLikeERC20Token extends ContractAddressDefinedToken { + constructor(options: Erc20ConstructorOptions) { + super(options); + } +} + /** * The AVAX C Chain network support tokens * AVAX C Chain Tokens are ERC20 coins @@ -797,6 +803,54 @@ export function gasTankAccount( ); } +/** + * Factory function for ethLikeErc20 token instances. + * + * @param id uuid v4 + * @param name unique identifier of the token + * @param fullName Complete human-readable name of the token + * @param decimalPlaces Number of decimal places this token supports + * @param contractAddress Contract address of this token + * @param asset Asset which this coin represents + * @param network Optional token network + * @param coinNames The parent coin names for mainnet and testnet + * @param features Features of this coin + * @param prefix Optional token prefix + * @param suffix Optional token suffix + * @param primaryKeyCurve The elliptic curve for this chain/token + */ +export function erc20Token( + id: string, + name: string, + fullName: string, + decimalPlaces: number, + contractAddress: string, + asset: UnderlyingAsset, + network: AccountNetwork, + features: CoinFeature[] = [...AccountCoin.DEFAULT_FEATURES, CoinFeature.EIP1559], + prefix = '', + suffix: string = name.toUpperCase(), + primaryKeyCurve: KeyCurve = KeyCurve.Secp256k1 +) { + return Object.freeze( + new EthLikeERC20Token({ + id, + name, + fullName, + network, + contractAddress, + decimalPlaces, + asset, + features, + prefix, + suffix, + primaryKeyCurve, + isToken: true, + baseUnit: BaseUnit.ETH, + }) + ); +} + /** * Factory function for erc20 token instances. * diff --git a/modules/statics/src/base.ts b/modules/statics/src/base.ts index d2f3524e84..1d5c40a423 100644 --- a/modules/statics/src/base.ts +++ b/modules/statics/src/base.ts @@ -397,6 +397,11 @@ export enum CoinFeature { */ SHARED_EVM_SDK = 'shared-evm-sdk', + /** + * This coin supports erc20 tokens + */ + SUPPORTS_ERC20 = 'supports-erc20-token', + /** * This coin is a Cosmos coin and should use shared Cosmos SDK module */