diff --git a/modules/bitgo/src/v2/coinFactory.ts b/modules/bitgo/src/v2/coinFactory.ts index e2d712a1e4..733117dd1c 100644 --- a/modules/bitgo/src/v2/coinFactory.ts +++ b/modules/bitgo/src/v2/coinFactory.ts @@ -82,6 +82,7 @@ import { EthLikeCoin, EvmCoin, Flr, + FlrToken, HashToken, TethLikeCoin, FiatAED, @@ -531,6 +532,10 @@ export function registerCoinConstructors(coinFactory: CoinFactory, coinMap: Coin coinFactory.register(name, coinConstructor) ); + FlrToken.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) @@ -963,6 +968,9 @@ export function getTokenConstructor(tokenConfig: TokenConfig): CoinConstructor | case 'hash': case 'thash': return HashToken.createTokenConstructor(tokenConfig as CosmosTokenConfig); + case 'flr': + case 'tflr': + return FlrToken.createTokenConstructor(tokenConfig as EthLikeTokenConfig); default: return undefined; } diff --git a/modules/bitgo/src/v2/coins/index.ts b/modules/bitgo/src/v2/coins/index.ts index 6989975922..1872404188 100644 --- a/modules/bitgo/src/v2/coins/index.ts +++ b/modules/bitgo/src/v2/coins/index.ts @@ -31,7 +31,7 @@ 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, EthLikeErc20Token } from '@bitgo/sdk-coin-evm'; -import { Flr, Tflr } from '@bitgo/sdk-coin-flr'; +import { Flr, Tflr, FlrToken } from '@bitgo/sdk-coin-flr'; import { Ethw } from '@bitgo/sdk-coin-ethw'; import { EthLikeCoin, TethLikeCoin } from '@bitgo/sdk-coin-ethlike'; import { Hash, Thash, HashToken } from '@bitgo/sdk-coin-hash'; @@ -107,7 +107,7 @@ export { Ethw }; export { EthLikeCoin, TethLikeCoin }; export { Etc, Tetc }; export { EvmCoin, EthLikeErc20Token }; -export { Flr, Tflr }; +export { Flr, Tflr, FlrToken }; export { Hash, Thash, HashToken }; export { Hbar, Thbar }; export { Icp, Ticp }; diff --git a/modules/bitgo/test/browser/browser.spec.ts b/modules/bitgo/test/browser/browser.spec.ts index b81743eb84..b9a983e385 100644 --- a/modules/bitgo/test/browser/browser.spec.ts +++ b/modules/bitgo/test/browser/browser.spec.ts @@ -56,6 +56,7 @@ describe('Coins', () => { VetToken: 1, EthLikeErc20Token: 1, HashToken: 1, + FlrToken: 1, }; Object.keys(BitGoJS.Coin) .filter((coinName) => !excludedKeys[coinName]) diff --git a/modules/sdk-coin-flr/src/flrToken.ts b/modules/sdk-coin-flr/src/flrToken.ts new file mode 100644 index 0000000000..3a93bf3ebb --- /dev/null +++ b/modules/sdk-coin-flr/src/flrToken.ts @@ -0,0 +1,58 @@ +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'; + +export { EthLikeTokenConfig }; + +export class FlrToken extends EthLikeToken { + public readonly tokenConfig: EthLikeTokenConfig; + static coinNames: CoinNames = { + Mainnet: 'flr', + Testnet: 'tflr', + }; + constructor(bitgo: BitGoBase, tokenConfig: EthLikeTokenConfig) { + super(bitgo, tokenConfig, FlrToken.coinNames); + } + static createTokenConstructor(config: EthLikeTokenConfig): CoinConstructor { + return super.createTokenConstructor(config, FlrToken.coinNames); + } + + static createTokenConstructors(): NamedCoinConstructor[] { + return super.createTokenConstructors(FlrToken.coinNames); + } + + protected getTransactionBuilder(): TransactionBuilder { + return new TransactionBuilder(coins.get(this.getBaseChain())); + } + + /** @inheritDoc **/ + getMPCAlgorithm(): MPCAlgorithm { + return 'ecdsa'; + } + + /** @inheritDoc */ + supportsTss(): boolean { + return true; + } + + /** + * Make a query to Flare explorer for information such as balance, token balance, solidity calls + * @param {Object} query key-value pairs of parameters to append after /api + * @param {string} apiKey optional API key to use instead of the one from the environment + * @returns {Promise} response from Flare explorer + */ + async recoveryBlockchainExplorerQuery( + query: Record, + apiKey?: string + ): Promise> { + const apiToken = apiKey || common.Environments[this.bitgo.getEnv()].flrExplorerApiToken; + const explorerUrl = common.Environments[this.bitgo.getEnv()].flrExplorerBaseUrl; + return await recoveryBlockchainExplorerQuery(query, explorerUrl as string, apiToken); + } + + getFullName(): string { + return 'Flare Token'; + } +} diff --git a/modules/sdk-coin-flr/src/index.ts b/modules/sdk-coin-flr/src/index.ts index 6debfc7f9a..3f92c8e3f7 100644 --- a/modules/sdk-coin-flr/src/index.ts +++ b/modules/sdk-coin-flr/src/index.ts @@ -2,3 +2,4 @@ export * from './lib'; export * from './flr'; export * from './tflr'; export * from './register'; +export * from './flrToken'; diff --git a/modules/sdk-coin-flr/src/register.ts b/modules/sdk-coin-flr/src/register.ts index d8aaccd508..3fdcf431d7 100644 --- a/modules/sdk-coin-flr/src/register.ts +++ b/modules/sdk-coin-flr/src/register.ts @@ -1,8 +1,12 @@ import { BitGoBase } from '@bitgo/sdk-core'; import { Flr } from './flr'; import { Tflr } from './tflr'; +import { FlrToken } from './flrToken'; export const register = (sdk: BitGoBase): void => { sdk.register('flr', Flr.createInstance); sdk.register('tflr', Tflr.createInstance); + FlrToken.createTokenConstructors().forEach(({ name, coinConstructor }) => { + sdk.register(name, coinConstructor); + }); }; diff --git a/modules/sdk-coin-flr/test/unit/flrToken.ts b/modules/sdk-coin-flr/test/unit/flrToken.ts new file mode 100644 index 0000000000..a4be537b1d --- /dev/null +++ b/modules/sdk-coin-flr/test/unit/flrToken.ts @@ -0,0 +1,31 @@ +import { TestBitGo, TestBitGoAPI } from '@bitgo/sdk-test'; +import { BitGoAPI } from '@bitgo/sdk-api'; + +import { FlrToken } from '../../src/flrToken'; + +describe('Flare Token:', function () { + let bitgo: TestBitGoAPI; + let flrTokenCoin; + const tokenName = 'tflr:wflr'; + + before(function () { + bitgo = TestBitGo.decorate(BitGoAPI, { env: 'test' }); + FlrToken.createTokenConstructors().forEach(({ name, coinConstructor }) => { + bitgo.safeRegister(name, coinConstructor); + }); + bitgo.initializeTestVars(); + flrTokenCoin = bitgo.coin(tokenName); + }); + + it('should return constants', function () { + flrTokenCoin.getChain().should.equal('tflr:wflr'); + flrTokenCoin.getBaseChain().should.equal('tflr'); + flrTokenCoin.getFullName().should.equal('Flare Token'); + flrTokenCoin.getBaseFactor().should.equal(1e18); + flrTokenCoin.type.should.equal(tokenName); + flrTokenCoin.name.should.equal('Wrapped Flare Testnet'); + flrTokenCoin.coin.should.equal('tflr'); + flrTokenCoin.network.should.equal('Testnet'); + flrTokenCoin.decimalPlaces.should.equal(18); + }); +}); diff --git a/modules/statics/src/account.ts b/modules/statics/src/account.ts index 4199ccb9fb..a74de2fc3b 100644 --- a/modules/statics/src/account.ts +++ b/modules/statics/src/account.ts @@ -506,6 +506,16 @@ export class WorldERC20Token extends ContractAddressDefinedToken { } } +/** + * The Flr Chain network supports tokens + * Flr Chain Tokens are ERC20 tokens + */ +export class FlrERC20Token extends ContractAddressDefinedToken { + constructor(options: Erc20ConstructorOptions) { + super(options); + } +} + /** * The Xrp network supports tokens * Xrp tokens are identified by their issuer address @@ -2686,6 +2696,96 @@ export function tworldErc20( ); } +/** + * Factory function for FlrErc20 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 (divisibility exponent) + * @param contractAddress Contract address of this token + * @param asset Asset which this coin represents. This is the same for both mainnet and testnet variants of a coin. + * @param prefix? Optional token prefix. Defaults to empty string + * @param suffix? Optional token suffix. Defaults to token name. + * @param network? Optional token network. Defaults to Flr Chain mainnet network. + * @param features? Features of this coin. Defaults to the DEFAULT_FEATURES defined in `AccountCoin` + * @param primaryKeyCurve The elliptic curve for this chain/token + */ +export function flrErc20( + id: string, + name: string, + fullName: string, + decimalPlaces: number, + contractAddress: string, + asset: UnderlyingAsset, + features: CoinFeature[] = [...AccountCoin.DEFAULT_FEATURES, CoinFeature.EIP1559], + prefix = '', + suffix: string = name.toUpperCase(), + network: AccountNetwork = Networks.main.flr, + primaryKeyCurve: KeyCurve = KeyCurve.Secp256k1 +) { + return Object.freeze( + new FlrERC20Token({ + id, + name, + fullName, + network, + contractAddress, + prefix, + suffix, + features, + decimalPlaces, + asset, + isToken: true, + primaryKeyCurve, + baseUnit: BaseUnit.ETH, + }) + ); +} + +/** + * Factory function for Flr testnet FlrErc20 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 (divisibility exponent) + * @param contractAddress Contract address of this token + * @param asset Asset which this coin represents. This is the same for both mainnet and testnet variants of a coin. + * @param prefix? Optional token prefix. Defaults to empty string + * @param suffix? Optional token suffix. Defaults to token name. + * @param network? Optional token network. Defaults to the Flr Chain test network. + * @param features? Features of this coin. Defaults to the DEFAULT_FEATURES defined in `AccountCoin` + * @param primaryKeyCurve The elliptic curve for this chain/token + */ +export function tflrErc20( + id: string, + name: string, + fullName: string, + decimalPlaces: number, + contractAddress: string, + asset: UnderlyingAsset, + features: CoinFeature[] = AccountCoin.DEFAULT_FEATURES, + prefix = '', + suffix: string = name.toUpperCase(), + network: AccountNetwork = Networks.test.flr, + primaryKeyCurve: KeyCurve = KeyCurve.Secp256k1 +) { + return flrErc20( + id, + name, + fullName, + decimalPlaces, + contractAddress, + asset, + features, + prefix, + suffix, + network, + primaryKeyCurve + ); +} + /** * Factory function for xrp token instances. * diff --git a/modules/statics/src/allCoinsAndTokens.ts b/modules/statics/src/allCoinsAndTokens.ts index bb27df74ae..cc8c1efee5 100644 --- a/modules/statics/src/allCoinsAndTokens.ts +++ b/modules/statics/src/allCoinsAndTokens.ts @@ -13,6 +13,7 @@ import { erc20CompatibleAccountCoin, erc721, fiat, + flrErc20, gasTankAccount, hederaCoin, hederaToken, @@ -30,6 +31,7 @@ import { teosToken, terc1155, terc721, + tflrErc20, topethErc20, tpolyxToken, tronToken, @@ -3952,6 +3954,24 @@ export const allCoinsAndTokens = [ UnderlyingAsset['tworld:usdc'], [...AccountCoin.DEFAULT_FEATURES, CoinFeature.STABLECOIN] ), + flrErc20( + '1a38ab45-a789-4810-8d1d-2970af380753', + 'flr:wflr', + 'Wrapped Flare', + 18, + '0x1d80c49bbbcd1c0911346656b529df9e5c2f783d', + UnderlyingAsset['flr:wflr'], + [...AccountCoin.DEFAULT_FEATURES, CoinFeature.STABLECOIN] + ), + tflrErc20( + 'ff4dd56d-8fa0-4e92-b764-88c56ea48549', + 'tflr:wflr', + 'Wrapped Flare Testnet', + 18, + '0xab6fad89389b73dbc887d31206a26fd88d719d1f', + UnderlyingAsset['tflr:wflr'], + [...AccountCoin.DEFAULT_FEATURES, CoinFeature.STABLECOIN] + ), txrpToken( '8ef16158-1015-4a67-b6fe-db669c18ab2b', 'txrp:tst-rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd', diff --git a/modules/statics/src/base.ts b/modules/statics/src/base.ts index c996363d13..8b9b7ba248 100644 --- a/modules/statics/src/base.ts +++ b/modules/statics/src/base.ts @@ -2814,6 +2814,12 @@ export enum UnderlyingAsset { 'tworld:wld' = 'tworld:wld', 'tworld:usdc' = 'tworld:usdc', + // Flr mainnet tokens + 'flr:wflr' = 'flr:wflr', + + // Flr testnet tokens + 'tflr:wflr' = 'tflr:wflr', + ERC721 = 'erc721', ERC1155 = 'erc1155', NONSTANDARD = 'nonstandard', diff --git a/modules/statics/src/tokenConfig.ts b/modules/statics/src/tokenConfig.ts index b9fdf05dbb..831e18b55c 100644 --- a/modules/statics/src/tokenConfig.ts +++ b/modules/statics/src/tokenConfig.ts @@ -13,6 +13,7 @@ import { Erc1155Coin, Erc20Coin, Erc721Coin, + FlrERC20Token, HederaToken, Nep141Token, OpethERC20Token, @@ -210,6 +211,9 @@ export interface Tokens { world: { tokens: EthLikeTokenConfig[]; }; + flr: { + tokens: EthLikeTokenConfig[]; + }; sol: { tokens: SolTokenConfig[]; }; @@ -332,6 +336,9 @@ export interface Tokens { world: { tokens: EthLikeTokenConfig[]; }; + flr: { + tokens: EthLikeTokenConfig[]; + }; apt: { tokens: AptTokenConfig[]; nftCollections: AptNFTCollectionConfig[]; @@ -724,6 +731,24 @@ const getFormattedWorldTokens = (customCoinMap = coins) => return acc; }, []); +function getFlrTokenConfig(coin: FlrERC20Token): EthLikeTokenConfig { + return { + type: coin.name, + coin: coin.network.type === NetworkType.MAINNET ? 'flr' : 'tflr', + network: coin.network.type === NetworkType.MAINNET ? 'Mainnet' : 'Testnet', + name: coin.fullName, + tokenContractAddress: coin.contractAddress.toString().toLowerCase(), + decimalPlaces: coin.decimalPlaces, + }; +} +const getFormattedFlrTokens = (customCoinMap = coins) => + customCoinMap.reduce((acc: EthLikeTokenConfig[], coin) => { + if (coin instanceof FlrERC20Token) { + acc.push(getFlrTokenConfig(coin)); + } + return acc; + }, []); + function getSolTokenConfig(coin: SolCoin): SolTokenConfig { return { type: coin.name, @@ -1108,6 +1133,9 @@ export const getFormattedTokens = (coinMap = coins): Tokens => { world: { tokens: getFormattedWorldTokens(coinMap).filter((token) => token.network === 'Mainnet'), }, + flr: { + tokens: getFormattedFlrTokens(coinMap).filter((token) => token.network === 'Mainnet'), + }, apt: { tokens: getFormattedAptTokens(coinMap).filter((token) => token.network === 'Mainnet'), nftCollections: formattedAptNFTCollections.filter( @@ -1215,6 +1243,9 @@ export const getFormattedTokens = (coinMap = coins): Tokens => { world: { tokens: getFormattedWorldTokens(coinMap).filter((token) => token.network === 'Testnet'), }, + flr: { + tokens: getFormattedFlrTokens(coinMap).filter((token) => token.network === 'Testnet'), + }, near: { tokens: getFormattedNep141Tokens(coinMap).filter((token) => token.network === 'Testnet'), },