diff --git a/CHANGELOG.md b/CHANGELOG.md index 72692f1c5..8f6923132 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Next version +- feat!: `MgvToken` has been renamed to `Token`. +- feat!: Removed `configuration.tokens.fetchDecimalsFromAddress`. Instead, use `Token.createTokenFromAddress` and read the decimals from that token. +- feat!: `Mangrove.toUnits|fromUnits` no longer accepts a token name/symbol as this was ambiguous. Instead, use `Token.createToken` and call `toUnits|fromUnits` on that. +- feat!: Static token configuration getters and setters have been removed from `Mangrove` and `Token`. Instead, use the methods on `configuration.token`. +- feat!: `Mangrove.getTokenAndAddress` has been removed. Instead, use `Mangrove.tokenFromAddress` and read the address from there. +- feat!: Remove `Mangrove.tokenFromConfig`. Use `Mangrove.token` instead. +- feat!/fix: Token `name` was misused: Sometimes it was assumed to be a symbol and sometimes an ID. It has therefore been replaced by `id` and `symbol` in all relevant places. Configuration files have been converted to use the token instance ID's from the context-addresses package to avoid ambiguity among (1) different tokens with the same symbol and (2) multiple token instances such as `USDC` (Circle issued) and `USDC.e` (bridged from Ethereum). + - Default token IDs can be registered for a symbol and network. And if there is only one ID for a given symbol on a network, it will be considered the default. `Mangrove.token()` will create an instance of the default token ID if found. +- feat!: `Mangrove.getNameFromAddress` has been removed: It was ambiguous (multiple names could be registered) and only used for resolving tokens which can now be done with the new `configuration.tokens.getTokenIdFromAddress()` function. +- feat!: The `Mangrove.openMarkets` function now uses `Token` instead of a bespoke token data struct. +- feat: `Mangrove.market` and `Market.connect` now accept either symbol, token ID, or `Token` for base and quote. +- feat: Added `displayName` and `displayedAsPriceDecimals` to `Token`. +- feat!: `Token.getDecimals` now throws an error instead of returning undefined if the decimals are not on record. + # 2.0.0-8 # 2.0.0-7 diff --git a/examples/aaveV3Module.ts b/examples/aaveV3Module.ts index 70a90f708..7dde499ec 100644 --- a/examples/aaveV3Module.ts +++ b/examples/aaveV3Module.ts @@ -27,10 +27,10 @@ class AaveV3Module { } async #debtToken( - tokenName: string, + tokenId: string, signer?: SignerOrProvider, ): Promise { - const asset_address = this.mgv.getAddress(tokenName); + const asset_address = this.mgv.getTokenAddress(tokenId); const debt_address = await this.contract.debtToken(asset_address); return typechain.ICreditDelegationToken__factory.connect( debt_address, @@ -39,11 +39,11 @@ class AaveV3Module { } async approveDelegation( - tokenName: string, + tokenId: string, borrower: string, overrides: ethers.Overrides = {}, ): Promise { - const dTtkn = await this.#debtToken(tokenName); + const dTtkn = await this.#debtToken(tokenId); return dTtkn.approveDelegation( borrower, ethers.constants.MaxUint256, @@ -52,11 +52,11 @@ class AaveV3Module { } async status( - tokenName: string, + tokenId: string, account: string, ): Promise<{ available: Big; borrowable: Big; borrowing: Big }> { - const asset = await this.mgv.token(tokenName); - const dToken = await this.#debtToken(tokenName); + const asset = await this.mgv.token(tokenId); + const dToken = await this.#debtToken(tokenId); const { maxRedeemableUnderlying, maxBorrowAfterRedeemInUnderlying } = await this.contract.maxGettableUnderlying(asset.address, true, account); return { @@ -66,11 +66,11 @@ class AaveV3Module { }; } - async logStatus(tokenNames: string[], account?: string): Promise { + async logStatus(tokenIds: string[], account?: string): Promise { account = account ? account : await this.mgv.signer.getAddress(); - for (const tokenName of tokenNames) { - const stat = await this.status(tokenName, account); - console.log(`----------${tokenName}----------`); + for (const tokenId of tokenIds) { + const stat = await this.status(tokenId, account); + console.log(`----------${tokenId}----------`); console.log("debit:", `\u001b[32m${stat.available}\u001b[0m`); console.log("credit:", `\u001b[33m${stat.borrowable}\u001b[0m`); console.log("debt:", `\u001b[31m${stat.borrowing}\u001b[0m`); diff --git a/package.json b/package.json index 59367bcb4..d049f58fb 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,6 @@ "just-clone": "^6.2.0", "logform": "^2.5.1", "loglevel": "^1.8.1", - "moize": "^6.1.6", "node-cleanup": "^2.1.2", "object-inspect": "^1.12.3", "semver": "^7.5.4", diff --git a/src/configuration.ts b/src/configuration.ts index bb186c435..29f14c015 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -19,7 +19,6 @@ import * as contextAddresses from "@mangrovedao/context-addresses"; import * as eth from "./eth"; import clone from "just-clone"; import deepmerge from "deepmerge"; -import moize from "moize"; import semver from "semver"; // Make keys optional at all levels of T @@ -33,13 +32,16 @@ export type RecursivePartial = { export type network = string; export type address = string; +export type tokenId = string; export type tokenSymbol = string; export type NamedAddresses = Record; export type AddressesConfig = Record; export type TokenConfig = { + symbol?: tokenSymbol; decimals?: number; + displayName?: string; displayedDecimals?: number; displayedAsPriceDecimals?: number; cashness?: number; @@ -114,7 +116,7 @@ export type PartialKandelAllConfigurationFields = Partial; export type PartialMarketConfig = PartialKandelAllConfigurationFields; export type PartialNetworkConfig = PartialKandelAllConfigurationFields & { - markets?: Record>; // base symbol -> quote symbol -> market config + markets?: Record>; // base ID -> quote ID -> market config }; export type PartialKandelConfiguration = PartialKandelAllConfigurationFields & { @@ -138,7 +140,8 @@ export type PartialMangroveOrderConfiguration = export type Configuration = { addressesByNetwork: AddressesConfig; tokenDefaults: TokenDefaults; - tokens: Record; + tokens: Record; + tokenSymbolDefaultIdsByNetwork: Record>; mangroveOrder: PartialMangroveOrderConfiguration; reliableEventSubscriber: ReliableEventSubscriberConfig; kandel: PartialKandelConfiguration; @@ -224,16 +227,80 @@ export const addressesConfiguration = { } } }, +}; + +/// TOKENS + +function getOrCreateTokenConfig(tokenId: tokenId): TokenConfig { + let tokenConfig = config.tokens[tokenId]; + if (tokenConfig === undefined) { + config.tokens[tokenId] = tokenConfig = {}; + } + return tokenConfig; +} + +function getOrCreateDefaultIdsForSymbol( + symbol: tokenSymbol, +): Record { + let defaultIdsForSymbol = config.tokenSymbolDefaultIdsByNetwork[symbol]; + if (defaultIdsForSymbol === undefined) { + config.tokenSymbolDefaultIdsByNetwork[symbol] = defaultIdsForSymbol = {}; + } + return defaultIdsForSymbol; +} + +export const tokensConfiguration = { + /** + * Returns true if the given token ID has been registered; otherwise, false. + */ + isTokenIdRegistered(tokenId: tokenId): boolean { + return config.tokens[tokenId] !== undefined; + }, /** - * Gets the name of an address on the current network. + * Gets the default token ID for a given symbol and network if + * (1) any has been registered or + * (2) if there is only one token with that symbol or + * (3) if there are no tokens with that symbol, then the symbol itself. * - * Note that this reads from the static `Mangrove` address registry which is shared across instances of this class. + * If no default is registered and there are multiple tokens with that symbol an error is thrown. */ - getNameFromAddress: ( + getDefaultIdForSymbolOnNetwork( + tokenSymbol: tokenSymbol, + network: network, + ): tokenId { + const registeredDefault = + getOrCreateDefaultIdsForSymbol(tokenSymbol)[network]; + if (registeredDefault !== undefined) { + return registeredDefault; + } + + // Loop through config.tokens to find the first token with the given symbol on the given network + let foundTokenId: tokenId | undefined; + for (const [tokenId, tokenConfig] of Object.entries(config.tokens)) { + if ( + tokenConfig.symbol === tokenSymbol && + addressesConfiguration.getAddress(tokenId, network) !== undefined + ) { + if (foundTokenId !== undefined) { + // If we already found a token with that symbol, we cannot decide which one is the default + throw Error( + `No default token ID registered for symbol ${tokenSymbol} and multiple tokens defined on network ${network} with that symbol`, + ); + } + foundTokenId = tokenId; + } + } + return foundTokenId ?? tokenSymbol; + }, + + /** + * Gets the token ID of an address on the given network. + */ + getTokenIdFromAddress: ( address: string, network: string, - ): string | undefined => { + ): tokenId | undefined => { const networkAddresses = config.addressesByNetwork[network]; address = ethers.utils.getAddress(address); // normalize @@ -242,141 +309,197 @@ export const addressesConfiguration = { networkAddresses, ) as any) { if (candidateAddress == address) { - return name; + if (tokensConfiguration.isTokenIdRegistered(name)) { + return name; + } } } } return undefined; }, -}; - -/// TOKENS - -function getOrCreateTokenConfig(tokenName: string) { - let tokenConfig = config.tokens[tokenName]; - if (tokenConfig === undefined) { - config.tokens[tokenName] = tokenConfig = {}; - } - return tokenConfig; -} - -export const tokensConfiguration = { - /** - * Read decimals for `tokenName`. - * To read decimals directly onchain, use `fetchDecimals`. - */ - getDecimals: (tokenName: string): number | undefined => { - return config.tokens[tokenName]?.decimals; - }, /** - * Read decimals for `tokenName`. Fails if the decimals are not in the configuration. + * Read decimals for `tokenId`. Fails if the decimals are not in the configuration. * To read decimals directly onchain, use `fetchDecimals`. */ - getDecimalsOrFail: (tokenName: string): number => { - const decimals = tokensConfiguration.getDecimals(tokenName); + getDecimals: (tokenId: tokenId): number => { + const decimals = getOrCreateTokenConfig(tokenId).decimals; if (decimals === undefined) { - throw Error(`No decimals on record for token ${tokenName}`); + throw Error(`No decimals on record for token ${tokenId}`); } return decimals; }, /** - * Read decimals for `tokenName` on given network. + * Read decimals for `tokenId` on given network. * If not found in the local configuration, fetch them from the current network and save them */ getOrFetchDecimals: async ( - tokenName: string, + tokenId: tokenId, provider: Provider, ): Promise => { - const decimals = tokensConfiguration.getDecimals(tokenName); + const decimals = tokensConfiguration.getDecimals(tokenId); if (decimals !== undefined) { return decimals; } - return tokensConfiguration.fetchDecimals(tokenName, provider); + return tokensConfiguration.fetchDecimals(tokenId, provider); }, /** - * Read chain for decimals of `tokenName` on current network and save them + * Read chain for decimals of `tokenId` on current network and save them */ fetchDecimals: async ( - tokenName: string, + tokenId: tokenId, provider: Provider, ): Promise => { const network = await eth.getProviderNetwork(provider); const token = typechain.IERC20__factory.connect( - addressesConfiguration.getAddress(tokenName, network.name), + addressesConfiguration.getAddress(tokenId, network.name), provider, ); const decimals = await token.decimals(); - tokensConfiguration.setDecimals(tokenName, decimals); + tokensConfiguration.setDecimals(tokenId, decimals); return decimals; }, /** - * Read chain for decimals of `address` on current network + * Read symbol for `tokenId`. + * To read symbol directly onchain, use `fetchSymbol`. */ - fetchDecimalsFromAddress: moize( - async (address: string, provider: Provider): Promise => { - const token = typechain.IERC20__factory.connect(address, provider); - return token.decimals(); - }, - ), + getSymbol: (tokenId: tokenId): tokenSymbol | undefined => { + return getOrCreateTokenConfig(tokenId).symbol; + }, /** - * Read displayed decimals for `tokenName`. + * Read symbol for `tokenId` on given network. + * If not found in the local configuration, fetch them from the current network and save them */ - getDisplayedDecimals: (tokenName: string): number => { + getOrFetchSymbol: async ( + tokenId: tokenId, + provider: Provider, + ): Promise => { + const symbol = tokensConfiguration.getSymbol(tokenId); + if (symbol !== undefined) { + return symbol; + } + + return tokensConfiguration.fetchSymbol(tokenId, provider); + }, + + /** + * Read chain for symbol of `tokenId` on current network and save them + */ + fetchSymbol: async ( + tokenId: tokenId, + provider: Provider, + ): Promise => { + const network = await eth.getProviderNetwork(provider); + const address = addressesConfiguration.getAddress(tokenId, network.name); + const symbol = await tokensConfiguration.fetchSymbolFromAddress( + address, + provider, + ); + tokensConfiguration.setSymbol(tokenId, symbol); + return symbol; + }, + + /** + * Read chain for symbol of `address` on current network. + */ + fetchSymbolFromAddress: async ( + address: address, + provider: Provider, + ): Promise => { + const token = typechain.IERC20__factory.connect(address, provider); + return await token.symbol(); + }, + + /** + * Read display name for `tokenId`. + */ + getDisplayName: (tokenId: tokenId): string | undefined => { + return getOrCreateTokenConfig(tokenId).displayName; + }, + + /** + * Read displayed decimals for `tokenId`. + */ + getDisplayedDecimals: (tokenId: tokenId): number => { return ( - config.tokens[tokenName]?.displayedDecimals || + getOrCreateTokenConfig(tokenId).displayedDecimals || config.tokenDefaults.defaultDisplayedDecimals ); }, /** - * Read displayed decimals for `tokenName` when displayed as a price. + * Read displayed decimals for `tokenId` when displayed as a price. */ - getDisplayedPriceDecimals: (tokenName: string): number => { + getDisplayedPriceDecimals: (tokenId: tokenId): number => { return ( - config.tokens[tokenName]?.displayedAsPriceDecimals || + getOrCreateTokenConfig(tokenId).displayedAsPriceDecimals || config.tokenDefaults.defaultDisplayedPriceDecimals ); }, /** Get the cashness of a token. See {@link setCashness} for details. */ - getCashness: (tokenName: string): number | undefined => { - return config.tokens[tokenName]?.cashness; + getCashness: (tokenId: tokenId): number | undefined => { + return getOrCreateTokenConfig(tokenId).cashness; + }, + + /** + * Set the default token ID for a given symbol and network. + */ + setDefaultIdForSymbolOnNetwork( + tokenSymbol: tokenSymbol, + network: network, + tokenId: tokenId, + ): void { + getOrCreateDefaultIdsForSymbol(tokenSymbol)[network] = tokenId; + }, + + /** + * Set decimals for `tokenId`. + */ + setDecimals: (tokenId: tokenId, dec: number): void => { + getOrCreateTokenConfig(tokenId).decimals = dec; }, /** - * Set decimals for `tokenName`. + * Set symbol for `tokenId`. */ - setDecimals: (tokenName: string, dec: number): void => { - getOrCreateTokenConfig(tokenName).decimals = dec; + setSymbol: (tokenId: tokenId, symbol: tokenSymbol): void => { + getOrCreateTokenConfig(tokenId).symbol = symbol; }, /** - * Set displayed decimals for `tokenName`. + * Set display name for `tokenId`. */ - setDisplayedDecimals: (tokenName: string, dec: number): void => { - getOrCreateTokenConfig(tokenName).displayedDecimals = dec; + setDisplayName: (tokenId: tokenId, displayName: string): void => { + getOrCreateTokenConfig(tokenId).displayName = displayName; }, /** - * Set displayed decimals for `tokenName` when displayed as a price. + * Set displayed decimals for `tokenId`. */ - setDisplayedPriceDecimals: (tokenName: string, dec: number): void => { - getOrCreateTokenConfig(tokenName).displayedAsPriceDecimals = dec; + setDisplayedDecimals: (tokenId: tokenId, dec: number): void => { + getOrCreateTokenConfig(tokenId).displayedDecimals = dec; + }, + + /** + * Set displayed decimals for `tokenId` when displayed as a price. + */ + setDisplayedPriceDecimals: (tokenId: tokenId, dec: number): void => { + getOrCreateTokenConfig(tokenId).displayedAsPriceDecimals = dec; }, /** Set the relative cashness of a token. This determines which token is base & which is quote in a {@link Market}. * Lower cashness is base, higher cashness is quote, tiebreaker is lexicographic ordering of name string (name is most likely the same as the symbol). */ - setCashness: (tokenName: string, cashness: number) => { - getOrCreateTokenConfig(tokenName).cashness = cashness; + setCashness: (tokenId: tokenId, cashness: number) => { + getOrCreateTokenConfig(tokenId).cashness = cashness; }, }; @@ -459,7 +582,8 @@ export function resetConfiguration(): void { defaultDisplayedDecimals: 2, defaultDisplayedPriceDecimals: 6, }, - tokens: clone(loadedTokens as Record), + tokens: clone(loadedTokens as Record), + tokenSymbolDefaultIdsByNetwork: {}, reliableEventSubscriber: { defaultBlockManagerOptions: { maxBlockCached: 50, @@ -506,7 +630,7 @@ export function resetConfiguration(): void { // 1. context-addresses addresses // 2. mangrove-deployments addresses // Last loaded address is used - readContextAddresses(); + readContextAddressesAndTokens(); readMangroveDeploymentAddresses(); } @@ -576,9 +700,9 @@ function readVersionDeploymentsAddresses( } } -function readContextAddresses() { +function readContextAddressesAndTokens() { readContextMulticallAddresses(); - readContextErc20Addresses(); + readContextErc20Tokens(); readContextAaveAddresses(); } @@ -592,10 +716,8 @@ function readContextMulticallAddresses() { } } -function readContextErc20Addresses() { - for (const [, /*tokenId*/ erc20] of Object.entries( - contextAddresses.getAllErc20s(), - )) { +function readContextErc20Tokens() { + for (const [, erc20] of Object.entries(contextAddresses.getAllErc20s())) { for (const [networkId, networkInstances] of Object.entries( erc20.networkInstances, )) { @@ -603,13 +725,23 @@ function readContextErc20Addresses() { for (const [erc20InstanceId, erc20Instance] of Object.entries( networkInstances, )) { + tokensConfiguration.setDecimals(erc20InstanceId, erc20.decimals); + tokensConfiguration.setSymbol(erc20InstanceId, erc20.symbol); + addressesConfiguration.setAddress( erc20InstanceId, erc20Instance.address, networkName, ); - // Also register the default instance as the token symbol for convenience + if (erc20Instance.default) { + tokensConfiguration.setDefaultIdForSymbolOnNetwork( + erc20.symbol, + networkName, + erc20InstanceId, + ); + + // Also register the default instance as the token symbol for convenience addressesConfiguration.setAddress( erc20.symbol, erc20Instance.address, diff --git a/src/constants/kandelConfiguration.json b/src/constants/kandelConfiguration.json index 1f4fbc19b..af0ac4811 100644 --- a/src/constants/kandelConfiguration.json +++ b/src/constants/kandelConfiguration.json @@ -13,35 +13,35 @@ "minimumBasePerOfferFactor": 10, "minimumQuotePerOfferFactor": 10, "markets": { - "WETH": { - "DAI": { + "WETH.T/MGV": { + "DAI.T/AAVEv3": { "aaveEnabled": false }, - "USDC": { + "USDC.T/MGV": { "minimumBasePerOfferFactor": 6, "minimumQuotePerOfferFactor": 6, "aaveEnabled": false } }, - "DAI": { - "USDC": { + "DAI.T/AAVEv3": { + "USDC.T/MGV": { "aaveEnabled": true } }, - "WMATIC": { - "USDT": { + "WMATIC.T/MGV": { + "USDT.T/MGV": { "minimumBasePerOfferFactor": 6, "minimumQuotePerOfferFactor": 6 } }, - "WBTC": { - "USDT": {}, - "DAI": { + "WBTC.T/AAVEv3": { + "USDT.T/MGV": {}, + "DAI.T/AAVEv3": { "aaveEnabled": true } }, - "CRV": { - "WBTC": { + "CRV.T/AAVEv3": { + "WBTC.T/AAVEv3": { "aaveEnabled": true } } @@ -54,17 +54,14 @@ "minimumQuotePerOfferFactor": 10, "aaveEnabled": true, "markets": { - "WETH": { - "USDC": { + "WETH.e": { + "USDC.e": { "minimumBasePerOfferFactor": 6, "minimumQuotePerOfferFactor": 6 } }, - "USDC": { - "USDT": {} - }, - "WMATIC": { - "WETH": {} + "USDC.e": { + "USDT.e": {} } } }, diff --git a/src/constants/tokens.json b/src/constants/tokens.json index 6f5b683f9..5f07b4c13 100644 --- a/src/constants/tokens.json +++ b/src/constants/tokens.json @@ -1,114 +1,59 @@ { - "cBAT": { - "decimals": 8 - }, - "cCOMP": { - "decimals": 8 - }, - "cDAI": { - "decimals": 8 - }, - "cETH": { - "decimals": 8 - }, - "cLINK": { - "decimals": 8 - }, - "cREP": { - "decimals": 8 - }, - "cSAI": { - "decimals": 8 - }, - "cTUSD": { - "decimals": 8 - }, - "cUNI": { - "decimals": 8 - }, - "cUSDC": { - "decimals": 8 - }, - "cUSDT": { - "decimals": 8 - }, - "cWBTC": { - "decimals": 8 - }, - "cZRX": { - "decimals": 8 - }, - "BAT": { - "decimals": 18 - }, - "BTC": { - "decimals": 8 - }, - "COMP": { - "decimals": 18 - }, - "DAI": { - "decimals": 18, + "DAI.": { "displayedDecimals": 2, "displayedAsPriceDecimals": 6 }, - "ETH": { - "decimals": 18 - }, - "GRT": { - "decimals": 18 - }, - "KNC": { - "decimals": 18 - }, - "LINK": { - "decimals": 18 + "DAI.e": { + "displayedDecimals": 2, + "displayedAsPriceDecimals": 6 }, - "MGV": { - "decimals": 18, + "DAI.T/AAVEv3": { "displayedDecimals": 2, "displayedAsPriceDecimals": 6 }, - "REP": { - "decimals": 18 + "DAI.T/MGV": { + "displayedDecimals": 2, + "displayedAsPriceDecimals": 6 }, - "SAI": { - "decimals": 18 + "USDC.": { + "displayedDecimals": 2, + "displayedAsPriceDecimals": 6 }, - "SNX": { - "decimals": 18 + "USDC.e": { + "displayedDecimals": 2, + "displayedAsPriceDecimals": 6 }, - "TUSD": { - "decimals": 18 + "USDC.T/MGV": { + "displayedDecimals": 2, + "displayedAsPriceDecimals": 6 }, - "UNI": { - "decimals": 18 + "USDT.": { + "displayedDecimals": 2, + "displayedAsPriceDecimals": 6 }, - "USDC": { - "decimals": 6, + "USDT.e": { "displayedDecimals": 2, "displayedAsPriceDecimals": 6 }, - "USDT": { - "decimals": 6, + "USDT.T/MGV": { "displayedDecimals": 2, "displayedAsPriceDecimals": 6 }, - "WBTC": { - "decimals": 8, + "WBTC.e": { "displayedDecimals": 6, "displayedAsPriceDecimals": 6 }, - "WMATIC": { - "decimals": 18, - "displayedDecimals": 2, + "WBTC.T/AAVEv3": { + "displayedDecimals": 6, "displayedAsPriceDecimals": 6 }, - "XTZ": { - "decimals": 6 + "WBTC.T/MGV": { + "displayedDecimals": 6, + "displayedAsPriceDecimals": 6 }, - "ZRX": { - "decimals": 18 + "WMATIC.T/MGV": { + "displayedDecimals": 2, + "displayedAsPriceDecimals": 6 }, "TokenA": { "decimals": 18, @@ -118,24 +63,16 @@ "decimals": 18, "cashness": 1000 }, - "WETH": { - "decimals": 18, + "WETH.": { "displayedDecimals": 4, "displayedAsPriceDecimals": 10 }, - "aWETH": { - "decimals": 18 - }, - "aDAI": { - "decimals": 18 - }, - "aUSDC": { - "decimals": 6 - }, - "PxUSDC": { - "decimals": 6 + "WETH.e": { + "displayedDecimals": 4, + "displayedAsPriceDecimals": 10 }, - "PxMATIC": { - "decimals": 17 + "WETH.T/MGV": { + "displayedDecimals": 4, + "displayedAsPriceDecimals": 10 } } diff --git a/src/index.ts b/src/index.ts index 1311011ef..7f537e40f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,7 +11,7 @@ import Mangrove from "./mangrove"; import Market from "./market"; import Semibook from "./semibook"; import OfferLogic from "./offerLogic"; -import MgvToken from "./mgvtoken"; +import Token from "./token"; import LiquidityProvider from "./liquidityProvider"; import KandelStrategies from "./kandelStrategies"; import * as mgvTestUtil from "./util/test/mgvIntegrationTestUtil"; @@ -29,7 +29,6 @@ import GeometricKandelDistribution from "./kandel/geometricKandel/geometricKande import GeneralKandelDistribution from "./kandel/generalKandelDistribution"; import GeometricKandelInstance from "./kandel/geometricKandel/geometricKandelInstance"; - // Turn off Ethers.js warnings // ethers.utils.Logger.setLogLevel(ethers.utils.Logger.levels.ERROR); @@ -41,7 +40,7 @@ export { Mangrove, Market, Semibook, - MgvToken, + Token, OfferLogic, LiquidityProvider, mgvTestUtil, diff --git a/src/kandel/coreKandelInstance.ts b/src/kandel/coreKandelInstance.ts index 9c372ca2c..a0defaaad 100644 --- a/src/kandel/coreKandelInstance.ts +++ b/src/kandel/coreKandelInstance.ts @@ -6,7 +6,7 @@ import * as KandelTypes from "../types/typechain/GeometricKandel"; import Big from "big.js"; import Market from "../market"; import UnitCalculations from "../util/unitCalculations"; -import { ApproveArgs } from "../mgvtoken"; +import { ApproveArgs } from "../token"; import KandelDistributionHelper, { OffersWithGives, } from "./kandelDistributionHelper"; @@ -499,8 +499,8 @@ class CoreKandelInstance { getMostSpecificConfig() { return this.configuration.getMostSpecificConfig( this.market.mgv.network.name, - this.getBase().name, - this.getQuote().name, + this.getBase().id, + this.getQuote().id, ); } diff --git a/src/kandel/kandelConfiguration.ts b/src/kandel/kandelConfiguration.ts index 090cf76ae..796ee026e 100644 --- a/src/kandel/kandelConfiguration.ts +++ b/src/kandel/kandelConfiguration.ts @@ -28,18 +28,18 @@ class KandelConfiguration { /** Gets the most specific available config for the network and the base/quote pair. * @param networkName The name of the network. - * @param baseName The name of the base token. - * @param quoteName The name of the quote token. + * @param baseId The ID of the base token. + * @param quoteId The ID of the quote token. * @returns The most specific configuration available for the network and the base/quote pair. */ public getMostSpecificConfig( networkName: string, - baseName: string, - quoteName: string, + baseId: string, + quoteId: string, ): KandelNetworkConfiguration & Partial { const networkSpecificConfig = this.rawConfiguration.networks?.[networkName]; - const baseSpecificConfig = networkSpecificConfig?.markets?.[baseName]; - const marketSpecificConfig = baseSpecificConfig?.[quoteName]; + const baseSpecificConfig = networkSpecificConfig?.markets?.[baseId]; + const marketSpecificConfig = baseSpecificConfig?.[quoteId]; const config = { ...this.rawConfiguration, @@ -70,21 +70,21 @@ class KandelConfiguration { /** Gets the config for the network and the base/quote pair. * @param networkName The name of the network. - * @param baseName The name of the base token. - * @param quoteName The name of the quote token. + * @param baseId The ID of the base token. + * @param quoteId The ID of the quote token. * @returns The configuration for the network and the base/quote pair. * @throws If the full config is not available for the network and the base/quote pair. */ public getConfigForBaseQuote( networkName: string, - baseName: string, - quoteName: string, + baseId: string, + quoteId: string, ): KandelNetworkConfiguration & KandelMarketConfiguration { - const config = this.getMostSpecificConfig(networkName, baseName, quoteName); + const config = this.getMostSpecificConfig(networkName, baseId, quoteId); function thrower(msg: string): never { throw new Error( - `${msg} for pair ${baseName}/${quoteName} on network ${networkName}.`, + `${msg} for pair ${baseId}/${quoteId} on network ${networkName}.`, ); } @@ -119,8 +119,8 @@ class KandelConfiguration { ): KandelNetworkConfiguration & KandelMarketConfiguration { return this.getConfigForBaseQuote( market.mgv.network.name, - market.base.name, - market.quote.name, + market.base.id, + market.quote.id, ); } diff --git a/src/kandel/kandelFarm.ts b/src/kandel/kandelFarm.ts index 6de6d569f..5a8d6f3cb 100644 --- a/src/kandel/kandelFarm.ts +++ b/src/kandel/kandelFarm.ts @@ -62,8 +62,8 @@ class KandelFarm { if (!olKey) { const offerList = filter?.baseQuoteOfferList; if (offerList) { - const baseAddress = this.mgv.getAddress(offerList.base); - const quoteAddress = this.mgv.getAddress(offerList.quote); + const baseAddress = this.mgv.getTokenAddress(offerList.base); + const quoteAddress = this.mgv.getTokenAddress(offerList.quote); const tickSpacing = offerList.tickSpacing ?? 0; olKey = { outbound_tkn: baseAddress, @@ -85,10 +85,10 @@ class KandelFarm { const olKeyStruct = this.mgv.getOlKeyStruct( x.args.baseQuoteOlKeyHash, ); - const baseToken = await this.mgv.getTokenAndAddress( + const baseToken = await this.mgv.tokenFromAddress( olKeyStruct!.outbound_tkn, ); - const quoteToken = await this.mgv.getTokenAndAddress( + const quoteToken = await this.mgv.tokenFromAddress( olKeyStruct!.inbound_tkn, ); return { @@ -96,9 +96,9 @@ class KandelFarm { ownerAddress: x.args.owner, onAave: false, baseAddress: baseToken.address, - base: baseToken.token, + base: baseToken, quoteAddress: quoteToken.address, - quote: quoteToken.token, + quote: quoteToken, }; }) : []; @@ -115,10 +115,10 @@ class KandelFarm { const olKeyStruct = this.mgv.getOlKeyStruct( x.args.baseQuoteOlKeyHash, ); - const baseToken = await this.mgv.getTokenAndAddress( + const baseToken = await this.mgv.tokenFromAddress( olKeyStruct!.outbound_tkn, ); - const quoteToken = await this.mgv.getTokenAndAddress( + const quoteToken = await this.mgv.tokenFromAddress( olKeyStruct!.inbound_tkn, ); return { @@ -126,9 +126,9 @@ class KandelFarm { ownerAddress: x.args.owner, onAave: true, baseAddress: baseToken.address, - base: baseToken.token, + base: baseToken, quoteAddress: quoteToken.address, - quote: quoteToken.token, + quote: quoteToken, }; }) : []; diff --git a/src/kandelStrategies.ts b/src/kandelStrategies.ts index 69e8fbf27..dc897ca52 100644 --- a/src/kandelStrategies.ts +++ b/src/kandelStrategies.ts @@ -10,6 +10,7 @@ import GeometricKandelLib from "./kandel/geometricKandel/geometricKandelLib"; import GeometricKandelInstance from "./kandel/geometricKandel/geometricKandelInstance"; import GeometricKandelDistributionHelper from "./kandel/geometricKandel/geometricKandelDistributionHelper"; import GeneralKandelDistributionHelper from "./kandel/generalKandelDistributionHelper"; +import configuration from "./configuration"; // eslint-disable-next-line @typescript-eslint/no-namespace namespace KandelStrategies {} @@ -58,11 +59,17 @@ class KandelStrategies { const market = params.market ?? ((baseAddress: string, quoteAddress: string, tickSpacing: Bigish) => { - const baseToken = this.mgv.getNameFromAddress(baseAddress); + const baseToken = configuration.tokens.getTokenIdFromAddress( + baseAddress, + this.mgv.network.name, + ); if (!baseToken) { throw new Error(`Unknown token at address ${baseAddress}`); } - const quoteToken = this.mgv.getNameFromAddress(quoteAddress); + const quoteToken = configuration.tokens.getTokenIdFromAddress( + quoteAddress, + this.mgv.network.name, + ); if (!quoteToken) { throw new Error(`Unknown token at address ${quoteAddress}`); } diff --git a/src/mangrove.ts b/src/mangrove.ts index 0178d6446..1df54fa49 100644 --- a/src/mangrove.ts +++ b/src/mangrove.ts @@ -1,4 +1,4 @@ -import { LiquidityProvider, Market, MgvToken, OfferLogic, Semibook } from "."; +import { LiquidityProvider, Market, Token, OfferLogic, Semibook } from "."; import configuration, { Configuration as MangroveJsConfiguration, PartialConfiguration as PartialMangroveJsConfiguration, @@ -8,7 +8,7 @@ import DevNode from "./util/devNode"; import { Bigish, Provider, Signer, typechain } from "./types"; import { logdataLimiter, logger } from "./util/logger"; import { TypedDataSigner } from "@ethersproject/abstract-signer"; -import { ApproveArgs } from "./mgvtoken"; +import { ApproveArgs } from "./token"; import Big from "big.js"; // Configure big.js global constructor @@ -89,8 +89,8 @@ namespace Mangrove { }; export type OpenMarketInfo = { - base: { name: string; address: string; symbol: string; decimals: number }; - quote: { name: string; address: string; symbol: string; decimals: number }; + base: Token; + quote: Token; tickSpacing: ethers.BigNumber; asksConfig?: LocalConfig; bidsConfig?: LocalConfig; @@ -436,8 +436,8 @@ class Mangrove { To set your own token, use `setDecimals` and `setAddress`. */ async market(params: { - base: string; - quote: string; + base: string | Token; + quote: string | Token; tickSpacing: Bigish; bookOptions?: Market.BookOptions; }): Promise { @@ -503,24 +503,32 @@ class Mangrove { } } - /** Return MgvToken instance, fetching data (decimals) from chain if needed. */ + /** Return Token instance, fetching data (decimals) from chain if needed. */ async token( - name: string, - options?: MgvToken.ConstructorOptions, - ): Promise { - return MgvToken.createToken(name, this, options); + symbolOrId: string, + options?: Token.ConstructorOptions, + ): Promise { + return Token.createTokenFromSymbolOrId(symbolOrId, this, options); } - async tokenFromAddress(address: string): Promise { - return MgvToken.createTokenFromAddress(address, this); + /** Return Token instance, fetching data (decimals) from chain if needed. */ + async tokenFromSymbol( + symbol: string, + options?: Token.ConstructorOptions, + ): Promise { + return Token.createTokenFromSymbol(symbol, this, options); } - /** Return MgvToken instance reading only from configuration, not from chain. */ - tokenFromConfig( - name: string, - options?: MgvToken.ConstructorOptions, - ): MgvToken { - return new MgvToken(name, this, options); + /** Return Token instance, fetching data (decimals) from chain if needed. */ + async tokenFromId( + tokenId: string, + options?: Token.ConstructorOptions, + ): Promise { + return Token.createTokenFromId(tokenId, this, options); + } + + async tokenFromAddress(address: string): Promise { + return Token.createTokenFromAddress(address, this); } /** @@ -536,46 +544,28 @@ class Mangrove { } /** - * Set a contract address on the current network. + * Read a token address on the current network. * - * Note that this writes to the static `Mangrove` address registry which is shared across instances of this class. + * Note that this reads from the static `Mangrove` address registry which is shared across instances of this class. */ - setAddress(name: string, address: string): void { - configuration.addresses.setAddress( - name, - address, - this.network.name || "mainnet", - ); + getTokenAddress(symbolOrId: string): string { + return Token.getTokenAddress(symbolOrId, this.network.name || "mainnet"); } /** - * Gets the name of an address on the current network. + * Set a contract address on the current network. * - * Note that this reads from the static `Mangrove` address registry which is shared across instances of this class. + * Note that this writes to the static `Mangrove` address registry which is shared across instances of this class. */ - getNameFromAddress(address: string): string | undefined { - return configuration.addresses.getNameFromAddress( + setAddress(name: string, address: string): void { + configuration.addresses.setAddress( + name, address, this.network.name || "mainnet", ); } - /** Gets the token corresponding to the address if it is known; otherwise, undefined. - */ - async getTokenAndAddress( - address: string, - ): Promise<{ address: string; token?: MgvToken }> { - const name = this.getNameFromAddress(address); - return { - address, - token: name === undefined ? undefined : await this.token(name), - }; - } - /** Convert public token amount to internal token representation. - * - * if `nameOrDecimals` is a string, it is interpreted as a token name. Otherwise - * it is the number of decimals. * * For convenience, has a static and an instance version. * @@ -585,32 +575,22 @@ class Mangrove { * mgv.toUnits(10,6) // 10e6 as ethers.BigNumber * ``` */ - static toUnits( - amount: Bigish, - nameOrDecimals: string | number, - ): ethers.BigNumber { - return UnitCalculations.toUnits(amount, nameOrDecimals); + static toUnits(amount: Bigish, decimals: number): ethers.BigNumber { + return UnitCalculations.toUnits(amount, decimals); } - toUnits(amount: Bigish, nameOrDecimals: string | number): ethers.BigNumber { - return Mangrove.toUnits(amount, nameOrDecimals); + toUnits(amount: Bigish, decimals: number): ethers.BigNumber { + return Mangrove.toUnits(amount, decimals); } /** Convert internal token amount to public token representation. - * - * if `nameOrDecimals` is a string, it is interpreted as a token name. Otherwise - * it is the number of decimals. * * @example * ``` - * mgv.fromUnits("1e19","DAI") // 10 * mgv.fromUnits("1e19",18) // 10 * ``` */ - fromUnits( - amount: number | string | ethers.BigNumber, - nameOrDecimals: string | number, - ): Big { - return UnitCalculations.fromUnits(amount, nameOrDecimals); + fromUnits(amount: number | string | ethers.BigNumber, decimals: number): Big { + return UnitCalculations.fromUnits(amount, decimals); } /** Provision available at mangrove for address given in argument, in ethers */ @@ -639,10 +619,10 @@ class Mangrove { } async approveMangrove( - tokenName: string, + tokenId: string, arg: ApproveArgs = {}, ): Promise { - const token = await this.token(tokenName); + const token = await this.token(tokenId); return token.approveMangrove(arg); } @@ -841,90 +821,6 @@ class Mangrove { configuration.addresses.setAddress(name, address, network); } - /** - * Gets the name of an address on the given network. - * - * Note that this reads from the static `Mangrove` address registry which is shared across instances of this class. - */ - static getNameFromAddress( - address: string, - network: string, - ): string | undefined { - return configuration.addresses.getNameFromAddress(address, network); - } - - /** - * Read decimals for `tokenName` on given network. - * To read decimals directly onchain, use `fetchDecimals`. - */ - static getDecimals(tokenName: string): number | undefined { - return configuration.tokens.getDecimals(tokenName); - } - - /** - * Read decimals for `tokenName`. Fails if the decimals are not in the configuration. - * To read decimals directly onchain, use `fetchDecimals`. - */ - static getDecimalsOrFail(tokenName: string): number { - return configuration.tokens.getDecimalsOrFail(tokenName); - } - - /** - * Read decimals for `tokenName` on given network. - * If not found in the local configuration, fetch them from the current network and save them - */ - static getOrFetchDecimals( - tokenName: string, - provider: Provider, - ): Promise { - return configuration.tokens.getOrFetchDecimals(tokenName, provider); - } - - /** - * Read chain for decimals of `tokenName` on current network and save them - */ - static async fetchDecimals( - tokenName: string, - provider: Provider, - ): Promise { - return configuration.tokens.fetchDecimals(tokenName, provider); - } - - /** - * Read displayed decimals for `tokenName`. - */ - static getDisplayedDecimals(tokenName: string): number { - return configuration.tokens.getDisplayedPriceDecimals(tokenName); - } - - /** - * Read displayed decimals for `tokenName` when displayed as a price. - */ - static getDisplayedPriceDecimals(tokenName: string): number { - return configuration.tokens.getDisplayedPriceDecimals(tokenName); - } - - /** - * Set decimals for `tokenName` on current network. - */ - static setDecimals(tokenName: string, dec: number): void { - configuration.tokens.setDecimals(tokenName, dec); - } - - /** - * Set displayed decimals for `tokenName`. - */ - static setDisplayedDecimals(tokenName: string, dec: number): void { - configuration.tokens.setDisplayedDecimals(tokenName, dec); - } - - /** - * Set displayed decimals for `tokenName` when displayed as a price. - */ - static setDisplayedPriceDecimals(tokenName: string, dec: number): void { - configuration.tokens.setDisplayedPriceDecimals(tokenName, dec); - } - /** * Setup dev node necessary contracts if needed, register dev Multicall2 * address, listen to future additions (a script external to mangrove.js may @@ -959,7 +855,6 @@ class Mangrove { * @param from: start at market `from`. Default 0. * @param maxLen: max number of markets returned. Default all. * @param configs: fetch market's config information. Default true. - * @param tokenInfo: fetch token information (symbol, decimals) * @note If an open market has a token with no/bad decimals/symbol function, this function will revert. */ async openMarketsData( @@ -967,14 +862,12 @@ class Mangrove { from?: number; maxLen?: number | ethers.BigNumber; configs?: boolean; - tokenInfos?: boolean; } = {}, ): Promise { // set default params params.from ??= 0; params.maxLen ??= ethers.constants.MaxUint256; params.configs ??= true; - params.tokenInfos ??= true; // read open markets and their configs off mgvReader const raw = await this.readerContract["openMarkets(uint256,uint256,bool)"]( params.from, @@ -982,15 +875,15 @@ class Mangrove { params.configs, ); - // structure data object as address => (symbol,decimals,address=>config) + // structure data object as address => (token,address=>config) const data: Record< string, { - symbol: string; - decimals: number; + token: Token; configs: Record; } > = {}; + raw.markets.forEach(([tkn0, tkn1], i) => { (data[tkn0] as any) ??= { configs: {} }; (data[tkn1] as any) ??= { configs: {} }; @@ -1001,80 +894,36 @@ class Mangrove { } }); + // TODO: Consider fetching missing decimals/symbols in one Multicall and dispatch to Token initializations instead of firing multiple RPC calls. + // However, viem (and maybe ethers6) automatically batches multiple read requests as a multicall, so not sure this is worth pursuing. const addresses = Object.keys(data); - - //read decimals & symbol for each token using Multicall - const ierc20 = typechain.IERC20__factory.createInterface(); - - const tryDecodeDecimals = (ary: any[], fnName: "decimals") => { - return ary.forEach((returnData, i) => { - // will raise exception if call reverted - data[addresses[i]][fnName] = ierc20.decodeFunctionResult( - fnName as any, - returnData, - )[0] as number; - }); - }; - const tryDecodeSymbol = (ary: any[], fnName: "symbol") => { - return ary.forEach((returnData, i) => { - // will raise exception if call reverted - data[addresses[i]][fnName] = ierc20.decodeFunctionResult( - fnName as any, - returnData, - )[0] as string; - }); - }; - - /* Grab decimals for all contracts */ - const decimalArgs = addresses.map((addr) => { - return { target: addr, callData: ierc20.encodeFunctionData("decimals") }; - }); - const symbolArgs = addresses.map((addr) => { - return { target: addr, callData: ierc20.encodeFunctionData("symbol") }; - }); - const { returnData } = await this.multicallContract.callStatic.aggregate([ - ...decimalArgs, - ...symbolArgs, - ]); - tryDecodeDecimals(returnData.slice(0, addresses.length), "decimals"); - tryDecodeSymbol(returnData.slice(addresses.length), "symbol"); + await Promise.all( + addresses.map(async (address) => { + data[address].token = await this.tokenFromAddress(address); + }), + ); // format return value return raw.markets.map(([tkn0, tkn1, tickSpacing]) => { - // Use internal mgv name if defined; otherwise use the symbol. - const tkn0Name = this.getNameFromAddress(tkn0) ?? data[tkn0].symbol; - const tkn1Name = this.getNameFromAddress(tkn1) ?? data[tkn1].symbol; - - const { baseName, quoteName } = Mangrove.toBaseQuoteByCashness( - tkn0Name, - tkn1Name, + const { base, quote } = this.toBaseQuoteByCashness( + data[tkn0].token, + data[tkn1].token, ); - const [base, quote] = baseName === tkn0Name ? [tkn0, tkn1] : [tkn1, tkn0]; return { - base: { - name: baseName, - address: base, - symbol: data[base].symbol, - decimals: data[base].decimals, - }, - quote: { - name: quoteName, - address: quote, - symbol: data[quote].symbol, - decimals: data[quote].decimals, - }, + base, + quote, tickSpacing: tickSpacing, asksConfig: params.configs ? Semibook.rawLocalConfigToLocalConfig( - data[base].configs[quote], - data[base].decimals, + data[base.address].configs[quote.address], + base.decimals, ) : undefined, bidsConfig: params.configs ? Semibook.rawLocalConfigToLocalConfig( - data[quote].configs[base], - data[quote].decimals, + data[quote.address].configs[base.address], + quote.decimals, ) : undefined, }; @@ -1103,24 +952,19 @@ class Mangrove { delete params.bookOptions; const openMarketsData = await this.openMarketsData({ ...params, - tokenInfos: true, configs: false, }); // TODO: fetch all semibook configs in one Multicall and dispatch to Semibook initializations (see openMarketsData) instead of firing multiple RPC calls. return Promise.all( openMarketsData.map(({ base, quote, tickSpacing }) => { - this.setAddress(base.name, base.address); - if (configuration.tokens.getDecimals(base.name) === undefined) { - configuration.tokens.setDecimals(base.name, base.decimals); - } - this.setAddress(quote.name, quote.address); - if (configuration.tokens.getDecimals(quote.name) === undefined) { - configuration.tokens.setDecimals(quote.name, quote.decimals); - } + this.setAddress(base.id, base.address); + configuration.tokens.setDecimals(base.id, base.decimals); + this.setAddress(quote.id, quote.address); + configuration.tokens.setDecimals(quote.id, quote.decimals); return Market.connect({ mgv: this, - base: base.name, - quote: quote.name, + base: base.id, + quote: quote.id, tickSpacing: tickSpacing.toString(), bookOptions: bookOptions, noInit: noInit, @@ -1129,27 +973,23 @@ class Mangrove { ); } - /** Set the relative cashness of a token. This determines which token is base & which is quote in a {@link Market}. - * Lower cashness is base, higher cashness is quote, tiebreaker is lexicographic ordering of name string (name is most likely the same as the symbol). - */ - setCashness(name: string, cashness: number) { - configuration.tokens.setCashness(name, cashness); - } - // cashness is "how similar to cash is a token". The cashier token is the quote. // toBaseQuoteByCashness orders tokens according to relative cashness. // Assume cashness of both to be 0 if cashness is undefined for at least one argument. // Ordering is lex order on cashness x (string order) - static toBaseQuoteByCashness(name0: string, name1: string) { - let cash0 = configuration.tokens.getCashness(name0); - let cash1 = configuration.tokens.getCashness(name1); + toBaseQuoteByCashness( + token0: Token, + token1: Token, + ): { base: Token; quote: Token } { + let cash0 = configuration.tokens.getCashness(token0.id); + let cash1 = configuration.tokens.getCashness(token1.id); if (cash0 === undefined || cash1 === undefined) { cash0 = cash1 = 0; } - if (cash0 < cash1 || (cash0 === cash1 && name0 < name1)) { - return { baseName: name0, quoteName: name1 }; + if (cash0 < cash1 || (cash0 === cash1 && token0.id < token1.id)) { + return { base: token0, quote: token1 }; } else { - return { baseName: name1, quoteName: name0 }; + return { base: token1, quote: token0 }; } } } diff --git a/src/mangroveEventSubscriber.ts b/src/mangroveEventSubscriber.ts index 51b2f8348..599f36257 100644 --- a/src/mangroveEventSubscriber.ts +++ b/src/mangroveEventSubscriber.ts @@ -92,14 +92,14 @@ class MangroveEventSubscriber extends LogSubscriber { @@ -120,7 +120,7 @@ class MangroveEventSubscriber extends LogSubscriber, ): Promise { - const base = await params.mgv.token(params.base); - const quote = await params.mgv.token(params.quote); + const base = + typeof params.base === "string" + ? await params.mgv.token(params.base) + : params.base; + const quote = + typeof params.quote === "string" + ? await params.mgv.token(params.quote) + : params.quote; canConstructMarket = true; const market = new Market({ mgv: params.mgv, @@ -357,8 +363,8 @@ class Market { */ private constructor(params: { mgv: Mangrove; - base: MgvToken; - quote: MgvToken; + base: Token; + quote: Token; tickSpacing: BigNumber; }) { if (!canConstructMarket) { @@ -636,8 +642,8 @@ class Market { action: "buy" | "sell", data: Omit, ): Promise { - let outbound_tkn: MgvToken; - let inbound_tkn: MgvToken; + let outbound_tkn: Token; + let inbound_tkn: Token; if (action === "buy") { outbound_tkn = this.base; @@ -962,8 +968,8 @@ class Market { /** Determine which token will be Mangrove's outbound/inbound depending on whether you're working with bids or asks. */ getOutboundInbound(ba: Market.BA): { - outbound_tkn: MgvToken; - inbound_tkn: MgvToken; + outbound_tkn: Token; + inbound_tkn: Token; } { return Market.getOutboundInbound(ba, this.base, this.quote); } @@ -971,11 +977,11 @@ class Market { /** Determine which token will be Mangrove's outbound/inbound depending on whether you're working with bids or asks. */ static getOutboundInbound( ba: Market.BA, - base: MgvToken, - quote: MgvToken, + base: Token, + quote: Token, ): { - outbound_tkn: MgvToken; - inbound_tkn: MgvToken; + outbound_tkn: Token; + inbound_tkn: Token; } { return { outbound_tkn: ba === "asks" ? base : quote, diff --git a/src/offerLogic.ts b/src/offerLogic.ts index 8c4589a82..4f1ed231a 100644 --- a/src/offerLogic.ts +++ b/src/offerLogic.ts @@ -53,14 +53,14 @@ class OfferLogic { * @param args optional `arg.amount` can be used if one wishes to approve a finite amount */ async approve( - tokenName: string, + tokenId: string, args?: { optSpender?: string; optAmount?: Bigish; optOverrides?: ethers.Overrides; }, ): Promise { - const token = await this.mgv.token(tokenName); + const token = await this.mgv.token(tokenId); const amount = args && args.optAmount != undefined ? token.toUnits(args.optAmount) @@ -114,16 +114,16 @@ class OfferLogic { /** * @note (contract admin action) activates logic - * @param tokenNames the names of the tokens one wishes the logic to trade + * @param tokenSymbolsOrIds the symbols or IDs of the tokens one wishes the logic to trade * @param overrides The ethers overrides to use when calling the activate function. * @returns The transaction used to activate the OfferLogic. * */ activate( - tokenNames: string[], + tokenSymbolsOrIds: string[], overrides: ethers.Overrides = {}, ): Promise { - const tokenAddresses = tokenNames.map((tokenName) => - this.mgv.getAddress(tokenName), + const tokenAddresses = tokenSymbolsOrIds.map((symbolOrId) => + this.mgv.getTokenAddress(symbolOrId), ); return this.contract.activate(tokenAddresses, overrides); } diff --git a/src/semibook.ts b/src/semibook.ts index 804aa74f5..705a0d6a2 100644 --- a/src/semibook.ts +++ b/src/semibook.ts @@ -2,7 +2,7 @@ import { Log } from "@ethersproject/providers"; import { Big } from "big.js"; import { BigNumber, ethers } from "ethers"; import clone from "just-clone"; -import { Mangrove, Market, MgvToken } from "."; +import { Mangrove, Market, Token } from "."; import { BlockManager, @@ -218,7 +218,7 @@ class Semibook canConstructSemibook = true; semibook = new Semibook(market, ba, eventListener, options); logger.debug( - `Semibook.connect() ${ba} ${market.base.name} / ${market.quote.name}`, + `Semibook.connect() ${ba} ${market.base.id} / ${market.quote.id}`, ); if (!market.mgv.shouldNotListenToNewEvents) { await market.mgv.mangroveEventSubscriber.subscribeToSemibook(semibook); @@ -918,8 +918,8 @@ class Semibook } & OfferFailEvent, removedOffer: Market.Offer | undefined, state: Semibook.State, - outbound_tkn: MgvToken, - inbound_tkn: MgvToken, + outbound_tkn: Token, + inbound_tkn: Token, log: Log, ) { const id = Semibook.rawIdToId(event.args.id); @@ -952,8 +952,8 @@ class Semibook } & OfferSuccessEvent, removedOffer: Market.Offer | undefined, state: Semibook.State, - outbound_tkn: MgvToken, - inbound_tkn: MgvToken, + outbound_tkn: Token, + inbound_tkn: Token, log: Log, ) { const id = Semibook.rawIdToId(event.args.id); diff --git a/src/mgvtoken.ts b/src/token.ts similarity index 64% rename from src/mgvtoken.ts rename to src/token.ts index 15f453a11..6f1b7ed94 100644 --- a/src/mgvtoken.ts +++ b/src/token.ts @@ -1,17 +1,20 @@ import Big from "big.js"; import * as ethers from "ethers"; import Mangrove from "./mangrove"; -import { Bigish, Provider } from "./types"; +import { Bigish } from "./types"; import { typechain } from "./types"; import UnitCalculations from "./util/unitCalculations"; import configuration from "./configuration"; // eslint-disable-next-line @typescript-eslint/no-namespace -namespace MgvToken { +namespace Token { export type ConstructorOptions = { address?: string; decimals?: number; + symbol?: string; + displayName?: string; displayedDecimals?: number; + displayedAsPriceDecimals?: number; }; } @@ -50,91 +53,142 @@ function convertToApproveArgs(arg: ApproveArgs): { return amount === undefined ? { overrides } : { amount, overrides }; } -class MgvToken { - mgv: Mangrove; - name: string; - address: string; - displayedDecimals: number; - decimals: number; +class Token { // Using most complete interface (burn, mint, blacklist etc.) to be able to access non standard ERC calls using ethers.js contract: typechain.TestToken; - constructor( - name: string, - mgv: Mangrove, - options?: MgvToken.ConstructorOptions, - ) { - this.mgv = mgv; - this.name = name; - MgvToken.#applyOptions(name, mgv, options); - - this.address = this.mgv.getAddress(this.name); - this.decimals = configuration.tokens.getDecimalsOrFail(this.name); - this.displayedDecimals = configuration.tokens.getDisplayedDecimals( - this.name, - ); + /** + * + * @param id ID which should be unique within a network, but can be used across networks. Typically the id from the context-addresses package. May be the symbol if the symbol is unique. NB: This uniqueness is not enforced and duplicates will give undefined behavior. + * @param address Address of the token contract. + * @param symbol Non-unique and optional symbol cf. ERC20. + * @param decimals Number of decimals used by the token. + * @param displayName Optional display name for the token. + * @param displayedDecimals Number of decimals to display in the UI. + * @param displayedAsPriceDecimals Number of decimals to display in the UI when showing a price. + * @param mgv The Mangrove instance this token is associated with. + */ + private constructor( + public id: string, + public address: string, + public symbol: string | undefined, + public decimals: number, + public displayName: string | undefined, + public displayedDecimals: number, + public displayedAsPriceDecimals: number, + public mgv: Mangrove, + ) { this.contract = typechain.TestToken__factory.connect( this.address, this.mgv.signer, ); } - /** Create a MgvToken instance, fetching data (decimals) from chain if needed. */ - static async createToken( - name: string, + /** Create a Token instance, fetching data (decimals) from chain if needed. */ + static async createTokenFromSymbolOrId( + symbolOrId: string, mgv: Mangrove, - options?: MgvToken.ConstructorOptions, - ): Promise { - MgvToken.#applyOptions(name, mgv, options); + options?: Token.ConstructorOptions, + ): Promise { + if (configuration.tokens.isTokenIdRegistered(symbolOrId)) { + return this.createTokenFromId(symbolOrId, mgv, options); + } else { + return this.createTokenFromSymbol(symbolOrId, mgv, options); + } + } - // Ensure decimals are known before token construction as it will otherwise fail. - await MgvToken.getOrFetchDecimals(name, mgv.provider); + /** Create a Token instance, fetching data (decimals) from chain if needed. */ + static async createTokenFromSymbol( + symbol: string, + mgv: Mangrove, + options?: Token.ConstructorOptions, + ): Promise { + const id = + configuration.tokens.getDefaultIdForSymbolOnNetwork( + symbol, + mgv.network.name, + ) ?? symbol; + + return this.createTokenFromId(id, mgv, { ...options, symbol }); + } - return new MgvToken(name, mgv, options); + /** Create a Token instance, fetching data (decimals) from chain if needed. */ + static async createTokenFromId( + id: string, + mgv: Mangrove, + options?: Token.ConstructorOptions, + ): Promise { + const address = + options?.address ?? Token.getTokenAddress(id, mgv.network.name); + const decimals = + options?.decimals ?? + (await configuration.tokens.getOrFetchDecimals(id, mgv.provider)); + const symbol = + options?.symbol ?? + (await configuration.tokens.getOrFetchSymbol(id, mgv.provider)); + const displayName = + options?.displayName ?? configuration.tokens.getDisplayName(id); + const displayedDecimals = + options?.displayedDecimals ?? + configuration.tokens.getDisplayedDecimals(id); + const displayedAsPriceDecimals = + options?.displayedAsPriceDecimals ?? + configuration.tokens.getDisplayedPriceDecimals(id); + + return new Token( + id, + address, + symbol, + decimals, + displayName, + displayedDecimals, + displayedAsPriceDecimals, + mgv, + ); } static async createTokenFromAddress( address: string, mgv: Mangrove, - ): Promise { - const contract = typechain.TestToken__factory.connect( + ): Promise { + let tokenId = configuration.tokens.getTokenIdFromAddress( address, - mgv.provider, + mgv.network.name, ); + if (tokenId !== undefined) { + return this.createTokenFromId(tokenId, mgv, { address }); + } - const name = await contract.callStatic.symbol(); + const symbol = await configuration.tokens.fetchSymbolFromAddress( + address, + mgv.provider, + ); + tokenId = symbol ?? address; - return this.createToken(name, mgv, { + return this.createTokenFromId(tokenId, mgv, { address, + symbol, }); } - static #applyOptions( - name: string, - mgv: Mangrove, - options?: MgvToken.ConstructorOptions, - ) { - if (options === undefined) { - return; - } - - if ("address" in options && options.address !== undefined) { - mgv.setAddress(name, options.address); - } - - if ("decimals" in options && options.decimals !== undefined) { - configuration.tokens.setDecimals(name, options.decimals); - } - - if ( - "displayedDecimals" in options && - options.displayedDecimals !== undefined - ) { - configuration.tokens.setDisplayedDecimals( - name, - options.displayedDecimals, + /** + * Read a token address on the current network. + * + * Note that this reads from the static `Mangrove` address registry which is shared across instances of this class. + */ + static getTokenAddress(symbolOrId: string, network: string): string { + const tokenId = configuration.tokens.isTokenIdRegistered(symbolOrId) + ? symbolOrId + : configuration.tokens.getDefaultIdForSymbolOnNetwork( + symbolOrId, + network, + ); + if (tokenId === undefined) { + throw new Error( + `No token with symbol or ID ${symbolOrId} on network ${network}`, ); } + return configuration.addresses.getAddress(tokenId, network); } /** @@ -223,40 +277,6 @@ class MgvToken { return await this.contract.allowance(params.owner, params.spender); } - /** - * Read decimals for `tokenName` on given network. - * To read decimals directly onchain, use `fetchDecimals`. - */ - static getDecimals(tokenName: string): number | undefined { - return configuration.tokens.getDecimals(tokenName); - } - - /** - * Read decimals for `tokenName`. Fails if the decimals are not in the configuration. - * To read decimals directly onchain, use `fetchDecimals`. - */ - static getDecimalsOrFail(tokenName: string): number { - return configuration.tokens.getDecimalsOrFail(tokenName); - } - - /** - * Read decimals for `tokenName` on given network. - * If not found in the local configuration, fetch them from the current network and save them - */ - static getOrFetchDecimals( - tokenName: string, - provider: Provider, - ): Promise { - return configuration.tokens.getOrFetchDecimals(tokenName, provider); - } - - /** - * Set decimals for `tokenName` on current network. - */ - static setDecimals(tokenName: string, dec: number): void { - configuration.tokens.setDecimals(tokenName, dec); - } - /** * Set approval for Mangrove to `amount`. */ @@ -353,4 +373,4 @@ class MgvToken { } } -export default MgvToken; +export default Token; diff --git a/src/util/test/TestMaker.ts b/src/util/test/TestMaker.ts index 3c044034d..b813c5bcb 100644 --- a/src/util/test/TestMaker.ts +++ b/src/util/test/TestMaker.ts @@ -79,8 +79,8 @@ class TestMaker { static async create( p: TestMaker.CreateParams & Partial, ): Promise { - const baseAddress = p.mgv.getAddress(p.base); - const quoteAddress = p.mgv.getAddress(p.quote); + const baseAddress = p.mgv.getTokenAddress(p.base); + const quoteAddress = p.mgv.getTokenAddress(p.quote); const contract = await new typechain.SimpleTestMaker__factory( p.mgv.signer, ).deploy(p.mgv.address, { diff --git a/src/util/test/mgvIntegrationTestUtil.ts b/src/util/test/mgvIntegrationTestUtil.ts index 504aa75fa..3c5d51302 100644 --- a/src/util/test/mgvIntegrationTestUtil.ts +++ b/src/util/test/mgvIntegrationTestUtil.ts @@ -1,7 +1,7 @@ // TODO do not distribute in browser version // Utility functions for writing integration tests against Mangrove. import { BigNumber, ContractTransaction, ethers } from "ethers"; -import { Market, MgvToken, Mangrove } from "../.."; +import { Market, Token, Mangrove } from "../.."; import { typechain } from "../../types"; import { Provider, TransactionReceipt } from "@ethersproject/abstract-provider"; @@ -218,8 +218,8 @@ export const getTokens = ( market: Market, ba: Market.BA, ): { - inboundToken: MgvToken; - outboundToken: MgvToken; + inboundToken: Token; + outboundToken: Token; } => { return { inboundToken: ba === "asks" ? market.quote : market.base, @@ -467,12 +467,12 @@ export const setMgvGasPrice = async ( }; const rawMint = async ( - token: MgvToken, + token: Token, receiverAddress: string, internalAmount: ethers.BigNumberish, ): Promise => { const deployer = await getAccount(AccountName.Deployer); - switch (token.name) { + switch (token.id) { case "TokenA": await waitForTransaction( deployer.connectedContracts.tokenA.mintTo( @@ -496,7 +496,7 @@ const rawMint = async ( }; export const mint = async ( - token: MgvToken, + token: Token, receiver: Account, amount: number, ): Promise => { diff --git a/src/util/trade.ts b/src/util/trade.ts index 75baf6a4b..7dc5729a7 100644 --- a/src/util/trade.ts +++ b/src/util/trade.ts @@ -501,8 +501,8 @@ class Trade { logger.debug("Creating market order", { contextInfo: "market.marketOrder", data: { - outboundTkn: outboundTkn.name, - inboundTkn: inboundTkn.name, + outboundTkn: outboundTkn.id, + inboundTkn: inboundTkn.id, fillWants: fillWants, tick: tick.toString(), fillVolume: fillVolume.toString(), @@ -754,8 +754,8 @@ class Trade { logger.debug("Creating cleans", { contextInfo: "market.clean", data: { - outboundTkn: outboundTkn.name, - inboundTkn: inboundTkn.name, + outboundTkn: outboundTkn.id, + inboundTkn: inboundTkn.id, }, }); diff --git a/src/util/tradeEventManagement.ts b/src/util/tradeEventManagement.ts index 256818e86..661e3123d 100644 --- a/src/util/tradeEventManagement.ts +++ b/src/util/tradeEventManagement.ts @@ -4,7 +4,7 @@ import { BaseContract, BigNumber } from "ethers"; import { LogDescription } from "ethers/lib/utils"; import Market from "../market"; import Semibook from "../semibook"; -import MgvToken from "../mgvtoken"; +import Token from "../token"; import { OfferFailEvent, OfferFailWithPosthookDataEvent, @@ -88,7 +88,7 @@ class TradeEventManagement { restingOrderId?: number; }; }, - fillToken: MgvToken, + fillToken: Token, ): Market.OrderSummary { if ( (!event.args.tick && !event.args.maxTick) || @@ -108,11 +108,7 @@ class TradeEventManagement { }; } - createSuccessFromEvent( - evt: OfferSuccessEvent, - got: MgvToken, - gave: MgvToken, - ) { + createSuccessFromEvent(evt: OfferSuccessEvent, got: Token, gave: Token) { const success = { offerId: evt.args.id.toNumber(), got: got.fromUnits(evt.args.takerWants), @@ -121,11 +117,7 @@ class TradeEventManagement { return success; } - createTradeFailureFromEvent( - evt: OfferFailEvent, - got: MgvToken, - gave: MgvToken, - ) { + createTradeFailureFromEvent(evt: OfferFailEvent, got: Token, gave: Token) { const tradeFailure = { offerId: evt.args.id.toNumber(), reason: evt.args.mgvData, @@ -171,8 +163,8 @@ class TradeEventManagement { if (olKeyHash != evt.args.olKeyHash) { logger.debug("OfferWrite for unknown market!", { contextInfo: "tradeEventManagement", - base: market.base.name, - quote: market.quote.name, + base: market.base.id, + quote: market.quote.id, tickSpacing: market.tickSpacing, data: { olKeyHash: evt.args.olKeyHash, @@ -188,7 +180,7 @@ class TradeEventManagement { createSummaryFromOrderSummaryEvent( evt: MangroveOrderStartEvent, - fillToken: MgvToken, + fillToken: Token, ): Market.OrderSummary { return this.createSummaryFromEvent( { diff --git a/src/util/unitCalculations.ts b/src/util/unitCalculations.ts index 2d50d9dab..1cc5bf67c 100644 --- a/src/util/unitCalculations.ts +++ b/src/util/unitCalculations.ts @@ -1,54 +1,30 @@ import Big from "big.js"; import * as ethers from "ethers"; -import configuration from "../configuration"; import { Bigish } from "../types"; class UnitCalculations { /** Convert public token amount to internal token representation. - * - * if `nameOrDecimals` is a string, it is interpreted as a token name. Otherwise - * it is the number of decimals. * * @example * ``` - * mgv.toUnits(10,"USDC") // 10e6 as ethers.BigNumber * mgv.toUnits(10,6) // 10e6 as ethers.BigNumber * ``` */ - static toUnits( - amount: Bigish, - nameOrDecimals: string | number, - ): ethers.BigNumber { - let decimals; - if (typeof nameOrDecimals === "number") { - decimals = nameOrDecimals; - } else { - decimals = configuration.tokens.getDecimalsOrFail(nameOrDecimals); - } + static toUnits(amount: Bigish, decimals: number): ethers.BigNumber { return ethers.BigNumber.from(Big(10).pow(decimals).mul(amount).toFixed(0)); } /** Convert internal token amount to public token representation. - * - * if `nameOrDecimals` is a string, it is interpreted as a token name. Otherwise - * it is the number of decimals. * * @example * ``` - * mgv.fromUnits("1e19","DAI") // 10 * mgv.fromUnits("1e19",18) // 10 * ``` */ static fromUnits( amount: number | string | ethers.BigNumber, - nameOrDecimals: string | number, + decimals: number, ): Big { - let decimals; - if (typeof nameOrDecimals === "number") { - decimals = nameOrDecimals; - } else { - decimals = configuration.tokens.getDecimalsOrFail(nameOrDecimals); - } if (amount instanceof ethers.BigNumber) { amount = amount.toString(); } diff --git a/test/integration/kandel/farm.integration.test.ts b/test/integration/kandel/farm.integration.test.ts index 38abd1fd6..f4a69ad42 100644 --- a/test/integration/kandel/farm.integration.test.ts +++ b/test/integration/kandel/farm.integration.test.ts @@ -116,15 +116,17 @@ describe(`${KandelFarm.prototype.constructor.name} integration tests suite`, fun const kandels = await farm.getKandels(); // Assert assert.equal(kandels.length, 5, "total count wrong"); - assert.equal(kandels.filter((x) => x.base?.name == "TokenA").length, 1); - assert.equal(kandels.filter((x) => x.base?.name == "WETH").length, 4); + assert.equal(kandels.filter((x) => x.base?.id == "TokenA").length, 1); + assert.equal(kandels.filter((x) => x.base?.id == "WETH").length, 4); assert.equal( - kandels.filter((x) => x.baseAddress == mgv.getAddress("WETH")).length, + kandels.filter((x) => x.baseAddress == mgv.getTokenAddress("WETH")) + .length, 4, ); - assert.equal(kandels.filter((x) => x.quote?.name == "USDC").length, 3); + assert.equal(kandels.filter((x) => x.quote?.id == "USDC").length, 3); assert.equal( - kandels.filter((x) => x.quoteAddress == mgv.getAddress("USDC")).length, + kandels.filter((x) => x.quoteAddress == mgv.getTokenAddress("USDC")) + .length, 3, ); assert.equal(kandels.filter((x) => x.onAave).length, 2); diff --git a/test/integration/kandel/seeder.integration.test.ts b/test/integration/kandel/seeder.integration.test.ts index 1ecdbbcf3..29c66fef4 100644 --- a/test/integration/kandel/seeder.integration.test.ts +++ b/test/integration/kandel/seeder.integration.test.ts @@ -103,8 +103,8 @@ describe(`${KandelSeeder.prototype.constructor.name} integration tests suite`, f // Assert const params = await kandel.getParameters(); - assert.equal("TokenA", kandel.getBase().name, "wrong base"); - assert.equal("TokenB", kandel.getQuote().name, "wrong base"); + assert.equal("TokenA", kandel.getBase().id, "wrong base"); + assert.equal("TokenB", kandel.getQuote().id, "wrong base"); assert.equal(market, kandel.market, "wrong market"); assert.equal( liquiditySharing && onAave diff --git a/test/integration/mangrove.integration.test.ts b/test/integration/mangrove.integration.test.ts index 4279e048b..41c39bc5f 100644 --- a/test/integration/mangrove.integration.test.ts +++ b/test/integration/mangrove.integration.test.ts @@ -5,7 +5,8 @@ import * as mgvTestUtil from "../../src/util/test/mgvIntegrationTestUtil"; import { toWei } from "../util/helpers"; import { serverType } from "../../src/util/node"; -import { Mangrove } from "../../src"; +import { Mangrove, Token } from "../../src"; +import { configuration } from "../../src/configuration"; import { Big } from "big.js"; @@ -50,25 +51,25 @@ describe("Mangrove integration tests suite", function () { describe("getMarkets", function () { it("updates with mgvReader", async function () { await mgvAdmin.contract.deactivate({ - outbound_tkn: mgv.getAddress("TokenA"), - inbound_tkn: mgv.getAddress("TokenB"), + outbound_tkn: mgv.getTokenAddress("TokenA"), + inbound_tkn: mgv.getTokenAddress("TokenB"), tickSpacing: 1, }); await mgvAdmin.contract.deactivate({ - outbound_tkn: mgv.getAddress("TokenB"), - inbound_tkn: mgv.getAddress("TokenA"), + outbound_tkn: mgv.getTokenAddress("TokenB"), + inbound_tkn: mgv.getTokenAddress("TokenA"), tickSpacing: 1, }); await mgv.readerContract.updateMarket({ - tkn0: mgv.getAddress("TokenA"), - tkn1: mgv.getAddress("TokenB"), + tkn0: mgv.getTokenAddress("TokenA"), + tkn1: mgv.getTokenAddress("TokenB"), tickSpacing: 1, }); const marketsBefore = await mgv.openMarkets(); await mgvAdmin.contract.activate( { - outbound_tkn: mgv.getAddress("TokenA"), - inbound_tkn: mgv.getAddress("TokenB"), + outbound_tkn: mgv.getTokenAddress("TokenA"), + inbound_tkn: mgv.getTokenAddress("TokenB"), tickSpacing: 1, }, 1, @@ -76,8 +77,8 @@ describe("Mangrove integration tests suite", function () { 1, ); await mgv.readerContract.updateMarket({ - tkn0: mgv.getAddress("TokenA"), - tkn1: mgv.getAddress("TokenB"), + tkn0: mgv.getTokenAddress("TokenA"), + tkn1: mgv.getTokenAddress("TokenB"), tickSpacing: 1, }); const markets = await mgv.openMarkets(); @@ -88,42 +89,39 @@ describe("Mangrove integration tests suite", function () { ); }); - it("has reverse lookup of address", function () { - const address = mgv.getAddress("TokenA"); - assert.equal(mgv.getNameFromAddress(address), "TokenA"); - assert.equal( - mgv.getNameFromAddress("0xdeaddeaddeaddaeddeaddeaddeaddeaddeaddead"), - null, - ); - }); - it("gets correct market info and updates with cashness", async function () { await mgv.readerContract.updateMarket({ - tkn0: mgv.getAddress("TokenA"), - tkn1: mgv.getAddress("TokenB"), + tkn0: mgv.getTokenAddress("TokenA"), + tkn1: mgv.getTokenAddress("TokenB"), tickSpacing: 1, }); let marketData = await mgv.openMarketsData(); const tokenAData = { - address: mgv.getAddress("TokenA"), + address: mgv.getTokenAddress("TokenA"), decimals: 18, - name: "TokenA", + id: "TokenA", symbol: "TokenA", }; const tokenBData = { - address: mgv.getAddress("TokenB"), + address: mgv.getTokenAddress("TokenB"), decimals: 6, - name: "TokenB", + id: "TokenB", symbol: "TokenB", }; - assert.deepEqual(marketData[0].base, tokenAData); - assert.deepEqual(marketData[0].quote, tokenBData); + const tokenToData = (token: Token) => ({ + address: token.address, + decimals: token.decimals, + id: token.id, + symbol: token.symbol, + }); + assert.deepEqual(tokenToData(marketData[0].base), tokenAData); + assert.deepEqual(tokenToData(marketData[0].quote), tokenBData); - mgv.setCashness("TokenA", 1000000); + configuration.tokens.setCashness("TokenA", 1000000); marketData = await mgv.openMarketsData(); - assert.deepEqual(marketData[0].base, tokenBData); - assert.deepEqual(marketData[0].quote, tokenAData); + assert.deepEqual(tokenToData(marketData[0].base), tokenBData); + assert.deepEqual(tokenToData(marketData[0].quote), tokenAData); }); }); describe("node utils", () => { diff --git a/test/integration/market.integration.test.ts b/test/integration/market.integration.test.ts index f8bdfa915..c8208a8fa 100644 --- a/test/integration/market.integration.test.ts +++ b/test/integration/market.integration.test.ts @@ -139,8 +139,8 @@ describe("Market integration tests suite", () => { // Act const result = market.getOutboundInbound("asks"); // Assert - assert.equal(result.outbound_tkn.name, "TokenA"); - assert.equal(result.inbound_tkn.name, "TokenB"); + assert.equal(result.outbound_tkn.id, "TokenA"); + assert.equal(result.inbound_tkn.id, "TokenB"); }); it("returns this.base as inbound and this.quote as outbound, when bids", async function () { @@ -153,8 +153,8 @@ describe("Market integration tests suite", () => { // Act const result = market.getOutboundInbound("bids"); // Assert - assert.equal(result.inbound_tkn.name, "TokenA"); - assert.equal(result.outbound_tkn.name, "TokenB"); + assert.equal(result.inbound_tkn.id, "TokenA"); + assert.equal(result.outbound_tkn.id, "TokenB"); }); }); diff --git a/test/integration/mgvtoken.integration.test.ts b/test/integration/mgvtoken.integration.test.ts index b43237fb2..02d5bc7c9 100644 --- a/test/integration/mgvtoken.integration.test.ts +++ b/test/integration/mgvtoken.integration.test.ts @@ -1,6 +1,7 @@ import assert from "assert"; import { afterEach, beforeEach, describe, it } from "mocha"; import { Mangrove, ethers } from "../../src"; +import { configuration } from "../../src/configuration"; import { Big } from "big.js"; import { @@ -22,6 +23,11 @@ describe("MGV Token integration tests suite", () => { privateKey: this.accounts.tester.key, }); + configuration.tokens.setDecimals("USDC", 6); + configuration.tokens.setDisplayedDecimals("USDC", 2); + configuration.tokens.setDecimals("WETH", 18); + configuration.tokens.setDisplayedDecimals("WETH", 4); + //shorten polling for faster tests // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore @@ -221,7 +227,7 @@ describe("MGV Token integration tests suite", () => { const usdc = await mgv.token("USDC"); const usdc2 = await mgv.tokenFromAddress(usdc.address); - assert.equal(usdc.name, usdc2.name); + assert.equal(usdc.id, usdc2.id); assert.equal(usdc.decimals, usdc2.decimals); }); }); diff --git a/test/integration/offermaker.integration.test.ts b/test/integration/offermaker.integration.test.ts index 3c11f2a11..672249f1d 100644 --- a/test/integration/offermaker.integration.test.ts +++ b/test/integration/offermaker.integration.test.ts @@ -420,7 +420,7 @@ describe("OfferMaker integration test suite", () => { const logic = onchain_lp.logic as OfferLogic; const signer_address = await logic.mgv.signer.getAddress(); - const tx = await logic.approve(base.name, { + const tx = await logic.approve(base.id, { optAmount: 42, optOverrides: { gasLimit: 80000 }, }); diff --git a/test/integration/restingOrder.integration.test.ts b/test/integration/restingOrder.integration.test.ts index 2baf1f0f8..99ee7fa17 100644 --- a/test/integration/restingOrder.integration.test.ts +++ b/test/integration/restingOrder.integration.test.ts @@ -8,7 +8,7 @@ import { LiquidityProvider, Mangrove, Market, - MgvToken, + Token, OfferLogic, mgvTestUtil, } from "../../src"; @@ -26,8 +26,8 @@ Big.prototype[Symbol.for("nodejs.util.inspect.custom")] = function () { describe("RestingOrder", () => { let mgv: Mangrove; - let tokenA: MgvToken; - let tokenB: MgvToken; + let tokenA: Token; + let tokenB: Token; let orderLogic: OfferLogic; let orderLP: LiquidityProvider; let router: AbstractRouter; diff --git a/test/unit/configuration.unit.test.ts b/test/unit/configuration.unit.test.ts index b7900aff7..2c4c25d13 100644 --- a/test/unit/configuration.unit.test.ts +++ b/test/unit/configuration.unit.test.ts @@ -8,7 +8,7 @@ describe("Configuration unit tests suite", () => { }); it("Can add token config of unknown token", () => { - assert.equal(configuration.tokens.getDecimals("UnknownToken"), undefined); + assert.throws(() => configuration.tokens.getDecimals("UnknownToken")); configuration.updateConfiguration({ tokens: { @@ -22,8 +22,8 @@ describe("Configuration unit tests suite", () => { }); it("Adding token config does not affect existing config", () => { - assert.equal(configuration.tokens.getDecimals("UnknownToken1"), undefined); - assert.equal(configuration.tokens.getDecimals("UnknownToken2"), undefined); + assert.throws(() => configuration.tokens.getDecimals("UnknownToken1")); + assert.throws(() => configuration.tokens.getDecimals("UnknownToken2")); configuration.updateConfiguration({ tokens: { @@ -49,7 +49,7 @@ describe("Configuration unit tests suite", () => { it("Reset of configuration reverts additions and changes", () => { assert.equal(configuration.tokens.getDecimals("TokenA"), 18); - assert.equal(configuration.tokens.getDecimals("UnknownToken"), undefined); + assert.throws(() => configuration.tokens.getDecimals("UnknownToken")); configuration.updateConfiguration({ tokens: { @@ -68,7 +68,7 @@ describe("Configuration unit tests suite", () => { configuration.resetConfiguration(); assert.equal(configuration.tokens.getDecimals("TokenA"), 18); - assert.equal(configuration.tokens.getDecimals("UnknownToken"), undefined); + assert.throws(() => configuration.tokens.getDecimals("UnknownToken")); }); it("can read mangroveOrder config", () => { diff --git a/test/unit/kandel/kandelConfiguration.unit.test.ts b/test/unit/kandel/kandelConfiguration.unit.test.ts index cfc3ce01e..34ef239a2 100644 --- a/test/unit/kandel/kandelConfiguration.unit.test.ts +++ b/test/unit/kandel/kandelConfiguration.unit.test.ts @@ -74,13 +74,13 @@ describe(`${KandelConfiguration.prototype.constructor.name} unit tests suite`, ( const markets = sut.getConfiguredMarketsForNetwork("maticmum"); // Assert assert.deepStrictEqual(markets, [ - { base: "WETH", quote: "DAI" }, - { base: "WETH", quote: "USDC" }, - { base: "DAI", quote: "USDC" }, - { base: "WMATIC", quote: "USDT" }, - { base: "WBTC", quote: "USDT" }, - { base: "WBTC", quote: "DAI" }, - { base: "CRV", quote: "WBTC" }, + { base: "WETH.T/MGV", quote: "DAI.T/AAVEv3" }, + { base: "WETH.T/MGV", quote: "USDC.T/MGV" }, + { base: "DAI.T/AAVEv3", quote: "USDC.T/MGV" }, + { base: "WMATIC.T/MGV", quote: "USDT.T/MGV" }, + { base: "WBTC.T/AAVEv3", quote: "USDT.T/MGV" }, + { base: "WBTC.T/AAVEv3", quote: "DAI.T/AAVEv3" }, + { base: "CRV.T/AAVEv3", quote: "WBTC.T/AAVEv3" }, ]); }); diff --git a/test/unit/trade.unit.test.ts b/test/unit/trade.unit.test.ts index d79fcadb8..1098e7230 100644 --- a/test/unit/trade.unit.test.ts +++ b/test/unit/trade.unit.test.ts @@ -12,7 +12,7 @@ import { verify, when, } from "ts-mockito"; -import { Market, MgvToken } from "../../src"; +import { Market, Token } from "../../src"; import { Bigish } from "../../src/types"; import Trade from "../../src/util/trade"; import { TickLib } from "../../src/util/coreCalculations/TickLib"; @@ -31,8 +31,8 @@ describe("Trade unit tests suite", () => { volume: 30, slippage: slippage, }; - const baseToken = mock(MgvToken); - const quoteToken = mock(MgvToken); + const baseToken = mock(Token); + const quoteToken = mock(Token); when(baseToken.toUnits(anything())).thenReturn( BigNumber.from(params.volume), ); @@ -88,8 +88,8 @@ describe("Trade unit tests suite", () => { total: 30, slippage: slippage, }; - const baseToken = mock(MgvToken); - const quoteToken = mock(MgvToken); + const baseToken = mock(Token); + const quoteToken = mock(Token); when(baseToken.toUnits(anything())).thenReturn( BigNumber.from(Big(params.total).div(price).toFixed(0)), ); @@ -136,8 +136,8 @@ describe("Trade unit tests suite", () => { fillVolume: 30, slippage: slippage, }; - const baseToken = mock(MgvToken); - const quoteToken = mock(MgvToken); + const baseToken = mock(Token); + const quoteToken = mock(Token); when(baseToken.toUnits(anything())).thenReturn( BigNumber.from(Big(params.fillVolume).toFixed(0)), ); @@ -180,8 +180,8 @@ describe("Trade unit tests suite", () => { fillWants: false, slippage: slippage, }; - const baseToken = mock(MgvToken); - const quoteToken = mock(MgvToken); + const baseToken = mock(Token); + const quoteToken = mock(Token); when(baseToken.toUnits(anything())).thenReturn( BigNumber.from(Big(params.fillVolume).toFixed(0)), ); @@ -272,8 +272,8 @@ describe("Trade unit tests suite", () => { volume: 30, slippage: slippage, }; - const baseToken = mock(MgvToken); - const quoteToken = mock(MgvToken); + const baseToken = mock(Token); + const quoteToken = mock(Token); when(baseToken.toUnits(anything())).thenReturn( BigNumber.from(params.volume), ); @@ -320,8 +320,8 @@ describe("Trade unit tests suite", () => { total: 30, slippage: slippage, }; - const baseToken = mock(MgvToken); - const quoteToken = mock(MgvToken); + const baseToken = mock(Token); + const quoteToken = mock(Token); when(quoteToken.toUnits(anything())).thenReturn( BigNumber.from(params.total), ); @@ -372,8 +372,8 @@ describe("Trade unit tests suite", () => { tick: 30, slippage: slippage, }; - const baseToken = mock(MgvToken); - const quoteToken = mock(MgvToken); + const baseToken = mock(Token); + const quoteToken = mock(Token); when(quoteToken.toUnits(anything())).thenReturn( BigNumber.from(params.fillVolume), ); @@ -415,8 +415,8 @@ describe("Trade unit tests suite", () => { fillWants: true, slippage: slippage, }; - const baseToken = mock(MgvToken); - const quoteToken = mock(MgvToken); + const baseToken = mock(Token); + const quoteToken = mock(Token); when(quoteToken.toUnits(anything())).thenReturn( BigNumber.from(params.fillVolume), ); diff --git a/test/unit/tradeEventManagement.unit.test.ts b/test/unit/tradeEventManagement.unit.test.ts index d420dc275..4b1b1e076 100644 --- a/test/unit/tradeEventManagement.unit.test.ts +++ b/test/unit/tradeEventManagement.unit.test.ts @@ -12,7 +12,7 @@ import { verify, when, } from "ts-mockito"; -import { Semibook, Market, MgvToken } from "../../src"; +import { Semibook, Market, Token } from "../../src"; import TradeEventManagement from "../../src/util/tradeEventManagement"; import UnitCalculations from "../../src/util/unitCalculations"; import { @@ -44,7 +44,7 @@ describe("TradeEventManagement unit tests suite", () => { const marketSide: Market.BA = "bids"; - const baseTokenMock = mock(MgvToken); + const baseTokenMock = mock(Token); const baseTokenDecimals: number = 3; when(baseTokenMock.decimals).thenReturn(baseTokenDecimals); @@ -54,7 +54,7 @@ describe("TradeEventManagement unit tests suite", () => { ); when(baseTokenMock.fromUnits(rawGives)).thenReturn(expectedGives); - const quoteTokenMock = mock(MgvToken); + const quoteTokenMock = mock(Token); const quoteTokenDecimals = 1; const tickPriceHelper = new TickPriceHelper(marketSide, { @@ -114,10 +114,10 @@ describe("TradeEventManagement unit tests suite", () => { const marketSide: Market.BA = "asks"; - const baseTokenMock = mock(MgvToken); + const baseTokenMock = mock(Token); const baseTokenDecimals: number = 3; - const quoteTokenMock = mock(MgvToken); + const quoteTokenMock = mock(Token); const quoteTokenDecimals = 1; const expectedGives = UnitCalculations.fromUnits( rawGives, @@ -195,7 +195,7 @@ describe("TradeEventManagement unit tests suite", () => { it("return summary with partialFill as true, when partialFill func returns true", async function () { //Arrange const tradeEventManagement = new TradeEventManagement(); - const mockedToken = mock(MgvToken); + const mockedToken = mock(Token); const token = instance(mockedToken); const evt: summaryEvent = { args: { @@ -227,7 +227,7 @@ describe("TradeEventManagement unit tests suite", () => { const tradeEventManagement = new TradeEventManagement(); const spyTradeEventManagement = spy(tradeEventManagement); - const mockedToken = mock(MgvToken); + const mockedToken = mock(Token); const token = instance(mockedToken); const event = instance(mock()); const summary: any = "summary"; @@ -253,7 +253,7 @@ describe("TradeEventManagement unit tests suite", () => { const tradeEventManagement = new TradeEventManagement(); const spyTradeEventManagement = spy(tradeEventManagement); const mockedEvent = mock(); - const mockedToken = mock(MgvToken); + const mockedToken = mock(Token); const token = instance(mockedToken); const event = instance(mockedEvent); const summary: Market.OrderSummary = { @@ -297,8 +297,8 @@ describe("TradeEventManagement unit tests suite", () => { takerWants: BigNumber.from(2), takerGives: BigNumber.from(3), }; - const gotToken = mock(MgvToken); - const gaveToken = mock(MgvToken); + const gotToken = mock(Token); + const gaveToken = mock(Token); const expectedGot = Big(args.takerWants.toNumber()); const expectedGave = Big(args.takerGives.toNumber()); @@ -333,8 +333,8 @@ describe("TradeEventManagement unit tests suite", () => { takerWants: BigNumber.from(2), takerGives: BigNumber.from(3), }; - const gotToken = mock(MgvToken); - const gaveToken = mock(MgvToken); + const gotToken = mock(Token); + const gaveToken = mock(Token); const expectedFailToDeliver = Big(args.takerWants.toNumber()); const expectedVolumeGiven = Big(args.takerGives.toNumber()); diff --git a/test/unit/unitCalculations.unit.test.ts b/test/unit/unitCalculations.unit.test.ts index 0153ed8c0..d04e86e65 100644 --- a/test/unit/unitCalculations.unit.test.ts +++ b/test/unit/unitCalculations.unit.test.ts @@ -7,7 +7,7 @@ import UnitCalculations from "../../src/util/unitCalculations"; describe("UnitCalculations unit tests suite", () => { describe("fromUnits", () => { - it("returns Big number, amount is number and nameOrDecimal is number", async function () { + it("returns Big number, amount is number", async function () { //Act const result = UnitCalculations.fromUnits(123, 11); @@ -15,7 +15,7 @@ describe("UnitCalculations unit tests suite", () => { equal(result.eq(Big(123).div(Big(10).pow(11))), true); }); - it("returns Big number, amount is string and nameOrDecimal is number", async function () { + it("returns Big number, amount is string", async function () { //Act const result = UnitCalculations.fromUnits("123", 11); @@ -23,20 +23,12 @@ describe("UnitCalculations unit tests suite", () => { equal(result.eq(Big(123).div(Big(10).pow(11))), true); }); - it("returns Big number, amount is BigNumber and nameOrDecimal is number", async function () { + it("returns Big number, amount is BigNumber", async function () { //Act const result = UnitCalculations.fromUnits(BigNumber.from(123), 11); //Assert equal(result.eq(Big(123).div(Big(10).pow(11))), true); }); - - it("returns Big number, amount is number and nameOrDecimal is string", async function () { - //Act - const result = UnitCalculations.fromUnits(123, "DAI"); - - //Assert - equal(result.eq(Big(123).div(Big(10).pow(18))), true); - }); }); }); diff --git a/test/util/helpers.ts b/test/util/helpers.ts index 3f8c93940..74e6240d4 100644 --- a/test/util/helpers.ts +++ b/test/util/helpers.ts @@ -1,5 +1,5 @@ import { BigNumber, BigNumberish, ContractTransaction, utils } from "ethers"; -import Mangrove, { MgvToken } from "../../src"; +import Mangrove, { Token } from "../../src"; import { Bigish } from "../../src/types"; import Big from "big.js"; import assert from "assert"; @@ -96,17 +96,17 @@ export type OfferData = { async function getAmountAndAddress( mgv: Mangrove, - token: string | MgvToken, + token: string | Token, amount: string, ) { - const mgvToken = await getAddress(token, mgv); - return { address: mgvToken.address, value: mgvToken.toUnits(amount) }; + const Token = await getAddress(token, mgv); + return { address: Token.address, value: Token.toUnits(amount) }; } export const newOffer = async ( mgv: Mangrove, - outbound_tkn: string | MgvToken, - inbound_tkn: string | MgvToken, + outbound_tkn: string | Token, + inbound_tkn: string | Token, { gives, gasreq, gasprice, tick }: OfferData, ): Promise => { const outboundInfo = await getAmountAndAddress(mgv, outbound_tkn, gives); @@ -124,6 +124,6 @@ export const newOffer = async ( gasprice || 1, ); }; -async function getAddress(token: string | MgvToken, mgv: Mangrove) { +async function getAddress(token: string | Token, mgv: Mangrove) { return typeof token === "string" ? await mgv.token(token) : token; } diff --git a/yarn.lock b/yarn.lock index 2dd53ec13..6fe9858bf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1325,7 +1325,6 @@ __metadata: mkdirp: ^3.0.1 mocha: ^10.2.0 mocha-multi-reporters: ^1.5.1 - moize: ^6.1.6 node-cleanup: ^2.1.2 npm-run-all: ^4.1.5 nyc: ^15.1.0 @@ -3254,13 +3253,6 @@ __metadata: languageName: node linkType: hard -"fast-equals@npm:^3.0.1": - version: 3.0.3 - resolution: "fast-equals@npm:3.0.3" - checksum: e7ac0ae5a10289c773f75654ced22563837336bde7ebb595b7d238a20b77008a821c1ca3526a50e96fe0662ced7454cf99b7488bb64506463a4f4729c523ac4c - languageName: node - linkType: hard - "fast-glob@npm:^3.2.9": version: 3.2.12 resolution: "fast-glob@npm:3.2.12" @@ -4812,13 +4804,6 @@ __metadata: languageName: node linkType: hard -"micro-memoize@npm:^4.1.2": - version: 4.1.2 - resolution: "micro-memoize@npm:4.1.2" - checksum: 4b02750622d44b5ab31573c629b5d91927dd0c2727743ff75e790c223ab6cd02c48cc3bddea69da0dffb688091a0a71a17944947dd165f8ba9e03728bc30a76d - languageName: node - linkType: hard - "micromatch@npm:4.0.5, micromatch@npm:^4.0.4": version: 4.0.5 resolution: "micromatch@npm:4.0.5" @@ -5069,16 +5054,6 @@ __metadata: languageName: node linkType: hard -"moize@npm:^6.1.6": - version: 6.1.6 - resolution: "moize@npm:6.1.6" - dependencies: - fast-equals: ^3.0.1 - micro-memoize: ^4.1.2 - checksum: a81c56e8d3d30ad0c324369ab636e48b9799abb7f5e0955250f99744dbea456848107d293436face8e12acf7bc820e759048118083cc37644a4c95a37c66e566 - languageName: node - linkType: hard - "ms@npm:2.1.2": version: 2.1.2 resolution: "ms@npm:2.1.2"