# -*- coding: utf-8 -*-

# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN:
# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code

from ccxt.async_support.base.exchange import Exchange
from ccxt.abstract.dydx import ImplicitAPI
import math
from ccxt.base.types import Account, Any, Balances, Currency, Int, LedgerEntry, Market, Num, Order, OrderBook, OrderSide, OrderType, Position, Str, Strings, Trade, Transaction, TransferEntry
from typing import List
from ccxt.base.errors import ExchangeError
from ccxt.base.errors import ArgumentsRequired
from ccxt.base.errors import BadRequest
from ccxt.base.errors import InsufficientFunds
from ccxt.base.errors import InvalidOrder
from ccxt.base.errors import NotSupported
from ccxt.base.decimal_to_precision import TICK_SIZE
from ccxt.base.precise import Precise


class dydx(Exchange, ImplicitAPI):

    def describe(self) -> Any:
        return self.deep_extend(super(dydx, self).describe(), {
            'id': 'dydx',
            'name': 'dYdX',
            'countries': ['US'],
            'rateLimit': 100,
            'version': 'v4',
            'certified': False,
            'dex': True,
            'pro': True,
            'has': {
                'CORS': None,
                'spot': False,
                'margin': False,
                'swap': True,
                'future': False,
                'option': False,
                'addMargin': False,
                'cancelAllOrders': False,
                'cancelAllOrdersAfter': False,
                'cancelOrder': True,
                'cancelOrders': True,
                'cancelWithdraw': False,
                'closeAllPositions': False,
                'closePosition': False,
                'createConvertTrade': False,
                'createDepositAddress': False,
                'createMarketBuyOrderWithCost': False,
                'createMarketOrder': False,
                'createMarketOrderWithCost': False,
                'createMarketSellOrderWithCost': False,
                'createOrder': True,
                'createOrderWithTakeProfitAndStopLoss': False,
                'createReduceOnlyOrder': False,
                'createStopLimitOrder': False,
                'createStopLossOrder': False,
                'createStopMarketOrder': False,
                'createStopOrder': False,
                'createTakeProfitOrder': False,
                'createTrailingAmountOrder': False,
                'createTrailingPercentOrder': False,
                'createTriggerOrder': False,
                'fetchAccounts': True,
                'fetchBalance': True,
                'fetchCanceledOrders': False,
                'fetchClosedOrder': False,
                'fetchClosedOrders': True,
                'fetchConvertCurrencies': False,
                'fetchConvertQuote': False,
                'fetchConvertTrade': False,
                'fetchConvertTradeHistory': False,
                'fetchCurrencies': False,
                'fetchDepositAddress': False,
                'fetchDepositAddresses': False,
                'fetchDepositAddressesByNetwork': False,
                'fetchDeposits': True,
                'fetchDepositsWithdrawals': True,
                'fetchFundingHistory': False,
                'fetchFundingInterval': False,
                'fetchFundingIntervals': False,
                'fetchFundingRate': False,
                'fetchFundingRateHistory': True,
                'fetchFundingRates': False,
                'fetchIndexOHLCV': False,
                'fetchLedger': True,
                'fetchLeverage': False,
                'fetchMarginAdjustmentHistory': False,
                'fetchMarginMode': False,
                'fetchMarkets': True,
                'fetchMarkOHLCV': False,
                'fetchMyTrades': False,
                'fetchOHLCV': True,
                'fetchOpenInterestHistory': False,
                'fetchOpenOrder': False,
                'fetchOpenOrders': True,
                'fetchOrder': True,
                'fetchOrderBook': True,
                'fetchOrders': True,
                'fetchOrderTrades': False,
                'fetchPosition': True,
                'fetchPositionHistory': False,
                'fetchPositionMode': False,
                'fetchPositions': True,
                'fetchPositionsHistory': False,
                'fetchPremiumIndexOHLCV': False,
                'fetchStatus': False,
                'fetchTicker': False,
                'fetchTickers': False,
                'fetchTime': True,
                'fetchTrades': True,
                'fetchTradingFee': False,
                'fetchTradingFees': False,
                'fetchTransactions': False,
                'fetchTransfers': True,
                'fetchWithdrawals': True,
                'reduceMargin': False,
                'sandbox': False,
                'setLeverage': False,
                'setMargin': False,
                'setPositionMode': False,
                'transfer': True,
                'withdraw': True,
            },
            'timeframes': {
                '1m': '1MIN',
                '5m': '5MINS',
                '15m': '15MINS',
                '30m': '30MINS',
                '1h': '1HOUR',
                '4h': '4HOURS',
                '1d': '1DAY',
            },
            'urls': {
                'logo': 'https://github.com/user-attachments/assets/617ea0c1-f05a-4d26-9fcb-a0d1d4091ae1',
                'api': {
                    'indexer': 'https://indexer.dydx.trade/v4',
                    'nodeRpc': 'https://dydx-ops-rpc.kingnodes.com',
                    'nodeRest': 'https://dydx-rest.publicnode.com',
                },
                'test': {
                    'indexer': 'https://indexer.v4testnet.dydx.exchange/v4',
                    'nodeRpc': 'https://test-dydx-rpc.kingnodes.com',
                    'nodeRest': 'https://test-dydx-rest.kingnodes.com',
                },
                'www': 'https://www.dydx.xyz',
                'doc': [
                    'https://docs.dydx.xyz',
                ],
                'fees': [
                    'https://docs.dydx.exchange/introduction-trading_fees',
                ],
                'referral': 'dydx.trade?ref=ccxt',
            },
            'api': {
                'indexer': {
                    'get': {
                        'addresses/{address}': 1,
                        'addresses/{address}/parentSubaccountNumber/{number}': 1,
                        'addresses/{address}/subaccountNumber/{subaccountNumber}': 1,
                        'assetPositions': 1,
                        'assetPositions/parentSubaccountNumber': 1,
                        'candles/perpetualMarkets/{market}': 1,
                        'compliance/screen/{address}': 1,
                        'fills': 1,
                        'fills/parentSubaccountNumber': 1,
                        'fundingPayments': 1,
                        'fundingPayments/parentSubaccount': 1,
                        'height': 0.1,
                        'historical-pnl': 1,
                        'historical-pnl/parentSubaccountNumber': 1,
                        'historicalBlockTradingRewards/{address}': 1,
                        'historicalFunding/{market}': 1,
                        'historicalTradingRewardAggregations/{address}': 1,
                        'orderbooks/perpetualMarket/{market}': 1,
                        'orders': 1,
                        'orders/parentSubaccountNumber': 1,
                        'orders/{orderId}': 1,
                        'perpetualMarkets': 1,
                        'perpetualPositions': 1,
                        'perpetualPositions/parentSubaccountNumber': 1,
                        'screen': 1,
                        'sparklines': 1,
                        'time': 1,
                        'trades/perpetualMarket/{market}': 1,
                        'transfers': 1,
                        'transfers/between': 1,
                        'transfers/parentSubaccountNumber': 1,
                        'vault/v1/megavault/historicalPnl': 1,
                        'vault/v1/megavault/positions': 1,
                        'vault/v1/vaults/historicalPnl': 1,
                        #
                        'perpetualMarketSparklines': 1,
                        'perpetualMarkets/{ticker}': 1,
                        'perpetualMarkets/{ticker}/orderbook': 1,
                        'trades/perpetualMarket/{ticker}': 1,
                        'historicalFunding/{ticker}': 1,
                        'candles/{ticker}/{resolution}': 1,
                        'addresses/{address}/subaccounts': 1,
                        'addresses/{address}/subaccountNumber/{subaccountNumber}/assetPositions': 1,
                        'addresses/{address}/subaccountNumber/{subaccountNumber}/perpetualPositions': 1,
                        'addresses/{address}/subaccountNumber/{subaccountNumber}/orders': 1,
                        'fills/parentSubaccount': 1,
                        'historical-pnl/parentSubaccount': 1,
                    },
                },
                'nodeRpc': {
                    'get': {
                        'abci_info': 1,
                        'block': 1,
                        'broadcast_tx_async': 1,
                        'broadcast_tx_sync': 1,
                        'tx': 1,
                    },
                },
                'nodeRest': {
                    'get': {
                        'cosmos/auth/v1beta1/account_info/{dydxAddress}': 1,
                    },
                    'post': {
                        'cosmos/tx/v1beta1/encode': 1,
                        'cosmos/tx/v1beta1/simulate': 1,
                    },
                },
            },
            'fees': {
                'trading': {
                    'tierBased': True,
                    'percentage': True,
                    'maker': self.parse_number('0.0001'),
                    'taker': self.parse_number('0.0005'),
                },
            },
            'requiredCredentials': {
                'apiKey': False,
                'secret': False,
                'privateKey': False,
            },
            'options': {
                'mnemonic': None,  # specify mnemonic, copy secret phrase from UI
                'chainName': 'dydx-mainnet-1',
                'chainId': 1,
                'sandboxMode': False,
                'defaultFeeDenom': 'uusdc',
                'defaultFeeMultiplier': '1.6',
                'feeDenom': {
                    'USDC_DENOM': 'ibc/8E27BA2D5493AF5636760E354E46004562C46AB7EC0CC4C1CA14E9E20E2545B5',
                    'USDC_GAS_DENOM': 'uusdc',
                    'USDC_DECIMALS': 6,
                    'USDC_GAS_PRICE': '0.025',
                    'CHAINTOKEN_DENOM': 'adydx',
                    'CHAINTOKEN_DECIMALS': 18,
                    'CHAINTOKEN_GAS_PRICE': '25000000000',
                },
            },
            'features': {
                'default': {
                    'sandbox': True,
                    'createOrder': {
                        'marginMode': False,
                        'triggerPrice': True,
                        'triggerPriceType': {
                            'last': True,
                            'mark': True,
                            'index': False,
                        },
                        'triggerDirection': False,
                        'stopLossPrice': False,  # todo by triggerPrice
                        'takeProfitPrice': False,  # todo by triggerPrice
                        'attachedStopLossTakeProfit': None,
                        'timeInForce': {
                            'IOC': True,
                            'FOK': True,
                            'PO': True,
                            'GTD': True,
                        },
                        'hedged': False,
                        'trailing': False,
                        'leverage': False,
                        'marketBuyByCost': False,
                        'marketBuyRequiresPrice': False,
                        'selfTradePrevention': False,
                        'iceberg': False,
                    },
                    'createOrders': None,
                    'fetchMyTrades': {
                        'marginMode': False,
                        'limit': 500,
                        'daysBack': 90,
                        'untilDays': 10000,
                        'symbolRequired': False,
                    },
                    'fetchOrder': {
                        'marginMode': False,
                        'trigger': True,
                        'trailing': False,
                        'symbolRequired': False,
                    },
                    'fetchOpenOrders': {
                        'marginMode': False,
                        'limit': 500,
                        'trigger': True,
                        'trailing': True,
                        'symbolRequired': False,
                    },
                    'fetchOrders': {
                        'marginMode': False,
                        'limit': 500,
                        'daysBack': None,
                        'untilDays': 100000,
                        'trigger': True,
                        'trailing': True,
                        'symbolRequired': False,
                    },
                    'fetchClosedOrders': {
                        'marginMode': False,
                        'limit': 500,
                        'daysBack': None,
                        'daysBackCanceled': None,
                        'untilDays': 100000,
                        'trigger': True,
                        'trailing': True,
                        'symbolRequired': False,
                    },
                    'fetchOHLCV': {
                        'limit': 1000,
                    },
                },
                'forSwap': {
                    'extends': 'default',
                    'createOrder': {
                        'hedged': True,
                    },
                },
                'swap': {
                    'linear': {
                        'extends': 'forSwap',
                    },
                    'inverse': None,
                },
                'future': {
                    'linear': None,
                    'inverse': None,
                },
            },
            'commonCurrencies': {},
            'exceptions': {
                'exact': {
                    # error collision for clob and sending modules from 2 - 8
                    # https://github.com/dydxprotocol/v4-chain/blob/5f9f6c9b95cc87d732e23de764909703b81a6e8b/protocol/x/clob/types/errors.go#L320
                    # https://github.com/dydxprotocol/v4-chain/blob/5f9f6c9b95cc87d732e23de764909703b81a6e8b/protocol/x/sending/types/errors.go
                    '9': InvalidOrder,  # A cancel already exists in the memclob for self order with a greater than or equal GoodTilBlock
                    '10': InvalidOrder,  # The next block height is greater than the GoodTilBlock of the message
                    '11': InvalidOrder,  # The GoodTilBlock of the message is further than ShortBlockWindow blocks into the future
                    '12': InvalidOrder,  # MsgPlaceOrder is invalid
                    '13': InvalidOrder,  # MsgProposedMatchOrders is invalid
                    '14': InvalidOrder,  # State filled amount cannot be unchanged
                    '15': InvalidOrder,  # State filled amount cannot decrease
                    '16': InvalidOrder,  # Cannot prune state fill amount that does not exist
                    '17': InvalidOrder,  # Subaccount cannot open more than 20 orders on a given CLOB and side
                    '18': InvalidOrder,  # `FillAmount` is not divisible by `StepBaseQuantums` of the specified `ClobPairId`
                    '19': InvalidOrder,  # The provided perpetual ID does not have any associated CLOB pairs
                    '20': InvalidOrder,  # Replacing an existing order failed
                    '21': InvalidOrder,  # Clob pair and perpetual ids do not match
                    '22': InvalidOrder,  # Matched order has negative fee
                    '23': InvalidOrder,  # Subaccounts updated for a matched order, but fee transfer to fee-collector failed
                    '24': InvalidOrder,  # Order is fully filled
                    '25': InvalidOrder,  # Attempting to get price premium with a non-perpetual CLOB pair
                    '26': InvalidOrder,  # Index price is zero when calculating price premium
                    '27': InvalidOrder,  # Invalid ClobPair parameter
                    '28': InvalidOrder,  # Oracle price must be > 0.
                    '29': InvalidOrder,  # Invalid stateful order cancellation
                    '30': InvalidOrder,  # An order with the same `OrderId` and `OrderHash` has already been processed for self CLOB
                    '31': InvalidOrder,  # Missing mid price for ClobPair
                    '32': InvalidOrder,  # Existing stateful order cancellation has higher-or-equal priority than the new one
                    '33': InvalidOrder,  # ClobPair with id already exists
                    '34': InvalidOrder,  # Order conflicts with ClobPair status
                    '35': InvalidOrder,  # Invalid ClobPair status transition
                    '36': InvalidOrder,  # Operation conflicts with ClobPair status
                    '37': InvalidOrder,  # Perpetual does not exist in state
                    '39': InvalidOrder,  # ClobPair update is invalid
                    '40': InvalidOrder,  # Authority is invalid
                    '41': InvalidOrder,  # perpetual ID is already associated with an existing CLOB pair
                    '42': InvalidOrder,  # Unexpected time in force
                    '43': InvalidOrder,  # Order has remaining size
                    '44': InvalidOrder,  # invalid time in force
                    '45': InvalidOrder,  # Invalid batch cancel message
                    '46': InvalidOrder,  # Batch cancel has failed
                    '47': InvalidOrder,  # CLOB has not been initialized
                    '48': InvalidOrder,  # This field has been deprecated
                    '49': InvalidOrder,  # Invalid TWAP order placement
                    '50': InvalidOrder,  # Invalid builder code
                    '1000': BadRequest,  # Proposed LiquidationsConfig is invalid
                    '1001': BadRequest,  # Subaccount has no perpetual positions to liquidate
                    '1002': BadRequest,  # Subaccount is not liquidatable
                    '1003': InvalidOrder,  # Subaccount does not have an open position for perpetual
                    '1004': InvalidOrder,  # Liquidation order has invalid size
                    '1005': InvalidOrder,  # Liquidation order is on the wrong side
                    '1006': InvalidOrder,  # Total fills amount exceeds size of liquidation order
                    '1007': InvalidOrder,  # Liquidation order does not contain any fills
                    '1008': InvalidOrder,  # Subaccount has previously liquidated self perpetual in the current block
                    '1009': InvalidOrder,  # Liquidation order has size smaller than min position notional specified in the liquidation config
                    '1010': InvalidOrder,  # Liquidation order has size greater than max position notional specified in the liquidation config
                    '1011': InvalidOrder,  # Liquidation exceeds the maximum notional amount that a single subaccount can have liquidated per block
                    '1012': InvalidOrder,  # Liquidation exceeds the maximum insurance fund payout amount for a given subaccount per block
                    '1013': InvalidOrder,  # Insurance fund does not have sufficient funds to cover liquidation losses
                    '1014': InvalidOrder,  # Invalid perpetual position size delta
                    '1015': InvalidOrder,  # Invalid delta base and/or quote quantums for insurance fund delta calculation
                    '1017': InvalidOrder,  # Cannot deleverage subaccount against itself
                    '1018': InvalidOrder,  # Deleveraging match cannot have fills with same id
                    '1019': InvalidOrder,  # Deleveraging match cannot have fills with zero amount
                    '1020': InvalidOrder,  # Position cannot be fully offset
                    '1021': InvalidOrder,  # Deleveraging match has incorrect value for isFinalSettlement flag
                    '1022': InvalidOrder,  # Liquidation conflicts with ClobPair status
                    '2000': InvalidOrder,  # FillOrKill order could not be fully filled
                    '2001': InvalidOrder,  # Reduce-only orders cannot increase the position size
                    '2002': InvalidOrder,  # Reduce-only orders cannot change the position side
                    '2003': InvalidOrder,  # Post-only order would cross one or more maker orders
                    '2004': InvalidOrder,  # IOC order is already filled, remaining size is cancelled.
                    '2005': InvalidOrder,  # Order would violate isolated subaccount constraints.
                    '3000': InvalidOrder,  # Invalid order flags
                    '3001': InvalidOrder,  # Invalid order goodTilBlockTime
                    '3002': InvalidOrder,  # Stateful orders cannot require immediate execution
                    '3003': InvalidOrder,  # The block time is greater than the GoodTilBlockTime of the message
                    '3004': InvalidOrder,  # The GoodTilBlockTime of the message is further than StatefulOrderTimeWindow into the future
                    '3005': InvalidOrder,  # Existing stateful order has higher-or-equal priority than the new one
                    '3006': InvalidOrder,  # Stateful order does not exist
                    '3007': InvalidOrder,  # Stateful order collateralization check failed
                    '3008': InvalidOrder,  # Stateful order was previously cancelled and therefore cannot be placed
                    '3009': InvalidOrder,  # Stateful order was previously removed and therefore cannot be placed
                    '3010': InvalidOrder,  # Stateful order cancellation failed because the order was already removed from state
                    '4000': InvalidOrder,  # MsgProposedOperations is invalid
                    '4001': InvalidOrder,  # Match Order is invalid
                    '4002': InvalidOrder,  # Order was not previously placed in operations queue
                    '4003': InvalidOrder,  # Fill amount cannot be zero
                    '4004': InvalidOrder,  # Deleveraging fill is invalid
                    '4005': InvalidOrder,  # Deleveraged subaccount in proposed deleveraged operation failed deleveraging validation
                    '4006': InvalidOrder,  # Order Removal is invalid
                    '4007': InvalidOrder,  # Order Removal reason is invalid
                    '4008': InvalidOrder,  # Zero-fill deleveraging operation included in block for non-negative TNC subaccount
                    '5000': InvalidOrder,  # Proposed BlockRateLimitConfig is invalid
                    '5001': InvalidOrder,  # Block rate limit exceeded
                    '6000': InvalidOrder,  # Conditional type is invalid
                    '6001': InvalidOrder,  # Conditional order trigger subticks is invalid
                    '6002': InvalidOrder,  # Conditional order is untriggered
                    '9000': InvalidOrder,  # Asset orders are not implemented
                    '9001': InvalidOrder,  # Updates for assets other than USDC are not implemented
                    '9002': InvalidOrder,  # This function is not implemented
                    '9003': InvalidOrder,  # Reduce-only is currently disabled for non-IOC orders
                    '10000': InvalidOrder,  # Proposed EquityTierLimitConfig is invalid
                    '10001': InvalidOrder,  # Subaccount cannot open more orders due to equity tier limit.
                    '11000': InvalidOrder,  # Invalid order router address
                },
                'broad': {
                    'insufficient funds': InsufficientFunds,
                },
            },
            'precisionMode': TICK_SIZE,
        })

    async def fetch_time(self, params={}) -> Int:
        """
        fetches the current integer timestamp in milliseconds from the exchange server

        https://docs.dydx.xyz/indexer-client/http#get-time

        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns int: the current integer timestamp in milliseconds from the exchange server
        """
        response = await self.indexerGetTime(params)
        #
        # {
        #     "iso": "2025-07-20T15:12:13.466Z",
        #     "epoch": 1753024333.466
        # }
        #
        return self.safe_integer(response, 'epoch')

    def parse_market(self, market: dict) -> Market:
        #
        # {
        #     "clobPairId": "0",
        #     "ticker": "BTC-USD",
        #     "status": "ACTIVE",
        #     "oraclePrice": "118976.5376",
        #     "priceChange24H": "659.9736",
        #     "volume24H": "1292729.3605",
        #     "trades24H": 9387,
        #     "nextFundingRate": "0",
        #     "initialMarginFraction": "0.02",
        #     "maintenanceMarginFraction": "0.012",
        #     "openInterest": "52.0691",
        #     "atomicResolution": -10,
        #     "quantumConversionExponent": -9,
        #     "tickSize": "1",
        #     "stepSize": "0.0001",
        #     "stepBaseQuantums": 1000000,
        #     "subticksPerTick": 100000,
        #     "marketType": "CROSS",
        #     "openInterestLowerCap": "0",
        #     "openInterestUpperCap": "0",
        #     "baseOpenInterest": "50.3776",
        #     "defaultFundingRate1H": "0"
        # }
        #
        quoteId = 'USDC'
        marketId = self.safe_string(market, 'ticker')
        parts = marketId.split('-')
        baseName = self.safe_string(parts, 0)
        baseId = self.safe_string(market, 'baseId', baseName)  # idk where 'baseId' comes from, but leaving
        base = self.safe_currency_code(baseId)
        quote = self.safe_currency_code(quoteId)
        settleId = 'USDC'
        settle = self.safe_currency_code(settleId)
        symbol = base + '/' + quote + ':' + settle
        contract = True
        swap = True
        amountPrecisionStr = self.safe_string(market, 'stepSize')
        pricePrecisionStr = self.safe_string(market, 'tickSize')
        status = self.safe_string(market, 'status')
        active = True
        if status != 'ACTIVE':
            active = False
        return self.safe_market_structure({
            'id': self.safe_string(market, 'ticker'),
            'symbol': symbol,
            'base': base,
            'quote': quote,
            'settle': settle,
            'baseId': baseId,
            'baseName': baseName,
            'quoteId': quoteId,
            'settleId': settleId,
            'type': 'swap',
            'spot': False,
            'margin': None,
            'swap': swap,
            'future': False,
            'option': False,
            'active': active,
            'contract': contract,
            'contractSize': self.parse_number('1'),  # trades seem in absolute size
            'linear': True,
            'inverse': False,
            'taker': None,
            'maker': None,
            'expiry': None,
            'expiryDatetime': None,
            'strike': None,
            'optionType': None,
            'precision': {
                'amount': self.parse_number(amountPrecisionStr),
                'price': self.parse_number(pricePrecisionStr),
            },
            'limits': {
                'leverage': {
                    'min': None,
                    'max': None,
                },
                'amount': {
                    'min': None,
                    'max': None,
                },
                'price': {
                    'min': None,
                    'max': None,
                },
                'cost': {
                    'min': None,
                    'max': None,
                },
            },
            'created': None,
            'info': market,
        })

    async def fetch_markets(self, params={}) -> List[Market]:
        """
        retrieves data on all markets for hyperliquid

        https://docs.dydx.xyz/indexer-client/http#get-perpetual-markets

        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict[]: an array of objects representing market data
        """
        request: dict = {
            # 'limit': 1000,
        }
        response = await self.indexerGetPerpetualMarkets(self.extend(request, params))
        #
        # {
        #     "markets": {
        #         "BTC-USD": {
        #             "clobPairId": "0",
        #             "ticker": "BTC-USD",
        #             "status": "ACTIVE",
        #             "oraclePrice": "118976.5376",
        #             "priceChange24H": "659.9736",
        #             "volume24H": "1292729.3605",
        #             "trades24H": 9387,
        #             "nextFundingRate": "0",
        #             "initialMarginFraction": "0.02",
        #             "maintenanceMarginFraction": "0.012",
        #             "openInterest": "52.0691",
        #             "atomicResolution": -10,
        #             "quantumConversionExponent": -9,
        #             "tickSize": "1",
        #             "stepSize": "0.0001",
        #             "stepBaseQuantums": 1000000,
        #             "subticksPerTick": 100000,
        #             "marketType": "CROSS",
        #             "openInterestLowerCap": "0",
        #             "openInterestUpperCap": "0",
        #             "baseOpenInterest": "50.3776",
        #             "defaultFundingRate1H": "0"
        #         }
        #     }
        # }
        #
        data = self.safe_dict(response, 'markets', {})
        markets = list(data.values())
        return self.parse_markets(markets)

    def parse_trade(self, trade: dict, market: Market = None) -> Trade:
        #
        # {
        #     "id": "02ac5b1f0000000200000002",
        #     "side": "BUY",
        #     "size": "0.0501",
        #     "price": "115732",
        #     "type": "LIMIT",
        #     "createdAt": "2025-07-25T05:11:09.800Z",
        #     "createdAtHeight": "44849951"
        # }
        #
        timestamp = self.parse8601(self.safe_string(trade, 'createdAt'))
        symbol = market['symbol']
        price = self.safe_string(trade, 'price')
        amount = self.safe_string(trade, 'size')
        side = self.safe_string_lower(trade, 'side')
        id = self.safe_string(trade, 'id')
        return self.safe_trade({
            'id': id,
            'timestamp': timestamp,
            'datetime': self.iso8601(timestamp),
            'symbol': symbol,
            'side': side,
            'price': price,
            'amount': amount,
            'cost': None,
            'order': None,
            'takerOrMaker': None,
            'type': None,
            'fee': None,
            'info': trade,
        }, market)

    async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]:
        """
        get the list of most recent trades for a particular symbol

        https://developer.woox.io/api-reference/endpoint/public_data/marketTrades

        :param str symbol: unified symbol of the market to fetch trades for
        :param int [since]: timestamp in ms of the earliest trade to fetch
        :param int [limit]: the maximum amount of trades to fetch
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns Trade[]: a list of `trade structures <https://docs.ccxt.com/?id=public-trades>`
        """
        await self.load_markets()
        market = self.market(symbol)
        request: dict = {
            'market': market['id'],
        }
        if limit is not None:
            request['limit'] = min(limit, 1000)
        response = await self.indexerGetTradesPerpetualMarketMarket(self.extend(request, params))
        #
        # {
        #     "trades": [
        #         {
        #             "id": "02ac5b1f0000000200000002",
        #             "side": "BUY",
        #             "size": "0.0501",
        #             "price": "115732",
        #             "type": "LIMIT",
        #             "createdAt": "2025-07-25T05:11:09.800Z",
        #             "createdAtHeight": "44849951"
        #         }
        #     ]
        # }
        #
        rows = self.safe_list(response, 'trades', [])
        return self.parse_trades(rows, market, since, limit)

    def parse_ohlcv(self, ohlcv, market: Market = None) -> list:
        #
        # {
        #     "startedAt": "2025-07-25T09:47:00.000Z",
        #     "ticker": "BTC-USD",
        #     "resolution": "1MIN",
        #     "low": "116099",
        #     "high": "116099",
        #     "open": "116099",
        #     "close": "116099",
        #     "baseTokenVolume": "0",
        #     "usdVolume": "0",
        #     "trades": 0,
        #     "startingOpenInterest": "54.0594",
        #     "orderbookMidPriceOpen": "115845.5",
        #     "orderbookMidPriceClose": "115845.5"
        # }
        #
        return [
            self.parse8601(self.safe_string(ohlcv, 'startedAt')),
            self.safe_number(ohlcv, 'open'),
            self.safe_number(ohlcv, 'high'),
            self.safe_number(ohlcv, 'low'),
            self.safe_number(ohlcv, 'close'),
            self.safe_number(ohlcv, 'baseTokenVolume'),
        ]

    async def fetch_ohlcv(self, symbol: str, timeframe='1m', since: Int = None, limit: Int = None, params={}) -> List[list]:
        """

        https://docs.dydx.xyz/indexer-client/http#get-candles

        fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market
        :param str symbol: unified symbol of the market to fetch OHLCV data for
        :param str timeframe: the length of time each candle represents
        :param int [since]: timestamp in ms of the earliest candle to fetch
        :param int [limit]: the maximum amount of candles to fetch
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :param int [params.until]: the latest time in ms to fetch entries for
        :returns int[][]: A list of candles ordered, open, high, low, close, volume
        """
        await self.load_markets()
        market = self.market(symbol)
        request: dict = {
            'market': market['id'],
            'resolution': self.safe_string(self.timeframes, timeframe, timeframe),
        }
        if limit is not None:
            request['limit'] = min(limit, 1000)
        if since is not None:
            request['fromIso'] = self.iso8601(since)
        until = self.safe_integer(params, 'until')
        params = self.omit(params, 'until')
        if until is not None:
            request['toIso'] = self.iso8601(until)
        response = await self.indexerGetCandlesPerpetualMarketsMarket(self.extend(request, params))
        #
        # {
        #     "candles": [
        #         {
        #             "startedAt": "2025-07-25T09:47:00.000Z",
        #             "ticker": "BTC-USD",
        #             "resolution": "1MIN",
        #             "low": "116099",
        #             "high": "116099",
        #             "open": "116099",
        #             "close": "116099",
        #             "baseTokenVolume": "0",
        #             "usdVolume": "0",
        #             "trades": 0,
        #             "startingOpenInterest": "54.0594",
        #             "orderbookMidPriceOpen": "115845.5",
        #             "orderbookMidPriceClose": "115845.5"
        #         }
        #     ]
        # }
        #
        rows = self.safe_list(response, 'candles', [])
        return self.parse_ohlcvs(rows, market, timeframe, since, limit)

    async def fetch_funding_rate_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}):
        """
        fetches historical funding rate prices

        https://docs.dydx.xyz/indexer-client/http#get-historical-funding

        :param str symbol: unified symbol of the market to fetch the funding rate history for
        :param int [since]: timestamp in ms of the earliest funding rate to fetch
        :param int [limit]: the maximum amount of `funding rate structures <https://docs.ccxt.com/?id=funding-rate-history-structure>` to fetch
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :param int [params.until]: timestamp in ms of the latest funding rate
        :returns dict[]: a list of `funding rate structures <https://docs.ccxt.com/?id=funding-rate-history-structure>`
        """
        if symbol is None:
            raise ArgumentsRequired(self.id + ' fetchFundingRateHistory() requires a symbol argument')
        await self.load_markets()
        market = self.market(symbol)
        request: dict = {
            'market': market['id'],
        }
        if limit is not None:
            request['limit'] = limit
        until = self.safe_integer(params, 'until')
        if until is not None:
            request['effectiveBeforeOrAt'] = self.iso8601(until)
        response = await self.indexerGetHistoricalFundingMarket(self.extend(request, params))
        #
        # {
        #     "historicalFunding": [
        #         {
        #             "ticker": "BTC-USD",
        #             "rate": "0",
        #             "price": "116302.62419",
        #             "effectiveAtHeight": "44865196",
        #             "effectiveAt": "2025-07-25T11:00:00.013Z"
        #         }
        #     ]
        # }
        #
        rates = []
        rows = self.safe_list(response, 'historicalFunding', [])
        for i in range(0, len(rows)):
            entry = rows[i]
            timestamp = self.parse8601(self.safe_string(entry, 'effectiveAt'))
            marketId = self.safe_string(entry, 'ticker')
            rates.append({
                'info': entry,
                'symbol': self.safe_symbol(marketId, market),
                'fundingRate': self.safe_number(entry, 'rate'),
                'timestamp': timestamp,
                'datetime': self.iso8601(timestamp),
            })
        sorted = self.sort_by(rates, 'timestamp')
        return self.filter_by_symbol_since_limit(sorted, symbol, since, limit)

    def handle_public_address(self, methodName: str, params: dict):
        userAux = None
        userAux, params = self.handle_option_and_params(params, methodName, 'user')
        user = userAux
        user, params = self.handle_option_and_params(params, methodName, 'address', userAux)
        if (user is not None) and (user != ''):
            return [user, params]
        if (self.walletAddress is not None) and (self.walletAddress != ''):
            return [self.walletAddress, params]
        raise ArgumentsRequired(self.id + ' ' + methodName + '() requires a user parameter inside \'params\' or the walletAddress set')

    def parse_order(self, order: dict, market: Market = None) -> Order:
        #
        # {
        #     "id": "dad46410-3444-5566-a129-19a619300fb7",
        #     "subaccountId": "8586bcf6-1f58-5ec9-a0bc-e53db273e7b0",
        #     "clientId": "716238006",
        #     "clobPairId": "0",
        #     "side": "BUY",
        #     "size": "0.001",
        #     "totalFilled": "0.001",
        #     "price": "400000",
        #     "type": "LIMIT",
        #     "status": "FILLED",
        #     "timeInForce": "GTT",
        #     "reduceOnly": False,
        #     "orderFlags": "64",
        #     "goodTilBlockTime": "2025-07-28T12:07:33.000Z",
        #     "createdAtHeight": "45058325",
        #     "clientMetadata": "2",
        #     "updatedAt": "2025-07-28T12:06:35.330Z",
        #     "updatedAtHeight": "45058326",
        #     "postOnly": False,
        #     "ticker": "BTC-USD",
        #     "subaccountNumber": 0
        # }
        #
        status = self.parse_order_status(self.safe_string_upper(order, 'status'))
        marketId = self.safe_string(order, 'ticker')
        symbol = self.safe_symbol(marketId, market)
        filled = self.safe_string(order, 'totalFilled')
        timestamp = self.parse8601(self.safe_string(order, 'updatedAt'))
        price = self.safe_string(order, 'price')
        amount = self.safe_string(order, 'size')
        type = self.parse_order_type(self.safe_string_upper(order, 'type'))
        side = self.safe_string_lower(order, 'side')
        timeInForce = self.safe_string_upper(order, 'timeInForce')
        return self.safe_order({
            'info': order,
            'id': self.safe_string(order, 'id'),
            'clientOrderId': self.safe_string(order, 'clientId'),
            'timestamp': timestamp,
            'datetime': self.iso8601(timestamp),
            'lastTradeTimestamp': None,
            'lastUpdateTimestamp': timestamp,
            'symbol': symbol,
            'type': type,
            'timeInForce': timeInForce,
            'postOnly': self.safe_bool(order, 'postOnly'),
            'reduceOnly': self.safe_bool(order, 'reduceOnly'),
            'side': side,
            'price': price,
            'triggerPrice': None,
            'amount': amount,
            'cost': None,
            'average': None,
            'filled': filled,
            'remaining': None,
            'status': status,
            'fee': None,
            'trades': None,
        }, market)

    def parse_order_status(self, status: Str):
        statuses: dict = {
            'UNTRIGGERED': 'open',
            'OPEN': 'open',
            'FILLED': 'closed',
            'CANCELED': 'canceled',
            'BEST_EFFORT_CANCELED': 'canceling',
        }
        return self.safe_string(statuses, status, status)

    def parse_order_type(self, type: Str):
        types: dict = {
            'LIMIT': 'LIMIT',
            'STOP_LIMIT': 'LIMIT',
            'TAKE_PROFIT_LIMIT': 'LIMIT',
            'MARKET': 'MARKET',
            'STOP_MARKET': 'MARKET',
            'TAKE_PROFIT_MARKET': 'MARKET',
            'TRAILING_STOP': 'MARKET',
        }
        return self.safe_string_upper(types, type, type)

    async def fetch_order(self, id: str, symbol: Str = None, params={}):
        """
        fetches information on an order made by the user

        https://docs.dydx.xyz/indexer-client/http#get-order

        :param str id: the order id
        :param str symbol: unified symbol of the market the order was made in
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict: An `order structure <https://docs.ccxt.com/?id=order-structure>`
        """
        await self.load_markets()
        request: dict = {
            'orderId': id,
        }
        order = await self.indexerGetOrdersOrderId(self.extend(request, params))
        return self.parse_order(order)

    async def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]:
        """
        fetches information on multiple orders made by the user

        https://docs.dydx.xyz/indexer-client/http#list-orders

        :param str symbol: unified market symbol of the market orders were made in
        :param int [since]: the earliest time in ms to fetch orders for
        :param int [limit]: the maximum number of order structures to retrieve
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :param str [params.address]: wallet address that made trades
        :param str [params.subAccountNumber]: sub account number
        :returns Order[]: a list of `order structures <https://docs.ccxt.com/?id=order-structure>`
        """
        userAddress = None
        subAccountNumber = None
        userAddress, params = self.handle_public_address('fetchOrders', params)
        subAccountNumber, params = self.handle_option_and_params(params, 'fetchOrders', 'subAccountNumber', '0')
        await self.load_markets()
        request: dict = {
            'address': userAddress,
            'subaccountNumber': subAccountNumber,
        }
        market = None
        if symbol is not None:
            market = self.market(symbol)
            request['ticker'] = market['id']
        if limit is not None:
            request['limit'] = limit
        response = await self.indexerGetOrders(self.extend(request, params))
        #
        # [
        #     {
        #         "id": "dad46410-3444-5566-a129-19a619300fb7",
        #         "subaccountId": "8586bcf6-1f58-5ec9-a0bc-e53db273e7b0",
        #         "clientId": "716238006",
        #         "clobPairId": "0",
        #         "side": "BUY",
        #         "size": "0.001",
        #         "totalFilled": "0.001",
        #         "price": "400000",
        #         "type": "LIMIT",
        #         "status": "FILLED",
        #         "timeInForce": "GTT",
        #         "reduceOnly": False,
        #         "orderFlags": "64",
        #         "goodTilBlockTime": "2025-07-28T12:07:33.000Z",
        #         "createdAtHeight": "45058325",
        #         "clientMetadata": "2",
        #         "updatedAt": "2025-07-28T12:06:35.330Z",
        #         "updatedAtHeight": "45058326",
        #         "postOnly": False,
        #         "ticker": "BTC-USD",
        #         "subaccountNumber": 0
        #     }
        # ]
        #
        return self.parse_orders(response, market, since, limit)

    async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]:
        """
        fetch all unfilled currently open orders

        https://docs.dydx.xyz/indexer-client/http#list-orders

        :param str symbol: unified market symbol of the market orders were made in
        :param int [since]: the earliest time in ms to fetch orders for
        :param int [limit]: the maximum number of order structures to retrieve
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :param str [params.address]: wallet address that made trades
        :param str [params.subAccountNumber]: sub account number
        :returns Order[]: a list of `order structures <https://docs.ccxt.com/?id=order-structure>`
        """
        request: dict = {
            'status': 'OPEN',  # ['OPEN', 'FILLED', 'CANCELED', 'BEST_EFFORT_CANCELED', 'UNTRIGGERED', 'BEST_EFFORT_OPENED']
        }
        return await self.fetch_orders(symbol, since, limit, self.extend(request, params))

    async def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]:
        """
        fetches information on multiple closed orders made by the user

        https://docs.dydx.xyz/indexer-client/http#list-orders

        :param str symbol: unified market symbol of the market orders were made in
        :param int [since]: the earliest time in ms to fetch orders for
        :param int [limit]: the maximum number of order structures to retrieve
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :param str [params.address]: wallet address that made trades
        :param str [params.subAccountNumber]: sub account number
        :returns Order[]: a list of `order structures <https://docs.ccxt.com/?id=order-structure>`
        """
        request: dict = {
            'status': 'FILLED',  # ['OPEN', 'FILLED', 'CANCELED', 'BEST_EFFORT_CANCELED', 'UNTRIGGERED', 'BEST_EFFORT_OPENED']
        }
        return await self.fetch_orders(symbol, since, limit, self.extend(request, params))

    def parse_position(self, position: dict, market: Market = None):
        #
        # {
        #     "market": "BTC-USD",
        #     "status": "OPEN",
        #     "side": "SHORT",
        #     "size": "-0.407",
        #     "maxSize": "-0.009",
        #     "entryPrice": "118692.04840909090909090909",
        #     "exitPrice": "119526.565625",
        #     "realizedPnl": "476.42665909090909090909088",
        #     "unrealizedPnl": "-57.26681734000000000000037",
        #     "createdAt": "2025-07-14T07:53:55.631Z",
        #     "createdAtHeight": "44140908",
        #     "closedAt": null,
        #     "sumOpen": "0.44",
        #     "sumClose": "0.032",
        #     "netFunding": "503.13121",
        #     "subaccountNumber": 0
        # }
        #
        marketId = self.safe_string(position, 'market')
        market = self.safe_market(marketId, market)
        symbol = market['symbol']
        side = self.safe_string_lower(position, 'side')
        quantity = self.safe_string(position, 'size')
        if side != 'long':
            quantity = Precise.string_mul('-1', quantity)
        timestamp = self.parse8601(self.safe_string(position, 'createdAt'))
        return self.safe_position({
            'info': position,
            'id': None,
            'symbol': symbol,
            'entryPrice': self.safe_number(position, 'entryPrice'),
            'markPrice': None,
            'notional': None,
            'collateral': None,
            'unrealizedPnl': self.safe_number(position, 'unrealizedPnl'),
            'side': side,
            'contracts': self.parse_number(quantity),
            'contractSize': None,
            'timestamp': timestamp,
            'datetime': self.iso8601(timestamp),
            'hedged': None,
            'maintenanceMargin': None,
            'maintenanceMarginPercentage': None,
            'initialMargin': None,
            'initialMarginPercentage': None,
            'leverage': None,
            'liquidationPrice': None,
            'marginRatio': None,
            'marginMode': None,
            'percentage': None,
        })

    async def fetch_position(self, symbol: str, params={}):
        """
        fetch data on an open position

        https://docs.dydx.xyz/indexer-client/http#list-positions

        :param str symbol: unified market symbol of the market the position is held in
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :param str [params.address]: wallet address that made trades
        :param str [params.subAccountNumber]: sub account number
        :returns dict: a `position structure <https://docs.ccxt.com/?id=position-structure>`
        """
        positions = await self.fetch_positions([symbol], params)
        return self.safe_dict(positions, 0, {})

    async def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]:
        """
        fetch all open positions

        https://docs.dydx.xyz/indexer-client/http#list-positions

        :param str[] [symbols]: list of unified market symbols
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :param str [params.address]: wallet address that made trades
        :param str [params.subAccountNumber]: sub account number
        :returns dict[]: a list of `position structure <https://docs.ccxt.com/?id=position-structure>`
        """
        userAddress = None
        subAccountNumber = None
        userAddress, params = self.handle_public_address('fetchPositions', params)
        subAccountNumber, params = self.handle_option_and_params(params, 'fetchOrders', 'subAccountNumber', '0')
        await self.load_markets()
        request: dict = {
            'address': userAddress,
            'subaccountNumber': subAccountNumber,
            'status': 'OPEN',  # ['OPEN', 'CLOSED', 'LIQUIDATED']
        }
        response = await self.indexerGetPerpetualPositions(self.extend(request, params))
        #
        # {
        #     "positions": [
        #         {
        #             "market": "BTC-USD",
        #             "status": "OPEN",
        #             "side": "SHORT",
        #             "size": "-0.407",
        #             "maxSize": "-0.009",
        #             "entryPrice": "118692.04840909090909090909",
        #             "exitPrice": "119526.565625",
        #             "realizedPnl": "476.42665909090909090909088",
        #             "unrealizedPnl": "-57.26681734000000000000037",
        #             "createdAt": "2025-07-14T07:53:55.631Z",
        #             "createdAtHeight": "44140908",
        #             "closedAt": null,
        #             "sumOpen": "0.44",
        #             "sumClose": "0.032",
        #             "netFunding": "503.13121",
        #             "subaccountNumber": 0
        #         }
        #     ]
        # }
        #
        rows = self.safe_list(response, 'positions', [])
        return self.parse_positions(rows, symbols)

    def hash_message(self, message):
        return self.hash(message, 'keccak', 'hex')

    def sign_hash(self, hash, privateKey):
        signature = self.ecdsa(hash[-64:], privateKey[-64:], 'secp256k1', None)
        r = signature['r']
        s = signature['s']
        return {
            'r': r.rjust(64, '0'),
            's': s.rjust(64, '0'),
            'v': self.sum(27, signature['v']),
        }

    def sign_message(self, message, privateKey):
        return self.sign_hash(self.hash_message(message), privateKey[-64:])

    def sign_onboarding_action(self) -> object:
        message = {'action': 'dYdX Chain Onboarding'}
        chainId = self.options['chainId']
        domain: dict = {
            'chainId': chainId,
            'name': 'dYdX Chain',
        }
        messageTypes: dict = {
            'dYdX': [
                {'name': 'action', 'type': 'string'},
            ],
        }
        msg = self.eth_encode_structured_data(domain, messageTypes, message)
        if self.privateKey is None or self.privateKey == '':
            raise ArgumentsRequired(self.id + ' signOnboardingAction() requires a privateKey to be set.')
        signature = self.sign_message(msg, self.privateKey)
        return signature

    def sign_dydx_tx(self, privateKey: str, message: Any, memo: str, chainId: str, account: Any, authenticators: Any, fee=None) -> str:
        encodedTx, signDoc = self.encode_dydx_tx_for_signing(message, memo, chainId, account, authenticators, fee)
        signature = self.sign_hash(encodedTx, privateKey)
        return self.encode_dydx_tx_raw(signDoc, signature['r'] + signature['s'])

    def retrieve_credentials(self) -> Any:
        credentials = self.safe_dict(self.options, 'dydxCredentials')
        if credentials is not None:
            return credentials
        entropy = self.safe_string(self.options, 'mnemonic')
        if entropy is None:
            signature = self.sign_onboarding_action()
            entropy = self.hash_message(self.base16_to_binary(signature['r'] + signature['s']))
        credentials = self.retrieve_dydx_credentials(entropy)
        credentials['privateKey'] = self.binary_to_base16(credentials['privateKey'])
        credentials['publicKey'] = self.binary_to_base16(credentials['publicKey'])
        self.options['dydxCredentials'] = credentials
        return credentials

    async def fetch_dydx_account(self):
        # required in js
        await self.load_dydx_protos()
        dydxAccount = self.safe_dict(self.options, 'dydxAccount')
        if dydxAccount is not None:
            return dydxAccount
        if self.walletAddress is None:
            raise ArgumentsRequired(self.id + ' fetchDydxAccount() requires the walletAddress to be set using the dydx chain address eg: dydx1cpb4tedmwq304c2kc9pwzjwq0sc6z2a4tasxrz')
        if not self.walletAddress.startswith('dydx'):
            raise ArgumentsRequired(self.id + ' fetchDydxAccount() requires a valid dydx chain address, starting with dydx, not the l1 address.')
        request = {
            'dydxAddress': self.walletAddress,
        }
        #
        # {
        #     "info": {
        #         "address": "string",
        #         "pub_key": {
        #             "type_url": "string",
        #             "key": "string"
        #         },
        #         "account_number": "string",
        #         "sequence": "string"
        #     }
        # }
        #
        response = await self.nodeRestGetCosmosAuthV1beta1AccountInfoDydxAddress(request)
        account = self.safe_dict(response, 'info')
        account['pub_key'] = {
            # encode with binary key would fail in python
            'key': account['pub_key']['key'],
        }
        self.options['dydxAccount'] = account
        return account

    def pow(self, n: str, m: str):
        r = Precise.string_mul(n, '1')
        c = self.parse_to_int(m)
        # TODO: cap
        for i in range(1, c):
            r = Precise.string_mul(r, n)
        return r

    def create_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}):
        reduceOnly = self.safe_bool_2(params, 'reduceOnly', 'reduce_only', False)
        orderType = type.upper()
        market = self.market(symbol)
        orderSide = side.upper()
        subaccountId = 0
        subaccountId, params = self.handle_option_and_params(params, 'createOrder', 'subAccountId', subaccountId)
        triggerPrice = self.safe_string_2(params, 'triggerPrice', 'stopPrice')
        stopLossPrice = self.safe_value(params, 'stopLossPrice', triggerPrice)
        takeProfitPrice = self.safe_value(params, 'takeProfitPrice')
        isConditional = triggerPrice is not None or stopLossPrice is not None or takeProfitPrice is not None
        isMarket = orderType == 'MARKET'
        timeInForce = self.safe_string_upper(params, 'timeInForce', 'GTT')
        postOnly = self.is_post_only(isMarket, None, params)
        amountStr = self.amount_to_precision(symbol, amount)
        priceStr = self.price_to_precision(symbol, price)
        marketInfo = self.safe_dict(market, 'info')
        atomicResolution = marketInfo['atomicResolution']
        quantumScale = self.pow('10', Precise.string_neg(atomicResolution))
        quantums = Precise.string_mul(amountStr, quantumScale)
        quantumConversionExponent = marketInfo['quantumConversionExponent']
        priceScale = self.pow('10', Precise.string_sub(Precise.string_sub(atomicResolution, quantumConversionExponent), '-6'))
        subticks = Precise.string_mul(priceStr, priceScale)
        clientMetadata = 0
        conditionalType = 0
        conditionalOrderTriggerSubticks = '0'
        orderFlag = None
        timeInForceNumber = None
        if timeInForce == 'FOK':
            raise InvalidOrder(self.id + ' timeInForce fok has been deprecated')
        if orderType == 'MARKET':
            # short-term
            orderFlag = 0
            clientMetadata = 1  # STOP_MARKET / TAKE_PROFIT_MARKET
            if timeInForce is not None:
                # default is ioc
                timeInForceNumber = 1
        elif orderType == 'LIMIT':
            if timeInForce == 'GTT':
                # long-term
                orderFlag = 64
                if postOnly:
                    timeInForceNumber = 2
                else:
                    timeInForceNumber = 0
            else:
                orderFlag = 0
                if timeInForce == 'IOC':
                    timeInForceNumber = 1
                else:
                    raise InvalidOrder('unexpected code path: timeInForce')
        if isConditional:
            # conditional
            orderFlag = 32
            if stopLossPrice is not None:
                conditionalType = 1
                conditionalOrderTriggerSubticks = self.price_to_precision(symbol, stopLossPrice)
            elif takeProfitPrice is not None:
                conditionalType = 2
                conditionalOrderTriggerSubticks = self.price_to_precision(symbol, takeProfitPrice)
            conditionalOrderTriggerSubticks = Precise.string_mul(conditionalOrderTriggerSubticks, priceScale)
        latestBlockHeight = self.safe_integer(params, 'latestBlockHeight')
        goodTillBlock = self.safe_integer(params, 'goodTillBlock')
        goodTillBlockTime = None
        goodTillBlockTimeInSeconds = 2592000
        goodTillBlockTimeInSeconds, params = self.handle_option_and_params(params, 'createOrder', 'goodTillBlockTimeInSeconds', goodTillBlockTimeInSeconds)  # default is 30 days
        if orderFlag == 0:
            if goodTillBlock is None:
                # short term order
                goodTillBlock = latestBlockHeight + 20
        else:
            if goodTillBlockTimeInSeconds is None:
                raise ArgumentsRequired('goodTillBlockTimeInSeconds is required.')
            goodTillBlockTime = self.seconds() + goodTillBlockTimeInSeconds
        sideNumber = 1 if (orderSide == 'BUY') else 2
        defaultClientOrderId = self.rand_number(9)  # 2**32 - 1 is 10 digits, but it may overflow with 10
        clientOrderId = self.safe_integer(params, 'clientOrderId', defaultClientOrderId)
        orderPayload = {
            'order': {
                'orderId': {
                    'subaccountId': {
                        'owner': self.get_wallet_address(),
                        'number': subaccountId,
                    },
                    'clientId': clientOrderId,
                    'orderFlags': orderFlag,
                    'clobPairId': marketInfo['clobPairId'],
                },
                'side': sideNumber,
                'quantums': self.to_dydx_long(quantums),
                'subticks': self.to_dydx_long(subticks),
                'goodTilBlock': goodTillBlock,
                'goodTilBlockTime': goodTillBlockTime,
                'timeInForce': timeInForceNumber,
                'reduceOnly': reduceOnly,
                'clientMetadata': clientMetadata,
                'conditionType': conditionalType,
                'conditionalOrderTriggerSubticks': self.to_dydx_long(conditionalOrderTriggerSubticks),
                'orderRouterAddress': self.safe_string(self.options, 'routerAddress', 'dydx165sfn2k3vucvq7gklauy2r3agyjw4c3m60ascn'),
            },
        }
        signingPayload = {
            'typeUrl': '/dydxprotocol.clob.MsgPlaceOrder',
            'value': orderPayload,
        }
        params = self.omit(params, ['reduceOnly', 'reduce_only', 'clientOrderId', 'postOnly', 'timeInForce', 'stopPrice', 'triggerPrice', 'stopLoss', 'takeProfit', 'latestBlockHeight', 'goodTillBlock', 'goodTillBlockTimeInSeconds', 'subaccountId'])
        orderId = self.create_order_id_from_parts(self.get_wallet_address(), subaccountId, clientOrderId, orderFlag, marketInfo['clobPairId'])
        return [orderId, self.extend(signingPayload, params)]

    def create_order_id_from_parts(self, address: str, subAccountNumber: float, clientOrderId: float, orderFlags: float, clobPairId: float) -> str:
        nameSp = self.safe_string(self.options, 'namespace', '0f9da948-a6fb-4c45-9edc-4685c3f3317d')
        prefixAddress = address + '-' + str(subAccountNumber)
        prefix = self.uuid5(nameSp, prefixAddress)
        orderInfo = prefix + '-' + self.number_to_string(clientOrderId) + '-' + self.number_to_string(clobPairId) + '-' + self.number_to_string(orderFlags)
        return self.uuid5(nameSp, orderInfo)

    async def fetch_latest_block_height(self, params={}) -> int:
        response = await self.nodeRpcGetAbciInfo(params)
        #
        # {
        #     "jsonrpc": "2.0",
        #     "id": -1,
        #     "result": {
        #         "response": {
        #             "data": "dydxprotocol",
        #             "version": "9.1.0-rc0",
        #             "last_block_height": "49157714",
        #             "last_block_app_hash": "9LHAcDDI5zmWiC6bGiiGtxuWPlKJV+/fTBZk/WQ/Y4U="
        #         }
        #     }
        # }
        #
        result = self.safe_dict(response, 'result')
        info = self.safe_dict(result, 'response')
        return self.safe_integer(info, 'last_block_height')

    async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}) -> Order:
        """

        https://docs.dydx.xyz/interaction/trading#place-an-order

        create a trade order
        :param str symbol: unified symbol of the market to create an order in
        :param str type: 'market' or 'limit'
        :param str side: 'buy' or 'sell'
        :param float amount: how much of currency you want to trade in units of base currency
        :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :param str [params.timeInForce]: "GTT", "IOC", or "PO"
        :param float [params.triggerPrice]: The price a trigger order is triggered at
        :param float [params.stopLossPrice]: price for a stoploss order
        :param float [params.takeProfitPrice]: price for a takeprofit order
        :param str [params.clientOrderId]: a unique id for the order
        :param bool [params.postOnly]: True or False whether the order is post-only
        :param bool [params.reduceOnly]: True or False whether the order is reduce-only
        :param float [params.goodTillBlock]: expired block number for the order, required for market order and non limit GTT order, default value is latestBlockHeight + 20
        :param float [params.goodTillBlockTimeInSeconds]: expired time elapsed for the order, required for limit GTT order and conditional, default value is 30 days
        :returns dict: an `order structure <https://docs.ccxt.com/?id=order-structure>`
        """
        await self.load_markets()
        credentials = self.retrieve_credentials()
        account = await self.fetch_dydx_account()
        lastBlockHeight = await self.fetch_latest_block_height()
        # params['latestBlockHeight'] = lastBlockHeight
        newParams = self.extend(params, {'latestBlockHeight': lastBlockHeight})
        orderRequestRes = self.create_order_request(symbol, type, side, amount, price, newParams)
        orderId = orderRequestRes[0]
        orderRequest = orderRequestRes[1]
        chainName = self.options['chainName']
        signedTx = self.sign_dydx_tx(credentials['privateKey'], orderRequest, '', chainName, account, None)
        request = {
            'tx': signedTx,
        }
        # nodeRpcGetBroadcastTxAsync
        response = await self.nodeRpcGetBroadcastTxSync(request)
        #
        # {
        #     "jsonrpc": "2.0",
        #     "id": -1,
        #     "result": {
        #         "code": 0,
        #         "data": "",
        #         "log": "[]",
        #         "codespace": "",
        #         "hash": "CBEDB0603E57E5CE21FA6954770A9403D2A81BED02E608C860356152D0AA1A81"
        #     }
        # }
        #
        result = self.safe_dict(response, 'result')
        return self.safe_order({
            'info': result,
            'id': orderId,
            'clientOrderId': orderRequest['value']['order']['orderId']['clientId'],
        })

    async def cancel_order(self, id: str, symbol: Str = None, params={}) -> Order:
        """
        cancels an open order

        https://docs.dydx.xyz/interaction/trading/#cancel-an-order

        :param str id: it should be the hasattr(self, clientOrderId) case
        :param str symbol: unified symbol of the market the order was made in
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :param str [params.clientOrderId]: client order id used when creating the order
        :param boolean [params.trigger]: whether the order is a trigger/algo order
        :param float [params.orderFlags]: default is 64, orderFlags for the order, market order and non limit GTT order is 0, limit GTT order is 64 and conditional order is 32
        :param float [params.goodTillBlock]: expired block number for the order, required for market order and non limit GTT order(orderFlags = 0), default value is latestBlockHeight + 20
        :param float [params.goodTillBlockTimeInSeconds]: expired time elapsed for the order, required for limit GTT order and conditional(orderFlagss > 0), default value is 30 days
        :param int [params.subAccountId]: sub account id, default is 0
        :returns dict: An `order structure <https://docs.ccxt.com/?id=order-structure>`
        """
        isTrigger = self.safe_bool_2(params, 'trigger', 'stop', False)
        params = self.omit(params, ['trigger', 'stop'])
        if not isTrigger and (symbol is None):
            raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument')
        await self.load_markets()
        market: Market = self.market(symbol)
        clientOrderId = self.safe_string_2(params, 'clientOrderId', 'clientId', id)
        if clientOrderId is None:
            raise ArgumentsRequired(self.id + ' cancelOrder() requires a clientOrderId parameter, cancelling using id is not currently supported.')
        idString = str(id)
        if id is not None and idString.find('-') > -1:
            raise NotSupported(self.id + ' cancelOrder() cancelling using id is not currently supported, please use provide the clientOrderId parameter.')
        goodTillBlock = self.safe_integer(params, 'goodTillBlock')
        goodTillBlockTimeInSeconds = 2592000
        goodTillBlockTimeInSeconds, params = self.handle_option_and_params(params, 'cancelOrder', 'goodTillBlockTimeInSeconds', goodTillBlockTimeInSeconds)  # default is 30 days
        goodTillBlockTime = None
        defaultOrderFlags = 32 if (isTrigger) else 64
        orderFlags = self.safe_integer(params, 'orderFlags', defaultOrderFlags)
        subAccountId = 0
        subAccountId, params = self.handle_option_and_params(params, 'cancelOrder', 'subAccountId', subAccountId)
        params = self.omit(params, ['clientOrderId', 'orderFlags', 'goodTillBlock', 'goodTillBlockTime', 'goodTillBlockTimeInSeconds', 'subaccountId', 'clientId'])
        if orderFlags != 0 and orderFlags != 64 and orderFlags != 32:
            raise InvalidOrder(self.id + ' invalid orderFlags, allowed values are(0, 64, 32).')
        if orderFlags > 0:
            if goodTillBlockTimeInSeconds is None:
                raise ArgumentsRequired(self.id + ' goodTillBlockTimeInSeconds is required in params for long term or conditional order.')
            if goodTillBlock is not None and goodTillBlock > 0:
                raise InvalidOrder(self.id + ' goodTillBlock should be 0 for long term or conditional order.')
            goodTillBlockTime = self.seconds() + goodTillBlockTimeInSeconds
        else:
            if goodTillBlock is None:
                latestBlockHeight = await self.fetch_latest_block_height()
                goodTillBlock = latestBlockHeight + 20
        credentials = self.retrieve_credentials()
        account = await self.fetch_dydx_account()
        cancelPayload = {
            'orderId': {
                'subaccountId': {
                    'owner': self.get_wallet_address(),
                    'number': subAccountId,
                },
                'clientId': clientOrderId,
                'orderFlags': orderFlags,
                'clobPairId': market['info']['clobPairId'],
            },
            'goodTilBlock': goodTillBlock,
            'goodTilBlockTime': goodTillBlockTime,
        }
        signingPayload = {
            'typeUrl': '/dydxprotocol.clob.MsgCancelOrder',
            'value': cancelPayload,
        }
        chainName = self.options['chainName']
        signedTx = self.sign_dydx_tx(credentials['privateKey'], signingPayload, '', chainName, account, None)
        request = {
            'tx': signedTx,
        }
        # nodeRpcGetBroadcastTxAsync
        response = await self.nodeRpcGetBroadcastTxSync(request)
        #
        # {
        #     "jsonrpc": "2.0",
        #     "id": -1,
        #     "result": {
        #         "code": 0,
        #         "data": "",
        #         "log": "[]",
        #         "codespace": "",
        #         "hash": "CBEDB0603E57E5CE21FA6954770A9403D2A81BED02E608C860356152D0AA1A81"
        #     }
        # }
        #
        result = self.safe_dict(response, 'result')
        return self.safe_order({
            'info': result,
        })

    async def cancel_orders(self, ids: List[str], symbol: Str = None, params={}):
        """
        cancel multiple orders
        :param str[] ids: order ids
        :param str [symbol]: unified market symbol
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :param str[] [params.clientOrderIds]: max length 10 e.g. ["my_id_1","my_id_2"], encode the double quotes. No space after comma
        :param int [params.subAccountId]: sub account id, default is 0
        :returns dict: an list of `order structures <https://docs.ccxt.com/?id=order-structure>`
        """
        await self.load_markets()
        market: Market = self.market(symbol)
        clientOrderIds = self.safe_list(params, 'clientOrderIds')
        if not clientOrderIds:
            raise NotSupported(self.id + ' cancelOrders only support clientOrderIds.')
        subAccountId = 0
        subAccountId, params = self.handle_option_and_params(params, 'cancelOrders', 'subAccountId', subAccountId)
        goodTillBlock = self.safe_integer(params, 'goodTillBlock')
        if goodTillBlock is None:
            latestBlockHeight = await self.fetch_latest_block_height()
            goodTillBlock = latestBlockHeight + 20
        params = self.omit(params, ['clientOrderIds', 'goodTillBlock', 'subaccountId'])
        credentials = self.retrieve_credentials()
        account = await self.fetch_dydx_account()
        cancelOrders = {
            'clientIds': clientOrderIds,
            'clobPairId': market['info']['clobPairId'],
        }
        cancelPayload = {
            'subaccountId': {
                'owner': self.get_wallet_address(),
                'number': subAccountId,
            },
            'shortTermCancels': [cancelOrders],
            'goodTilBlock': goodTillBlock,
        }
        signingPayload = {
            'typeUrl': '/dydxprotocol.clob.MsgBatchCancel',
            'value': cancelPayload,
        }
        chainName = self.options['chainName']
        signedTx = self.sign_dydx_tx(credentials['privateKey'], signingPayload, '', chainName, account, None)
        request = {
            'tx': signedTx,
        }
        # nodeRpcGetBroadcastTxAsync
        response = await self.nodeRpcGetBroadcastTxSync(request)
        #
        # {
        #     "jsonrpc": "2.0",
        #     "id": -1,
        #     "result": {
        #         "code": 0,
        #         "data": "",
        #         "log": "[]",
        #         "codespace": "",
        #         "hash": "CBEDB0603E57E5CE21FA6954770A9403D2A81BED02E608C860356152D0AA1A81"
        #     }
        # }
        #
        result = self.safe_dict(response, 'result')
        return [self.safe_order({
            'info': result,
        })]

    async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook:
        """
        fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data

        https://docs.dydx.xyz/indexer-client/http#get-perpetual-market-orderbook

        :param str symbol: unified symbol of the market to fetch the order book for
        :param int [limit]: the maximum amount of order book entries to return
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict: A dictionary of `order book structures <https://docs.ccxt.com/?id=order-book-structure>` indexed by market symbols
        """
        await self.load_markets()
        market = self.market(symbol)
        request: dict = {
            'market': market['id'],
        }
        response = await self.indexerGetOrderbooksPerpetualMarketMarket(self.extend(request, params))
        #
        # {
        #     "bids": [
        #         {
        #             "price": "118267",
        #             "size": "0.3182"
        #         }
        #     ],
        #     "asks": [
        #         {
        #             "price": "118485",
        #             "size": "0.0001"
        #         }
        #     ]
        # }
        #
        return self.parse_order_book(response, market['symbol'], None, 'bids', 'asks', 'price', 'size')

    def parse_ledger_entry(self, item: dict, currency: Currency = None) -> LedgerEntry:
        #
        # {
        #     "id": "6a6075bc-7183-5fd9-bc9d-894e238aa527",
        #     "sender": {
        #         "address": "dydx14zzueazeh0hj67cghhf9jypslcf9sh2n5k6art",
        #         "subaccountNumber": 0
        #     },
        #     "recipient": {
        #         "address": "dydx1slanxj8x9ntk9knwa6cvfv2tzlsq5gk3dshml0",
        #         "subaccountNumber": 1
        #     },
        #     "size": "0.000001",
        #     "createdAt": "2025-07-29T09:43:02.105Z",
        #     "createdAtHeight": "45116125",
        #     "symbol": "USDC",
        #     "type": "TRANSFER_OUT",
        #     "transactionHash": "92B4744BA1B783CF37C79A50BEBC47FFD59C8D5197D62A8485D3DCCE9AF220AF"
        # }
        #
        currencyId = self.safe_string(item, 'symbol')
        code = self.safe_currency_code(currencyId, currency)
        currency = self.safe_currency(currencyId, currency)
        type = self.safe_string_upper(item, 'type')
        direction = None
        if type is not None:
            if type == 'TRANSFER_IN' or type == 'DEPOSIT':
                direction = 'in'
            elif type == 'TRANSFER_OUT' or type == 'WITHDRAWAL':
                direction = 'out'
        amount = self.safe_string(item, 'size')
        timestamp = self.parse8601(self.safe_string(item, 'createdAt'))
        sender = self.safe_dict(item, 'sender')
        recipient = self.safe_dict(item, 'recipient')
        return self.safe_ledger_entry({
            'info': item,
            'id': self.safe_string(item, 'id'),
            'direction': direction,
            'account': self.safe_string(sender, 'address'),
            'referenceAccount': self.safe_string(recipient, 'address'),
            'referenceId': self.safe_string(item, 'transactionHash'),
            'type': self.parse_ledger_entry_type(type),
            'currency': code,
            'amount': self.parse_number(amount),
            'timestamp': timestamp,
            'datetime': self.iso8601(timestamp),
            'before': None,
            'after': None,
            'status': None,
            'fee': None,
        }, currency)

    def parse_ledger_entry_type(self, type):
        ledgerType: dict = {
            'TRANSFER_IN': 'transfer',
            'TRANSFER_OUT': 'transfer',
            'DEPOSIT': 'deposit',
            'WITHDRAWAL': 'withdrawal',
        }
        return self.safe_string(ledgerType, type, type)

    async def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LedgerEntry]:
        """
        fetch the history of changes, actions done by the user or operations that altered balance of the user

        https://docs.dydx.xyz/indexer-client/http#get-transfers

        :param str [code]: unified currency code, default is None
        :param int [since]: timestamp in ms of the earliest ledger entry, default is None
        :param int [limit]: max number of ledger entries to return, default is None
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :param str [params.address]: wallet address that made trades
        :param str [params.subAccountNumber]: sub account number
        :returns dict: a `ledger structure <https://docs.ccxt.com/?id=ledger-entry-structure>`
        """
        await self.load_markets()
        currency = None
        if code is not None:
            currency = self.currency(code)
        response = await self.fetch_transactions_helper(code, since, limit, self.extend(params, {'methodName': 'fetchLedger'}))
        return self.parse_ledger(response, currency, since, limit)

    async def estimate_tx_fee(self, message: Any, memo: str, account: Any) -> Any:
        txBytes = self.encode_dydx_tx_for_simulation(message, memo, account['sequence'], account['pub_key'])
        request = {
            'txBytes': txBytes,
        }
        response = await self.nodeRestPostCosmosTxV1beta1Simulate(request)
        #
        # {
        #     gas_info: {gas_wanted: '18446744073709551615', gas_used: '86055'},
        #     result: {
        #         ...
        #     }
        # }
        #
        gasInfo = self.safe_dict(response, 'gas_info')
        if gasInfo is None:
            raise ExchangeError(self.id + ' failed to simulate transaction.')
        gasUsed = self.safe_string(gasInfo, 'gas_used')
        if gasUsed is None:
            raise ExchangeError(self.id + ' failed to simulate transaction.')
        defaultFeeDenom = self.safe_string(self.options, 'defaultFeeDenom')
        defaultFeeMultiplier = self.safe_string(self.options, 'defaultFeeMultiplier')
        feeDenom = self.safe_dict(self.options, 'feeDenom')
        gasPrice = None
        denom = None
        if defaultFeeDenom == 'uusdc':
            gasPrice = feeDenom['USDC_GAS_PRICE']
            denom = feeDenom['USDC_DENOM']
        else:
            gasPrice = feeDenom['CHAINTOKEN_GAS_PRICE']
            denom = feeDenom['CHAINTOKEN_DENOM']
        gasLimit = int(math.ceil(self.parse_to_numeric(Precise.string_mul(gasUsed, defaultFeeMultiplier))))
        feeAmount = Precise.string_mul(self.number_to_string(gasLimit), gasPrice)
        if feeAmount.find('.') >= 0:
            feeAmount = self.number_to_string(int(math.ceil(self.parse_to_numeric(feeAmount))))
        feeObj = {
            'amount': feeAmount,
            'denom': denom,
        }
        return {
            'amount': [feeObj],
            'gasLimit': gasLimit,
        }

    async def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry:
        """
        transfer currency internally between wallets on the same account
        :param str code: unified currency code
        :param float amount: amount to transfer
        :param str fromAccount: account to transfer from *main, subaccount*
        :param str toAccount: account to transfer to *subaccount, address*
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :param str [params.vaultAddress]: the vault address for order
        :returns dict: a `transfer structure <https://docs.ccxt.com/?id=transfer-structure>`
        """
        if code != 'USDC':
            raise NotSupported(self.id + ' transfer() only support USDC')
        await self.load_markets()
        fromSubaccountId = self.safe_integer(params, 'fromSubaccountId')
        toSubaccountId = self.safe_integer(params, 'toSubaccountId')
        if fromAccount != 'main':
            # raise error if from subaccount id is undefind
            if fromAccount is None:
                raise NotSupported(self.id + ' transfer only support main > subaccount and subaccount <> subaccount.')
            if fromSubaccountId is None or toSubaccountId is None:
                raise ArgumentsRequired(self.id + ' transfer requires fromSubaccountId and toSubaccountId.')
        params = self.omit(params, ['fromSubaccountId', 'toSubaccountId'])
        credentials = self.retrieve_credentials()
        account = await self.fetch_dydx_account()
        usd = self.parse_to_int(Precise.string_mul(self.number_to_string(amount), '1000000'))
        payload = None
        signingPayload = None
        if fromAccount == 'main':
            # deposit to subaccount
            if toSubaccountId is None:
                raise ArgumentsRequired(self.id + ' transfer() requeire toSubaccoutnId.')
            payload = {
                'sender': self.get_wallet_address(),
                'recipient': {
                    'owner': self.get_wallet_address(),
                    'number': toSubaccountId,
                },
                'assetId': 0,
                'quantums': usd,
            }
            signingPayload = {
                'typeUrl': '/dydxprotocol.sending.MsgDepositToSubaccount',
                'value': payload,
            }
        else:
            payload = {
                'transfer': {
                    'sender': {
                        'owner': fromAccount,
                        'number': fromSubaccountId,
                    },
                    'recipient': {
                        'owner': toAccount,
                        'number': toSubaccountId,
                    },
                    'assetId': 0,
                    'amount': usd,
                },
            }
            signingPayload = {
                'typeUrl': '/dydxprotocol.sending.MsgCreateTransfer',
                'value': payload,
            }
        txFee = await self.estimate_tx_fee(signingPayload, '', account)
        chainName = self.options['chainName']
        signedTx = self.sign_dydx_tx(credentials['privateKey'], signingPayload, '', chainName, account, None, txFee)
        request = {
            'tx': signedTx,
        }
        # nodeRpcGetBroadcastTxAsync
        response = await self.nodeRpcGetBroadcastTxSync(request)
        #
        # {
        #     "jsonrpc": "2.0",
        #     "id": -1,
        #     "result": {
        #         "code": 0,
        #         "data": "",
        #         "log": "[]",
        #         "codespace": "",
        #         "hash": "CBEDB0603E57E5CE21FA6954770A9403D2A81BED02E608C860356152D0AA1A81"
        #     }
        # }
        #
        return self.parse_transfer(response)

    def parse_transfer(self, transfer: dict, currency: Currency = None) -> TransferEntry:
        #
        # {
        #     "id": "6a6075bc-7183-5fd9-bc9d-894e238aa527",
        #     "sender": {
        #         "address": "dydx14zzueazeh0hj67cghhf9jypslcf9sh2n5k6art",
        #         "subaccountNumber": 0
        #     },
        #     "recipient": {
        #         "address": "dydx1slanxj8x9ntk9knwa6cvfv2tzlsq5gk3dshml0",
        #         "subaccountNumber": 1
        #     },
        #     "size": "0.000001",
        #     "createdAt": "2025-07-29T09:43:02.105Z",
        #     "createdAtHeight": "45116125",
        #     "symbol": "USDC",
        #     "type": "TRANSFER_OUT",
        #     "transactionHash": "92B4744BA1B783CF37C79A50BEBC47FFD59C8D5197D62A8485D3DCCE9AF220AF"
        # }
        #
        id = self.safe_string(transfer, 'id')
        currencyId = self.safe_string(transfer, 'symbol')
        code = self.safe_currency_code(currencyId, currency)
        amount = self.safe_number(transfer, 'size')
        sender = self.safe_dict(transfer, 'sender')
        recipient = self.safe_dict(transfer, 'recipient')
        fromAccount = self.safe_string(sender, 'address')
        toAccount = self.safe_string(recipient, 'address')
        timestamp = self.parse8601(self.safe_string(transfer, 'createdAt'))
        return {
            'info': transfer,
            'id': id,
            'timestamp': timestamp,
            'datetime': self.iso8601(timestamp),
            'currency': code,
            'amount': amount,
            'fromAccount': fromAccount,
            'toAccount': toAccount,
            'status': None,
        }

    async def fetch_transfers(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[TransferEntry]:
        """
        fetch a history of internal transfers made on an account

        https://docs.dydx.xyz/indexer-client/http#get-transfers

        :param str code: unified currency code of the currency transferred
        :param int [since]: the earliest time in ms to fetch transfers for
        :param int [limit]: the maximum number of transfers structures to retrieve
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :param str [params.address]: wallet address that made trades
        :param str [params.subAccountNumber]: sub account number
        :returns dict[]: a list of `transfer structures <https://docs.ccxt.com/?id=transfer-structure>`
        """
        await self.load_markets()
        currency = None
        if code is not None:
            currency = self.currency(code)
        response = await self.fetch_transactions_helper(code, since, limit, self.extend(params, {'methodName': 'fetchTransfers'}))
        transferIn = self.filter_by(response, 'type', 'TRANSFER_IN')
        transferOut = self.filter_by(response, 'type', 'TRANSFER_OUT')
        rows = self.array_concat(transferIn, transferOut)
        return self.parse_transfers(rows, currency, since, limit)

    def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction:
        #
        # {
        #     "id": "6a6075bc-7183-5fd9-bc9d-894e238aa527",
        #     "sender": {
        #         "address": "dydx14zzueazeh0hj67cghhf9jypslcf9sh2n5k6art",
        #         "subaccountNumber": 0
        #     },
        #     "recipient": {
        #         "address": "dydx1slanxj8x9ntk9knwa6cvfv2tzlsq5gk3dshml0",
        #         "subaccountNumber": 1
        #     },
        #     "size": "0.000001",
        #     "createdAt": "2025-07-29T09:43:02.105Z",
        #     "createdAtHeight": "45116125",
        #     "symbol": "USDC",
        #     "type": "TRANSFER_OUT",
        #     "transactionHash": "92B4744BA1B783CF37C79A50BEBC47FFD59C8D5197D62A8485D3DCCE9AF220AF"
        # }
        #
        id = self.safe_string(transaction, 'id')
        sender = self.safe_dict(transaction, 'sender')
        recipient = self.safe_dict(transaction, 'recipient')
        addressTo = self.safe_string(recipient, 'address')
        addressFrom = self.safe_string(sender, 'address')
        txid = self.safe_string(transaction, 'transactionHash')
        currencyId = self.safe_string(transaction, 'symbol')
        code = self.safe_currency_code(currencyId, currency)
        timestamp = self.parse8601(self.safe_string(transaction, 'createdAt'))
        amount = self.safe_number(transaction, 'size')
        return {
            'info': transaction,
            'id': id,
            'txid': txid,
            'timestamp': timestamp,
            'datetime': self.iso8601(timestamp),
            'network': None,
            'address': addressTo,
            'addressTo': addressTo,
            'addressFrom': addressFrom,
            'tag': None,
            'tagTo': None,
            'tagFrom': None,
            'type': self.safe_string_lower(transaction, 'type'),  # 'deposit', 'withdrawal'
            'amount': amount,
            'currency': code,
            'status': None,
            'updated': None,
            'internal': None,
            'comment': None,
            'fee': None,
        }

    async def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction:
        """
        make a withdrawal
        :param str code: unified currency code
        :param float amount: the amount to withdraw
        :param str address: the address to withdraw to
        :param str tag:
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict: a `transaction structure <https://docs.ccxt.com/?id=transaction-structure>`
        """
        if code != 'USDC':
            raise NotSupported(self.id + ' withdraw() only support USDC')
        await self.load_markets()
        self.check_address(address)
        subaccountId = self.safe_integer(params, 'subaccountId')
        if subaccountId is None:
            raise ArgumentsRequired(self.id + ' withdraw requires subaccountId.')
        params = self.omit(params, ['subaccountId'])
        currency = self.currency(code)
        credentials = self.retrieve_credentials()
        account = await self.fetch_dydx_account()
        usd = self.parse_to_int(Precise.string_mul(self.number_to_string(amount), '1000000'))
        payload = {
            'sender': {
                'owner': self.get_wallet_address(),
                'number': subaccountId,
            },
            'recipient': address,
            'assetId': 0,
            'quantums': usd,
        }
        signingPayload = {
            'typeUrl': '/dydxprotocol.sending.MsgWithdrawFromSubaccount',
            'value': payload,
        }
        txFee = await self.estimate_tx_fee(signingPayload, tag, account)
        chainName = self.options['chainName']
        signedTx = self.sign_dydx_tx(credentials['privateKey'], signingPayload, tag, chainName, account, None, txFee)
        request = {
            'tx': signedTx,
        }
        # nodeRpcGetBroadcastTxAsync
        response = await self.nodeRpcGetBroadcastTxSync(request)
        #
        # {
        #     "jsonrpc": "2.0",
        #     "id": -1,
        #     "result": {
        #         "code": 0,
        #         "data": "",
        #         "log": "[]",
        #         "codespace": "",
        #         "hash": "CBEDB0603E57E5CE21FA6954770A9403D2A81BED02E608C860356152D0AA1A81"
        #     }
        # }
        #
        data = self.safe_dict(response, 'result', {})
        return self.parse_transaction(data, currency)

    async def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]:
        """
        fetch all withdrawals made from an account

        https://docs.dydx.xyz/indexer-client/http#get-transfers

        :param str code: unified currency code
        :param int [since]: the earliest time in ms to fetch withdrawals for
        :param int [limit]: the maximum number of withdrawals structures to retrieve
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :param str [params.address]: wallet address that made trades
        :param str [params.subAccountNumber]: sub account number
        :returns dict[]: a list of `transaction structures <https://docs.ccxt.com/?id=transaction-structure>`
        """
        await self.load_markets()
        currency = None
        if code is not None:
            currency = self.currency(code)
        response = await self.fetch_transactions_helper(code, since, limit, self.extend(params, {'methodName': 'fetchWithdrawals'}))
        rows = self.filter_by(response, 'type', 'WITHDRAWAL')
        return self.parse_transactions(rows, currency, since, limit)

    async def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]:
        """
        fetch all deposits made to an account

        https://docs.dydx.xyz/indexer-client/http#get-transfers

        :param str code: unified currency code
        :param int [since]: the earliest time in ms to fetch deposits for
        :param int [limit]: the maximum number of deposits structures to retrieve
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :param str [params.address]: wallet address that made trades
        :param str [params.subAccountNumber]: sub account number
        :returns dict[]: a list of `transaction structures <https://docs.ccxt.com/?id=transaction-structure>`
        """
        await self.load_markets()
        currency = None
        if code is not None:
            currency = self.currency(code)
        response = await self.fetch_transactions_helper(code, since, limit, self.extend(params, {'methodName': 'fetchDeposits'}))
        rows = self.filter_by(response, 'type', 'DEPOSIT')
        return self.parse_transactions(rows, currency, since, limit)

    async def fetch_deposits_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]:
        """
        fetch history of deposits and withdrawals

        https://docs.dydx.xyz/indexer-client/http#get-transfers

        :param str [code]: unified currency code for the currency of the deposit/withdrawals, default is None
        :param int [since]: timestamp in ms of the earliest deposit/withdrawal, default is None
        :param int [limit]: max number of deposit/withdrawals to return, default is None
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :param str [params.address]: wallet address that made trades
        :param str [params.subAccountNumber]: sub account number
        :returns dict: a list of `transaction structure <https://docs.ccxt.com/?id=transaction-structure>`
        """
        await self.load_markets()
        currency = None
        if code is not None:
            currency = self.currency(code)
        response = await self.fetch_transactions_helper(code, since, limit, self.extend(params, {'methodName': 'fetchDepositsWithdrawals'}))
        withdrawals = self.filter_by(response, 'type', 'WITHDRAWAL')
        deposits = self.filter_by(response, 'type', 'DEPOSIT')
        rows = self.array_concat(withdrawals, deposits)
        return self.parse_transactions(rows, currency, since, limit)

    async def fetch_transactions_helper(self, code: Str = None, since: Int = None, limit: Int = None, params={}):
        methodName = self.safe_string(params, 'methodName')
        params = self.omit(params, 'methodName')
        userAddress = None
        subAccountNumber = None
        userAddress, params = self.handle_public_address(methodName, params)
        subAccountNumber, params = self.handle_option_and_params(params, methodName, 'subAccountNumber', '0')
        request: dict = {
            'address': userAddress,
            'subaccountNumber': subAccountNumber,
        }
        response = await self.indexerGetTransfers(self.extend(request, params))
        #
        # {
        #     "transfers": [
        #         {
        #             "id": "6a6075bc-7183-5fd9-bc9d-894e238aa527",
        #             "sender": {
        #                 "address": "dydx14zzueazeh0hj67cghhf9jypslcf9sh2n5k6art",
        #                 "subaccountNumber": 0
        #             },
        #             "recipient": {
        #                 "address": "dydx1slanxj8x9ntk9knwa6cvfv2tzlsq5gk3dshml0",
        #                 "subaccountNumber": 1
        #             },
        #             "size": "0.000001",
        #             "createdAt": "2025-07-29T09:43:02.105Z",
        #             "createdAtHeight": "45116125",
        #             "symbol": "USDC",
        #             "type": "TRANSFER_OUT",
        #             "transactionHash": "92B4744BA1B783CF37C79A50BEBC47FFD59C8D5197D62A8485D3DCCE9AF220AF"
        #         }
        #     ]
        # }
        #
        return self.safe_list(response, 'transfers', [])

    async def fetch_accounts(self, params={}) -> List[Account]:
        """
        fetch all the accounts associated with a profile

        https://docs.dydx.xyz/indexer-client/http#get-subaccounts

        :param dict [params]: extra parameters specific to the exchange API endpoint
        :param str [params.address]: wallet address that made trades
        :returns dict: a dictionary of `account structures <https://docs.ccxt.com/?id=account-structure>` indexed by the account type
        """
        userAddress = None
        userAddress, params = self.handle_public_address('fetchAccounts', params)
        request: dict = {
            'address': userAddress,
        }
        response = await self.indexerGetAddressesAddress(self.extend(request, params))
        #
        # {
        #     "subaccounts": [
        #         {
        #             "address": "dydx14zzueazeh0hj67cghhf9jypslcf9sh2n5k6art",
        #             "subaccountNumber": 0,
        #             "equity": "25346.73993597",
        #             "freeCollateral": "24207.8530595294",
        #             "openPerpetualPositions": {
        #                 "BTC-USD": {
        #                     "market": "BTC-USD",
        #                     "status": "OPEN",
        #                     "side": "SHORT",
        #                     "size": "-0.491",
        #                     "maxSize": "-0.009",
        #                     "entryPrice": "118703.60811320754716981132",
        #                     "exitPrice": "119655.95",
        #                     "realizedPnl": "3075.17994830188679245283016",
        #                     "unrealizedPnl": "1339.12776155490566037735812",
        #                     "createdAt": "2025-07-14T07:53:55.631Z",
        #                     "createdAtHeight": "44140908",
        #                     "closedAt": null,
        #                     "sumOpen": "0.53",
        #                     "sumClose": "0.038",
        #                     "netFunding": "3111.36894",
        #                     "subaccountNumber": 0
        #                 }
        #             },
        #             "assetPositions": {
        #                 "USDC": {
        #                     "size": "82291.083758",
        #                     "symbol": "USDC",
        #                     "side": "LONG",
        #                     "assetId": "0",
        #                     "subaccountNumber": 0
        #                 }
        #             },
        #             "marginEnabled": True,
        #             "updatedAtHeight": "45234659",
        #             "latestProcessedBlockHeight": "45293477"
        #         }
        #     ]
        # }
        #
        rows = self.safe_list(response, 'subaccounts', [])
        result = []
        for i in range(0, len(rows)):
            account = rows[i]
            accountId = self.safe_string(account, 'subaccountNumber')
            result.append({
                'id': accountId,
                'type': None,
                'currency': None,
                'info': account,
                'code': None,
            })
        return result

    async def fetch_balance(self, params={}) -> Balances:
        """
        query for balance and get the amount of funds available for trading or funds locked in orders

        https://docs.dydx.xyz/indexer-client/http#get-subaccount

        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict: a `balance structure <https://docs.ccxt.com/?id=balance-structure>`
        """
        await self.load_markets()
        userAddress = None
        userAddress, params = self.handle_public_address('fetchAccounts', params)
        subaccountNumber = None
        subaccountNumber, params = self.handle_option_and_params(params, 'fetchAccounts', 'subaccountNumber', 0)
        request: dict = {
            'address': userAddress,
            'subaccountNumber': subaccountNumber,
        }
        response = await self.indexerGetAddressesAddressSubaccountNumberSubaccountNumber(self.extend(request, params))
        #
        # {
        #     "subaccount": {
        #         "address": "dydx14zzueazeh0hj67cghhf9jypslcf9sh2n5k6art",
        #         "subaccountNumber": 0,
        #         "equity": "161451.040416029",
        #         "freeCollateral": "152508.28819133578",
        #         "openPerpetualPositions": {
        #             "ETH-USD": {
        #                 "market": "ETH-USD",
        #                 "status": "OPEN",
        #                 "side": "LONG",
        #                 "size": "0.001",
        #                 "maxSize": "0.002",
        #                 "entryPrice": "3894.7",
        #                 "exitPrice": "3864.5",
        #                 "realizedPnl": "-0.034847",
        #                 "unrealizedPnl": "-0.044675155",
        #                 "createdAt": "2025-10-22T08:34:05.883Z",
        #                 "createdAtHeight": "52228825",
        #                 "closedAt": null,
        #                 "sumOpen": "0.002",
        #                 "sumClose": "0.001",
        #                 "netFunding": "-0.004647",
        #                 "subaccountNumber": 0
        #             },
        #             "BTC-USD": {
        #                 "market": "BTC-USD",
        #                 "status": "OPEN",
        #                 "side": "SHORT",
        #                 "size": "-4.1368",
        #                 "maxSize": "-0.009",
        #                 "entryPrice": "112196.87848803433219017636",
        #                 "exitPrice": "113885.21872652924977050823",
        #                 "realizedPnl": "-15180.426770788459736511679821",
        #                 "unrealizedPnl": "17002.285719484425404321566048",
        #                 "createdAt": "2025-07-14T07:53:55.631Z",
        #                 "createdAtHeight": "44140908",
        #                 "closedAt": null,
        #                 "sumOpen": "5.3361",
        #                 "sumClose": "1.1983",
        #                 "netFunding": "-13157.288663",
        #                 "subaccountNumber": 0
        #             }
        #         },
        #         "assetPositions": {
        #             "USDC": {
        #                 "size": "608580.951601",
        #                 "symbol": "USDC",
        #                 "side": "LONG",
        #                 "assetId": "0",
        #                 "subaccountNumber": 0
        #             }
        #         },
        #         "marginEnabled": True,
        #         "updatedAtHeight": "52228833",
        #         "latestProcessedBlockHeight": "52246761"
        #     }
        # }
        #
        data = self.safe_dict(response, 'subaccount')
        return self.parse_balance(data)

    def parse_balance(self, response) -> Balances:
        account = self.account()
        account['free'] = self.safe_string(response, 'freeCollateral')
        result: dict = {
            'info': response,
            'USDC': account,
        }
        return self.safe_balance(result)

    def nonce(self):
        return self.milliseconds() - self.options['timeDifference']

    def get_wallet_address(self):
        if self.walletAddress is not None and self.walletAddress != '':
            return self.walletAddress
        dydxAccount = self.safe_dict(self.options, 'dydxAccount')
        if dydxAccount is not None:
            # return dydxAccount
            wallet = self.safe_string(dydxAccount, 'address')
            if wallet is not None:
                return wallet
        raise ArgumentsRequired(self.id + ' getWalletAddress() requires a wallet address. Set `walletAddress` or `dydxAccount` in exchange options.')

    def sign(self, path, section='public', method='GET', params={}, headers=None, body=None):
        pathWithParams = self.implode_params(path, params)
        url = self.implode_hostname(self.urls['api'][section])
        params = self.omit(params, self.extract_params(path))
        params = self.keysort(params)
        url += '/' + pathWithParams
        if method == 'GET':
            if params:
                url += '?' + self.urlencode(params)
        else:
            body = self.json(params)
            headers = {
                'Content-type': 'application/json',
            }
        return {'url': url, 'method': method, 'body': body, 'headers': headers}

    def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody):
        if not response:
            return None  # fallback to default error handler
        #
        # abci response
        # {"result": {"code": 0}}
        #
        # rest response
        # {"code": 123}
        #
        result = self.safe_dict(response, 'result')
        errorCode = self.safe_string(result, 'code')
        if not errorCode:
            errorCode = self.safe_string(response, 'code')
        if errorCode:
            errorCodeNum = self.parse_to_numeric(errorCode)
            if errorCodeNum > 0:
                feedback = self.id + ' ' + self.json(response)
                self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback)
                self.throw_broadly_matched_exception(self.exceptions['broad'], body, feedback)
                raise ExchangeError(feedback)
        return None

    def set_sandbox_mode(self, enable: bool):
        super(dydx, self).set_sandbox_mode(enable)
        # rewrite testnet parameters
        self.options['chainName'] = 'dydx-testnet-4'
        self.options['chainId'] = 11155111
        self.options['feeDenom']['CHAINTOKEN_DENOM'] = 'adv4tnt'
