diff --git a/.github/workflows/validate-other-market-data.yml b/.github/workflows/validate-other-market-data.yml index 63dfbb83a..2e6f79c25 100644 --- a/.github/workflows/validate-other-market-data.yml +++ b/.github/workflows/validate-other-market-data.yml @@ -62,4 +62,4 @@ jobs: path: 'v4-web-main-other-market-validation' - name: Validate other market data - run: pnpx tsx scripts/validate-other-market-data.ts + run: pnpx tsx scripts/markets/validate-other-market-data.ts diff --git a/scripts/markets/add-markets.ts b/scripts/markets/add-markets.ts new file mode 100644 index 000000000..786dcd8c4 --- /dev/null +++ b/scripts/markets/add-markets.ts @@ -0,0 +1,282 @@ +/* +This script adds markets to a dYdX chain. Markets are read from public/config/otherMarketData.json. + +Supported environments: local, dev, dev2, dev3, dev4, dev5, staging. + +Usage: + $ pnpx tsx scripts/markets/add-markets.ts +Example (add 10 markets on staging): + $ pnpx tsx scripts/markets/add-markets.ts staging 10 +*/ + +import { + CompositeClient, + IndexerConfig, + LocalWallet as LocalWalletType, + Network, + ValidatorConfig +} from '@dydxprotocol/v4-client-js'; +import { + PerpetualMarketType +} from '@dydxprotocol/v4-client-js/build/node_modules/@dydxprotocol/v4-proto/src/codegen/dydxprotocol/perpetuals/perpetual'; +import { readFileSync } from 'fs'; +import Long from 'long'; +import { Proposal, retry, sleep, voteOnProposals } from './help'; + +const LocalWalletModule = await import( + '@dydxprotocol/v4-client-js/src/clients/modules/local-wallet' +); +const LocalWallet = LocalWalletModule.default; + +// TODO: Query MIN_DEPOSIT from chain. +const MIN_DEPOSIT = '10000000'; +// Markets become active `DELAY_BLOCKS` blocks after markets are added. +const DELAY_BLOCKS: number = 100; + +const MNEMONICS = [ + // alice + 'merge panther lobster crazy road hollow amused security before critic about cliff exhibit cause coyote talent happy where lion river tobacco option coconut small', + + // bob + 'color habit donor nurse dinosaur stable wonder process post perfect raven gold census inside worth inquiry mammal panic olive toss shadow strong name drum', + + // carl + 'school artefact ghost shop exchange slender letter debris dose window alarm hurt whale tiger find found island what engine ketchup globe obtain glory manage', + + // dave + 'switch boring kiss cash lizard coconut romance hurry sniff bus accident zone chest height merit elevator furnace eagle fetch quit toward steak mystery nest', + + // emily + 'brave way sting spin fog process matrix glimpse volcano recall day lab raccoon hand path pig rent mixture just way blouse alone upon prefer', + + // fiona + 'suffer claw truly wife simple mean still mammal bind cake truly runway attack burden lazy peanut unusual such shock twice appear gloom priority kind', + + // greg + 'step vital slight present group gallery flower gap copy sweet travel bitter arena reject evidence deal ankle motion dismiss trim armed slab life future', + + // henry + 'piece choice region bike tragic error drive defense air venture bean solve income upset physical sun link actor task runway match gauge brand march', + + // ian + 'burst section toss rotate law thumb shoe wire only decide meadow aunt flight humble story mammal radar scene wrist essay taxi leisure excess milk', + + // jeff + 'fashion charge estate devote jaguar fun swift always road lend scrap panic matter core defense high gas athlete permit crane assume pact fitness matrix', +]; + +enum Env { + LOCAL = 'local', + DEV = 'dev', + DEV2 = 'dev2', + DEV3 = 'dev3', + DEV4 = 'dev4', + DEV5 = 'dev5', + STAGING = 'staging', +} + +const ENV_CONFIG = { + [Env.LOCAL]: { + chainId: 'localdydxprotocol', + blockTimeSeconds: 5, + numValidators: 4, + validatorEndpoint: 'http://localhost:26657', + indexerRestEndpoint: '', + indexerWsEndpoint: '', + }, + [Env.DEV]: { + chainId: 'dydxprotocol-testnet', + blockTimeSeconds: 1, + numValidators: 4, + validatorEndpoint: 'http://3.134.210.83:26657', + indexerRestEndpoint: 'https://indexer.v4dev.dydx.exchange', + indexerWsEndpoint: 'wss://indexer.v4dev.dydx.exchange', + }, + [Env.DEV2]: { + chainId: 'dydxprotocol-testnet', + blockTimeSeconds: 1, + numValidators: 4, + validatorEndpoint: 'http://18.220.125.195:26657', + indexerRestEndpoint: '', + indexerWsEndpoint: '', + }, + [Env.DEV3]: { + chainId: 'dydxprotocol-testnet', + blockTimeSeconds: 1, + numValidators: 4, + validatorEndpoint: 'http://3.21.4.182:26657', + indexerRestEndpoint: '', + indexerWsEndpoint: '', + }, + [Env.DEV4]: { + chainId: 'dydxprotocol-testnet', + blockTimeSeconds: 1, + numValidators: 4, + validatorEndpoint: 'http://3.23.254.51:26657', + indexerRestEndpoint: '', + indexerWsEndpoint: '', + }, + [Env.DEV5]: { + chainId: 'dydxprotocol-testnet', + blockTimeSeconds: 1, + numValidators: 4, + validatorEndpoint: 'http://18.223.78.50:26657', + indexerRestEndpoint: '', + indexerWsEndpoint: '', + }, + [Env.STAGING]: { + chainId: 'dydxprotocol-testnet', + blockTimeSeconds: 1, + numValidators: 10, + validatorEndpoint: 'http://18.188.95.153:26657', + indexerRestEndpoint: 'https://indexer.v4staging.dydx.exchange', + indexerWsEndpoint: 'wss://indexer.v4staging.dydx.exchange', + }, +} + +async function addMarkets( + env: Env, + numMarkets: number, + proposals: Proposal[], +): Promise { + // Initialize client and wallets. + const config = ENV_CONFIG[env]; + const indexerConfig = new IndexerConfig( + config.indexerRestEndpoint, + config.indexerWsEndpoint, + ); + const validatorConfig = new ValidatorConfig( + config.validatorEndpoint, + config.chainId, + { + CHAINTOKEN_DENOM: 'adv4tnt', + USDC_DENOM: 'ibc/8E27BA2D5493AF5636760E354E46004562C46AB7EC0CC4C1CA14E9E20E2545B5', + USDC_GAS_DENOM: 'uusdc', + USDC_DECIMALS: 6, + CHAINTOKEN_DECIMALS: 18, + }, + undefined, + 'Client Example', + ); + const network = new Network(env, indexerConfig, validatorConfig); + + const client = await CompositeClient.connect(network); + const wallets: LocalWalletType[] = await Promise.all( + MNEMONICS.slice(0, config.numValidators).map((mnemonic) => { + return LocalWallet.fromMnemonic(mnemonic, 'dydx'); + }) + ); + + // Send proposals to add all markets (skip markets that already exist). + const allPerps = await client.validatorClient.get.getAllPerpetuals(); + const allTickers = allPerps.perpetual.map((perp) => perp.params!.ticker); + const filteredProposals = proposals.filter( + (proposal) => !allTickers.includes(proposal.params.ticker) + ); + + console.log(`Adding ${numMarkets} new markets to ${env}...`); + + const sleepMsBtwTxs = 3.5 * config.blockTimeSeconds * 1000; + let numProposalsToSend = Math.min(numMarkets, filteredProposals.length); + let numProposalsSent = 0; + const numExistingMarkets = allPerps.perpetual.reduce( + (max, perp) => (perp.params!.id > max ? perp.params!.id : max), + 0 + ); + + for (let i = 0; i < numProposalsToSend; i += config.numValidators) { + // Send out proposals in groups. + const proposalsToSend = filteredProposals.slice(i, i + config.numValidators); + const proposalIds: number[] = []; + for (let j = 0; j < proposalsToSend.length; j++) { + if (numProposalsSent >= numProposalsToSend) { + break; + } + const proposal = proposalsToSend[j]; + const proposalId: number = i + j + 1; + const marketId: number = numExistingMarkets + proposalId; + + // Send proposal. + const exchangeConfigString = `{"exchanges":${JSON.stringify( + proposal.params.exchangeConfigJson + )}}`; + await retry(() => + client.submitGovAddNewMarketProposal( + wallets[j], + { + id: marketId, + ticker: proposal.params.ticker, + priceExponent: proposal.params.priceExponent, + minPriceChange: proposal.params.minPriceChange, + minExchanges: proposal.params.minExchanges, + exchangeConfigJson: exchangeConfigString, + liquidityTier: proposal.params.liquidityTier, + atomicResolution: proposal.params.atomicResolution, + quantumConversionExponent: proposal.params.quantumConversionExponent, + stepBaseQuantums: Long.fromNumber(proposal.params.stepBaseQuantums), + subticksPerTick: proposal.params.subticksPerTick, + delayBlocks: DELAY_BLOCKS, + marketType: + proposal.params.marketType === 'PERPETUAL_MARKET_TYPE_ISOLATED' + ? PerpetualMarketType.PERPETUAL_MARKET_TYPE_ISOLATED + : PerpetualMarketType.PERPETUAL_MARKET_TYPE_CROSS, + }, + proposal.title, + proposal.summary, + MIN_DEPOSIT, + ) + ); + console.log(`Proposed market ${marketId} with ticker ${proposal.params.ticker}`); + + // Record proposed market. + proposalIds.push(proposalId); + numProposalsSent++; + } + + // Wait for proposals to be processed. + await sleep(sleepMsBtwTxs); + + // Vote YES on proposals from every wallet. + for (const wallet of wallets) { + retry(() => voteOnProposals(proposalIds, client, wallet)); + } + + // Wait for votes to be processed. + await sleep(sleepMsBtwTxs); + } +} + +async function main(): Promise { + // Get which env and how many markets to add. + const args = process.argv.slice(2); + const env = args[0] as Env; + const numMarkets = parseInt(args[1], 10); + + // Validate inputs. + if (!Object.values(Env).includes(env)) { + throw new Error(`Invalid environment: ${env}`); + } else if (isNaN(numMarkets) || numMarkets <= 0) { + throw new Error(`Invalid number of markets: ${numMarkets}`); + } + + // Read proposals. + const proposals: Record = JSON.parse( + readFileSync('public/configs/otherMarketData.json', 'utf8'), + ); + + // Add markets. + await addMarkets( + env, + numMarkets, + Object.values(proposals), + ); +} + +main() + .then(() => { + console.log('\nDone'); + }) + .catch((error) => { + console.error(error); + process.exit(1); + }); diff --git a/scripts/markets/help.ts b/scripts/markets/help.ts new file mode 100644 index 000000000..6a1104cb5 --- /dev/null +++ b/scripts/markets/help.ts @@ -0,0 +1,133 @@ +import { EncodeObject } from '@cosmjs/proto-signing'; +import { Account, StdFee } from '@cosmjs/stargate'; +import { Method } from '@cosmjs/tendermint-rpc'; +import { BroadcastTxSyncResponse } from '@cosmjs/tendermint-rpc/build/tendermint37'; +import { + CompositeClient, + LocalWallet as LocalWalletType, + TransactionOptions, + VoteOption, +} from '@dydxprotocol/v4-client-js'; +import { MsgVote } from '@dydxprotocol/v4-proto/src/codegen/cosmos/gov/v1/tx'; +import Long from 'long'; + +const VOTE_FEE: StdFee = { + amount: [ + { + amount: '25000000000000000', + denom: 'adv4tnt', + }, + ], + gas: '1000000', +}; + +export interface Exchange { + exchangeName: ExchangeName; + ticker: string; + adjustByMarket?: string; +} + +export enum ExchangeName { + Binance = 'Binance', + BinanceUS = 'BinanceUS', + Bitfinex = 'Bitfinex', + Bitstamp = 'Bitstamp', + Bybit = 'Bybit', + CoinbasePro = 'CoinbasePro', + CryptoCom = 'CryptoCom', + Gate = 'Gate', + Huobi = 'Huobi', + Kraken = 'Kraken', + Kucoin = 'Kucoin', + Mexc = 'Mexc', + Okx = 'Okx', + Raydium = 'Raydium', +} + +export interface Params { + id: number; + ticker: string; + marketType: 'PERPETUAL_MARKET_TYPE_ISOLATED' | 'PERPETUAL_MARKET_TYPE_CROSS'; + priceExponent: number; + minPriceChange: number; + minExchanges: number; + exchangeConfigJson: Exchange[]; + liquidityTier: number; + atomicResolution: number; + quantumConversionExponent: number; + defaultFundingPpm: number; + stepBaseQuantums: number; + subticksPerTick: number; + delayBlocks: number; +} + +export interface Proposal { + id: Long.Long; + title: string; + summary: string; + params: Params; +} + +export async function sleep(ms: number): Promise { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); +} + +export async function retry( + fn: () => Promise, + retries: number = 5, + delay: number = 2000 +): Promise { + try { + return await fn(); + } catch (error) { + console.error(`Function ${fn.name} failed: ${error}. Retrying in ${delay}ms...`); + if (retries <= 0) { + throw error; + } + await sleep(delay); + return retry(fn, retries - 1, delay); + } +} + +// Vote YES on all `proposalIds` from `wallet`. +export async function voteOnProposals( + proposalIds: number[], + client: CompositeClient, + wallet: LocalWalletType +): Promise { + // Construct Tx. + const encodedVotes: EncodeObject[] = proposalIds.map((proposalId) => { + return { + typeUrl: '/cosmos.gov.v1.MsgVote', + value: { + proposalId: Long.fromNumber(proposalId), + voter: wallet.address!, + option: VoteOption.VOTE_OPTION_YES, + metadata: '', + } as MsgVote, + } as EncodeObject; + }); + const account: Account = await client.validatorClient.get.getAccount(wallet.address!); + const signedTx = await wallet.signTransaction( + encodedVotes, + { + sequence: account.sequence, + accountNumber: account.accountNumber, + chainId: client.network.validatorConfig.chainId, + } as TransactionOptions, + VOTE_FEE + ); + + // Broadcast Tx. + const resp = await client.validatorClient.get.tendermintClient.broadcastTransaction( + signedTx, + Method.BroadcastTxSync + ); + if ((resp as BroadcastTxSyncResponse).code) { + throw new Error(`Failed to vote on proposals ${proposalIds}`); + } else { + console.log(`Voted on proposals ${proposalIds} with wallet ${wallet.address}`); + } +} \ No newline at end of file diff --git a/scripts/validate-other-market-data.ts b/scripts/markets/validate-other-market-data.ts similarity index 89% rename from scripts/validate-other-market-data.ts rename to scripts/markets/validate-other-market-data.ts index 96933e955..4817da838 100644 --- a/scripts/validate-other-market-data.ts +++ b/scripts/markets/validate-other-market-data.ts @@ -7,23 +7,17 @@ /* eslint-disable no-restricted-syntax */ /* eslint-disable no-await-in-loop */ -import { EncodeObject } from '@cosmjs/proto-signing'; -import { Account, StdFee } from '@cosmjs/stargate'; -import { Method } from '@cosmjs/tendermint-rpc'; -import { BroadcastTxSyncResponse } from '@cosmjs/tendermint-rpc/build/tendermint37'; +import { StdFee } from '@cosmjs/stargate'; import { CompositeClient, LocalWallet as LocalWalletType, Network, - ProposalStatus, - TransactionOptions, - VoteOption, + ProposalStatus } from '@dydxprotocol/v4-client-js'; import { Perpetual, PerpetualMarketType, } from '@dydxprotocol/v4-client-js/build/node_modules/@dydxprotocol/v4-proto/src/codegen/dydxprotocol/perpetuals/perpetual'; -import { MsgVote } from '@dydxprotocol/v4-proto/src/codegen/cosmos/gov/v1/tx'; import { ClobPair } from '@dydxprotocol/v4-proto/src/codegen/dydxprotocol/clob/clob_pair'; import { MarketPrice } from '@dydxprotocol/v4-proto/src/codegen/dydxprotocol/prices/market_price'; import Ajv from 'ajv'; @@ -32,6 +26,8 @@ import { readFileSync } from 'fs'; import Long from 'long'; import { PrometheusDriver } from 'prometheus-query'; +import { Exchange, ExchangeName, Proposal, retry, sleep, voteOnProposals } from './help'; + const LocalWalletModule = await import( '@dydxprotocol/v4-client-js/src/clients/modules/local-wallet' ); @@ -72,53 +68,6 @@ const MNEMONICS = [ 'switch boring kiss cash lizard coconut romance hurry sniff bus accident zone chest height merit elevator furnace eagle fetch quit toward steak mystery nest', ]; -interface Exchange { - exchangeName: ExchangeName; - ticker: string; - adjustByMarket?: string; -} - -interface Params { - id: number; - ticker: string; - marketType: 'PERPETUAL_MARKET_TYPE_ISOLATED' | 'PERPETUAL_MARKET_TYPE_CROSS'; - priceExponent: number; - minPriceChange: number; - minExchanges: number; - exchangeConfigJson: Exchange[]; - liquidityTier: number; - atomicResolution: number; - quantumConversionExponent: number; - defaultFundingPpm: number; - stepBaseQuantums: number; - subticksPerTick: number; - delayBlocks: number; -} - -interface Proposal { - id: Long.Long; - title: string; - summary: string; - params: Params; -} - -enum ExchangeName { - Binance = 'Binance', - BinanceUS = 'BinanceUS', - Bitfinex = 'Bitfinex', - Bitstamp = 'Bitstamp', - Bybit = 'Bybit', - CoinbasePro = 'CoinbasePro', - CryptoCom = 'CryptoCom', - Gate = 'Gate', - Huobi = 'Huobi', - Kraken = 'Kraken', - Kucoin = 'Kucoin', - Mexc = 'Mexc', - Okx = 'Okx', - Raydium = 'Raydium', -} - interface PrometheusTimeSeries { // value of the time serie value: number; @@ -351,47 +300,6 @@ async function validateExchangeConfigJson(exchangeConfigJson: Exchange[]): Promi } } -// Vote YES on all `proposalIds` from `wallet`. -async function voteOnProposals( - proposalIds: number[], - client: CompositeClient, - wallet: LocalWalletType -): Promise { - // Construct Tx. - const encodedVotes: EncodeObject[] = proposalIds.map((proposalId) => { - return { - typeUrl: '/cosmos.gov.v1.MsgVote', - value: { - proposalId: Long.fromNumber(proposalId), - voter: wallet.address!, - option: VoteOption.VOTE_OPTION_YES, - metadata: '', - } as MsgVote, - } as EncodeObject; - }); - const account: Account = await client.validatorClient.get.getAccount(wallet.address!); - const signedTx = await wallet.signTransaction( - encodedVotes, - { - sequence: account.sequence, - accountNumber: account.accountNumber, - chainId: client.network.validatorConfig.chainId, - } as TransactionOptions, - VOTE_FEE - ); - - // Broadcast Tx. - const resp = await client.validatorClient.get.tendermintClient.broadcastTransaction( - signedTx, - Method.BroadcastTxSync - ); - if ((resp as BroadcastTxSyncResponse).code) { - throw new Error(`Failed to vote on proposals ${proposalIds}`); - } else { - console.log(`Voted on proposals ${proposalIds} with wallet ${wallet.address}`); - } -} - async function validateAgainstLocalnet(proposals: Proposal[]): Promise { // Initialize wallets. const network = Network.local(); @@ -730,29 +638,6 @@ function validateParamsSchema(proposal: Proposal): void { } } -async function sleep(ms: number): Promise { - return new Promise((resolve) => { - setTimeout(resolve, ms); - }); -} - -async function retry( - fn: () => Promise, - retries: number = 5, - delay: number = 2000 -): Promise { - try { - return await fn(); - } catch (error) { - console.error(`Function ${fn.name} failed: ${error}. Retrying in ${delay}ms...`); - if (retries <= 0) { - throw error; - } - await sleep(delay); - return retry(fn, retries - 1, delay); - } -} - // getProposalsToValidate finds proposals that are either added or whose params are modified, // i.e. ignoring initialDeposit, meta, summary, title, etc. function getProposalsToValidate(newProposals: Record): Set { diff --git a/src/hooks/Orderbook/useDrawOrderbook.ts b/src/hooks/Orderbook/useDrawOrderbook.ts index f331506f5..e7bf805b7 100644 --- a/src/hooks/Orderbook/useDrawOrderbook.ts +++ b/src/hooks/Orderbook/useDrawOrderbook.ts @@ -48,6 +48,8 @@ enum OrderbookRowAnimationType { NONE, } +const GRADIENT_MULTIPLIER = 1.3; + export type Rekt = { x1: number; x2: number; y1: number; y2: number }; export const useDrawOrderbook = ({ @@ -103,18 +105,16 @@ export const useDrawOrderbook = ({ }, [canvas]); const drawBars = ({ - barType, ctx, - depthOrSizeValue, - gradientMultiplier, + value, + gradientMultiplier = GRADIENT_MULTIPLIER, histogramAccentColor, histogramSide: inHistogramSide, rekt, }: { - barType: 'depth' | 'size'; ctx: CanvasRenderingContext2D; - depthOrSizeValue: number; - gradientMultiplier: number; + value: number; + gradientMultiplier?: number; histogramAccentColor: string; histogramSide: 'left' | 'right'; rekt: Rekt; @@ -122,9 +122,9 @@ export const useDrawOrderbook = ({ const { x1, x2, y1, y2 } = rekt; // X values - const maxHistogramBarWidth = x2 - x1 - (barType === 'size' ? 8 : 2); - const barWidth = depthOrSizeValue - ? Math.min((depthOrSizeValue / histogramRange) * maxHistogramBarWidth, maxHistogramBarWidth) + const maxHistogramBarWidth = x2 - x1 - 2; + const barWidth = value + ? Math.min((value / histogramRange) * maxHistogramBarWidth, maxHistogramBarWidth) : 0; const { gradient, bar } = getHistogramXValues({ @@ -294,27 +294,14 @@ export const useDrawOrderbook = ({ // Depth Bar if (depth) { drawBars({ - barType: 'depth', ctx, - depthOrSizeValue: depth, - gradientMultiplier: 1.3, + value: depth, histogramAccentColor, histogramSide, rekt, }); } - // Size Bar - drawBars({ - barType: 'size', - ctx, - depthOrSizeValue: size, - gradientMultiplier: 5, - histogramAccentColor, - histogramSide, - rekt, - }); - if (mine && mine > 0) { drawMineCircle({ ctx, rekt }); } diff --git a/src/pages/portfolio/Fees.tsx b/src/pages/portfolio/Fees.tsx index 1cce1db44..976ff1930 100644 --- a/src/pages/portfolio/Fees.tsx +++ b/src/pages/portfolio/Fees.tsx @@ -28,6 +28,8 @@ import { getFeeTiers } from '@/state/configsSelectors'; import { isTruthy } from '@/lib/isTruthy'; +const MARKET_SHARE_PERCENTAGE_FRACTION_DIGITS = 1; + const EQUALITY_SYMBOL_MAP = { '>=': '≥', '<=': '≤', @@ -62,7 +64,11 @@ export const Fees = () => { {isAdditional && stringGetter({ key: STRING_KEYS.AND })}{' '} {stringGetter({ key: STRING_KEYS.EXCHANGE_MARKET_SHARE })}{' '} <$Highlighted>{'>'}{' '} - <$HighlightOutput type={OutputType.Percent} value={totalShare} fractionDigits={0} /> + <$HighlightOutput + type={OutputType.Percent} + value={totalShare} + fractionDigits={MARKET_SHARE_PERCENTAGE_FRACTION_DIGITS} + /> )} {!!makerShare && ( @@ -70,7 +76,11 @@ export const Fees = () => { {isAdditional && stringGetter({ key: STRING_KEYS.AND })}{' '} {stringGetter({ key: STRING_KEYS.MAKER_MARKET_SHARE })}{' '} <$Highlighted>{'>'}{' '} - <$HighlightOutput type={OutputType.Percent} value={makerShare} fractionDigits={0} /> + <$HighlightOutput + type={OutputType.Percent} + value={makerShare} + fractionDigits={MARKET_SHARE_PERCENTAGE_FRACTION_DIGITS} + /> )}