diff --git a/modules/account-lib/src/index.ts b/modules/account-lib/src/index.ts index c417204d99..14111b0aaa 100644 --- a/modules/account-lib/src/index.ts +++ b/modules/account-lib/src/index.ts @@ -8,7 +8,7 @@ import { accountLibBaseCoin, acountLibCrypto, } from '@bitgo/sdk-core'; -import { BaseCoin as CoinConfig, coins } from '@bitgo/statics'; +import { BaseCoin as CoinConfig, coins, CoinFeature } from '@bitgo/statics'; export { Ed25519BIP32, Eddsa }; /** @@ -191,6 +191,9 @@ export { Stt }; import * as Soneium from '@bitgo/sdk-coin-soneium'; export { Soneium }; +import * as EvmCoin from '@bitgo/sdk-coin-evm'; +export { EvmCoin }; + const coinBuilderMap = { trx: Trx.WrappedBuilder, ttrx: Trx.WrappedBuilder, @@ -303,6 +306,16 @@ const coinBuilderMap = { tpolyx: Polyx.TransactionBuilderFactory, }; +/** + * coins.filter(coin => coin.coinFeature.has(EVM_SHARED_SDK)).forEach(coin => coinBuilderMap[coin.name] = EvmCoin.TransactionBuilder); + */ + +coins + .filter((coin) => coin.features.includes(CoinFeature.SHARED_EVM_SDK)) + .forEach((coin) => { + coinBuilderMap[coin.name] = EvmCoin.TransactionBuilder; + }); + /** * Get the list of coin tickers supported by this library. */ diff --git a/modules/bitgo/src/v2/coinFactory.ts b/modules/bitgo/src/v2/coinFactory.ts index ca544dab64..0aa02715f7 100644 --- a/modules/bitgo/src/v2/coinFactory.ts +++ b/modules/bitgo/src/v2/coinFactory.ts @@ -9,7 +9,7 @@ import { Near, TNear } from '@bitgo/sdk-coin-near'; import { SolToken } from '@bitgo/sdk-coin-sol'; import { TrxToken } from '@bitgo/sdk-coin-trx'; import { CoinFactory } from '@bitgo/sdk-core'; -import { CoinMap, coins, getFormattedTokens } from '@bitgo/statics'; +import { CoinFeature, CoinMap, coins, getFormattedTokens } from '@bitgo/statics'; import { Ada, Algo, @@ -49,6 +49,7 @@ import { Eth, Ethw, EthLikeCoin, + EvmCoin, FetchAi, Flr, TethLikeCoin, @@ -345,6 +346,12 @@ export function registerCoinConstructors(coinFactory: CoinFactory, coinMap: Coin coinFactory.register('zeta', Zeta.createInstance); coinFactory.register('zketh', Zketh.createInstance); + coins + .filter((coin) => coin.features.includes(CoinFeature.SHARED_EVM_SDK)) + .forEach((coin) => { + coinFactory.register(coin.name, EvmCoin.createInstance); + }); + const tokens = getFormattedTokens(coinMap); Erc20Token.createTokenConstructors([...tokens.bitcoin.eth.tokens, ...tokens.testnet.eth.tokens]).forEach( diff --git a/modules/bitgo/src/v2/coins/index.ts b/modules/bitgo/src/v2/coins/index.ts index 94805fd7ad..53ec924a0c 100644 --- a/modules/bitgo/src/v2/coins/index.ts +++ b/modules/bitgo/src/v2/coins/index.ts @@ -31,6 +31,7 @@ import { Erc20Token, Eth, Gteth, Hteth, Teth } from '@bitgo/sdk-coin-eth'; import { FetchAi, TfetchAi } from '@bitgo/sdk-coin-fetchai'; import { Flr, Tflr } from '@bitgo/sdk-coin-flr'; import { Ethw } from '@bitgo/sdk-coin-ethw'; +import { EvmCoin } from '@bitgo/sdk-coin-evm'; import { EthLikeCoin, TethLikeCoin } from '@bitgo/sdk-coin-ethlike'; import { Hash, Thash } from '@bitgo/sdk-coin-hash'; import { Hbar, Thbar } from '@bitgo/sdk-coin-hbar'; @@ -102,6 +103,7 @@ export { Erc20Token, Eth, Gteth, Hteth, Teth }; export { Ethw }; export { EthLikeCoin, TethLikeCoin }; export { Etc, Tetc }; +export { EvmCoin }; export { FetchAi, TfetchAi }; export { Flr, Tflr }; export { Hash, Thash }; diff --git a/modules/sdk-coin-evm/.eslintignore b/modules/sdk-coin-evm/.eslintignore new file mode 100644 index 0000000000..849ddff3b7 --- /dev/null +++ b/modules/sdk-coin-evm/.eslintignore @@ -0,0 +1 @@ +dist/ diff --git a/modules/sdk-coin-evm/.gitignore b/modules/sdk-coin-evm/.gitignore new file mode 100644 index 0000000000..67ccce4c64 --- /dev/null +++ b/modules/sdk-coin-evm/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +.idea/ +dist/ diff --git a/modules/sdk-coin-evm/.mocharc.yml b/modules/sdk-coin-evm/.mocharc.yml new file mode 100644 index 0000000000..b2cacd8968 --- /dev/null +++ b/modules/sdk-coin-evm/.mocharc.yml @@ -0,0 +1,8 @@ +require: 'ts-node/register' +timeout: '60000' +reporter: 'min' +reporter-option: + - 'cdn=true' + - 'json=false' +exit: true +spec: ['test/**/*.ts'] diff --git a/modules/sdk-coin-evm/.npmignore b/modules/sdk-coin-evm/.npmignore new file mode 100644 index 0000000000..7df125fca2 --- /dev/null +++ b/modules/sdk-coin-evm/.npmignore @@ -0,0 +1,12 @@ +!dist/ +.idea/ +.prettierrc.yml +tsconfig.json +src/ +test/ +scripts/ +.nyc_output +CODEOWNERS +node_modules/ +.prettierignore +.mocharc.js diff --git a/modules/sdk-coin-evm/.prettierignore b/modules/sdk-coin-evm/.prettierignore new file mode 100644 index 0000000000..3a11d6af29 --- /dev/null +++ b/modules/sdk-coin-evm/.prettierignore @@ -0,0 +1,2 @@ +.nyc_output/ +dist/ diff --git a/modules/sdk-coin-evm/.prettierrc.yml b/modules/sdk-coin-evm/.prettierrc.yml new file mode 100644 index 0000000000..7c3d8dd32a --- /dev/null +++ b/modules/sdk-coin-evm/.prettierrc.yml @@ -0,0 +1,3 @@ +printWidth: 120 +singleQuote: true +trailingComma: 'es5' diff --git a/modules/sdk-coin-evm/CHANGELOG.md b/modules/sdk-coin-evm/CHANGELOG.md new file mode 100644 index 0000000000..e4d87c4d45 --- /dev/null +++ b/modules/sdk-coin-evm/CHANGELOG.md @@ -0,0 +1,4 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. diff --git a/modules/sdk-coin-evm/README.md b/modules/sdk-coin-evm/README.md new file mode 100644 index 0000000000..7c96b88462 --- /dev/null +++ b/modules/sdk-coin-evm/README.md @@ -0,0 +1,3 @@ +# @bitgo/sdk-coin-evm + +Configurable common module for EVM assets, using @bitgo/abstract-eth for reduced coin integration boilerplate diff --git a/modules/sdk-coin-evm/package.json b/modules/sdk-coin-evm/package.json new file mode 100644 index 0000000000..97319e0a71 --- /dev/null +++ b/modules/sdk-coin-evm/package.json @@ -0,0 +1,44 @@ +{ + "name": "@bitgo/sdk-coin-evm", + "version": "1.1.0", + "description": "Configurable common module for EVM assets, using @bitgo/abstract-eth for reduced coin integration boilerplate.", + "main": "./dist/src/index.js", + "types": "./dist/src/index.d.ts", + "scripts": { + "build": "yarn tsc --build --incremental --verbose .", + "fmt": "prettier --write .", + "check-fmt": "prettier --check .", + "clean": "rm -r ./dist", + "lint": "eslint --quiet .", + "test": "npm run coverage", + "coverage": "nyc -- npm run unit-test", + "unit-test": "mocha", + "prepare": "npm run build" + }, + "dependencies": {}, + "devDependencies": {}, + "author": "BitGo SDK Team ", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/BitGo/BitGoJS.git", + "directory": "modules/sdk-coin-evm" + }, + "files": [ + "dist/src" + ], + "lint-staged": { + "*.{js,ts}": [ + "yarn prettier --write", + "yarn eslint --fix" + ] + }, + "publishConfig": { + "access": "public" + }, + "nyc": { + "extension": [ + ".ts" + ] + } +} diff --git a/modules/sdk-coin-evm/src/evm.ts b/modules/sdk-coin-evm/src/evm.ts new file mode 100644 index 0000000000..90de95f665 --- /dev/null +++ b/modules/sdk-coin-evm/src/evm.ts @@ -0,0 +1,68 @@ +/** + * @prettier + */ +import { BaseCoin, BitGoBase, common, MPCAlgorithm, MultisigType, multisigTypes } from '@bitgo/sdk-core'; +import { BaseCoin as StaticsBaseCoin, CoinFeature, coins } from '@bitgo/statics'; +import { + AbstractEthLikeNewCoins, + OfflineVaultTxInfo, + RecoverOptions, + recoveryBlockchainExplorerQuery, + TransactionBuilder as EthLikeTransactionBuilder, + UnsignedSweepTxMPCv2, +} from '@bitgo/abstract-eth'; +import { TransactionBuilder } from './lib'; +import assert from 'assert'; + +export class EvmCoin extends AbstractEthLikeNewCoins { + protected constructor(bitgo: BitGoBase, staticsCoin?: Readonly) { + super(bitgo, staticsCoin); + } + + static createInstance(bitgo: BitGoBase, staticsCoin?: Readonly): BaseCoin { + return new EvmCoin(bitgo, staticsCoin); + } + + protected getTransactionBuilder(): EthLikeTransactionBuilder { + return new TransactionBuilder(coins.get(this.getBaseChain())); + } + + /** @inheritDoc */ + supportsTss(): boolean { + return this.staticsCoin?.features.includes(CoinFeature.TSS) ?? false; + } + + /** inherited doc */ + getDefaultMultisigType(): MultisigType { + return this.staticsCoin?.features.includes(CoinFeature.TSS) ? multisigTypes.tss : multisigTypes.onchain; + } + + /** @inheritDoc */ + getMPCAlgorithm(): MPCAlgorithm { + return 'ecdsa'; + } + + protected async buildUnsignedSweepTxnTSS(params: RecoverOptions): Promise { + if (this.staticsCoin?.features.includes(CoinFeature.MPCV2)) { + return this.buildUnsignedSweepTxnMPCv2(params); + } + return super.buildUnsignedSweepTxnTSS(params); + } + + /** + * Make a query to chain explorer for information such as balance, token balance, solidity calls + * @param {Object} query key-value pairs of parameters to append after /api + * @returns {Promise} response from chain explorer + */ + async recoveryBlockchainExplorerQuery(query: Record): Promise> { + 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 apiToken = evmConfig[this.getFamily()].apiToken; + const explorerUrl = evmConfig[this.getFamily()].baseUrl; + return await recoveryBlockchainExplorerQuery(query, explorerUrl as string, apiToken as string); + } +} diff --git a/modules/sdk-coin-evm/src/index.ts b/modules/sdk-coin-evm/src/index.ts new file mode 100644 index 0000000000..973643ac42 --- /dev/null +++ b/modules/sdk-coin-evm/src/index.ts @@ -0,0 +1,3 @@ +export * from './evm'; +export * from './lib'; +export * from './register'; diff --git a/modules/sdk-coin-evm/src/lib/index.ts b/modules/sdk-coin-evm/src/lib/index.ts new file mode 100644 index 0000000000..20f9e761a1 --- /dev/null +++ b/modules/sdk-coin-evm/src/lib/index.ts @@ -0,0 +1,6 @@ +import * as Utils from './utils'; + +export { TransactionBuilder } from './transactionBuilder'; +export { TransferBuilder } from './transferBuilder'; +export { Transaction, KeyPair } from '@bitgo/abstract-eth'; +export { Utils }; diff --git a/modules/sdk-coin-evm/src/lib/transactionBuilder.ts b/modules/sdk-coin-evm/src/lib/transactionBuilder.ts new file mode 100644 index 0000000000..3a245844a4 --- /dev/null +++ b/modules/sdk-coin-evm/src/lib/transactionBuilder.ts @@ -0,0 +1,36 @@ +import { BaseCoin as CoinConfig } from '@bitgo/statics'; +import { BuildTransactionError, TransactionType } from '@bitgo/sdk-core'; +import { TransactionBuilder as AbstractTransactionBuilder, Transaction } from '@bitgo/abstract-eth'; +import { getCommon } from './utils'; +import { TransferBuilder } from './transferBuilder'; + +export class TransactionBuilder extends AbstractTransactionBuilder { + protected _transfer: TransferBuilder; + private _signatures: any; + + constructor(_coinConfig: Readonly) { + super(_coinConfig); + this._common = getCommon(this._coinConfig); + this.transaction = new Transaction(this._coinConfig, this._common); + } + + /** @inheritdoc */ + transfer(data?: string): TransferBuilder { + if (this._type !== TransactionType.Send) { + throw new BuildTransactionError('Transfers can only be set for send transactions'); + } + if (!this._transfer) { + this._transfer = new TransferBuilder(data); + } + return this._transfer; + } + + addSignature(publicKey, signature) { + this._signatures = []; + this._signatures.push({ publicKey, signature }); + } + + protected getContractData(addresses: string[]): string { + throw new Error('Method not implemented.'); + } +} diff --git a/modules/sdk-coin-evm/src/lib/transferBuilder.ts b/modules/sdk-coin-evm/src/lib/transferBuilder.ts new file mode 100644 index 0000000000..7447c0cf3d --- /dev/null +++ b/modules/sdk-coin-evm/src/lib/transferBuilder.ts @@ -0,0 +1 @@ +export { TransferBuilder } from '@bitgo/abstract-eth'; diff --git a/modules/sdk-coin-evm/src/lib/utils.ts b/modules/sdk-coin-evm/src/lib/utils.ts new file mode 100644 index 0000000000..0e9ce9a2c3 --- /dev/null +++ b/modules/sdk-coin-evm/src/lib/utils.ts @@ -0,0 +1,29 @@ +import { CoinFeature, NetworkType, BaseCoin, EthereumNetwork } from '@bitgo/statics'; +import EthereumCommon from '@ethereumjs/common'; +import { InvalidTransactionError } from '@bitgo/sdk-core'; + +/** + * @param {NetworkType} network either mainnet or testnet + * @returns {EthereumCommon} Ethereum common configuration object + */ +export function getCommon(coin: Readonly): EthereumCommon { + if (!coin.features.includes(CoinFeature.SHARED_EVM_SDK)) { + throw new InvalidTransactionError(`Cannot use common sdk module for the coin ${coin.name}`); + } + + if (!coin.features.includes(CoinFeature.SHARED_EVM_SDK)) { + throw new InvalidTransactionError(`Cannot use common sdk module for the coin ${coin.name}`); + } + return EthereumCommon.custom( + { + name: coin.network.name, + networkId: (coin.network as EthereumNetwork).chainId, + chainId: (coin.network as EthereumNetwork).chainId, + }, + { + baseChain: coin.network.type === NetworkType.MAINNET ? 'mainnet' : 'sepolia', + hardfork: coin.features.includes(CoinFeature.EIP1559) ? 'london' : undefined, + eips: coin.features.includes(CoinFeature.EIP1559) ? [1559] : undefined, + } + ); +} diff --git a/modules/sdk-coin-evm/src/register.ts b/modules/sdk-coin-evm/src/register.ts new file mode 100644 index 0000000000..81acd9130a --- /dev/null +++ b/modules/sdk-coin-evm/src/register.ts @@ -0,0 +1,11 @@ +import { BitGoBase } from '@bitgo/sdk-core'; +import { CoinFeature, coins } from '@bitgo/statics'; +import { EvmCoin } from './evm'; + +export const register = (sdk: BitGoBase): void => { + coins + .filter((coin) => coin.features.includes(CoinFeature.SHARED_EVM_SDK)) + .forEach((coin) => { + sdk.register(coin.name, EvmCoin.createInstance); + }); +}; diff --git a/modules/sdk-coin-evm/tsconfig.json b/modules/sdk-coin-evm/tsconfig.json new file mode 100644 index 0000000000..9a4e1ca6ba --- /dev/null +++ b/modules/sdk-coin-evm/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./", + "strictPropertyInitialization": false, + "esModuleInterop": true, + "typeRoots": ["../../types", "./node_modules/@types", "../../node_modules/@types"] + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +} diff --git a/modules/sdk-core/src/bitgo/environments.ts b/modules/sdk-core/src/bitgo/environments.ts index 196bc713d3..c7aedcc7b5 100644 --- a/modules/sdk-core/src/bitgo/environments.ts +++ b/modules/sdk-core/src/bitgo/environments.ts @@ -85,6 +85,12 @@ interface EnvironmentTemplate { soneiumExplorerApiToken?: string; stxNodeUrl: string; vetNodeUrl: string; + evm?: { + [key: string]: { + baseUrl: string; + apiToken?: string; + }; + }; } export interface Environment extends EnvironmentTemplate { diff --git a/modules/statics/src/base.ts b/modules/statics/src/base.ts index 33a12c5e6d..d8dbd38a99 100644 --- a/modules/statics/src/base.ts +++ b/modules/statics/src/base.ts @@ -365,6 +365,11 @@ export enum CoinFeature { */ SHARED_EVM_SIGNING = 'shared-evm-signing', + /** + * This coin is an EVM compatible coin and should use common EVM SDK module + */ + SHARED_EVM_SDK = 'shared-evm-sdk', + /** * This coin supports multisig wallets */