diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 00000000..4169a713 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,15 @@ +name: Lint +on: + push: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: '20' + - run: npm install + - run: npm run build + - run: npm run lint diff --git a/package.json b/package.json index 22b18f7a..09406a0c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@curvefi/api", - "version": "2.63.1", + "version": "2.63.2", "description": "JavaScript library for curve.fi", "main": "lib/index.js", "author": "Macket", @@ -14,7 +14,8 @@ "url": "https://github.com/curvefi/curve-js/issues" }, "scripts": { - "build": "rm -rf lib && tsc -p tsconfig.build.json" + "build": "rm -rf lib && tsc -p tsconfig.build.json", + "lint": "eslint src --ext .ts" }, "type": "module", "devDependencies": { diff --git a/src/curve.ts b/src/curve.ts index f011c592..667f906e 100644 --- a/src/curve.ts +++ b/src/curve.ts @@ -14,7 +14,7 @@ import { getFactoryPoolData } from "./factory/factory.js"; import { getFactoryPoolsDataFromApi } from "./factory/factory-api.js"; import { getCryptoFactoryPoolData } from "./factory/factory-crypto.js"; import { getTricryptoFactoryPoolData } from "./factory/factory-tricrypto.js"; -import { IPoolData, IDict, ICurve, INetworkName, IChainId, IFactoryPoolType } from "./interfaces"; +import {IPoolData, IDict, ICurve, INetworkName, IChainId, IFactoryPoolType, Abi} from "./interfaces"; import ERC20Abi from './constants/abis/ERC20.json' assert { type: 'json' }; import cERC20Abi from './constants/abis/cERC20.json' assert { type: 'json' }; import yERC20Abi from './constants/abis/yERC20.json' assert { type: 'json' }; @@ -419,13 +419,16 @@ export const NETWORK_CONSTANTS: { [index: number]: any } = { const OLD_CHAINS = [1, 10, 56, 100, 137, 250, 1284, 2222, 8453, 42161, 42220, 43114, 1313161554]; // these chains have non-ng pools + +export type ContractItem = { contract: Contract, multicallContract: MulticallContract, abi: Abi }; + class Curve implements ICurve { provider: ethers.BrowserProvider | ethers.JsonRpcProvider; multicallProvider: MulticallProvider; signer: ethers.Signer | null; signerAddress: string; chainId: IChainId; - contracts: { [index: string]: { contract: Contract, multicallContract: MulticallContract } }; + contracts: { [index: string]: ContractItem }; feeData: { gasPrice?: number, maxFeePerGas?: number, maxPriorityFeePerGas?: number }; constantOptions: { gasLimit?: number }; options: { gasPrice?: number | bigint, maxFeePerGas?: number | bigint, maxPriorityFeePerGas?: number | bigint }; diff --git a/src/interfaces.ts b/src/interfaces.ts index ee733716..945ca146 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -270,4 +270,24 @@ export interface IBasePoolShortItem { token: string, } -export type TVoteType = "PARAMETER" | "OWNERSHIP" \ No newline at end of file +export type TVoteType = "PARAMETER" | "OWNERSHIP" + +export type AbiParameter = { type: string, name?:string, components?: readonly AbiParameter[] } +type CtorMutability = 'payable' | 'nonpayable'; +export type AbiStateMutability = 'pure' | 'view' | CtorMutability +export type AbiFunction = { + type: 'function' + constant?: boolean + gas?: number + inputs: readonly AbiParameter[] + name: string + outputs: readonly AbiParameter[] + payable?: boolean | undefined + stateMutability: AbiStateMutability +} +export type AbiConstructor = { type: 'constructor', inputs: readonly AbiParameter[], payable?: boolean, stateMutability: CtorMutability } +export type AbiFallback = { type: 'fallback', payable?: boolean, stateMutability: CtorMutability } +export type AbiReceive = {type: 'receive', stateMutability: Extract} +export type AbiEvent = {type: 'event', anonymous?: boolean, inputs: readonly AbiParameter[], name: string} +export type AbiError = {type: 'error', inputs: readonly AbiParameter[], name: string} +export type Abi = (AbiConstructor | AbiError | AbiEvent | AbiFallback | AbiFunction | AbiReceive)[] diff --git a/src/pools/PoolTemplate.ts b/src/pools/PoolTemplate.ts index a93719a9..32583f6a 100644 --- a/src/pools/PoolTemplate.ts +++ b/src/pools/PoolTemplate.ts @@ -27,7 +27,7 @@ import { smartNumber, DIGas, _getAddress, - isMethodExist, + findAbiFunction, getVolumeApiController, } from '../utils.js'; import {IDict, IReward, IProfit, IPoolType} from '../interfaces'; @@ -256,9 +256,8 @@ export class PoolTemplate { public rewardsOnly(): boolean { if (curve.chainId === 2222 || curve.chainId === 324) return true; // TODO remove this for Kava and ZkSync if (this.gauge.address === curve.constants.ZERO_ADDRESS) throw Error(`${this.name} doesn't have gauge`); - const gaugeContract = curve.contracts[this.gauge.address].contract; - - return !('inflation_rate()' in gaugeContract || 'inflation_rate(uint256)' in gaugeContract); + return !findAbiFunction(curve.contracts[this.gauge.address].abi, 'inflation_rate') + .find((func) => ['', 'uint256'].includes(func.inputs.map((a) => `${a.type}`).join(','))) } private statsParameters = async (): Promise<{ @@ -2323,8 +2322,7 @@ export class PoolTemplate { } //for crvusd and stable-ng implementations - const isUseStoredRates = isMethodExist(curve.contracts[this.address].contract, 'stored_rates') && this.isPlain; - if (isUseStoredRates) { + if (findAbiFunction(curve.contracts[this.address].abi, 'stored_rates').length > 0 && this.isPlain) { const _stored_rates: bigint[] = await curve.contracts[this.address].contract.stored_rates(); return _stored_rates.map((_r, i) => toBN(_r, 36 - this.wrappedDecimals[i])); } diff --git a/src/pools/poolConstructor.ts b/src/pools/poolConstructor.ts index d27228c0..b0a67466 100644 --- a/src/pools/poolConstructor.ts +++ b/src/pools/poolConstructor.ts @@ -20,7 +20,7 @@ import { swapWrappedMixin, swapWrappedRequiredMixin, } from "./mixins/swapWrappedMixins.js"; -import { getCountArgsOfMethodByContract } from "../utils.js"; +import { getCountArgsOfMethodByAbi, findAbiSignature } from "../utils.js"; export const getPool = (poolId: string): PoolTemplate => { @@ -59,7 +59,7 @@ export const getPool = (poolId: string): PoolTemplate => { } } else if (poolDummy.zap && poolId !== 'susd') { Object.assign(Pool.prototype, depositZapMixin); - } else if (getCountArgsOfMethodByContract(curve.contracts[poolDummy.address].contract, 'add_liquidity') > 2) { + } else if (getCountArgsOfMethodByAbi(curve.contracts[poolDummy.address].abi, 'add_liquidity') > 2) { Object.assign(Pool.prototype, depositLendingOrCryptoMixin); } else { Object.assign(Pool.prototype, depositPlainMixin); @@ -92,7 +92,7 @@ export const getPool = (poolId: string): PoolTemplate => { } } else if (poolDummy.zap && poolId !== 'susd') { Object.assign(Pool.prototype, withdrawZapMixin); - } else if (getCountArgsOfMethodByContract(curve.contracts[poolDummy.address].contract, 'remove_liquidity') > 2) { + } else if (getCountArgsOfMethodByAbi(curve.contracts[poolDummy.address].abi, 'remove_liquidity') > 2) { Object.assign(Pool.prototype, withdrawLendingOrCryptoMixin); } else { Object.assign(Pool.prototype, withdrawPlainMixin); @@ -151,7 +151,7 @@ export const getPool = (poolId: string): PoolTemplate => { } } else if (poolDummy.zap) { // including susd Object.assign(Pool.prototype, withdrawOneCoinZapMixin); - } else if (getCountArgsOfMethodByContract(curve.contracts[poolDummy.address].contract, 'remove_liquidity_one_coin') > 3) { + } else if (getCountArgsOfMethodByAbi(curve.contracts[poolDummy.address].abi, 'remove_liquidity_one_coin') > 3) { Object.assign(Pool.prototype, withdrawOneCoinLendingOrCryptoMixin); } else { Object.assign(Pool.prototype, withdrawOneCoinPlainMixin); @@ -176,7 +176,7 @@ export const getPool = (poolId: string): PoolTemplate => { } // swap and swapEstimateGas - if ('exchange(uint256,uint256,uint256,uint256,bool)' in curve.contracts[poolDummy.address].contract && + if (findAbiSignature(curve.contracts[poolDummy.address].abi, 'exchange', 'uint256,uint256,uint256,uint256,bool') && !(curve.chainId === 100 && poolDummy.id === "tricrypto")) { // tricrypto2 (eth), tricrypto (arbitrum), avaxcrypto (avalanche); 100 is xDAI Object.assign(Pool.prototype, swapTricrypto2Mixin); } else if (poolDummy.isMetaFactory && (getPool(poolDummy.basePool).isLending || getPool(poolDummy.basePool).isFake || poolDummy.isCrypto)) { @@ -193,7 +193,7 @@ export const getPool = (poolId: string): PoolTemplate => { if (!poolDummy.isPlain && !poolDummy.isFake) { Object.assign(Pool.prototype, swapWrappedExpectedAndApproveMixin); Object.assign(Pool.prototype, swapWrappedRequiredMixin); - if ('exchange(uint256,uint256,uint256,uint256,bool)' in curve.contracts[poolDummy.address].contract) { // tricrypto2 (eth), tricrypto (arbitrum) + if (findAbiSignature(curve.contracts[poolDummy.address].abi, 'exchange', 'uint256,uint256,uint256,uint256,bool')) { // tricrypto2 (eth), tricrypto (arbitrum) Object.assign(Pool.prototype, swapWrappedTricrypto2Mixin); } else { Object.assign(Pool.prototype, swapWrappedMixin); diff --git a/src/utils.ts b/src/utils.ts index 64071c89..71b02fa3 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -3,6 +3,7 @@ import {Contract} from 'ethers'; import {Contract as MulticallContract} from "@curvefi/ethcall"; import BigNumber from 'bignumber.js'; import { + Abi, AbiFunction, IBasePoolShortItem, IChainId, IDict, @@ -634,6 +635,7 @@ export const getVolume = async (network: INetworkName | IChainId = curve.chainId export const _setContracts = (address: string, abi: any): void => { curve.contracts[address] = { + abi, contract: new Contract(address, abi, curve.signer || curve.provider), multicallContract: new MulticallContract(address, abi), } @@ -709,17 +711,13 @@ export const getCoinsData = async (...coins: string[] | string[][]): Promise<{na export const hasDepositAndStake = (): boolean => curve.constants.ALIASES.deposit_and_stake !== curve.constants.ZERO_ADDRESS; export const hasRouter = (): boolean => curve.constants.ALIASES.router !== curve.constants.ZERO_ADDRESS; -export const getCountArgsOfMethodByContract = (contract: Contract, methodName: string): number => { - const func = contract.interface.fragments.find((item: any) => item.name === methodName); - if(func) { - return func.inputs.length; - } else { - return -1; - } -} +export const findAbiFunction = (abi: Abi, methodName: string) => + abi.filter((item) => item.type == 'function' && item.name === methodName) as AbiFunction[] -export const isMethodExist = (contract: Contract, methodName: string): boolean => - contract.interface.fragments.find((item: any) => item.name === methodName) !== undefined +export const getCountArgsOfMethodByAbi = (abi: Abi, methodName: string): number => findAbiFunction(abi, methodName)[0]?.inputs.length ?? -1 + +export const findAbiSignature = (abi: Abi, methodName: string, signature: string) => + findAbiFunction(abi, methodName).find((func) => func.inputs.map((i) => `${i.type}`).join(',') == signature) export const getPoolName = (name: string): string => { const separatedName = name.split(": ") @@ -730,9 +728,7 @@ export const getPoolName = (name: string): string => { } } -export const isStableNgPool = (name: string): boolean => { - return name.includes('factory-stable-ng') -} +export const isStableNgPool = (name: string): boolean => name.includes('factory-stable-ng') export const assetTypeNameHandler = (assetTypeName: string): REFERENCE_ASSET => { if (assetTypeName.toUpperCase() === 'UNKNOWN') {