Skip to content

feat: automate ERC20 automation #6665

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

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
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
14 changes: 14 additions & 0 deletions modules/bitgo/src/v2/coinFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -532,6 +533,19 @@ 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))
.forEach((coin) => {
const coinNames = {
[coin.network.type]: coin.name,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

would only populate for Mainnet

};

EthLikeErc20Token.createTokenConstructors(coinNames).forEach(({ name, coinConstructor }) => {
coinFactory.register(name, coinConstructor);
});
});
}

export function getCoinConstructor(coinName: string): CoinConstructor | undefined {
Expand Down
51 changes: 51 additions & 0 deletions modules/sdk-coin-evm/src/ethLikeErc20Token.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
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<string, string>): Promise<Record<string, unknown>> {
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);
}

getFullName(): string {
return 'ERC20 Token';
}
}
1 change: 1 addition & 0 deletions modules/sdk-coin-evm/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './evmCoin';
export * from './lib';
export * from './register';
export * from './ethLikeErc20Token';
59 changes: 59 additions & 0 deletions modules/statics/src/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,15 @@ export class AdaCoin extends AccountCoinToken {
}
}

export class EthLikeERC20Token extends ContractAddressDefinedToken {
public readonly coinNames: { Mainnet: string; Testnet: string };

constructor(options: Erc20ConstructorOptions & { coinNames: { Mainnet: string; Testnet: string } }) {
super(options);
this.coinNames = options.coinNames;
}
}

/**
* The AVAX C Chain network support tokens
* AVAX C Chain Tokens are ERC20 coins
Expand Down Expand Up @@ -797,6 +806,56 @@ 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,
coinNames: { Mainnet: string; Testnet: string },
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,
coinNames,
isToken: true,
baseUnit: BaseUnit.ETH,
})
);
}

/**
* Factory function for erc20 token instances.
*
Expand Down
5 changes: 5 additions & 0 deletions modules/statics/src/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,11 @@ export enum CoinFeature {
*/
SHARED_EVM_SDK = 'shared-evm-sdk',

/**
* This coin supports erc20 tokens
*/
SUPPORTS_ERC20 = 'supports-erc20',

/**
* This coin is a Cosmos coin and should use shared Cosmos SDK module
*/
Expand Down
74 changes: 73 additions & 1 deletion modules/statics/src/tokenConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import {
XrpCoin,
ZkethERC20Token,
} from './account';
import { CoinFamily, CoinKind, BaseCoin } from './base';
import { CoinFamily, CoinKind, BaseCoin, CoinFeature } from './base';
import { coins } from './coins';
import { Networks, NetworkType } from './networks';
import { OfcCoin } from './ofc';
Expand Down Expand Up @@ -748,6 +748,23 @@ export const getFormattedAlgoTokens = (customCoinMap = coins) =>
return acc;
}, []);

// Get the list of ERC-20 tokens from statics and format it properly
// TODO: use it instead of getFormatteCoredaoTokens
// const getFormattedEthLikeTokens = (customCoinMap = coins) =>
// customCoinMap.reduce((acc: EthLikeTokenConfig[], coin) => {
// if (coin instanceof EthLikeERC20Token && coin.features.includes(CoinFeature.SHARED_EVM_SDK)) {
// acc.push({
// type: coin.name,
// coin: coin.network.type === NetworkType.MAINNET ? coin.coinNames.Mainnet : coin.coinNames.Testnet,
// network: coin.network.type === NetworkType.MAINNET ? 'Mainnet' : 'Testnet',
// name: coin.fullName,
// tokenContractAddress: coin.contractAddress.toString().toLowerCase(),
// decimalPlaces: coin.decimalPlaces,
// });
// }
// return acc;
// }, []);

function getHbarTokenConfig(coin: HederaToken): HbarTokenConfig {
return {
type: coin.name,
Expand Down Expand Up @@ -995,6 +1012,59 @@ function getCosmosTokenConfig(coin: CosmosChainToken): CosmosTokenConfig {
};
}

export interface FormattedTokensByChain {
bitcoin: {
[chainName: string]: {
tokens: EthLikeTokenConfig[];
nfts?: EthLikeTokenConfig[];
};
};
testnet: {
[chainName: string]: {
tokens: EthLikeTokenConfig[];
nfts?: EthLikeTokenConfig[];
};
};
}

/**
* Dynamically populate tokens by looping through coins once
*/
function populateEthLikeTokens(customCoinMap = coins): FormattedTokensByChain {
const tokensMap: FormattedTokensByChain = {
bitcoin: {},
testnet: {},
};

customCoinMap.forEach((coin) => {
if (coin.features.includes(CoinFeature.SUPPORTS_ERC20)) {
const isMainnet = coin.network.type === NetworkType.MAINNET;
const networkKey = isMainnet ? 'bitcoin' : 'testnet';
const chainKey = isMainnet ? coin.coinNames.Mainnet : coin.coinNames.Testnet;

const formattedToken: EthLikeTokenConfig = {
type: coin.name,
coin: chainKey,
network: isMainnet ? 'Mainnet' : 'Testnet',
name: coin.fullName,
tokenContractAddress: coin.contractAddress.toString().toLowerCase(),
decimalPlaces: coin.decimalPlaces,
};

// Initialize chain tokens array if it doesn't exist
if (!tokensMap[networkKey][chainKey]) {
tokensMap[networkKey][chainKey] = { tokens: [] };
}

tokensMap[networkKey][chainKey].tokens.push(formattedToken);
}
});

return tokensMap;
}

const ethLikeTokens = populateEthLikeTokens(coins);

export const getFormattedTokens = (coinMap = coins): Tokens => {
const formattedAptNFTCollections = getFormattedAptNFTCollections(coinMap);
return {
Expand Down Expand Up @@ -1092,6 +1162,7 @@ export const getFormattedTokens = (coinMap = coins): Tokens => {
cosmos: {
tokens: getFormattedCosmosChainTokens(coinMap).filter((token) => token.network === 'Mainnet'),
},
...ethLikeTokens.bitcoin,
},
testnet: {
eth: {
Expand Down Expand Up @@ -1187,6 +1258,7 @@ export const getFormattedTokens = (coinMap = coins): Tokens => {
cosmos: {
tokens: getFormattedCosmosChainTokens(coinMap).filter((token) => token.network === 'Testnet'),
},
...ethLikeTokens.testnet,
},
};
};
Expand Down
Loading