diff --git a/.env.example b/.env.example index 10a4f81..4835a25 100644 --- a/.env.example +++ b/.env.example @@ -2,7 +2,7 @@ LANG=zh # Exchange selection -EXCHANGE=aster # Pick aster (default) or grvt/lighter/backpack/paradex +EXCHANGE=aster # Pick aster (default) or grvt/lighter/backpack/paradex/nado # Aster API credentials ASTER_API_KEY= @@ -117,3 +117,26 @@ PARADEX_WALLET_ADDRESS= # Enable verbose adapter logging: set to "1" or "true" # PARADEX_DEBUG=false + +# Nado exchange configuration (Ink mainnet) +# Requires a linked signer private key + your original subaccount owner EVM address. +# When EXCHANGE=nado these values are used automatically. + +NADO_ENV=inkMainnet # inkMainnet | inkTestnet +NADO_SYMBOL=BTC-PERP # Trading product symbol (e.g., BTC-PERP / ETH-PERP) +NADO_SIGNER_PRIVATE_KEY= # 32-byte 0x-prefixed private key (0x...) +NADO_SUBACCOUNT_OWNER= # EVM address of the subaccount owner (0x...) +NADO_SUBACCOUNT_NAME=default # Subaccount name (bytes12, default "default") + +# Optional: market-order slippage buffer (used for IOC limit-as-market, e.g. 0.01 => 1%) +NADO_MARKET_SLIPPAGE_PCT=0.01 + +# Optional: stop trigger source for STOP_MARKET orders (oracle | last | mid) +NADO_STOP_TRIGGER_SOURCE=oracle + +# Optional endpoint overrides +# NADO_GATEWAY_WS_URL=wss://gateway.prod.nado.xyz/v1/ws +# NADO_SUBSCRIPTIONS_WS_URL=wss://gateway.prod.nado.xyz/v1/subscribe +# NADO_ARCHIVE_URL=https://archive.prod.nado.xyz/v1 +# NADO_TRIGGER_URL=https://trigger.prod.nado.xyz/v1 +# NADO_DEBUG=false diff --git a/README.md b/README.md index 403bab9..599032d 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ curl -fsSL https://github.com/discountry/ritmex-bot/raw/refs/heads/main/setup.sh | 变量 | 说明 | | --- | --- | -| `EXCHANGE` | 选择交易所(`aster`/`grvt`/`lighter`/`backpack`/`paradex`) | +| `EXCHANGE` | 选择交易所(`aster`/`grvt`/`lighter`/`backpack`/`paradex`/`nado`) | | `TRADE_SYMBOL` | 交易对(默认 `BTCUSDT`) | | `TRADE_AMOUNT` | 单笔下单数量(标的资产计) | | `LOSS_LIMIT` | 单笔最大亏损触发的强平额度(USDT) | @@ -131,6 +131,12 @@ curl -fsSL https://github.com/discountry/ritmex-bot/raw/refs/heads/main/setup.sh 3. 默认连接主网,若需测试网,将 `PARADEX_SANDBOX=true` 并根据需要调整 `PARADEX_SYMBOL`。 4. 复杂环境可额外设置 `PARADEX_USE_PRO`、`PARADEX_RECONNECT_DELAY_MS` 或调试开关。 +### Nado +1. 设置 `EXCHANGE=nado`。 +2. 提供 `NADO_SIGNER_PRIVATE_KEY`(已 link 的 Signer 私钥)与 `NADO_SUBACCOUNT_OWNER`(或 `NADO_EVM_ADDRESS`)。 +3. 选择网络 `NADO_ENV=inkMainnet`(主网)或 `inkTestnet`(测试网)。 +4. 设置交易品种 `NADO_SYMBOL`(例如 `BTC-PERP`;也支持输入 `BTCUSDT0`,会自动映射为 `BTC-PERP`)。 + ## 命令速查 ```bash bun run index.ts # 启动 CLI(默认入口) diff --git a/README_en.md b/README_en.md index 57c41d1..9203d47 100644 --- a/README_en.md +++ b/README_en.md @@ -74,7 +74,7 @@ The script installs Bun, project dependencies, collects Aster API credentials, g | Variable | Purpose | | --- | --- | -| `EXCHANGE` | Choose the venue (`aster` / `grvt` / `lighter` / `backpack` / `paradex`) | +| `EXCHANGE` | Choose the venue (`aster` / `grvt` / `lighter` / `backpack` / `paradex` / `nado`) | | `TRADE_SYMBOL` | Contract symbol (defaults to `BTCUSDT`) | | `TRADE_AMOUNT` | Order size in base asset units | | `LOSS_LIMIT` | Max per-trade loss in USDT before forced close | @@ -123,6 +123,12 @@ The script installs Bun, project dependencies, collects Aster API credentials, g 3. The adapter connects to mainnet by default; enable `PARADEX_SANDBOX=true` and adjust `PARADEX_SYMBOL` for testnet usage. 4. Advanced tuning: use `PARADEX_USE_PRO`, `PARADEX_RECONNECT_DELAY_MS`, or debug flags as needed. +### Nado +1. Set `EXCHANGE=nado`. +2. Provide `NADO_SIGNER_PRIVATE_KEY` (linked signer) and `NADO_SUBACCOUNT_OWNER` (or `NADO_EVM_ADDRESS`). +3. Select network via `NADO_ENV=inkMainnet` (mainnet) or `inkTestnet` (testnet). +4. Set `NADO_SYMBOL` (for example `BTC-PERP`; `BTCUSDT0` is also accepted and mapped to `BTC-PERP`). + ## Command Cheatsheet ```bash bun run index.ts # Launch the CLI (default entrypoint) diff --git a/docs/nado/SUMMARY.md b/docs/nado/SUMMARY.md new file mode 100644 index 0000000..584bcc2 --- /dev/null +++ b/docs/nado/SUMMARY.md @@ -0,0 +1,136 @@ +# SUMMARY + +> Source: https://docs.nado.xyz + +## Pages +- https://docs.nado.xyz +- https://docs.nado.xyz/mission +- https://docs.nado.xyz/orderbook-architecture +- https://docs.nado.xyz/products +- https://docs.nado.xyz/margin-types +- https://docs.nado.xyz/subaccounts-and-health +- https://docs.nado.xyz/liquidations +- https://docs.nado.xyz/pnl-settlements +- https://docs.nado.xyz/oracles +- https://docs.nado.xyz/fees-and-rebates +- https://docs.nado.xyz/funding-rates +- https://docs.nado.xyz/order-types +- https://docs.nado.xyz/nlp +- https://docs.nado.xyz/onboarding-tutorial +- https://docs.nado.xyz/onboarding-tutorial/bridging-usdt0-to-ink +- https://docs.nado.xyz/faqs +- https://docs.nado.xyz/contracts +- https://docs.nado.xyz/legal +- https://docs.nado.xyz/legal/restricted-territories +- https://docs.nado.xyz/maintenance-windows +- https://docs.nado.xyz/market-parameters +- https://docs.nado.xyz/developer-resources/api +- https://docs.nado.xyz/developer-resources/api/endpoints +- https://docs.nado.xyz/developer-resources/api/gateway +- https://docs.nado.xyz/developer-resources/api/gateway/executes +- https://docs.nado.xyz/developer-resources/api/gateway/executes/place-order +- https://docs.nado.xyz/developer-resources/api/gateway/executes/place-orders +- https://docs.nado.xyz/developer-resources/api/gateway/executes/cancel-orders +- https://docs.nado.xyz/developer-resources/api/gateway/executes/cancel-product-orders +- https://docs.nado.xyz/developer-resources/api/gateway/executes/cancel-and-place +- https://docs.nado.xyz/developer-resources/api/gateway/executes/withdraw-collateral +- https://docs.nado.xyz/developer-resources/api/gateway/executes/transfer-quote +- https://docs.nado.xyz/developer-resources/api/gateway/executes/liquidate-subaccount +- https://docs.nado.xyz/developer-resources/api/gateway/executes/mint-nlp +- https://docs.nado.xyz/developer-resources/api/gateway/executes/burn-nlp +- https://docs.nado.xyz/developer-resources/api/gateway/executes/link-signer +- https://docs.nado.xyz/developer-resources/api/gateway/queries +- https://docs.nado.xyz/developer-resources/api/gateway/queries/status +- https://docs.nado.xyz/developer-resources/api/gateway/queries/contracts +- https://docs.nado.xyz/developer-resources/api/gateway/queries/nonces +- https://docs.nado.xyz/developer-resources/api/gateway/queries/order +- https://docs.nado.xyz/developer-resources/api/gateway/queries/orders +- https://docs.nado.xyz/developer-resources/api/gateway/queries/subaccount-info +- https://docs.nado.xyz/developer-resources/api/gateway/queries/isolated-positions +- https://docs.nado.xyz/developer-resources/api/gateway/queries/market-liquidity +- https://docs.nado.xyz/developer-resources/api/gateway/queries/symbols +- https://docs.nado.xyz/developer-resources/api/gateway/queries/all-products +- https://docs.nado.xyz/developer-resources/api/gateway/queries/edge-all-products +- https://docs.nado.xyz/developer-resources/api/gateway/queries/market-prices +- https://docs.nado.xyz/developer-resources/api/gateway/queries/max-order-size +- https://docs.nado.xyz/developer-resources/api/gateway/queries/max-withdrawable +- https://docs.nado.xyz/developer-resources/api/gateway/queries/max-nlp-mintable +- https://docs.nado.xyz/developer-resources/api/gateway/queries/max-nlp-burnable +- https://docs.nado.xyz/developer-resources/api/gateway/queries/nlp-pool-info +- https://docs.nado.xyz/developer-resources/api/gateway/queries/nlp-locked-balances +- https://docs.nado.xyz/developer-resources/api/gateway/queries/fee-rates +- https://docs.nado.xyz/developer-resources/api/gateway/queries/health-groups +- https://docs.nado.xyz/developer-resources/api/gateway/queries/linked-signer +- https://docs.nado.xyz/developer-resources/api/gateway/queries/insurance +- https://docs.nado.xyz/developer-resources/api/gateway/signing +- https://docs.nado.xyz/developer-resources/api/gateway/signing/examples +- https://docs.nado.xyz/developer-resources/api/gateway/signing/q-and-a +- https://docs.nado.xyz/developer-resources/api/subscriptions +- https://docs.nado.xyz/developer-resources/api/subscriptions/authentication +- https://docs.nado.xyz/developer-resources/api/subscriptions/streams +- https://docs.nado.xyz/developer-resources/api/subscriptions/events +- https://docs.nado.xyz/developer-resources/api/subscriptions/rate-limits +- https://docs.nado.xyz/developer-resources/api/archive-indexer +- https://docs.nado.xyz/developer-resources/api/archive-indexer/orders +- https://docs.nado.xyz/developer-resources/api/archive-indexer/matches +- https://docs.nado.xyz/developer-resources/api/archive-indexer/events +- https://docs.nado.xyz/developer-resources/api/archive-indexer/candlesticks +- https://docs.nado.xyz/developer-resources/api/archive-indexer/edge-candlesticks +- https://docs.nado.xyz/developer-resources/api/archive-indexer/product-snapshots +- https://docs.nado.xyz/developer-resources/api/archive-indexer/funding-rate +- https://docs.nado.xyz/developer-resources/api/archive-indexer/interest-and-funding-payments +- https://docs.nado.xyz/developer-resources/api/archive-indexer/oracle-price +- https://docs.nado.xyz/developer-resources/api/archive-indexer/oracle-snapshots +- https://docs.nado.xyz/developer-resources/api/archive-indexer/perp-prices +- https://docs.nado.xyz/developer-resources/api/archive-indexer/market-snapshots +- https://docs.nado.xyz/developer-resources/api/archive-indexer/edge-market-snapshots +- https://docs.nado.xyz/developer-resources/api/archive-indexer/subaccounts +- https://docs.nado.xyz/developer-resources/api/archive-indexer/subaccount-snapshots +- https://docs.nado.xyz/developer-resources/api/archive-indexer/linked-signers +- https://docs.nado.xyz/developer-resources/api/archive-indexer/linked-signer-rate-limit +- https://docs.nado.xyz/developer-resources/api/archive-indexer/isolated-subaccounts +- https://docs.nado.xyz/developer-resources/api/archive-indexer/signatures +- https://docs.nado.xyz/developer-resources/api/archive-indexer/fast-withdrawal-signature +- https://docs.nado.xyz/developer-resources/api/archive-indexer/nlp-funding-payments +- https://docs.nado.xyz/developer-resources/api/archive-indexer/nlp-interest-payments +- https://docs.nado.xyz/developer-resources/api/archive-indexer/nlp-snapshots +- https://docs.nado.xyz/developer-resources/api/archive-indexer/liquidation-feed +- https://docs.nado.xyz/developer-resources/api/archive-indexer/sequencer-backlog +- https://docs.nado.xyz/developer-resources/api/archive-indexer/direct-deposit-address +- https://docs.nado.xyz/developer-resources/api/archive-indexer/quote-price +- https://docs.nado.xyz/developer-resources/api/archive-indexer/ink-airdrop +- https://docs.nado.xyz/developer-resources/api/trigger +- https://docs.nado.xyz/developer-resources/api/trigger/executes +- https://docs.nado.xyz/developer-resources/api/trigger/executes/place-order +- https://docs.nado.xyz/developer-resources/api/trigger/executes/place-orders +- https://docs.nado.xyz/developer-resources/api/trigger/executes/cancel-orders +- https://docs.nado.xyz/developer-resources/api/trigger/executes/cancel-product-orders +- https://docs.nado.xyz/developer-resources/api/trigger/queries +- https://docs.nado.xyz/developer-resources/api/trigger/queries/list-trigger-orders +- https://docs.nado.xyz/developer-resources/api/trigger/queries/list-twap-executions +- https://docs.nado.xyz/developer-resources/api/v2 +- https://docs.nado.xyz/developer-resources/api/v2/assets +- https://docs.nado.xyz/developer-resources/api/v2/pairs +- https://docs.nado.xyz/developer-resources/api/v2/apr +- https://docs.nado.xyz/developer-resources/api/v2/orderbook +- https://docs.nado.xyz/developer-resources/api/v2/tickers +- https://docs.nado.xyz/developer-resources/api/v2/contracts +- https://docs.nado.xyz/developer-resources/api/v2/trades +- https://docs.nado.xyz/developer-resources/api/order-appendix +- https://docs.nado.xyz/developer-resources/api/rate-limits +- https://docs.nado.xyz/developer-resources/api/errors +- https://docs.nado.xyz/developer-resources/api/symbols +- https://docs.nado.xyz/developer-resources/api/depositing +- https://docs.nado.xyz/developer-resources/api/withdrawing-on-chain +- https://docs.nado.xyz/developer-resources/api/integrate-via-smart-contracts +- https://docs.nado.xyz/developer-resources/api/definitions-formulas +- https://docs.nado.xyz/developer-resources/api/api-changelog +- https://docs.nado.xyz/developer-resources/typescript-sdk +- https://docs.nado.xyz/developer-resources/typescript-sdk/getting-started +- https://docs.nado.xyz/developer-resources/typescript-sdk/how-to +- https://docs.nado.xyz/developer-resources/typescript-sdk/how-to/create-a-nado-client +- https://docs.nado.xyz/developer-resources/typescript-sdk/how-to/useful-common-functions +- https://docs.nado.xyz/developer-resources/typescript-sdk/how-to/query-markets-and-products +- https://docs.nado.xyz/developer-resources/typescript-sdk/how-to/deposit-funds +- https://docs.nado.xyz/developer-resources/typescript-sdk/how-to/withdraw-funds +- https://docs.nado.xyz/developer-resources/typescript-sdk/how-to/manage-orders \ No newline at end of file diff --git a/docs/nado/contracts.md b/docs/nado/contracts.md new file mode 100644 index 0000000..06b60d9 --- /dev/null +++ b/docs/nado/contracts.md @@ -0,0 +1,35 @@ +# Contracts + +> **Nado Github Repo**: + +### Mainnet Contracts — Ink + +> **Explorer URL**: [https://explorer.inkonchain.com](https://explorer.inkonchain.com/) + +| Contract Name | Address | +| :---------------: | :------------------------------------------------------------------------------------------------------------------------------: | +| **Deployer** | [0xC1cC56caB60e832665E6c3780BfEBe3C1C971603](https://explorer.inkonchain.com/address/0xC1cC56caB60e832665E6c3780BfEBe3C1C971603) | +| **Quote** | [0x0200C29006150606B650577BBE7B6248F58470c1](https://explorer.inkonchain.com/address/0x0200C29006150606B650577BBE7B6248F58470c1) | +| **Querier** | [0x68798229F88251b31D534733D6C4098318c9dff8](https://explorer.inkonchain.com/address/0x68798229F88251b31D534733D6C4098318c9dff8) | +| **Clearinghouse** | [0xD218103918C19D0A10cf35300E4CfAfbD444c5fE](https://explorer.inkonchain.com/address/0xD218103918C19D0A10cf35300E4CfAfbD444c5fE) | +| **Endpoint** | [0x05ec92D78ED421f3D3Ada77FFdE167106565974E](https://explorer.inkonchain.com/address/0x05ec92D78ED421f3D3Ada77FFdE167106565974E) | +| **SpotEngine** | [0xFcD94770B95fd9Cc67143132BB172EB17A0907fE](https://explorer.inkonchain.com/address/0xFcD94770B95fd9Cc67143132BB172EB17A0907fE) | +| **PerpEngine** | [0xF8599D58d1137fC56EcDd9C16ee139C8BDf96da1](https://explorer.inkonchain.com/address/0xF8599D58d1137fC56EcDd9C16ee139C8BDf96da1) | +| **WithdrawPool** | [0x09fb495AA7859635f755E827d64c4C9A2e5b9651](https://explorer.inkonchain.com/address/0x09fb495AA7859635f755E827d64c4C9A2e5b9651) | + +*** + +### Testnet Contracts — Ink Sepolia + +> **Explorer URL**: + +| | | +| :---------------: | :--------------------------------------------------------------------------------------------------------------------------------------: | +| **Deployer** | [0x59841b3761Ed1D089a783E4d7CB49E4534CD4F85](https://explorer-sepolia.inkonchain.com/address/0x59841b3761Ed1D089a783E4d7CB49E4534CD4F85) | +| **Quote** | [0x60F50F902b2E91aef7D6c700Eb22599e297fa86F](https://explorer-sepolia.inkonchain.com/address/0x60F50F902b2E91aef7D6c700Eb22599e297fa86F) | +| **Querier** | [0x8E693BEa316bcC0F4f8be403081b954a0E3743C8](https://explorer-sepolia.inkonchain.com/address/0x8E693BEa316bcC0F4f8be403081b954a0E3743C8) | +| **Clearinghouse** | [0x23a283B359D55A941bBeEC58801B6b17D955CC73](https://explorer-sepolia.inkonchain.com/address/0x23a283B359D55A941bBeEC58801B6b17D955CC73) | +| **Endpoint** | [0x698D87105274292B5673367DEC81874Ce3633Ac2](https://explorer-sepolia.inkonchain.com/address/0x698D87105274292B5673367DEC81874Ce3633Ac2) | +| **SpotEngine** | [0x3352b2fF0fAc4ce38A6eA1C188cF4F924df54E5D](https://explorer-sepolia.inkonchain.com/address/0x3352b2fF0fAc4ce38A6eA1C188cF4F924df54E5D) | +| **PerpEngine** | [0x4E859C47fea3666B5053B16C81AF64e77567702e](https://explorer-sepolia.inkonchain.com/address/0x4E859C47fea3666B5053B16C81AF64e77567702e) | +| **WithdrawPool** | [0xBD672Fe513acbA5c1ceE7b02F998A1B542852b3b](https://explorer-sepolia.inkonchain.com/address/0xBD672Fe513acbA5c1ceE7b02F998A1B542852b3b) | diff --git a/docs/nado/developer-resources/api.md b/docs/nado/developer-resources/api.md new file mode 100644 index 0000000..db8fe7b --- /dev/null +++ b/docs/nado/developer-resources/api.md @@ -0,0 +1,31 @@ +# API + +## Overview + +Nado's API is divided into the following categories: + +1. A **websocket/REST** API (**gateway**) that supports writes (executes) and polling (queries). +2. A **subscriptions** API that allows to subscribe to live data feeds. +3. An **indexer** API (**archive**) that allows you to query historical data. +4. A **trigger** API that allows to execute orders only under specified price conditions. + +{% hint style="info" %} **NOTE**: To contact the Nado team with any questions or inquiries about Private Gateway connections, such as increased limits, please reach out on the Telegram channel below: + +* **Telegram Group =** TBD + {% endhint %} + +{% content-ref url="api/gateway" %} +[gateway](https://docs.nado.xyz/developer-resources/api/gateway) +{% endcontent-ref %} + +{% content-ref url="api/subscriptions" %} +[subscriptions](https://docs.nado.xyz/developer-resources/api/subscriptions) +{% endcontent-ref %} + +{% content-ref url="api/archive-indexer" %} +[archive-indexer](https://docs.nado.xyz/developer-resources/api/archive-indexer) +{% endcontent-ref %} + +{% content-ref url="api/trigger" %} +[trigger](https://docs.nado.xyz/developer-resources/api/trigger) +{% endcontent-ref %} diff --git a/docs/nado/developer-resources/api/api-changelog.md b/docs/nado/developer-resources/api/api-changelog.md new file mode 100644 index 0000000..9d58d56 --- /dev/null +++ b/docs/nado/developer-resources/api/api-changelog.md @@ -0,0 +1,409 @@ +# API Changelog + +This document tracks all changes to the Nado API. + +*** + +
+ +December 11, 2025 + +**Risk System Updates** + +**Spread Weight Caps** + +* Introduced upper bounds for spread weights to manage risk at extreme leverage levels: + * `initial_spread_weight`: Maximum **0.99** + * `maintenance_spread_weight`: Maximum **0.994** +* **Impact**: + * Existing markets (≤20x leverage): No change in behavior + * Future high-leverage markets (30x+): Spread positions will have capped health benefits + * Prevents extreme leverage abuse via spread positions +* **Technical Details**: + * Base spread weight calculated as: `spread_weight = 1 - (1 - product_weight) / 5` + * Final spread weight: `min(spread_weight, cap)` + * Cap applies during health calculations for spread positions + +**Minimum Liquidation Penalties** + +* Introduced minimum distance requirements between oracle price and liquidation price: + * **Non-spread liquidations**: Minimum **0.5%** from oracle price + * **Spread liquidations**: Minimum **0.25%** from oracle price +* **Impact**: + * Ensures liquidators always have sufficient incentive to execute liquidations + * Prevents unprofitable liquidation scenarios for low-volatility assets + * Particularly important for high-leverage positions where natural penalties may be very small +* **Technical Details**: + * **Non-spread longs**: `oracle_price × (1 - max((1 - maint_asset_weight) / 5, 0.005))` + * **Non-spread shorts**: `oracle_price × (1 + max((maint_liability_weight - 1) / 5, 0.005))` + * **Spread selling**: `spot_price × (1 - max((1 - perp_maint_asset_weight) / 10, 0.0025))` + * **Spread buying**: `spot_price × (1 + max((spot_maint_liability_weight - 1) / 10, 0.0025))` + +**API Response Changes** + +* No breaking changes to API response structure +* Health calculations and liquidation prices automatically reflect new risk parameters + +**Documentation Updates** + +* See [Subaccounts & Health](https://docs.nado.xyz/subaccounts-and-health#spreads) for spread weight cap details +* See [Liquidations](https://docs.nado.xyz/liquidations#liquidation-price) for minimum liquidation penalty details + +
+ +*** + +
+ +December 1, 2025 + +**Query Enhancements** + +**Pre-State Simulation for SubaccountInfo Query** + +* Added `pre_state` parameter to `SubaccountInfo` query + * Type: `string` (accepts `"true"` or `"false"`) + * When set to `"true"` along with `txns`, returns a `pre_state` object in the response + * `pre_state` contains the subaccount state **before** the simulated transactions were applied + * Useful for comparing before/after states when simulating trades + * `pre_state` includes: + * `healths`: Health information before transactions + * `health_contributions`: Per-product health contributions before transactions + * `spot_balances`: Spot balances before transactions + * `perp_balances`: Perpetual balances before transactions + +**Use Cases:** + +* Position simulation and preview +* Risk analysis for potential trades +* UI/UX for showing before/after comparisons +* Testing transaction impacts without on-chain execution + +**Documentation:** See [Subaccount Info Query](https://docs.nado.xyz/developer-resources/gateway/queries/subaccount-info#example-with-pre_state) for detailed examples. + +
+ +*** + +
+ +November 20, 2025 - Initial Launch + +#### Core Changes + +**1. Removal of LP Functionality** + +* `SubaccountInfo` no longer has: + * `lp_balance` in `spot_balances` and `perp_balances` + * `lp_state` in `spot_products` and `perp_products` + * `lp_spread_x18` in `book_info` of both `spot_products` and `perp_products` +* Historical `events` no longer include: + * `net_entry_lp_unrealized` + * `net_entry_lp_cumulative` + +**2. Removal of Redundant Fields** + +* `SubaccountInfo` no longer has: + * `last_cumulative_multiplier_x18` in `balance` of `spot_balances` + +**3. Products Config Model Updates** + +* Added: `withdraw_fee_x18` and `min_deposit_rate_x18` to `spot_products.config` + +**4. Products Risk Model Updates** + +* Added: `price_x18` to both `spot_products.risk` and `perp_products.risk` +* Removed: `large_position_penalty_x18` + +**5. Deposit Rate Query** + +* Removed: `min_deposit_rates` query +* Use `min_deposit_rate_x18` in `spot_products.config` instead + +#### Market Structure Changes + +**6. Removal of Virtual Books** + +* `Contracts` query no longer returns `book_addrs` +* `PlaceOrder` verify contract is now `address(product_id)`\ + \&#xNAN;*Example: product 18`0x0000000000000000000000000000000000000012`* + +**7. Minimum Size denomination** + +* `min_size` is now **USDT0 denominated** (not base denominated) + * `min_size = 10` → minimum order size = 10 USDT0 (`order_price * order_amount`) +* `size_increment` remains **base denominated** + * Example: BTC with `size_increment = 0.0001` and `min_size = 20`: + * ✅ Valid: 100,000 \* 0.0002 = 20 USDT0 + * ❌ Invalid: 100,000 \* 0.0001 = 10 USDT0 + * ❌ Invalid: 100,000 \* 0.00025 (not multiple of 0.0001) + +#### Orders & Signing + +**8. Place Orders Execute** + +* Added: `place_orders` execute - place multiple orders in a single request + * Accepts array of orders with same structure as `place_order` + * Optional `stop_on_failure` parameter to stop processing remaining orders on first failure + * Returns array of results with `digest` (if successful) or `error` (if failed) for each order + * Rate limit weight calculated per order + +See [Place Orders](https://docs.nado.xyz/developer-resources/api/gateway/executes/place-orders) for details. + +**9. EIP712 `Order` Struct Update** + +```solidity +struct Order { + bytes32 sender; + int128 priceX18; + int128 amount; + uint64 expiration; + uint64 nonce; + uint128 appendix; +} +``` + +* New field: `appendix` +* All order flags (IOC, post only, reduce-only, triggers) moved into `appendix` +* `expiration` is now strictly a timestamp +* `appendix` bitfield: + +```json +| value | reserved | trigger | reduce only | order type | isolated | version | +| 64 bits | 50 bits | 2 bits | 1 bit | 2 bits | 1 bit | 8 bits | +``` + +* Special encodings: + * `trigger` = 2 or 3 → `value` encodes TWAP settings (`times`, `slippage_x6`) + * `isolated = 1``value` encodes isolated margin +* Constraints: + * Isolated orders cannot be TWAP + * TWAP orders must use IOC execution type + +See [Order Appendix Docs](https://docs.nado.xyz/developer-resources/api/order-appendix). + +**10. TWAP Order Execution** + +* Added `list_twap_executions` query to trigger service +* TWAP orders track individual execution status (pending, executed, failed, cancelled) +* TWAP execution statuses include execution time and engine response data + +**11. Trigger Service Rate Limits** + +* Updated trigger order limits from 100 pending orders per subaccount to `25 pending orders per product per subaccount` + +**12. EIP712 Domain Change** + +* Signing domain updated from **`Vertex` → `Nado`**\ + See [Signing Docs](https://docs.nado.xyz/developer-resources/api/gateway/signing). + +#### Query Updates + +**13. ****`max_order_size`** + +* Added: `isolated` parameter - when set to `true`, calculates max order size for an isolated margin position. Defaults to `false`. + +**14. ****`orders`**** Query** + +* Added: `trigger_types` parameter - filter orders by trigger type(s) + +**15. Historical Events** + +* Added: `quote_volume_cumulative` - tracks cumulative trading volume for the subaccount in quote units + * Available in: `events` and `subaccount_snapshots` queries + +**16. ****`subaccount_snapshots`**** Query** + +* Added: `active` parameter - filter snapshots by position status + * `true`: returns only products with **non-zero balance** at the timestamp + * `false`: returns products with **event history** before the timestamp (default) + +**17. Trigger Orders** + +* Added: `place_at` field - timestamp when trigger order should be placed + +**18. Removal of ****`summary`**** Query** + +* Removed: `summary` query from indexer API +* Use `subaccount_snapshots` query instead for historical subaccount data + +**19. Query Renaming** + +* Renamed: `usdc_price``quote_price` query + * See [Quote Price](https://docs.nado.xyz/developer-resources/api/archive-indexer/quote-price) + +**20. Multi-Subaccount `events`, `matches`, `orders`** + +* The indexer `events`, `matches`, and `orders` queries now accept a `subaccounts` array so you can fetch history for multiple subaccounts in a single request instead of fanning out per subaccount. Please note that the old single-subaccount version is **no longer supported**. + +#### Streams + +{% hint style="info" %} +See [Subscriptions > Streams](https://docs.nado.xyz/developer-resources/api/subscriptions/streams) for more details +{% endhint %} + +**21. ****`OrderUpdate`** + +* Can now subscribe across all products by setting `product_id = null` +* `product_id` type changed from `u32` → `Option` + +**22. ****`Fill`** + +* Added: `fee`, `submission_idx`, and `appendix` +* Can now subscribe across all products by setting `product_id = null` + +**23. ****`PositionChange`** + +* Can now subscribe across all products by setting `product_id = null` +* `product_id` type changed from `u32` → `Option` +* Added: `isolated` - indicates whether the position change is for an isolated margin position + +**24. ****`FundingPayment`** + +* New stream: `FundingPayment` +* Param: `product_id: u32` +* Emits hourly funding payment events + +**Request** + +```json +{ + "method": "subscribe", + "stream": { + "type": "funding_payment", + "product_id": 1 + }, + "id": 123 +} +``` + +**Response** + +```json +{ + "type": "funding_payment", + "timestamp": 1234567890000, + "product_id": 1, + "payment_amount": "1000000000000000000", + "open_interest": "50000000000000000000", + "cumulative_funding_long_x18": "100000000000000000", + "cumulative_funding_short_x18": "-100000000000000000", + "dt": 3600000 +} +``` + +**25. ****`Liquidation`** + +* New stream: `Liquidation` +* Param: `product_id` or `null` (all products) +* Emits liquidation info (liquidator, liquidatee, amount, price) + +**Request** + +```json +{ + "method": "subscribe", + "stream": { + "type": "liquidation", + "product_id": 1 + }, + "id": 123 +} +``` + +**Response** + +```json +{ + "type": "liquidation", + "timestamp": "1234567890000", + "product_ids": [1], + "liquidator": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43746573743000000000000000", + "liquidatee": "0x8b6fd3859f7065794491a8d29dcf3f9edb8d7c43746573743000000000000000", + "amount": "1000000000000000000", + "price": "50000000000000000000" +} +``` + +**26. ****`LatestCandlestick`** + +* New stream: `LatestCandlestick` +* Params: `product_id`, `granularity` (seconds) +* Emits candlestick updates on every trade + +**Request** + +```json +{ + "method": "subscribe", + "stream": { + "type": "latest_candlestick", + "product_id": 1, + "granularity": 60 + }, + "id": 123 +} +``` + +**Response** + +```json +{ + "type": "latest_candlestick", + "timestamp": 1234567890000, + "product_id": 1, + "granularity": 60, + "open_x18": "50000000000000000000", + "high_x18": "51000000000000000000", + "low_x18": "49000000000000000000", + "close_x18": "50500000000000000000", + "volume": "1000000000000000000" +} +``` + +**27. ****`FundingRate`** + +* New stream: `FundingRate` +* Param: `product_id` or `null` (all products) +* Emits funding rate updates every 20 seconds +* `funding_rate_x18` and `update_time` values are identical to those from the [Funding Rate](https://docs.nado.xyz/developer-resources/api/archive-indexer/funding-rate) indexer endpoint + +**Request** + +```json +{ + "method": "subscribe", + "stream": { + "type": "funding_rate", + "product_id": 1 + }, + "id": 123 +} +``` + +**Subscribe to all products:** + +```json +{ + "method": "subscribe", + "stream": { + "type": "funding_rate", + "product_id": null + }, + "id": 123 +} +``` + +**Response** + +```json +{ + "type": "funding_rate", + "timestamp": "1234567890123456789", + "product_id": 1, + "funding_rate_x18": "50000000000000000", + "update_time": "1234567890" +} +``` + +
diff --git a/docs/nado/developer-resources/api/archive-indexer.md b/docs/nado/developer-resources/api/archive-indexer.md new file mode 100644 index 0000000..886204a --- /dev/null +++ b/docs/nado/developer-resources/api/archive-indexer.md @@ -0,0 +1,127 @@ +# Archive (indexer) + +Using Nado's indexer API you can access historical data in the platform as it is processed by our offchain sequencer. This includes: trading activity, events, candlesticks and more. + +You can interact with our indexer by sending `HTTP` requests at `POST [ARCHIVE_ENDPOINT]` alongside a json payload of the query. Endpoints: + +`HTTP` requests must set the `Accept-Encoding` to include `gzip`, `br` or `deflate` + +## Endpoints + +### Testnet: + +* `https://archive.test.nado.xyz/v1` + +## Available Queries: + +{% content-ref url="archive-indexer/orders" %} +[orders](https://docs.nado.xyz/developer-resources/api/archive-indexer/orders) +{% endcontent-ref %} + +{% content-ref url="archive-indexer/matches" %} +[matches](https://docs.nado.xyz/developer-resources/api/archive-indexer/matches) +{% endcontent-ref %} + +{% content-ref url="archive-indexer/events" %} +[events](https://docs.nado.xyz/developer-resources/api/archive-indexer/events) +{% endcontent-ref %} + +{% content-ref url="archive-indexer/candlesticks" %} +[candlesticks](https://docs.nado.xyz/developer-resources/api/archive-indexer/candlesticks) +{% endcontent-ref %} + +{% content-ref url="archive-indexer/edge-candlesticks" %} +[edge-candlesticks](https://docs.nado.xyz/developer-resources/api/archive-indexer/edge-candlesticks) +{% endcontent-ref %} + +{% content-ref url="archive-indexer/product-snapshots" %} +[product-snapshots](https://docs.nado.xyz/developer-resources/api/archive-indexer/product-snapshots) +{% endcontent-ref %} + +{% content-ref url="archive-indexer/funding-rate" %} +[funding-rate](https://docs.nado.xyz/developer-resources/api/archive-indexer/funding-rate) +{% endcontent-ref %} + +{% content-ref url="archive-indexer/interest-and-funding-payments" %} +[interest-and-funding-payments](https://docs.nado.xyz/developer-resources/api/archive-indexer/interest-and-funding-payments) +{% endcontent-ref %} + +{% content-ref url="archive-indexer/oracle-price" %} +[oracle-price](https://docs.nado.xyz/developer-resources/api/archive-indexer/oracle-price) +{% endcontent-ref %} + +{% content-ref url="archive-indexer/oracle-snapshots" %} +[oracle-snapshots](https://docs.nado.xyz/developer-resources/api/archive-indexer/oracle-snapshots) +{% endcontent-ref %} + +{% content-ref url="archive-indexer/perp-prices" %} +[perp-prices](https://docs.nado.xyz/developer-resources/api/archive-indexer/perp-prices) +{% endcontent-ref %} + +{% content-ref url="archive-indexer/market-snapshots" %} +[market-snapshots](https://docs.nado.xyz/developer-resources/api/archive-indexer/market-snapshots) +{% endcontent-ref %} + +{% content-ref url="archive-indexer/edge-market-snapshots" %} +[edge-market-snapshots](https://docs.nado.xyz/developer-resources/api/archive-indexer/edge-market-snapshots) +{% endcontent-ref %} + +{% content-ref url="archive-indexer/subaccounts" %} +[subaccounts](https://docs.nado.xyz/developer-resources/api/archive-indexer/subaccounts) +{% endcontent-ref %} + +{% content-ref url="archive-indexer/subaccount-snapshots" %} +[subaccount-snapshots](https://docs.nado.xyz/developer-resources/api/archive-indexer/subaccount-snapshots) +{% endcontent-ref %} + +{% content-ref url="archive-indexer/linked-signers" %} +[linked-signers](https://docs.nado.xyz/developer-resources/api/archive-indexer/linked-signers) +{% endcontent-ref %} + +{% content-ref url="archive-indexer/linked-signer-rate-limit" %} +[linked-signer-rate-limit](https://docs.nado.xyz/developer-resources/api/archive-indexer/linked-signer-rate-limit) +{% endcontent-ref %} + +{% content-ref url="archive-indexer/isolated-subaccounts" %} +[isolated-subaccounts](https://docs.nado.xyz/developer-resources/api/archive-indexer/isolated-subaccounts) +{% endcontent-ref %} + +{% content-ref url="archive-indexer/signatures" %} +[signatures](https://docs.nado.xyz/developer-resources/api/archive-indexer/signatures) +{% endcontent-ref %} + +{% content-ref url="archive-indexer/fast-withdrawal-signature" %} +[fast-withdrawal-signature](https://docs.nado.xyz/developer-resources/api/archive-indexer/fast-withdrawal-signature) +{% endcontent-ref %} + +{% content-ref url="archive-indexer/nlp-funding-payments" %} +[nlp-funding-payments](https://docs.nado.xyz/developer-resources/api/archive-indexer/nlp-funding-payments) +{% endcontent-ref %} + +{% content-ref url="archive-indexer/nlp-interest-payments" %} +[nlp-interest-payments](https://docs.nado.xyz/developer-resources/api/archive-indexer/nlp-interest-payments) +{% endcontent-ref %} + +{% content-ref url="archive-indexer/nlp-snapshots" %} +[nlp-snapshots](https://docs.nado.xyz/developer-resources/api/archive-indexer/nlp-snapshots) +{% endcontent-ref %} + +{% content-ref url="archive-indexer/liquidation-feed" %} +[liquidation-feed](https://docs.nado.xyz/developer-resources/api/archive-indexer/liquidation-feed) +{% endcontent-ref %} + +{% content-ref url="archive-indexer/sequencer-backlog" %} +[sequencer-backlog](https://docs.nado.xyz/developer-resources/api/archive-indexer/sequencer-backlog) +{% endcontent-ref %} + +{% content-ref url="archive-indexer/direct-deposit-address" %} +[direct-deposit-address](https://docs.nado.xyz/developer-resources/api/archive-indexer/direct-deposit-address) +{% endcontent-ref %} + +{% content-ref url="archive-indexer/quote-price" %} +[quote-price](https://docs.nado.xyz/developer-resources/api/archive-indexer/quote-price) +{% endcontent-ref %} + +{% content-ref url="archive-indexer/ink-airdrop" %} +[ink-airdrop](https://docs.nado.xyz/developer-resources/api/archive-indexer/ink-airdrop) +{% endcontent-ref %} diff --git a/docs/nado/developer-resources/api/archive-indexer/candlesticks.md b/docs/nado/developer-resources/api/archive-indexer/candlesticks.md new file mode 100644 index 0000000..5a71c36 --- /dev/null +++ b/docs/nado/developer-resources/api/archive-indexer/candlesticks.md @@ -0,0 +1,97 @@ +# Candlesticks + +## Rate limits + +* Dynamic based on `limit` param provided (**weight = 1 + limit / 20**) + * E.g: With `limit=100`, you can make up to 400 requests per min or 66 requests / 10 secs. + +{% hint style="info" %} +See more details in [API Rate limits](https://docs.nado.xyz/developer-resources/api/rate-limits) +{% endhint %} + +## Available Granularities + +The following granularities / periods are supported (in seconds): + +| Granularity name | Granularity value (in seconds) | +| :--------------: | :----------------------------: | +| 1 minute | 60 | +| 5 minutes | 300 | +| 15 minutes | 900 | +| 1 hour | 3600 | +| 2 hours | 7200 | +| 4 hours | 14400 | +| 1 day | 86400 | +| 1 week | 604800 | +| 4 weeks | 2419200 | + +## Request + +{% tabs %} +{% tab title="Product candlesticks" %} +Query product candlesticks ordered by `timestamp` desc. + +`POST [ARCHIVE_ENDPOINT]` + +**Body** + +```json +{ + "candlesticks": { + "product_id": 1, + "granularity": 60, + "limit": 2 + } +} +``` + +{% endtab %} +{% endtabs %} + +## Request Parameters + +
ParameterTypeRequiredDescription
product_idnumberYesId of product to fetch candlesticks for.
granularitynumberYesGranularity value in seconds.
max_timenumber / stringNoWhen providing max_time (unix epoch in seconds), only return candlesticks with timestamp <= max_time
limitnumberNoMax number of candlesticks to return. defaults to 100. max possible of 500.
+ +## Response + +```json +{ + "candlesticks": [ + { + "product_id": 1, + "granularity": 60, + "submission_idx": "627709", + "timestamp": "1680118140", + "open_x18": "27235000000000000000000", + "high_x18": "27298000000000000000000", + "low_x18": "27235000000000000000000", + "close_x18": "27298000000000000000000", + "volume": "1999999999999999998" + }, + { + "product_id": 1, + "granularity": 60, + "submission_idx": "627699", + "timestamp": "1680118080", + "open_x18": "27218000000000000000000", + "high_x18": "27245000000000000000000", + "low_x18": "27218000000000000000000", + "close_x18": "27245000000000000000000", + "volume": "11852999999999999995" + } + ] +} +``` + +## Response Fields + +| Field name | Description | +| --------------- | ------------------------------------------------------------------------------------------------------------------------------- | +| submission\_idx | Id of the latest recorded transaction that contributes to the candle. | +| product\_id | Id of product candle is associated to. | +| granularity | Candle time interval, expressed in seconds, representing the aggregation period for trading volume and price data | +| open\_x18 | The first fill price of the candle, multiplied by 10^18 | +| high\_x18 | The highest recorded fill price during the defined interval of the candle, multiplied by 10^18 | +| low\_x18 | The lowest recorded fill price during the defined interval of the candle, multiplied by 10^18 | +| close\_x18 | The last price of the candle, multiplied by 10^18 | +| volume | Asset volume, which represents the absolute cumulative fill amounts during the time interval of the candle, multiplied by 10^18 | diff --git a/docs/nado/developer-resources/api/archive-indexer/direct-deposit-address.md b/docs/nado/developer-resources/api/archive-indexer/direct-deposit-address.md new file mode 100644 index 0000000..4d073b4 --- /dev/null +++ b/docs/nado/developer-resources/api/archive-indexer/direct-deposit-address.md @@ -0,0 +1,54 @@ +# Direct Deposit Address + +## Rate limits + +* 240 requests/min or 40 requests/10secs per IP address. (**weight = 10**) + +{% hint style="info" %} +See more details in [API Rate limits](https://docs.nado.xyz/developer-resources/api/rate-limits) +{% endhint %} + +## Request + +{% tabs %} +{% tab title="Direct Deposit Address" %} +Query the unique direct deposit address for a subaccount. + +`POST [ARCHIVE_ENDPOINT]` + +**Body** + +```json +{ + "direct_deposit_address": { + "subaccount": "0x79cc76364b5fb263a25bd52930e3d9788fcfeea864656661756c740000000000" + } +} +``` + +{% endtab %} +{% endtabs %} + +## Request Parameters + +
ParameterTypeRequiredDescription
subaccountstringYesHex string of the subaccount to fetch the direct deposit address for.
+ +## Response + +```json +{ + "subaccount": "0x79cc76364b5fb263a25bd52930e3d9788fcfeea864656661756c740000000000", + "deposit_address": "0x1234567890123456789012345678901234567890", + "created_at": "1683315718" +} +``` + +## Response Fields + +### Direct Deposit Address + +
Field nameDescription
subaccountHex string of the subaccount
deposit_addressUnique deposit address for this subaccount
created_atUnix epoch time in seconds when the deposit address was created
+ +{% hint style="info" %} +Direct deposit addresses allow users to deposit funds directly to their subaccount without needing to interact with the smart contract. Funds sent to this address will automatically be credited to the associated subaccount. +{% endhint %} diff --git a/docs/nado/developer-resources/api/archive-indexer/edge-candlesticks.md b/docs/nado/developer-resources/api/archive-indexer/edge-candlesticks.md new file mode 100644 index 0000000..bd6a932 --- /dev/null +++ b/docs/nado/developer-resources/api/archive-indexer/edge-candlesticks.md @@ -0,0 +1,97 @@ +# Edge Candlesticks + +## Rate limits + +* Dynamic based on `limit` param provided (**weight = 1 + limit / 20**) + * E.g: With `limit=100`, you can make up to 400 requests per min or 66 requests / 10 secs. + +{% hint style="info" %} +See more details in [API Rate limits](https://docs.nado.xyz/developer-resources/api/rate-limits) +{% endhint %} + +## Available Granularities + +The following granularities / periods are supported (in seconds): + +| Granularity name | Granularity value (in seconds) | +| :--------------: | :----------------------------: | +| 1 minute | 60 | +| 5 minutes | 300 | +| 15 minutes | 900 | +| 1 hour | 3600 | +| 2 hours | 7200 | +| 4 hours | 14400 | +| 1 day | 86400 | +| 1 week | 604800 | +| 4 weeks | 2419200 | + +## Request + +{% tabs %} +{% tab title="Ede candlesticks" %} +Query edge candlesticks ordered by `timestamp` desc. + +`POST [ARCHIVE_ENDPOINT]` + +**Body** + +```json +{ + "edge_candlesticks": { + "product_id": 1, + "granularity": 60, + "limit": 2 + } +} +``` + +{% endtab %} +{% endtabs %} + +## Request Parameters + +
ParameterTypeRequiredDescription
product_idnumberYesId of product to fetch candlesticks for.
granularitynumberYesGranularity value in seconds.
max_timenumber / stringNoWhen providing max_time (unix epoch in seconds), only return candlesticks with timestamp <= max_time
limitnumberNoMax number of candlesticks to return. defaults to 100. max possible of 500.
+ +## Response + +```json +{ + "candlesticks": [ + { + "product_id": 1, + "granularity": 60, + "submission_idx": "627709", + "timestamp": "1680118140", + "open_x18": "27235000000000000000000", + "high_x18": "27298000000000000000000", + "low_x18": "27235000000000000000000", + "close_x18": "27298000000000000000000", + "volume": "1999999999999999998" + }, + { + "product_id": 1, + "granularity": 60, + "submission_idx": "627699", + "timestamp": "1680118080", + "open_x18": "27218000000000000000000", + "high_x18": "27245000000000000000000", + "low_x18": "27218000000000000000000", + "close_x18": "27245000000000000000000", + "volume": "11852999999999999995" + } + ] +} +``` + +## Response Fields + +| Field name | Description | +| --------------- | ------------------------------------------------------------------------------------------------------------------------------- | +| submission\_idx | Id of the latest recorded transaction that contributes to the candle. | +| product\_id | Id of product candle is associated to. | +| granularity | Candle time interval, expressed in seconds, representing the aggregation period for trading volume and price data | +| open\_x18 | The first fill price of the candle, multiplied by 10^18 | +| high\_x18 | The highest recorded fill price during the defined interval of the candle, multiplied by 10^18 | +| low\_x18 | The lowest recorded fill price during the defined interval of the candle, multiplied by 10^18 | +| close\_x18 | The last price of the candle, multiplied by 10^18 | +| volume | Asset volume, which represents the absolute cumulative fill amounts during the time interval of the candle, multiplied by 10^18 | diff --git a/docs/nado/developer-resources/api/archive-indexer/edge-market-snapshots.md b/docs/nado/developer-resources/api/archive-indexer/edge-market-snapshots.md new file mode 100644 index 0000000..514937d --- /dev/null +++ b/docs/nado/developer-resources/api/archive-indexer/edge-market-snapshots.md @@ -0,0 +1,193 @@ +# Edge Market Snapshots + +## Rate limits + +**Dynamic based on interval.count.** + +* IP weight = `(interval.count.min(500) / 20) + (interval.count.clamp(2, 20) * 2)` +* Scales mainly with interval count. + * Example: `interval.count=500 → weight=65`, `interval.count=100 → weight=45`. +* Minimum weight per request is `4`. + +{% hint style="info" %} +See more details in [API Rate limits](https://docs.nado.xyz/developer-resources/api/rate-limits) +{% endhint %} + +### Request + +{% tabs %} +{% tab title="Market snapshots" %} +Query market snapshots ordered by `timestamp` desc. + +`POST [ARCHIVE_ENDPOINT]` + +**Body** + +```json +{ + "edge_market_snapshots": { + "interval": { + "count": 2, + "granularity": 3600, + "max_time": 1691083697, + }, + "product_ids": [1, 2] + } +} +``` + +{% endtab %} +{% endtabs %} + +### Request Parameters + +
ParameterTypeRequiredDescription
intervalobjectYesObject to specify desired time period for data
interval.countnumberYesNumber of snapshots to return, limit 100. Also limited to interval.count * # product_ids < 2000
interval.granularitynumberYesGranularity value in seconds
interval.max_timenumber / stringNoWhen providing max_time (unix epoch in seconds), only return snapshots with timestamp <= max_time. If no value is entered, max_time defaults to the current time.
product_idsnumber[]Nolist of product ids to fetch snapshots for, defaults to all products
+ +### Response + +{% hint style="info" %} +**Note**: + +* Returns a mapping of `chain_id -> snapshots` + {% endhint %} + +```json +{ + "snapshots": { + "42161": [ + { + "timestamp": 1689965194, + "cumulative_users": 2774, + "daily_active_users": 251, + "cumulative_trades": { + "1": 54287, + "2": 172435 + }, + "cumulative_volumes": { + "1": "259549132367035103631071564", + "2": "1134008547778337985156988339" + }, + "cumulative_trade_sizes": { + "1": "9209508999999999995173", + "2": "40246259000000000000000" + }, + "cumulative_taker_fees": { + "1": "88916428908427788322799", + "2": "259205794197801680292645" + }, + "cumulative_sequencer_fees": { + "1": "11038200000000000000000", + "2": "32353000000000000000000" + }, + "cumulative_maker_fees": { + "1": "-12421730086012739050725", + "2": "-36124007075181485948604" + }, + "cumulative_liquidation_amounts": { + "1": "848311398835000694508", + "2": "1013231566414935056343898" + }, + "open_interests": { + "2": "2907581091676822842104781" + }, + "total_deposits": { + "1": "37722308770940799414" + }, + "total_borrows": { + "1": "1441397740941092000" + }, + "funding_rates": { + "2": "3611102723387" + }, + "deposit_rates": { + "1": "1001376785714" + }, + "borrow_rates": { + "1": "32059880416879" + }, + "cumulative_inflows": { + "1": "238791614019999999853", + "2": "0" + }, + "cumulative_outflows": { + "1": "-202514202990000000306", + "2": "0" + }, + "tvl": "7560079507311601381352742" + } + ], + "5000": [ + { + "timestamp": 1689965194, + "cumulative_users": 2774, + "daily_active_users": 251, + "cumulative_trades": { + "1": 54287, + "2": 172435 + }, + "cumulative_volumes": { + "1": "259549132367035103631071564", + "2": "1134008547778337985156988339" + }, + "cumulative_trade_sizes": { + "1": "9209508999999999995173", + "2": "40246259000000000000000" + }, + "cumulative_taker_fees": { + "1": "88916428908427788322799", + "2": "259205794197801680292645" + }, + "cumulative_sequencer_fees": { + "1": "11038200000000000000000", + "2": "32353000000000000000000" + }, + "cumulative_maker_fees": { + "1": "-12421730086012739050725", + "2": "-36124007075181485948604" + }, + "cumulative_liquidation_amounts": { + "1": "848311398835000694508", + "2": "1013231566414935056343898" + }, + "open_interests": { + "2": "2907581091676822842104781" + }, + "total_deposits": { + "1": "37722308770940799414" + }, + "total_borrows": { + "1": "1441397740941092000" + }, + "funding_rates": { + "2": "3611102723387" + }, + "deposit_rates": { + "1": "1001376785714" + }, + "borrow_rates": { + "1": "32059880416879" + }, + "cumulative_inflows": { + "1": "238791614019999999853", + "2": "0" + }, + "cumulative_outflows": { + "1": "-202514202990000000306", + "2": "0" + }, + "tvl": "7560079507311601381352742" + } + ] + } +} +``` + +### Response Fields + +#### Snapshots + +{% hint style="info" %} +**Note**: For product specific fields (i.e. cumulative\_volume, open\_interests), the value is an object which maps product\_ids to their corresponding values. +{% endhint %} + +
Field nameDescription
timestampTimestamp of the snapshot. This may not be perfectly rounded to the granularity since it uses the nearest transaction timestamp less than or equal to max_time
cumulative_usersThe cumulative number of subaccounts on Nado. It is updated daily at 9AM ET for historical counts. For current day counts, it is updated every hour.
daily_active_usersDaily active users count, updated daily at 9AM ET for historical counts. For current day counts, it is updated every hour.
cumulative_tradesA map of product_id -> the cumulative number of trades for the given product_id.
cumulative_volumesA map of product_id -> cumulative volumes in USDT0 units.
cumulative_trade_sizesA map of product_id -> cumulative trade sizes in base token
cumulative_taker_feesA map of product_id -> cumulative taker fees. Taker fees include sequencer fees.
cumulative_sequencer_feesA map of product_id -> cumulative sequencer fees.
cumulative_maker_feesA map of product_id -> cumulative maker rebates.
cumulative_liquidation_amountsA map of product_id -> cumulative liquidation amounts in USDT0 units.
open_interestsA map of product_id -> open interests in USDT0 units.
total_depositsA map of product_id -> total deposits held by Nado for a given product at the given time in the base token units.
total_borrowsA map of product_id -> total borrows lent by Nado for a given product at the given time in the base token units.
funding_ratesA map of product_id -> hourly historical funding rates, value returned as decimal rates (% = rate * 100), derived from funding payment amounts. Requires a minimum granularity of 3600 to see non-zero funding rates. Use a granularity where granularity % 3600 = 0 for best results.
deposit_ratesA map of product_id -> daily deposit rates, values returned as decimal rates (% = rate * 100).
borrow_ratesA map of product_id -> daily borrow rates, values returned as decimal rates (% = rate * 100).
cumulative_inflowsA map of product_id -> cumulative inflows a.k.a deposits in base token units.
cumulative_outflowsA map of product_id -> cumulative outflows a.k.a withdraws in base token units.
tvlThe total value locked in USD.
diff --git a/docs/nado/developer-resources/api/archive-indexer/events.md b/docs/nado/developer-resources/api/archive-indexer/events.md new file mode 100644 index 0000000..5beb2a7 --- /dev/null +++ b/docs/nado/developer-resources/api/archive-indexer/events.md @@ -0,0 +1,268 @@ +# Events + +## Rate limits + +* IP weight = `2 + (limit * subaccounts.length / 10)` where `limit` defaults to 100 (max 500) and `subaccounts.length` defaults to 1 + * E.g: With `limit=100` and 1 subaccount, weight = 12, allowing up to 200 requests per min or 33 requests / 10 secs. + +{% hint style="info" %} +See more details in [API Rate limits](https://docs.nado.xyz/developer-resources/api/rate-limits) +{% endhint %} + +## Available Events + +Each event corresponds to a transaction type in Nado. See below available events and their `event_type` mapping: + +| Event Name | Event Type Value | +| :---------------------------------------------------: | :-------------------: | +| `LiquidateSubaccount` | liquidate\_subaccount | +| `DepositCollateral` | deposit\_collateral | +| `WithdrawCollateral` | withdraw\_collateral | +| `SettlePnl` | settle\_pnl | +| `MatchOrders` | match\_orders | +| `MintLp` | mint\_lp | +| `BurnLp` | burn\_lp | + +## Event Limits + +You can specify 2 types of `limit` on the query: + +* `raw`: the max number of events to return. +* `txs`: the max number of transactions to return. **note**: one transaction can emit multiple events, by specifying this limit, you will get all the events associated to the transactions in the response. + +## Request + +{% tabs %} +{% tab title="Events by subaccount" %} +Query events corresponding to specific subaccounts, ordered by `submission index` desc. E.g: all `MatchOrder` events for subaccounts `xxx` specific to spot wBTC. + +`POST [ARCHIVE_ENDPOINT]` + +**Body** + +```json +{ + "events": { + "product_ids": [ + 1 + ], + "subaccounts": [ + "0x12a0b4888021576eb10a67616dd3dd3d9ce206b664656661756c740000000000" + ], + "event_types": ["match_orders"], + "max_time": 1679728762, + "limit": { + "raw": 1 + }, + "isolated": false + } +} +``` + +{% endtab %} + +{% tab title="Events by product" %} +Query events corresponding to specific products, ordered by `submission index` desc. Uses `txs` limit, will only return a single `tx` and one or more events associated with the `tx`. + +`POST [ARCHIVE_ENDPOINT]` + +**Body** + +```json +{ + "events": { + "product_ids": [ + 1, + 2 + ], + "max_time": "1679728762", + "limit": { + "txs": 1 + } + } +} +``` + +{% endtab %} + +{% tab title="Events by type" %} +Query events corresponding to specific types, ordered by `submission index` desc. + +`POST [ARCHIVE_ENDPOINT]` + +**Body** + +```json +{ + "events": { + "event_types": ["deposit_collateral", "withdraw_collateral"], + "max_time": "1679728762", + "limit": { + "raw": 1 + } + } +} +``` + +{% endtab %} + +{% tab title="All events" %} +Query all events ordered by `submission index` desc. + +`POST [ARCHIVE_ENDPOINT]` + +**Body** + +```json +{ + "events": { + "max_time": "1679728762", + "limit": { + "raw": 1 + } + } +} +``` + +{% endtab %} +{% endtabs %} + +## Request Parameters + +
ParameterTypeRequiredDescription
subaccountsstring[]NoArray of bytes32 sent as hex strings; each includes the address and the subaccount identifier. When provided, only return events for the specified subaccounts.
product_idsnumber[]Nowhen provided, only return events for the specified product ids; return events for all products otherwise.
event_typesstring[]Nowhen provided, only return events for the specified event types; return all events otherwise.
idxnumber / stringNowhen provided, only return events with submission_idx <= idx
max_timenumber / stringNowhen idx is not provided, max_time (unix epoch in seconds) can be used to only return events created <= max_time
limit

object
{"raw": number } or

{"txs": number }

No
  • specifying raw limit: max number of events to return. defaults to 100. max possible of 500.
  • specifying txs limit: max number of txs to return.
isolatedboolNoWhen provided --
- true: only returns evens associated to isolated positions.
- false: only return events associated to the cross-subaccount.
defaults to null. In which case it returns everything.

See Isolated Margin to learn more.
+ +## Response + +{% hint style="info" %} +**Note:** + +* the response includes a `txs` field which contains the relevant transactions to the events. There are `>=1 events` per transaction. +* both `events` and `txs` are in descending order by `submission_idx``.` +* use the `submission_idx` to associate an `event` to it's corresponding transaction. + {% endhint %} + +```json +{ + "events": [ + { + "subaccount": "0x12a0b4888021576eb10a67616dd3dd3d9ce206b664656661756c740000000000", + "product_id": 1, + "submission_idx": "563011", + "event_type": "match_orders", + "isolated": false, + "isolated_product_id": null, + "pre_balance": { + "spot": { + "product_id": 1, + "balance": { + "amount": "26766781157882079846319" + } + } + }, + "post_balance": { + "spot": { + "product_id": 1, + "balance": { + "amount": "26767505157882079846318", + "last_cumulative_multiplier_x18": "1001292804799204317" + } + } + }, + "product": { + "product_id": 1, + "oracle_price_x18": "115575316424148798147115", + "risk": { + "long_weight_initial_x18": "900000000000000000", + "short_weight_initial_x18": "1100000000000000000", + "long_weight_maintenance_x18": "950000000000000000", + "short_weight_maintenance_x18": "1050000000000000000", + "price_x18": "115575316424148798147115" + }, + "config": { + "token": "0xc57c1c64561a37ac9e8f9039cb6deab7539d99fc", + "interest_inflection_util_x18": "800000000000000000", + "interest_floor_x18": "10000000000000000", + "interest_small_cap_x18": "40000000000000000", + "interest_large_cap_x18": "1000000000000000000", + "withdraw_fee_x18": "40000000000000", + "min_deposit_rate_x18": "0" + }, + "state": { + "cumulative_deposits_multiplier_x18": "1000000000000318713", + "cumulative_borrows_multiplier_x18": "1000347390679880473", + "total_deposits_normalized": "9000399823280682696107190850", + "total_borrows_normalized": "9580268570661550719" + }, + "book_info": { + "size_increment": "1000000000000000", + "price_increment_x18": "1000000000000000000", + "min_size": "4000000000000000", + "collected_fees": "0" + } + }, + "net_interest_unrealized": "49040544804593257", + "net_interest_cumulative": "51596254598679857", + "net_funding_unrealized": "0", + "net_funding_cumulative": "0", + "net_entry_unrealized": "748947727410369682388339518", + "net_entry_cumulative": "749148081870171307027129958", + "quote_volume_cumulative": "1234567890123456789" + } + ], + "txs": [ + { + "tx": { + "match_orders": { + "product_id": 1, + "amm": true, + "taker": { + "order": { + "sender": "0x12a0b4888021576eb10a67616dd3dd3d9ce206b664656661756c740000000000", + "price_x18": "27540000000000000000000", + "amount": "2000000000000000000", + "appendix": "1537", + "expiration": 4611686020107120000, + "nonce": 1761322602510418000 + }, + "signature": "0x826c68f1a3f76d9ffbe8041f8d45e969d31f1ab6f2ae2f6379d1493e479e56436091d6cf4c72e212dd2f1d2fa17c627c4c21bd6d281c77172b8af030488478b71c" + }, + "maker": { + "order": { + "sender": "0xf8d240d9514c9a4715d66268d7af3b53d619642564656661756c740000000000", + "price_x18": "27540000000000000000000", + "amount": "-724000000000000000", + "appendix": "1537", + "expiration": 1679731656, + "nonce": 1761322565506171000 + }, + "signature": "0xd8b6505b8d9b8c3cbfe793080976388035682c02a27893fb26b48a5b2bfe943f4162dea3a42e24e0dff5e2f74fbf77e33d83619140a2a581117c55e6cc236bdb1c" + } + } + }, + "submission_idx": "563011", + "timestamp": "1679728127" + } + ] +} +``` + +## Response Fields + +### Events + +{% hint style="info" %} + +* **Net cumulative**: the net difference in that quantity since the beginning of time. For example, if I want to compute total amount paid out in funding between two events, you can subtract the `net_funding_cumulative` of the larger event by the `net_funding_cumulative` of the smaller event. +* **Net unrealized**: similar to `net_cumulative`, but for `net_unrealized`, we have the caveat that when the magnitude of your position decreases, the magnitude of net\_unrealized `decreases` by the same amount. + {% endhint %} + +
Field nameDescription
submission_idxUsed to uniquely identify the blockchain transaction that generated the event; you can use it to grab the relevant transaction in the txs section.
product_idThe id of of the product the event is associated with.
event_typeName of the transaction type this event corresponds to.
subaccountThe subaccount associated to the event.
pre_balanceThe state of your balance before the event happened.
post_balanceThe state of your balance after the event happened.
productThe state of the product throughout the event.
+ +### Txs + +| Field name | Description | +| --------------- | --------------------------------------------------------------------------------------- | +| submission\_idx | Unique identifier of the transaction. | +| product\_id | Product associated to the transaction. | +| tx | Raw data of the corresponding transaction e.g: `match_orders` with all associated data. | +| timestamp | The unix epoch in seconds of when the transaction took place. | diff --git a/docs/nado/developer-resources/api/archive-indexer/fast-withdrawal-signature.md b/docs/nado/developer-resources/api/archive-indexer/fast-withdrawal-signature.md new file mode 100644 index 0000000..b8f0e15 --- /dev/null +++ b/docs/nado/developer-resources/api/archive-indexer/fast-withdrawal-signature.md @@ -0,0 +1,53 @@ +# Fast Withdrawal Signature + +## Rate limits + +* 240 requests/min or 40 requests/10secs per IP address. (**weight = 10**) + +{% hint style="info" %} +See more details in [API Rate limits](https://docs.nado.xyz/developer-resources/api/rate-limits) +{% endhint %} + +## Request + +{% tabs %} +{% tab title="Fast Withdrawal Signature" %} +Query the signature required for a fast withdrawal at a specific submission index. + +`POST [ARCHIVE_ENDPOINT]` + +**Body** + +```json +{ + "fast_withdrawal_signature": { + "idx": "12345" + } +} +``` + +{% endtab %} +{% endtabs %} + +## Request Parameters + +
ParameterTypeRequiredDescription
idxnumber / stringYesSubmission index to fetch the fast withdrawal signature for.
+ +## Response + +```json +{ + "signature": "0x1234567890abcdef...", + "submission_idx": "12345", + "subaccount": "0x79cc76364b5fb263a25bd52930e3d9788fcfeea864656661756c740000000000", + "product_id": 0, + "amount": "1000000000000000000", + "nonce": "1" +} +``` + +## Response Fields + +### Fast Withdrawal Signature + +
Field nameDescription
signatureHex string of the signature for fast withdrawal
submission_idxTransaction submission index
subaccountHex string of the subaccount
product_idProduct ID (0 for quote asset)
amountWithdrawal amount (x18 format)
nonceNonce for the withdrawal transaction
diff --git a/docs/nado/developer-resources/api/archive-indexer/funding-rate.md b/docs/nado/developer-resources/api/archive-indexer/funding-rate.md new file mode 100644 index 0000000..5731373 --- /dev/null +++ b/docs/nado/developer-resources/api/archive-indexer/funding-rate.md @@ -0,0 +1,94 @@ +# Funding Rate + +## Rate limits + +* 1200 requests/min or 20 requests/sec per IP address. (**weight = 2**) + +{% hint style="info" %} +See more details in [API Rate limits](https://docs.nado.xyz/developer-resources/api/rate-limits) +{% endhint %} + +## Single Product + +### Request + +{% tabs %} +{% tab title="Funding Rate" %} +Query perp product 24hr funding rate. + +`POST [ARCHIVE_ENDPOINT]` + +**Body** + +```json +{ + "funding_rate": { + "product_id": 2 + } +} +``` + +{% endtab %} +{% endtabs %} + +### Request Parameters + +
ParameterTypeRequiredDescription
product_idnumberYesId of perp product to fetch funding rate for.
+ +### Response + +```json +{ + "product_id": 2, + "funding_rate_x18": "2447900598160952", + "update_time": "1680116326" +} +``` + +## Multiple Products + +### Request + +{% tabs %} +{% tab title="Perp Prices" %} `POST [ARCHIVE_ENDPOINT]` + +**Body** + +```json +{ + "funding_rates": { + "product_ids": [2] + } +} +``` + +{% endtab %} +{% endtabs %} + +### Request Parameters + +
ParameterTypeRequiredDescription
product_idsnumber[]YesIds of perp products to fetch funding rate for.
+ +### Response + +{% hint style="info" %} +**Note**: the response is a map of `product_id -> funding_rate` for each requested product. +{% endhint %} + +```json +{ + "2": { + "product_id": 2, + "funding_rate_x18": "-697407056090986", + "update_time": "1692825387" + } +} +``` + +## Response Fields + +| Field name | Description | +| ------------------ | ----------------------------------------------------------------------- | +| product\_id | Id of the perp product this funding rate corresponds to. | +| funding\_rate\_x18 | Latest 24hr funding rate for the specified product, multiplied by 10^18 | +| update\_time | Epoch time in seconds this funding rate was last updated at | diff --git a/docs/nado/developer-resources/api/archive-indexer/ink-airdrop.md b/docs/nado/developer-resources/api/archive-indexer/ink-airdrop.md new file mode 100644 index 0000000..6b14775 --- /dev/null +++ b/docs/nado/developer-resources/api/archive-indexer/ink-airdrop.md @@ -0,0 +1,51 @@ +# Ink Airdrop + +Query the Ink token airdrop allocation for a specific wallet address. + +## Rate limits + +* 1200 requests/min or 200 requests/10secs per IP address. (**weight = 2**) + +{% hint style="info" %} +See more details in [API Rate limits](https://docs.nado.xyz/developer-resources/api/rate-limits) +{% endhint %} + +## Request + +{% tabs %} +{% tab title="Ink Airdrop" %} `POST [ARCHIVE_ENDPOINT]` + +**Body** + +```json +{ + "ink_airdrop": { + "address": "0x1234567890123456789012345678901234567890" + } +} +``` + +{% endtab %} +{% endtabs %} + +## Request Parameters + +
ParameterTypeRequiredDescription
addressstringYesWallet address (20-byte address) sent as a hex string.
+ +## Response + +{% hint style="info" %} +**Note**: The amount is returned as a string to preserve precision. +{% endhint %} + +```json +{ + "amount": "1000000000000000000" +} +``` + +## Response Fields + +| Field name | Description | +| ---------- | ------------------------------------------------------ | +| amount | The Ink token airdrop amount allocated to the address. | diff --git a/docs/nado/developer-resources/api/archive-indexer/interest-and-funding-payments.md b/docs/nado/developer-resources/api/archive-indexer/interest-and-funding-payments.md new file mode 100644 index 0000000..860da95 --- /dev/null +++ b/docs/nado/developer-resources/api/archive-indexer/interest-and-funding-payments.md @@ -0,0 +1,89 @@ +# Interest & funding payments + +## Rate limits + +* 480 requests/min or 80 requests/10secs per IP address. (**weight = 5**) + +{% hint style="info" %} +See more details in [API Rate limits](https://docs.nado.xyz/developer-resources/api/rate-limits) +{% endhint %} + +## Request + +{% tabs %} +{% tab title="Interest and funding" %} +Query subaccount historical interest and funding payments. + +`POST [ARCHIVE_ENDPOINT]` + +**Body** + +```json +{ + "interest_and_funding": { + "subaccount": "0xD028878bF5c96218E53DA859e587cb8398B17b3f64656661756c740000000000", + "product_ids": [1, 2], + "limit": 10, + "max_idx": 1315836 + } +} +``` + +{% endtab %} +{% endtabs %} + +## Request Parameters + +
ParameterTypeRequiredDescription
subaccountstringYesA bytes32 sent as a hex string; includes the address and the subaccount identifier.
product_idsnumber[]YesIds of products to historical interest/funding payments for.
max_idxstring/numberNoWhen provided, only return records with idx <= max_idx.
limitnumberYesMax number of records to return. Max possible of 100.
+ +## Response + +```json +{ + "interest_payments": [ + { + "product_id": 4, + "idx": "5968022", + "timestamp": "1701698400", + "amount": "-12273223338657163", + "balance_amount": "1000000000000000000", + "rate_x18": "47928279191008320", + "oracle_price_x18": "2243215034242228224820" + }, + ... + ], + "funding_payments": [ + { + "product_id": 2, + "idx": "5968022", + "timestamp": "1701698400", + "amount": "-12273223338657163", + "balance_amount": "1000000000000000000", + "rate_x18": "47928279191008320", + "oracle_price_x18": "2243215034242228224820" + }, + ... + ], + "next_idx": "1314805" +} +``` + +## Response Fields + +| Field name | Description | +| ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| interest\_payments.product\_id | Id of spot product the interest payment is associated to. | +| interest\_payments.idx | Id of transaction that triggered the interest payment. | +| interest\_payments.timestamp | Timestamp of the transaction that triggered the interest payment. | +| interest\_payments.amount | Amount of interest paid multiplied by 10\*\*18. | +| interest\_payments.balance\_amount | Previous spot balance at the moment of payment (exclusive of payment amount) | +| interest\_payments.rate\_x18 | Spot interest rate at the moment of payment, multiplied by 10\*\*18. | +| interest\_payments.oracle\_price\_x18 | Oracle price for the spot product at the moment of payment, multiplied by 10\*\*18. | +| funding\_payments.product\_id | Id of perp product the funding payment is associated to. | +| funding\_payments.idx | Id of transaction that triggered the funding payment. | +| funding\_payments.timestamp | Timestamp of the transaction that triggered the funding payment. | +| funding\_payments.amount | Amount of funding paid multiplied by 10\*\*18. | +| funding\_payments.balance\_amount | Previous perp balance at the moment of payment +amount of perps locked in LPs (exclusive of payment amount). | +| funding\_payments.rate\_x18 | Perp funding rate at the moment of payment, multiplied by 10\*\*18. | +| funding\_payments.oracle\_price\_x18 | Oracle price for the perp product at the moment of payment, multiplied by 10\*\*18. | +| next\_idx | Id of the next payment snapshot. Use this as `max_idx` on a subsequent call to get the next page. This will be `null` when there are no more records. | diff --git a/docs/nado/developer-resources/api/archive-indexer/isolated-subaccounts.md b/docs/nado/developer-resources/api/archive-indexer/isolated-subaccounts.md new file mode 100644 index 0000000..65283bf --- /dev/null +++ b/docs/nado/developer-resources/api/archive-indexer/isolated-subaccounts.md @@ -0,0 +1,74 @@ +# Isolated Subaccounts + +## Rate limits + +* 1200 requests/min or 200 requests/10secs per IP address. (**weight = 2**) + +{% hint style="info" %} +See more details in [API Rate limits](https://docs.nado.xyz/developer-resources/api/rate-limits) +{% endhint %} + +## Request + +{% tabs %} +{% tab title="List all isolated subaccounts" %} +Query all isolated subaccounts. + +`POST [ARCHIVE_ENDPOINT]` + +**Body** + +```json +{ + "isolated_subaccounts": { + "start_idx": 0, + "limit": 100 + } +} +``` + +{% endtab %} + +{% tab title="List isolated subaccounts for a subaccount" %} +Query isolated subaccounts associated with a specific subaccount. + +`POST [ARCHIVE_ENDPOINT]` + +**Body** + +```json +{ + "isolated_subaccounts": { + "subaccount": "0x79cc76364b5fb263a25bd52930e3d9788fcfeea864656661756c740000000000", + "limit": 100 + } +} +``` + +{% endtab %} +{% endtabs %} + +## Request Parameters + +
ParameterTypeRequiredDescription
subaccountstringNoHex string of the parent subaccount to filter by.
start_idxnumber / stringNoStarting index for pagination. Defaults to 0.
limitnumberNoMax number of isolated subaccounts to return. Defaults to 100. Max of 500.
+ +## Response + +```json +{ + "isolated_subaccounts": [ + { + "subaccount": "0x79cc76364b5fb263a25bd52930e3d9788fcfeea864656661756c740000000000", + "isolated_subaccount": "0x79cc76364b5fb263a25bd52930e3d9788fcfeea800000000000000010069736f", + "product_id": 1, + "created_at": "1683315718" + } + ] +} +``` + +## Response Fields + +### Isolated Subaccounts + +
Field nameDescription
subaccountHex string of the parent subaccount
isolated_subaccountHex string of the isolated margin subaccount
product_idProduct ID for which this isolated subaccount was created
created_atUnix epoch time in seconds when the isolated subaccount was created
diff --git a/docs/nado/developer-resources/api/archive-indexer/linked-signer-rate-limit.md b/docs/nado/developer-resources/api/archive-indexer/linked-signer-rate-limit.md new file mode 100644 index 0000000..bcfa7e0 --- /dev/null +++ b/docs/nado/developer-resources/api/archive-indexer/linked-signer-rate-limit.md @@ -0,0 +1,52 @@ +# Linked Signer Rate Limit + +A subaccount can perform a max of 50 [LinkSigner](https://docs.nado.xyz/developer-resources/api/gateway/executes/link-signer) requests in 7 days. Use this query to check current usage and wait time. + +## Rate limits + +* 1200 requests/min or 200 requests/10secs per IP address. (**weight = 2**) + +{% hint style="info" %} +See more details in [API Rate limits](https://docs.nado.xyz/developer-resources/api/rate-limits) +{% endhint %} + +## Request + +{% tabs %} +{% tab title="Link Signer Rate Limit" %} +Queries a subaccount's linked signer rate limits. + +`POST [ARCHIVE_ENDPOINT]` + +**Body** + +```json +{ + "linked_signer_rate_limit": { + "subaccount": "0x9b9989a4E0b260B84a5f367d636298a8bfFb7a9b42544353504f540000000000" + } +} +``` + +{% endtab %} +{% endtabs %} + +## Response + +```json +{ + "remaining_tx": "50", + "wait_time": 0, + "signer": "0x0000000000000000000000000000000000000000", + "total_tx_limit": "50" +} +``` + +{% hint style="info" %} +**Notes**: + +* `remaining_tx`: keeps track of the remaining `LinkSigner` executes that can be performed. +* `total_tx_limit`: that max weekly tx limit. +* `wait_time`: the total seconds you need to wait before performing another `LinkSigner` execute. Can only perform another request when `wait_time` is `0`. +* `signer`: the current linked signer address (20 bytes) associated to the provided `subaccount`. It returns the zero address when no signer is linked. + {% endhint %} diff --git a/docs/nado/developer-resources/api/archive-indexer/linked-signers.md b/docs/nado/developer-resources/api/archive-indexer/linked-signers.md new file mode 100644 index 0000000..b0f3224 --- /dev/null +++ b/docs/nado/developer-resources/api/archive-indexer/linked-signers.md @@ -0,0 +1,55 @@ +# Linked Signers + +## Rate limits + +* 1200 requests/min or 200 requests/10secs per IP address. (**weight = 2**) + +{% hint style="info" %} +See more details in [API Rate limits](https://docs.nado.xyz/developer-resources/api/rate-limits) +{% endhint %} + +## Request + +{% tabs %} +{% tab title="List linked signers" %} +Query linked signers ordered by creation time. + +`POST [ARCHIVE_ENDPOINT]` + +**Body** + +```json +{ + "linked_signers": { + "start_idx": 0, + "limit": 100 + } +} +``` + +{% endtab %} +{% endtabs %} + +## Request Parameters + +
ParameterTypeRequiredDescription
start_idxnumber / stringNoStarting index for pagination. Defaults to 0.
limitnumberNoMax number of linked signers to return. Defaults to 100. Max of 500.
+ +## Response + +```json +{ + "linked_signers": [ + { + "subaccount": "0x79cc76364b5fb263a25bd52930e3d9788fcfeea864656661756c740000000000", + "signer": "0x1234567890123456789012345678901234567890", + "created_at": "1683315718" + } + ] +} +``` + +## Response Fields + +### Linked Signers + +
Field nameDescription
subaccountHex string of the subaccount
signerHex string of the linked signer address
created_atUnix epoch time in seconds when the signer was linked
diff --git a/docs/nado/developer-resources/api/archive-indexer/liquidation-feed.md b/docs/nado/developer-resources/api/archive-indexer/liquidation-feed.md new file mode 100644 index 0000000..64ea3b7 --- /dev/null +++ b/docs/nado/developer-resources/api/archive-indexer/liquidation-feed.md @@ -0,0 +1,58 @@ +# Liquidation Feed + +## Rate limits + +* 1200 requests/min or 200 requests/10secs per IP address. (**weight = 2**) + +{% hint style="info" %} +See more details in [API Rate limits](https://docs.nado.xyz/developer-resources/api/rate-limits) +{% endhint %} + +## Request + +{% tabs %} +{% tab title="Liquidation feed" %} +Queries liquidatable accounts. + +`POST [ARCHIVE_ENDPOINT]` + +**Body** + +```json +{ + "liquidation_feed": {} +} +``` + +{% endtab %} +{% endtabs %} + +## Response + +```json +[ + { + "subaccount": "0xf2b7cec33cac30582b94979bf03a3cbc73954b2c64656661756c740000000000", + "update_time": 1680118943 + }, + { + "subaccount": "0xcb6f1e2ece124a150dcc681c180df2a890432d6a64656661756c740000000000", + "update_time": 1680118943 + }, + { + "subaccount": "0x9e6e13be7ea2866c2c7c6e4a118a6c05eee6b44e64656661756c740000000000", + "update_time": 1680118943 + }, + { + "subaccount": "0x75008754ffae2889c055961c1b0c5c3ab743c59664656661756c740000000000", + "update_time": 1680118943 + } +] +``` + +## Response Fields + +| Field name | Description | +| ------------ | ------------------------------------ | +| subaccount | Subaccount eligible for liquidation. | +| update\_time | Last time feed was updated. | diff --git a/docs/nado/developer-resources/api/archive-indexer/market-snapshots.md b/docs/nado/developer-resources/api/archive-indexer/market-snapshots.md new file mode 100644 index 0000000..c0edc6f --- /dev/null +++ b/docs/nado/developer-resources/api/archive-indexer/market-snapshots.md @@ -0,0 +1,124 @@ +# Market Snapshots + +## Rate limits + +* IP weight = `max((snapshot_count * product_ids.length / 100), 2)` where `snapshot_count = interval.count.min(500)`. If no `product_ids` are specified, `product_ids.length = 100`. + * E.g: With `product_ids=[1, 2, 3, 4]` and `interval.count=60`, weight = max((60 \* 4 / 100), 2) = 2, allowing up to 1200 requests per min or 200 requests/10 secs. + +{% hint style="info" %} +See more details in [API Rate limits](https://docs.nado.xyz/developer-resources/api/rate-limits) +{% endhint %} + +### Request + +{% tabs %} +{% tab title="Market snapshots" %} +Query market snapshots ordered by `timestamp` desc. + +`POST [ARCHIVE_ENDPOINT]` + +**Body** + +```json +{ + "market_snapshots": { + "interval": { + "count": 2, + "granularity": 3600, + "max_time": 1691083697, + }, + "product_ids": [1, 2] + } +} +``` + +{% endtab %} +{% endtabs %} + +### Request Parameters + +
ParameterTypeRequiredDescription
intervalobjectYesObject to specify desired time period for data
interval.countnumberYesNumber of snapshots to return, limit 100. Also limited to interval.count * # product_ids < 2000
interval.granularitynumberYesGranularity value in seconds
interval.max_timenumber / stringNoWhen providing max_time (unix epoch in seconds), only return snapshots with timestamp <= max_time. If no value is entered, max_time defaults to the current time.
product_idsnumber[]Nolist of product ids to fetch snapshots for, defaults to all products
+ +### Response + +{% hint style="info" %} +**Note**: Please note that this endpoint is currently in beta stage. This feature might be subject to changes without prior notice. +{% endhint %} + +```json +{ + "snapshots": [ + { + "timestamp": 1689965194, + "cumulative_users": 2774, + "daily_active_users": 251, + "cumulative_trades": { + "1": 54287, + "2": 172435 + }, + "cumulative_volumes": { + "1": "259549132367035103631071564", + "2": "1134008547778337985156988339" + }, + "cumulative_trade_sizes": { + "1": "9209508999999999995173", + "2": "40246259000000000000000" + }, + "cumulative_taker_fees": { + "1": "88916428908427788322799", + "2": "259205794197801680292645" + }, + "cumulative_sequencer_fees": { + "1": "11038200000000000000000", + "2": "32353000000000000000000" + }, + "cumulative_maker_fees": { + "1": "-12421730086012739050725", + "2": "-36124007075181485948604" + }, + "cumulative_liquidation_amounts": { + "1": "848311398835000694508", + "2": "1013231566414935056343898" + }, + "open_interests": { + "2": "2907581091676822842104781" + }, + "total_deposits": { + "1": "37722308770940799414" + }, + "total_borrows": { + "1": "1441397740941092000" + }, + "funding_rates": { + "2": "3611102723387" + }, + "deposit_rates": { + "1": "1001376785714" + }, + "borrow_rates": { + "1": "32059880416879" + }, + "cumulative_inflows": { + "1": "238791614019999999853", + "2": "0" + }, + "cumulative_outflows": { + "1": "-202514202990000000306", + "2": "0" + }, + "tvl": "7560079507311601381352742" + }, + ... + ] +} +``` + +### Response Fields + +#### Snapshots + +{% hint style="info" %} +**Note**: For product specific fields (i.e. cumulative\_volume, open\_interests), the value is an object which maps product\_ids to their corresponding values. +{% endhint %} + +
Field nameDescription
timestampTimestamp of the snapshot. This may not be perfectly rounded to the granularity since it uses the nearest transaction timestamp less than or equal to max_time
cumulative_usersThe cumulative number of subaccounts on Nado. It is updated daily at 9AM ET for historical counts. For current day counts, it is updated every hour.
daily_active_usersDaily active users count, updated daily at 9AM ET for historical counts. For current day counts, it is updated every hour.
cumulative_tradesA map of product_id -> the cumulative number of trades for the given product_id.
cumulative_volumesA map of product_id -> cumulative volumes in USDT0 units.
cumulative_trade_sizesA map of product_id -> cumulative trade sizes in base token
cumulative_taker_feesA map of product_id -> cumulative taker fees. Taker fees include sequencer fees.
cumulative_sequencer_feesA map of product_id -> cumulative sequencer fees.
cumulative_maker_feesA map of product_id -> cumulative maker rebates.
cumulative_liquidation_amountsA map of product_id -> cumulative liquidation amounts in USDT0 units.
open_interestsA map of product_id -> open interests in USDT0 units.
total_depositsA map of product_id -> total deposits held by Nado for a given product at the given time in the base token units.
total_borrowsA map of product_id -> total borrows lent by Nado for a given product at the given time in the base token units.
funding_ratesA map of product_id -> hourly historical funding rates, value returned as decimal rates (% = rate * 100), derived from funding payment amounts. Requires a minimum granularity of 3600 to see non-zero funding rates. Use a granularity where granularity % 3600 = 0 for best results.
deposit_ratesA map of product_id -> daily deposit rates, values returned as decimal rates (% = rate * 100).
borrow_ratesA map of product_id -> daily borrow rates, values returned as decimal rates (% = rate * 100).
cumulative_inflowsA map of product_id -> cumulative inflows a.k.a deposits in base token units.
cumulative_outflowsA map of product_id -> cumulative outflows a.k.a withdraws in base token units.
tvlThe total value locked in USD.
diff --git a/docs/nado/developer-resources/api/archive-indexer/matches.md b/docs/nado/developer-resources/api/archive-indexer/matches.md new file mode 100644 index 0000000..a247341 --- /dev/null +++ b/docs/nado/developer-resources/api/archive-indexer/matches.md @@ -0,0 +1,279 @@ +# Matches + +## Rate limits + +* IP weight = `2 + (limit * subaccounts.length / 10)` where `limit` defaults to 100 (max 500) and `subaccounts.length` defaults to 1 + * E.g: With `limit=100` and 1 subaccount, weight = 12, allowing up to 200 requests per min or 33 requests / 10 secs. + +{% hint style="info" %} +See more details in [API Rate limits](https://docs.nado.xyz/developer-resources/api/rate-limits) +{% endhint %} + +## Request + +{% tabs %} +{% tab title="Matches by subaccount" %} +Query subaccounts matches ordered by `submission index` desc. Response includes order fill and fee information. + +`POST [ARCHIVE_ENDPOINT]` + +**Body** + +```json +{ + "matches": { + "product_ids": [ + 1, + 2 + ], + "subaccounts": [ + "0x12a0b4888021576eb10a67616dd3dd3d9ce206b664656661756c740000000000" + ], + "max_time": 1679728762, + "limit": 5, + "isolated": false + } +} +``` + +{% endtab %} + +{% tab title="Matches by product" %} +Query matches for provided products ordered by `submission index` desc. Response includes order fill and fee information. + +`POST [ARCHIVE_ENDPOINT]` + +**Body** + +```json +{ + "matches": { + "product_ids": [ + 1, + 2 + ], + "max_time": "1679728762", + "limit": 5 + } +} +``` + +{% endtab %} +{% endtabs %} + +## Request Parameters + +
ParameterTypeRequiredDescription
subaccountsstring[]NoArray of bytes32 sent as hex strings; each includes the address and the subaccount identifier. When provided, only return matches for the specified subaccounts.
product_idsnumber[]NoWhen provided, only return matches for the specified product ids; return matches for all products otherwise.
idxnumber / stringNoWhen provided, only return matches with submission_idx <= idx
max_timenumber / stringNoWhen idx is not provided, max_time (unix epoch in seconds) can be used to only return matches created <= max_time
limitnumberNoMax number of matches to return. defaults to 100. max possible of 500.
isolatedbooleanNoWhen provided --
- true: only returns matches associated to isolated positions.
- false: only return matches associated to the cross-subaccount.
defaults to null. In which case it returns everything.

See Isolated Margin to learn more.
+ +## Response + +{% hint style="info" %} +**Note:** + +* the response includes a `txs` field which contains the relevant transactions for the returned matches. There are `>=1 match events` per transaction. +* both `matches` and `txs` are in descending order by `submission_idx``.` +* use the `submission_idx` to associate a match to it's corresponding transaction. +* the `fee` provided in the response includes taker / maker fees + sequencer fees. See [fees](https://github.com/nadohq/nado-docs/blob/main/docs/basics/fees.md) for more details. + {% endhint %} + +```json +{ + "matches": [ + { + "digest": "0x80ce789702b670b7d33f2aa67e12c85f124395c3f9acdb422dde3b4973ccd50c", + "order": { + "sender": "0x12a0b4888021576eb10a67616dd3dd3d9ce206b664656661756c740000000000", + "priceX18": "27544000000000000000000", + "amount": "2000000000000000000", + "expiration": "4611686020107119633", + "nonce": "1761322608857448448" + }, + "base_filled": "736000000000000000", + "quote_filled": "-20276464287857571514302", + "fee": "4055287857571514302", + "sequencer_fee": "0", + "cumulative_fee": "4055287857571514302", + "cumulative_base_filled": "736000000000000000", + "cumulative_quote_filled": "-20276464287857571514302", + "submission_idx": "563012", + "isolated": false, + "is_taker": true, + "pre_balance": { + "base": { + "perp": { + "product_id": 2, + "balance": { + "amount": "2686684000000000000000", + "v_quote_balance": "-76348662407149297671587247", + "last_cumulative_funding_x18": "134999841911604906604576" + } + } + }, + "quote": null + }, + "post_balance": { + "base": { + "perp": { + "product_id": 2, + "balance": { + "amount": "2686013000000000000000", + "v_quote_balance": "-76328351274188497671587247", + "last_cumulative_funding_x18": "134999841911604906604576" + } + } + }, + "quote": null + } + }, + { + "digest": "0x0f6e5a0434e36d8e6d4fed950d3624b0d8c91a8a84efd156bb25c1382561c0c2", + "order": { + "sender": "0x12a0b4888021576eb10a67616dd3dd3d9ce206b664656661756c740000000000", + "priceX18": "27540000000000000000000", + "amount": "2000000000000000000", + "appendix": "1537", + "expiration": "4611686020107119623", + "nonce": "1761322602510417920" + }, + "base_filled": "723999999999999999", + "quote_filled": "-19944943483044913474043", + "fee": "5983483044913474042", + "sequencer_fee": "0", + "cumulative_fee": "11958484645393618085", + "cumulative_base_filled": "1446999999999999998", + "cumulative_quote_filled": "-39861640484645393618087", + "submission_idx": "563011", + "isolated": false, + "is_taker": true, + "pre_balance": { + "base": { + "perp": { + "product_id": 2, + "balance": { + "amount": "2686684000000000000000", + "v_quote_balance": "-76348662407149297671587247", + "last_cumulative_funding_x18": "134999841911604906604576" + } + } + }, + "quote": null + }, + "post_balance": { + "base": { + "perp": { + "product_id": 2, + "balance": { + "amount": "2686013000000000000000", + "v_quote_balance": "-76328351274188497671587247", + "last_cumulative_funding_x18": "134999841911604906604576" + } + } + }, + "quote": null + } + } + ], + "txs": [ + { + "tx": { + "match_orders": { + "product_id": 2, + "amm": true, + "taker": { + "order": { + "sender": "0x12a0b4888021576eb10a67616dd3dd3d9ce206b664656661756c740000000000", + "price_x18": "27544000000000000000000", + "amount": "2000000000000000000", + "expiration": 4611686020107120000, + "appendix": "1537", + "nonce": 1761322608857448400 + }, + "signature": "0xe8fa7151bde348afa3b46dc52798046b7c8318f1b0a7f689710debbc094658cc1bf5a7e478ccc8278b625da0b9402c86b580d2e31e13831337dfd6153f4b37811b" + }, + "maker": { + "order": { + "sender": "0xebdbbcdbd2646c5f23a1e0806027eee5f71b074664656661756c740000000000", + "price_x18": "27544000000000000000000", + "amount": "-736000000000000000", + "expiration": 1679731669, + "appendix": "1537", + "nonce": 1761322585591644200 + }, + "signature": "0x47f9d47f0777f3ca0b13f07b7682dbeea098c0e377b87dcb025754fe34c900e336b8c7744e021fb9c46a4f8c6a1478bafa28bf0d023ae496aa3efa4d8e81df181c" + } + } + }, + "submission_idx": "563012", + "timestamp": "1679728133" + }, + { + "tx": { + "match_orders": { + "product_id": 1, + "amm": true, + "taker": { + "order": { + "sender": "0x12a0b4888021576eb10a67616dd3dd3d9ce206b664656661756c740000000000", + "price_x18": "27540000000000000000000", + "amount": "2000000000000000000", + "appendix": "1537", + "expiration": 4611686020107120000, + "nonce": 1761322602510418000 + }, + "signature": "0x826c68f1a3f76d9ffbe8041f8d45e969d31f1ab6f2ae2f6379d1493e479e56436091d6cf4c72e212dd2f1d2fa17c627c4c21bd6d281c77172b8af030488478b71c" + }, + "maker": { + "order": { + "sender": "0xf8d240d9514c9a4715d66268d7af3b53d619642564656661756c740000000000", + "price_x18": "27540000000000000000000", + "amount": "-724000000000000000", + "appendix": "1537", + "expiration": 1679731656, + "nonce": 1761322565506171000 + }, + "signature": "0xd8b6505b8d9b8c3cbfe793080976388035682c02a27893fb26b48a5b2bfe943f4162dea3a42e24e0dff5e2f74fbf77e33d83619140a2a581117c55e6cc236bdb1c" + } + } + }, + "submission_idx": "563011", + "timestamp": "1679728127" + } + ] +} +``` + +## Response Fields + +### Matches + +| Field name | Description | +| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | +| submission\_idx | Wsed to uniquely identify the blockchain transaction that generated the match; you can use it to grab the relevant transaction in the `txs` section. | +| isolated | Whether the match is associated with an isolated position. `true` for isolated positions, `false` for cross-subaccount positions. | +| is\_taker | Whether the order in this match was the taker. `true` if the order was the taker, `false` if the order was the maker. | +| digest | The unique hash of the order. | +| order.sender | The sender that placed the order. | +| order.priceX18 | The original order price. | +| order.amount | The original order amount. | +| order.expiration | The original order expiration. | +| order.nonce | The original order nonce. | +| order.appendix | The original order appendix. | +| pre\_balance | The state of your balance before the match happened. | +| post\_balance | The state of your balance after the match happened. | +| base\_filled | The amount of base (e.g: BTC) filled on this match. | +| quote\_filled | The amount of quote (e.g: USDT0) filled on this match. | +| fee | The amount of trading fees + sequencer fees paid on this match. | +| sequencer\_fee | The amount of sequencer fees paid on this match. | +| cumulative\_base\_filled | The total amount of base (e.g: BTC) filled on this order up this match. | +| cumulative\_quote\_filled | The total amount of quote (e.g: USDT0) filled up to this match. | +| cumulative\_fee | The total amount of fee paid up to this match. | + +### Txs + +| Field name | Description | +| --------------- | ------------------------------------------------------------- | +| submission\_idx | Unique identifier of the transaction. | +| product\_id | Product associated to the transaction. | +| taker | The taker order. | +| maker | The maker order. | +| timestamp | The unix epoch in seconds of when the transaction took place. | diff --git a/docs/nado/developer-resources/api/archive-indexer/nlp-funding-payments.md b/docs/nado/developer-resources/api/archive-indexer/nlp-funding-payments.md new file mode 100644 index 0000000..78d44ca --- /dev/null +++ b/docs/nado/developer-resources/api/archive-indexer/nlp-funding-payments.md @@ -0,0 +1,60 @@ +# NLP Funding Payments + +## Rate limits + +* 480 requests/min or 80 requests/10secs per IP address. (**weight = 5**) + +{% hint style="info" %} +See more details in [API Rate limits](https://docs.nado.xyz/developer-resources/api/rate-limits) +{% endhint %} + +## Request + +{% tabs %} +{% tab title="NLP Funding Payments" %} +Query historical NLP funding payments. + +`POST [ARCHIVE_ENDPOINT]` + +**Body** + +```json +{ + "nlp_funding_payments": { + "max_idx": "1315836", + "max_time": "1683315718", + "limit": 100 + } +} +``` + +{% endtab %} +{% endtabs %} + +## Request Parameters + +
ParameterTypeRequiredDescription
max_idxnumber / stringNoWhen provided, only return payments with idx <= max_idx.
max_timenumber / stringNoWhen provided, only return payments with timestamp <= max_time (unix epoch in seconds).
limitnumberNoMax number of payments to return. Defaults to 100. Max of 500.
+ +## Response + +```json +{ + "funding_payments": [ + { + "product_id": 2, + "idx": "5968022", + "timestamp": "1701698400", + "total_payment": "12273223338657163", + "rate_x18": "47928279191008320", + "oracle_price_x18": "2243215034242228224820" + } + ], + "next_idx": "5968021" +} +``` + +## Response Fields + +### Funding Payments + +
Field nameDescription
product_idId of the perp product
idxSubmission index of the transaction that triggered the payment
timestampUnix epoch time in seconds when the payment occurred
total_paymentTotal funding payment amount (x18 format)
rate_x18Funding rate used for calculation (x18 format)
oracle_price_x18Oracle price at the time of payment (x18 format)
diff --git a/docs/nado/developer-resources/api/archive-indexer/nlp-interest-payments.md b/docs/nado/developer-resources/api/archive-indexer/nlp-interest-payments.md new file mode 100644 index 0000000..a666d48 --- /dev/null +++ b/docs/nado/developer-resources/api/archive-indexer/nlp-interest-payments.md @@ -0,0 +1,59 @@ +# NLP Interest Payments + +## Rate limits + +* 480 requests/min or 80 requests/10secs per IP address. (**weight = 5**) + +{% hint style="info" %} +See more details in [API Rate limits](https://docs.nado.xyz/developer-resources/api/rate-limits) +{% endhint %} + +## Request + +{% tabs %} +{% tab title="NLP Interest Payments" %} +Query historical NLP interest payments. + +`POST [ARCHIVE_ENDPOINT]` + +**Body** + +```json +{ + "nlp_interest_payments": { + "max_idx": "1315836", + "max_time": "1683315718", + "limit": 100 + } +} +``` + +{% endtab %} +{% endtabs %} + +## Request Parameters + +
ParameterTypeRequiredDescription
max_idxnumber / stringNoWhen provided, only return payments with idx <= max_idx.
max_timenumber / stringNoWhen provided, only return payments with timestamp <= max_time (unix epoch in seconds).
limitnumberNoMax number of payments to return. Defaults to 100. Max of 500.
+ +## Response + +```json +{ + "interest_payments": [ + { + "product_id": 0, + "idx": "5968022", + "timestamp": "1701698400", + "amount": "12273223338657163", + "balance_amount": "45382847293847329847" + } + ], + "next_idx": "5968021" +} +``` + +## Response Fields + +### Interest Payments + +
Field nameDescription
product_idId of the spot product (typically quote/collateral products)
idxSubmission index of the transaction that triggered the payment
timestampUnix epoch time in seconds when the payment occurred
amountInterest payment amount (x18 format)
balance_amountBalance amount at the time of payment (x18 format)
diff --git a/docs/nado/developer-resources/api/archive-indexer/nlp-snapshots.md b/docs/nado/developer-resources/api/archive-indexer/nlp-snapshots.md new file mode 100644 index 0000000..0df9999 --- /dev/null +++ b/docs/nado/developer-resources/api/archive-indexer/nlp-snapshots.md @@ -0,0 +1,82 @@ +# NLP Snapshots + +## Rate limits + +* Dynamic based on snapshot count (**weight = (limit.min(500) / 100)**) + * E.g: With `limit=100`, weight = 1 + * E.g: With `limit=500`, weight = 5 + +{% hint style="info" %} +See more details in [API Rate limits](https://docs.nado.xyz/developer-resources/api/rate-limits) +{% endhint %} + +## Request + +{% tabs %} +{% tab title="By interval" %} +Query NLP snapshots at specific time intervals. + +`POST [ARCHIVE_ENDPOINT]` + +**Body** + +```json +{ + "nlp_snapshots": { + "interval": { + "count": 10, + "max_time": "1683315718", + "granularity": 3600 + } + } +} +``` + +{% endtab %} + +{% tab title="By pagination" %} +Query NLP snapshots with pagination. + +`POST [ARCHIVE_ENDPOINT]` + +**Body** + +```json +{ + "nlp_snapshots": { + "idx": "12345", + "max_time": "1683315718", + "limit": 100 + } +} +``` + +{% endtab %} +{% endtabs %} + +## Request Parameters + +
ParameterTypeRequiredDescription
intervalobjectNoObject specifying time interval parameters: count, max_time, granularity
idxnumber / stringNoSubmission index for pagination.
max_timenumber / stringNoUnix epoch time in seconds. Only return snapshots with timestamp <= max_time
limitnumberNoMax number of snapshots to return. Defaults to 100. Max of 500.
+ +## Response + +```json +{ + "snapshots": [ + { + "submission_idx": "12345", + "timestamp": "1683315718", + "total_deposits": "1000000000000000000000", + "total_borrows": "500000000000000000000", + "base_interest_rate": "50000000000000000", + "quote_interest_rate": "30000000000000000" + } + ] +} +``` + +## Response Fields + +### NLP Snapshots + +
Field nameDescription
submission_idxTransaction submission index
timestampUnix epoch time in seconds when snapshot was taken
total_depositsTotal deposits in the NLP pool (x18 format)
total_borrowsTotal borrows from the NLP pool (x18 format)
base_interest_rateInterest rate for base assets (x18 format)
quote_interest_rateInterest rate for quote assets (x18 format)
diff --git a/docs/nado/developer-resources/api/archive-indexer/oracle-price.md b/docs/nado/developer-resources/api/archive-indexer/oracle-price.md new file mode 100644 index 0000000..c1313ea --- /dev/null +++ b/docs/nado/developer-resources/api/archive-indexer/oracle-price.md @@ -0,0 +1,70 @@ +# Oracle Price + +## Rate limits + +* 1200 requests/min or 200 requests/10secs per IP address. (**weight = 2**) + +{% hint style="info" %} +See more details in [API Rate limits](https://docs.nado.xyz/developer-resources/api/rate-limits) +{% endhint %} + +## Request + +{% tabs %} +{% tab title="Oracle Price" %} `POST [ARCHIVE_ENDPOINT]` + +**Body** + +```json +{ + "oracle_price": { + "product_ids": [1, 2, 3, 4] + } +} +``` + +{% endtab %} +{% endtabs %} + +## Request Parameters + +
ParameterTypeRequiredDescription
product_idsnumber[]YesIds of products to fetch oracles price for.
+ +## Response + +```json +{ + "prices": [ + { + "product_id": 1, + "oracle_price_x18": "29464023750000000000000", + "update_time": "1683315718" + }, + { + "product_id": 2, + "oracle_price_x18": "29430225194712740000000", + "update_time": "1683315721" + }, + { + "product_id": 3, + "oracle_price_x18": "1983367400000000000000", + "update_time": "1683315720" + }, + { + "product_id": 4, + "oracle_price_x18": "1981528989642697000000", + "update_time": "1683315721" + } + ] +} +``` + +## Response Fields + +### Prices + +| Field name | Description | +| ------------------ | ------------------------------------------------------ | +| product\_id | Id of product oracle price corresponds to. | +| oracle\_price\_x18 | Latest oracle price multiplied by 10^18. | +| update\_time | Epoch in seconds the oracle price was last updated at. | diff --git a/docs/nado/developer-resources/api/archive-indexer/oracle-snapshots.md b/docs/nado/developer-resources/api/archive-indexer/oracle-snapshots.md new file mode 100644 index 0000000..7f1deb8 --- /dev/null +++ b/docs/nado/developer-resources/api/archive-indexer/oracle-snapshots.md @@ -0,0 +1,67 @@ +# Oracle Snapshots + +## Rate limits + +* IP weight = `max((snapshot_count * product_ids.length / 100), 2)` where `snapshot_count = interval.count.min(500)`. If no `product_ids` are specified, `product_ids.length = 100`. + * E.g: With `product_ids=[1, 2, 3, 4]` and `interval.count=60`, weight = max((60 \* 4 / 100), 2) = 2, allowing up to 1200 requests per min or 200 requests/10 secs. + +{% hint style="info" %} +See more details in [API Rate limits](https://docs.nado.xyz/developer-resources/api/rate-limits) +{% endhint %} + +## Request + +{% tabs %} +{% tab title="Oracle Price" %} +Query oracle snapshots ordered by `timestamp` desc. + +`POST [ARCHIVE_ENDPOINT]` + +**Body** + +```json +{ + "oracle_snapshots": { + "interval": { + "count": 2, + "granularity": 3600, + "max_time": 1691083697, + }, + "product_ids": [1, 2] + } +} +``` + +{% endtab %} +{% endtabs %} + +### Request Parameters + +
ParameterTypeRequiredDescription
intervalobjectYesObject to specify desired time period for data
interval.countnumberYesNumber of snapshots to return, limit 100. Also limited to interval.count * # product_ids < 2000
interval.granularitynumberYesGranularity value in seconds
interval.max_timenumber / stringNoWhen providing max_time (unix epoch in seconds), only return snapshots with timestamp <= max_time. If no value is entered, max_time defaults to the current time.
product_idsnumber[]Nolist of product ids to fetch snapshots for, defaults to all products
+ +## Response + +{% hint style="info" %} +**Note**: Returns a map of `product_id -> oracle_price` +{% endhint %} + +```json +{ + "snapshots": [ + { + "timestamp": 1750947789, + "oracle_prices": { + "1": "107070085854928675234384", + "2": "107142264360834928244199" + } + }, + { + "timestamp": 1750946389, + "oracle_prices": { + "1": "106963557680819440289916", + "2": "106954360458642468300594" + } + } + ] +} +``` diff --git a/docs/nado/developer-resources/api/archive-indexer/orders.md b/docs/nado/developer-resources/api/archive-indexer/orders.md new file mode 100644 index 0000000..896ed8a --- /dev/null +++ b/docs/nado/developer-resources/api/archive-indexer/orders.md @@ -0,0 +1,127 @@ +# Orders + +## Rate limits + +* IP weight = `2 + (limit * subaccounts.length / 20)` where `limit` defaults to 100 (max 500) and `subaccounts.length` defaults to 1 + +{% hint style="info" %} +See more details in [API Rate limits](https://docs.nado.xyz/developer-resources/api/rate-limits) +{% endhint %} + +## Request + +{% tabs %} +{% tab title="Subaccount orders" %} +Query subaccounts `matched` orders, ordered by `submission index` desc. + +`POST [ARCHIVE_ENDPOINT]` + +**Body** + +```json +{ + "orders": { + "product_ids": [ + 1, + 2 + ], + "subaccounts": [ + "0x12a0b4888021576eb10a67616dd3dd3d9ce206b664656661756c740000000000" + ], + "max_time": 1679728762, + "trigger_types": [ + "price_trigger", + "time_trigger" + ], + "isolated": false, + "limit": 5 + } +} +``` + +{% endtab %} + +{% tab title="Orders by digests" %} +Query orders by digests. + +`POST [ARCHIVE_ENDPOINT]` + +**Body** + +
{
+  "orders": {
+    "digests": [
+      "0xf4f7a8767faf0c7f72251a1f9e5da590f708fd9842bf8fcdeacbaa0237958fff",
+      "0x0495a88fb3b1c9bed9b643b8e264a391d04cdd48890d81cd7c4006473f28e361"
+    ]
+  }
+}
+
+ +{% endtab %} +{% endtabs %} + +## Request Parameters + +
ParameterTypeRequiredDescription
subaccountsstring[]conditionalArray of bytes32 sent as hex strings; each includes the address and the subaccount identifier. Must be provided when querying by subaccounts.
product_idsnumber[]NoWhen provided, only return orders for the specified product ids; return orders for all products otherwise.
idxnumber / stringNoWhen provided, only return orders with submission_idx <= idx
max_timenumber / stringNoWhen idx is not provided, max_time (unix epoch in seconds) can be used to only return orders created <= max_time
digestsstring[]conditionalMust be provided when querying by digests. only return orders matching the specified digests. note: cannot specify digests alongside with subaccounts , product_ids or max_time
trigger_typesstring[]NoWhen provided, only return orders matching the specified trigger types. Possible values: price_trigger, time_trigger, none. If not provided, returns orders of all trigger types.
limitnumberNoMax number of orders to return. defaults to 100. max possible of 500. note: when querying by digests limit must be <= total digests provided
isolatedboolNo

When provided --

  • true: only returns orders associated to isolated positions.
  • false: only return matches associated to the cross-subaccount.

defaults to null. In which case it returns everything.

See Isolated Margin to learn more.

+ +## Response + +```json +{ + "orders": [ + { + "digest": "0xf4f7a8767faf0c7f72251a1f9e5da590f708fd9842bf8fcdeacbaa0237958fff", + "subaccount": "0x12a0b4888021576eb10a67616dd3dd3d9ce206b664656661756c740000000000", + "product_id": 1, + "submission_idx": "563024", + "last_fill_submission_idx": "563024", + "amount": "20000000000000000000", + "price_x18": "1751900000000000000000", + "base_filled": "2320000000000000000", + "quote_filled": "-4064898974794958991797", + "fee": "812974794958991797", + "expiration": "4611686020107120163", + "appendix": "1537", + "nonce": "1761323164913106944", + "isolated": false + }, + { + "digest": "0x0495a88fb3b1c9bed9b643b8e264a391d04cdd48890d81cd7c4006473f28e361", + "subaccount": "0x12a0b4888021576eb10a67616dd3dd3d9ce206b664656661756c740000000000", + "product_id": 2, + "submission_idx": "563019", + "last_fill_submission_idx": "563019", + "amount": "-20000000000000000000", + "price_x18": "1750800000000000000000", + "base_filled": "-1159999999999999999", + "quote_filled": "2030293721599999999999", + "fee": "609278400000000000", + "expiration": "4611686020107119905", + "appendix": "1537", + "nonce": "1761322893628669952", + "isolated": false + }, + { + "digest": "0x29078702ad95615f0040eafdccc85cbf92569bf9656be928f9f17c5ccbb52041", + "subaccount": "0x12a0b4888021576eb10a67616dd3dd3d9ce206b664656661756c740000000000", + "product_id": 2, + "submission_idx": "563018", + "last_fill_submission_idx": "563018", + "amount": "-20000000000000000000", + "price_x18": "1750700000000000000000", + "base_filled": "-1160000000000000000", + "quote_filled": "2030380837600000000000", + "fee": "406162400000000000", + "expiration": "4611686020107119880", + "appendix": "1537", + "nonce": "1761322865074896896", + "isolated": false + }, + ] +} +``` + +## Response Fields + +
Field nameDescription
digestThe unique hash of the order.
subaccountThe subaccount that placed the order.
product_idThe id of of the product the order was executed for.
submission_idxUsed to uniquely identify the blockchain transaction that generated the order. For multi-fills orders, this is the submission_idx of the first fill.
last_fill_submission_idxFor multi-fills orders, this is the submission_idx of the last fill. For single fill orders, it has the same value as submission_idx.
amountThe original amount of base to buy or sell.
price_x18The original order price.
base_filledThe total amount of base (e.g: BTC) filled on this order.
quote_filledThe total amount of quote (e.g: USDT0) filled on this order.
feeThe total amount of fee paid on this order.
expirationThe original order expiration.
nonceThe original order nonce.
appendixThe original order appendix.
diff --git a/docs/nado/developer-resources/api/archive-indexer/perp-prices.md b/docs/nado/developer-resources/api/archive-indexer/perp-prices.md new file mode 100644 index 0000000..4b157c1 --- /dev/null +++ b/docs/nado/developer-resources/api/archive-indexer/perp-prices.md @@ -0,0 +1,94 @@ +# Perp Prices + +## Rate limits + +* 1200 requests/min or 200 requests/10secs per IP address. (**weight = 2**) + +{% hint style="info" %} +See more details in [API Rate limits](https://docs.nado.xyz/developer-resources/api/rate-limits) +{% endhint %} + +## Single Product + +### Request + +{% tabs %} +{% tab title="Perp Prices" %} `POST [ARCHIVE_ENDPOINT]` + +**Body** + +```json +{ + "price": { + "product_id": 2 + } +} +``` + +{% endtab %} +{% endtabs %} + +### Request Parameters + +
ParameterTypeRequiredDescription
product_idnumberYesId of perp product to fetch prices for.
+ +### Response + +```json +{ + "product_id": 2, + "index_price_x18": "28180063400000000000000", + "mark_price_x18": "28492853627394637978665", + "update_time": "1680734493" +} +``` + +## Multiple Products + +### Request + +{% tabs %} +{% tab title="Perp Prices" %} `POST [ARCHIVE_ENDPOINT]` + +**Body** + +```json +{ + "perp_prices": { + "product_ids": [2] + } +} +``` + +{% endtab %} +{% endtabs %} + +### Request Parameters + +
ParameterTypeRequiredDescription
product_idsnumber[]YesIds of perp products to fetch prices for.
+ +### Response + +{% hint style="info" %} +**Note**: the response is a map of `product_id -> perp_prices` for each requested product. +{% endhint %} + +```json +{ + "2": { + "product_id": 2, + "index_price_x18": "31483202055051853950444", + "mark_price_x18": "31514830401018841708801", + "update_time": "1689281222" + } +} +``` + +## Response Fields + +| Field name | Description | +| ----------------- | ------------------------------------------------------------ | +| product\_id | Id of the perp product. | +| index\_price\_x18 | Latest index price of the perp product, multiplied by 10^18. | +| mark\_price\_x18 | Latest mark price of the perp product, multiplied by 10^18. | +| update\_time | Epoch time in seconds the perp prices were last updated at. | diff --git a/docs/nado/developer-resources/api/archive-indexer/product-snapshots.md b/docs/nado/developer-resources/api/archive-indexer/product-snapshots.md new file mode 100644 index 0000000..32bf24c --- /dev/null +++ b/docs/nado/developer-resources/api/archive-indexer/product-snapshots.md @@ -0,0 +1,228 @@ +# Product Snapshots + +## Rate limits + +* 240 requests/min or 40 requests/10secs per IP address. (**weight = 10**) + +{% hint style="info" %} +See more details in [API Rate limits](https://docs.nado.xyz/developer-resources/api/rate-limits) +{% endhint %} + +## Single Product + +### Request + +{% tabs %} +{% tab title="Product snapshots" %} +Query snapshots for a given product ordered by `submission index` desc. + +`POST [ARCHIVE_ENDPOINT]` + +**Body** + +```json +{ + "products": { + "product_id": 2, + "max_time": 1679728762, + "limit": 1 + } +} +``` + +{% endtab %} +{% endtabs %} + +### Request Parameters + +
ParameterTypeRequiredDescription
product_idnumberYesid of product to fetch snapshots for.
idxnumber / stringNowhen provided, only return product snapshots with submission_idx <= idx
max_timenumber / stringNowhen idx is not provided, max_time (unix epoch in seconds) can be used to only return snapshots created <= max_time
limitnumberNomax number of snapshots to return. defaults to 100. max possible of 500.
+ +### Response + +{% hint style="info" %} +**Note**: + +* the response includes a `txs` field which contains the relevant transactions to the product snapshots. There are `>=1 product snapshots` per transaction. +* both `products` and `txs` are in descending order by `submission_idx`. +* use the `submission_idx` to associate a `product snapshot` to it's corresponding transaction. + {% endhint %} + +```json +{ + "products": [ + { + "product_id": 1, + "submission_idx": "563014", + "product": { + "spot": { + "product_id": 1, + "oracle_price_x18": "115575316424148798147115", + "risk": { + "long_weight_initial_x18": "900000000000000000", + "short_weight_initial_x18": "1100000000000000000", + "long_weight_maintenance_x18": "950000000000000000", + "short_weight_maintenance_x18": "1050000000000000000", + "price_x18": "115575316424148798147115" + }, + "config": { + "token": "0xc57c1c64561a37ac9e8f9039cb6deab7539d99fc", + "interest_inflection_util_x18": "800000000000000000", + "interest_floor_x18": "10000000000000000", + "interest_small_cap_x18": "40000000000000000", + "interest_large_cap_x18": "1000000000000000000", + "withdraw_fee_x18": "40000000000000", + "min_deposit_rate_x18": "0" + }, + "state": { + "cumulative_deposits_multiplier_x18": "1000000000000318713", + "cumulative_borrows_multiplier_x18": "1000347390679880473", + "total_deposits_normalized": "9000399823280682696107190850", + "total_borrows_normalized": "9580268570661550719" + }, + "book_info": { + "size_increment": "1000000000000000", + "price_increment_x18": "1000000000000000000", + "min_size": "4000000000000000", + "collected_fees": "0" + } + } + } + ], + "txs": [ + { + "tx": { + "update_price": { + "product_id": 3, + "price_x18": "1750710375000000000000" + } + }, + "submission_idx": "563014", + "timestamp": "1679728271" + } + ] +} +``` + +### Response Fields + +#### Products + +
Field nameDescription
submission_idxUsed to uniquely identify the blockchain transaction that generated the product snapshot; you can use it to grab the relevant transaction in the txs section.
product_idThe id of of the product the event is associated with.
productThe state of the product at the time of the transaction.
+ +#### Txs + +| Field name | Description | +| --------------- | ------------------------------------------------------------- | +| submission\_idx | Unique identifier of the transaction. | +| tx | Raw data of the corresponding transaction | +| timestamp | The unix epoch in seconds of when the transaction took place. | + +## Multiple Products + +### Request + +{% tabs %} +{% tab title="Multiple Products snapshots" %} +Query the latest snapshot for the provided products. + +`POST [ARCHIVE_ENDPOINT]` + +**Body** + +```json +{ + "product_snapshots": { + "product_ids": [1, 2], + "max_time": 1679728762 + } +} +``` + +{% endtab %} +{% endtabs %} + +### Request Parameters + +
ParameterTypeRequiredDescription
product_idsnumber[]YesIds of products to fetch snapshots for.
max_timenumber / stringNoWhen provided, returns the last snapshot created <= max_time for each product. Otherwise, the latest snapshot is returned.
+ +### Response + +{% hint style="info" %} +**Note**: the response is a map of `product_id -> snapshot` for each requested product. +{% endhint %} + +```json +{ + "1": { + "product_id": 1, + "submission_idx": "459743", + "product": { + "spot": { + "product_id": 1, + "oracle_price_x18": "115575316424148798147115", + "risk": { + "long_weight_initial_x18": "900000000000000000", + "short_weight_initial_x18": "1100000000000000000", + "long_weight_maintenance_x18": "950000000000000000", + "short_weight_maintenance_x18": "1050000000000000000", + "price_x18": "115575316424148798147115" + }, + "config": { + "token": "0xc57c1c64561a37ac9e8f9039cb6deab7539d99fc", + "interest_inflection_util_x18": "800000000000000000", + "interest_floor_x18": "10000000000000000", + "interest_small_cap_x18": "40000000000000000", + "interest_large_cap_x18": "1000000000000000000", + "withdraw_fee_x18": "40000000000000", + "min_deposit_rate_x18": "0" + }, + "state": { + "cumulative_deposits_multiplier_x18": "1000000000000318713", + "cumulative_borrows_multiplier_x18": "1000347390679880473", + "total_deposits_normalized": "9000399823280682696107190850", + "total_borrows_normalized": "9580268570661550719" + }, + "book_info": { + "size_increment": "1000000000000000", + "price_increment_x18": "1000000000000000000", + "min_size": "4000000000000000", + "collected_fees": "0" + } + } + } + }, + "2": { + "product_id": 2, + "submission_idx": "459842", + "product": { + "perp": { + "product_id": 2, + "oracle_price_x18": "115432187703236794231754", + "risk": { + "long_weight_initial_x18": "950000000000000000", + "short_weight_initial_x18": "1050000000000000000", + "long_weight_maintenance_x18": "970000000000000000", + "short_weight_maintenance_x18": "1030000000000000000", + "price_x18": "115432187703236794231754" + }, + "state": { + "cumulative_funding_long_x18": "-394223711772447555304", + "cumulative_funding_short_x18": "-394223711772447555304", + "available_settle": "20092193239667417956947", + "open_interest": "113605000000000000000" + }, + "book_info": { + "size_increment": "1000000000000000", + "price_increment_x18": "1000000000000000000", + "min_size": "4000000000000000", + "collected_fees": "0" + } + } + } + } +} +``` + +### Response Fields + +
Field nameDescription
submission_idxUsed to uniquely identify the blockchain transaction that generated the product snapshot.
product_idThe id of of the product the event is associated with.
productThe state of the product at the time of the transaction.
diff --git a/docs/nado/developer-resources/api/archive-indexer/quote-price.md b/docs/nado/developer-resources/api/archive-indexer/quote-price.md new file mode 100644 index 0000000..139f7ab --- /dev/null +++ b/docs/nado/developer-resources/api/archive-indexer/quote-price.md @@ -0,0 +1,33 @@ +# Quote Price + +## Rate limits + +* 1200 requests/min or 200 requests/10secs per IP address. (**weight = 2**) + +{% hint style="info" %} +See more details in [API Rate limits](https://docs.nado.xyz/developer-resources/api/rate-limits) +{% endhint %} + +## Request + +{% tabs %} +{% tab title="Get quote price" %} `POST [ARCHIVE_ENDPOINT]` + +**Body** + +```json +{ + "quote_price": {} +} +``` + +{% endtab %} +{% endtabs %} + +## Response + +```json +{ + "price_x18": "999944870000000000" +} +``` diff --git a/docs/nado/developer-resources/api/archive-indexer/sequencer-backlog.md b/docs/nado/developer-resources/api/archive-indexer/sequencer-backlog.md new file mode 100644 index 0000000..fd091c8 --- /dev/null +++ b/docs/nado/developer-resources/api/archive-indexer/sequencer-backlog.md @@ -0,0 +1,49 @@ +# Sequencer Backlog + +## Rate limits + +* 2400 requests/min or 400 requests/10secs per IP address. (**weight = 1**) + +{% hint style="info" %} +See more details in [API Rate limits](https://docs.nado.xyz/developer-resources/api/rate-limits) +{% endhint %} + +## Request + +{% tabs %} +{% tab title="Get sequencer backlog" %} `POST [ARCHIVE_ENDPOINT]` + +**Body** + +```json +{ + "backlog": {} +} +``` + +{% endtab %} +{% endtabs %} + +## Response + +```json +{ + "total_txs": "45479039", + "total_submissions": "45478914", + "backlog_size": "125", + "updated_at": "1750365790", + "backlog_eta_in_seconds": "500", + "txs_per_second": "0.25" +} +``` + +### Response Fields + +| Field name | Description | +| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | +| total\_txs | Total number of transactions stored in the indexer DB. | +| total\_submissions | Total number of transactions submitted on-chain. | +| backlog\_size | Number of unprocessed transactions (`total_txs - total_submissions`). | +| backlog\_eta\_in\_seconds | Estimated time in seconds (`float`) to clear the entire backlog (`null` if unavailable). | +| txs\_per\_second | Current submission rate in transactions per second (float) (`null` if unavailable). | +| updated\_at | UNIX timestamp (in seconds) of when the data was last updated. | diff --git a/docs/nado/developer-resources/api/archive-indexer/signatures.md b/docs/nado/developer-resources/api/archive-indexer/signatures.md new file mode 100644 index 0000000..3759d6e --- /dev/null +++ b/docs/nado/developer-resources/api/archive-indexer/signatures.md @@ -0,0 +1,60 @@ +# Signatures + +## Rate limits + +* Dynamic based on `digests` param provided (**weight = 2 + len(digests) / 10**) + * E.g: With `digests=100`, you can make up to 200 requests per min or 33 requests / 10 secs. + +{% hint style="info" %} +See more details in [API Rate limits](https://docs.nado.xyz/developer-resources/api/rate-limits) +{% endhint %} + +## Request + +{% tabs %} +{% tab title="Get order signatures by digests" %} `POST [ARCHIVE_ENDPOINT]` + +**Body** + +```json +{ + "signatures": { + "digests": [ + "0xf4f7a8767faf0c7f72251a1f9e5da590f708fd9842bf8fcdeacbaa0237958fff", + "0x0495a88fb3b1c9bed9b643b8e264a391d04cdd48890d81cd7c4006473f28e361" + ] + } +} +``` + +{% endtab %} +{% endtabs %} + +## Request Parameters + +
ParameterTypeRequiredDescription
digestsstring[]YesA list of order digests to retrieve signatures for.
+ +## Response + +```json +{ + "signatures": [ + { + "digest": "0xf4f7a8767faf0c7f72251a1f9e5da590f708fd9842bf8fcdeacbaa0237958fff", + "signature": "0xe8fa7151bde348afa3b46dc52798046b7c8318f1b0a7f689710debbc094658cc1bf5a7e478ccc8278b625da0b9402c86b580d2e31e13831337dfd6153f4b37811b", + "signer": "0x12a0b4888021576eb10a67616dd3dd3d9ce206b664656661756c740000000000", + "is_linked": false + }, + { + "digest": "0x0495a88fb3b1c9bed9b643b8e264a391d04cdd48890d81cd7c4006473f28e361", + "signature": "0x826c68f1a3f76d9ffbe8041f8d45e969d31f1ab6f2ae2f6379d1493e479e56436091d6cf4c72e212dd2f1d2fa17c627c4c21bd6d281c77172b8af030488478b71c", + "signer": "0x44b525f7bf3441464e406a094bc5e791f13dd79f64656661756c740000000000", + "is_linked": true + }, + ] +} +``` + +## Response Fields + +
Field nameDescription
digestThe order's generated digest.
signatureThe order's generated signature.
signerThe address that signed the order / generated the signature.
is_linkedIndicates whether this is a signature from a linked signer or the original sender.
diff --git a/docs/nado/developer-resources/api/archive-indexer/subaccount-snapshots.md b/docs/nado/developer-resources/api/archive-indexer/subaccount-snapshots.md new file mode 100644 index 0000000..aaf40ab --- /dev/null +++ b/docs/nado/developer-resources/api/archive-indexer/subaccount-snapshots.md @@ -0,0 +1,139 @@ +# Subaccount Snapshots + +Use this query to get a summary of the latest actions per product on Nado for provided subaccounts. Tracked variables (ex. net interest) are extrapolated to the timestamp or set of timestamps provided. + +## Rate limits + +* 480 requests/min or 80 requests/10secs per IP address. (**weight = 5**) + +{% hint style="info" %} +See more details in [API Rate limits](https://docs.nado.xyz/developer-resources/api/rate-limits) +{% endhint %} + +## Request + +{% tabs %} +{% tab title="Subaccount snapshots" %} +Query latest subaccount events/actions ordered by `submission index` desc. + +`POST [ARCHIVE_ENDPOINT]` + +**Body** + +```json +{ + "account_snapshots": { + "subaccounts": [ + "0xec132d41e542c7129268d9d4431f105e0830a81164656661756c745f31000000" + ], + "timestamps": [ + 1738703761 + ], + "isolated": false, + "active": true + } +} +``` + +{% endtab %} +{% endtabs %} + +## Request Parameters + +
ParameterTypeRequiredDescription
subaccountsarrayYesA list of bytes32 sent as a hex string; includes the address and the subaccount identifier.
timestamparrayYesA list of timestamps to retrieve multiple subaccount snapshots (one per timestamp).
isolatedbooleanNo

A filter to include only isolated or cross margin events.

  • If true: returns only isolated margin events.
  • If false: returns only cross margin events.
  • If omitted: returns both isolated and cross events.
activebooleanNo

Filters which products to include in the snapshot:

  • true: returns only products with non-zero balance at the timestamp (currently active positions)
  • false: returns products with event history before the timestamp (any historical activity)
  • If omitted: defaults to false
+ +## Response + +{% tabs %} +{% tab title="Single timestamp" %} + +```json +{ + "snapshots": { + "0xec132d41e542c7129268d9d4431f105e0830a81164656661756c745f31000000": { + "1738703761": [ + { + "subaccount": "0xec132d41e542c7129268d9d4431f105e0830a81164656661756c745f31000000", + "product_id": 0, + "submission_idx": "17286676", + "event_type": "liquidate_subaccount", + "isolated": false, + "isolated_product_id": null, + "pre_balance": { + "spot": { + "product_id": 0, + "balance": { + "amount": "53278293456559329896" + } + } + }, + "post_balance": { + "spot": { + "product_id": 0, + "balance": { + "amount": "0" + } + } + }, + "product": { + "spot": { + "product_id": 0, + "oracle_price_x18": "1000000000000000000", + "risk": { + "long_weight_initial_x18": "1000000000000000000", + "short_weight_initial_x18": "1000000000000000000", + "long_weight_maintenance_x18": "1000000000000000000", + "short_weight_maintenance_x18": "1000000000000000000", + "price_x18": "1000000000000000000" + }, + "config": { + "token": "0x5f65358d61a9a281ea3bb930d05889aca21e3f4f", + "interest_inflection_util_x18": "800000000000000000", + "interest_floor_x18": "10000000000000000", + "interest_small_cap_x18": "40000000000000000", + "interest_large_cap_x18": "1000000000000000000", + "withdraw_fee_x18": "1000000000000000000", + "min_deposit_rate_x18": "0" + }, + "state": { + "cumulative_deposits_multiplier_x18": "1000000000025524653", + "cumulative_borrows_multiplier_x18": "1000347390837434279", + "total_deposits_normalized": "20001011744258817298755054194662", + "total_borrows_normalized": "1617724891363505323532211" + }, + "book_info": { + "size_increment": "0", + "price_increment_x18": "0", + "min_size": "0", + "collected_fees": "0" + } + } + }, + "net_interest_unrealized": "0", + "net_interest_cumulative": "1443761232166478119", + "net_funding_unrealized": "0", + "net_funding_cumulative": "0", + "net_entry_unrealized": "0", + "net_entry_cumulative": "13458165999999999998", + "quote_volume_cumulative": "1234567890123456789" + } + ] + } + } +} +``` + +{% endtab %} +{% endtabs %} + +## Response Fields + +### Events + +{% hint style="info" %} + +* **Net cumulative**: the net difference in that quantity since the beginning of time. For example, if I want to compute total amount paid out in funding between two events, you can subtract the `net_funding_cumulative` of the larger event by the `net_funding_cumulative` of the smaller event. +* **Net unrealized**: similar to `net_cumulative`, but for `net_unrealized`, we have the caveat that when the magnitude of your position decreases, the magnitude of net\_unrealized `decreases` by the same amount. + {% endhint %} + +
Field nameDescription
submission_idxUsed to uniquely identify the blockchain transaction that generated the event; you can use it to grab the relevant transaction in the txs section.
product_idThe id of of the product the event is associated with.
event_typeName of the transaction type this event corresponds to.
subaccountThe subaccount associated to the event.
pre_balanceThe state of your balance before the event happened.
post_balanceThe state of your balance after the event happened.
productThe state of the product throughout the event.
diff --git a/docs/nado/developer-resources/api/archive-indexer/subaccounts.md b/docs/nado/developer-resources/api/archive-indexer/subaccounts.md new file mode 100644 index 0000000..9d98c02 --- /dev/null +++ b/docs/nado/developer-resources/api/archive-indexer/subaccounts.md @@ -0,0 +1,91 @@ +# Subaccounts + +## Rate limits + +* 1200 requests/min or 200 requests/10secs per IP address. (**weight = 2**) + +{% hint style="info" %} +See more details in [API Rate limits](https://docs.nado.xyz/developer-resources/api/rate-limits) +{% endhint %} + +## Request + +{% tabs %} +{% tab title="List subaccounts" %} +Query subaccounts ordered by `subaccount id` ASC. + +`POST [ARCHIVE_ENDPOINT]` + +**Body** + +```json +{ + "subaccounts": { + "start": 100, + "limit": 10, + } +} +``` + +{% endtab %} + +{% tab title="Find subaccounts by address" %} +Query all subaccounts associated to an address ordered by `subaccount id` ASC. + +`POST [ARCHIVE_ENDPOINT]` + +**Body** + +```json +{ + "subaccounts": { + "address": "0x79CC76364b5Fb263A25bD52930E3d9788fCfEEA8" + } +} +``` + +{% endtab %} +{% endtabs %} + +## Request Parameters + +
ParameterTypeRequiredDescription
startstring/numberNoSubaccount id to start from (used for pagination). Defaults to 0.
limitstring/numberNoMax number of subaccounts to return. Defaults to 100, max of 500.
addressstringNoAn optional wallet address to find all subaccounts associated to it.
+ +## Response + +```json +{ + "subaccounts": [ + { + "id": "25", + "subaccount": "0x12a0b4888021576eb10a67616dd3dd3d9ce206b664656661756c740000000000", + "address": "0x12a0b4888021576eb10a67616dd3dd3d9ce206b6", + "subaccount_name": "default", + "created_at": "1699949771", + "isolated": false + }, + { + "id": "948", + "subaccount": "0x12a0b4888021576eb10a67616dd3dd3d9ce206b664656661756c745f31000000", + "address": "0x12a0b4888021576eb10a67616dd3dd3d9ce206b6", + "subaccount_name": "default_1", + "created_at": "1738000782", + "isolated": false + }, + { + "id": "1094", + "subaccount": "0x12a0b4888021576eb10a67616dd3dd3d9ce206b600000000000000020069736f", + "address": "0x12a0b4888021576eb10a67616dd3dd3d9ce206b6", + "subaccount_name": "0x00000000000000020069736f", + "created_at": "1748982886", + "isolated": true + } + ] +} +``` + +## Response Fields + +### Subaccounts + +
Field nameDescription
idInternal subaccount id
subaccountHex string of the subaccount (wallet + subaccount name)
addressHex string of wallet address
subaccount_nameSubaccount identifier
created_atWhen subaccount was created
isolatedWhether it's a subaccount for an isolated position
diff --git a/docs/nado/developer-resources/api/definitions-formulas.md b/docs/nado/developer-resources/api/definitions-formulas.md new file mode 100644 index 0000000..10866d8 --- /dev/null +++ b/docs/nado/developer-resources/api/definitions-formulas.md @@ -0,0 +1,62 @@ +# Definitions / Formulas + +## Definitions + +### **Unsettled USDT0** + +Perp balances have two main components: + +* `amount` +* `v_quote_balance` + +When you buy a perp, `amount` increments and `v_quote_balance` decrements, and vice versa for selling. + +Settlement is the process of converting from v\_quote\_balance into actual USDT0 balance. This happens mostly on position close, but may happen on extremely negative PNL positions when we need to pay out positive PNL positions. + +The amount that is transferred between `v_quote_balance` in the perp and your USDT0 balance is an amount that results in `amount * oracle_price + v_quote_balance == 0`. Unsettled USDT0 is the total amount that would be transferred between `v_quote_balance` and your USDT0 balance summed across all perps. + +### **Unsettled PNL** + +**Note:** Technically, there is no such concept as "Unsettled PNL" in our system. However, the UI displays "Unsettled PnL" in some places (e.g., in the USDT0 Balance section) for user clarity. + +**What the UI actually shows:** When you see "Unsettled PnL" in the UI, it refers to **Unsettled USDT0** (see above) - the total unsettled quote balance across all perp positions. + +**For developers:** Always use **Unsettled USDT0** when referring to this value programmatically. It represents the sum of `amount × oracle_price + v_quote_balance` across all perp positions, which is the amount that would be settled into your USDT0 balance. + +### **Unrealized PNL** + +Refers to the estimated gains or losses of a current position based on the difference between the average entry price and the current oracle price. + +## Formulas + +### **Unrealized PNL** + +Using the [indexer's events query](https://docs.nado.xyz/developer-resources/api/archive-indexer/events), your unrealized PNL at the end of some event is given by: + +{% code lineNumbers="true" %} + +```python +unrealized_pnl = ( + event.post_balance.amount * event.product.oracle_price_x18 + - event.net_entry_unrealized +) +``` + +{% endcode %} + +### Total PNL + +Your total PNL between `event1` and `event2`, assuming `event1` is after `event2` - is given by: + +
total_pnl = (
+    (event1.post_balance.amount * event1.product.oracle_price_x18 - event1.net_entry_cumulative)
+    - (event2.post_balance.amount * event2.product.oracle_price_x18 - event2.net_entry_cumulative)
+)
+
+ +{% hint style="info" %} +**Notes**: + +* You can use 0 for the second term for the PNL to compute since the beginning of time. +* For spots, we will count deposits and withdraws towards your PNL. i.e. if you deposit BTC, for PNL tracking purposes it is counted as a BTC long at the oracle price. + {% endhint %} diff --git a/docs/nado/developer-resources/api/depositing.md b/docs/nado/developer-resources/api/depositing.md new file mode 100644 index 0000000..1b4c7e6 --- /dev/null +++ b/docs/nado/developer-resources/api/depositing.md @@ -0,0 +1,170 @@ +# Depositing + +There are two ways to deposit funds into Nado: + +1. **Direct Deposit** - Simple transfer to your unique deposit address (recommended for most users) +2. **On-Chain Contract Call** - Direct interaction with the Endpoint contract + +*** + +## Method 1: Direct Deposit (Recommended) + +Each subaccount has a unique deposit address. Simply send funds to this address and they will automatically be credited to your subaccount. + +### Getting Your Deposit Address + +Query your unique deposit address using the [Direct Deposit Address](https://docs.nado.xyz/developer-resources/api/archive-indexer/direct-deposit-address) endpoint: + +**Request:** + +```json +{ + "direct_deposit_address": { + "subaccount": "0x79cc76364b5fb263a25bd52930e3d9788fcfeea864656661756c740000000000" + } +} +``` + +**Response:** + +```json +{ + "subaccount": "0x79cc76364b5fb263a25bd52930e3d9788fcfeea864656661756c740000000000", + "deposit_address": "0x1234567890123456789012345678901234567890", + "created_at": "1683315718" +} +``` + +### Depositing Funds + +1. Get your deposit address using the API call above +2. Send the supported token to this address (e.g., USDT0, wETH, etc.) +3. Funds will be automatically credited to your subaccount within a few seconds + +**Advantages:** + +* No need to interact with smart contracts +* No need to approve allowances +* Works with any wallet (including CEX withdrawals) +* Simpler integration for users + +**Notes:** + +* Only send supported tokens to this address +* Find supported tokens via the [All Products](https://docs.nado.xyz/developer-resources/api/gateway/queries/all-products) query +* Deposits are processed automatically after blockchain confirmation + +*** + +## Method 2: On-Chain Contract Call + +Advanced users can deposit directly by calling the Endpoint contract. + +### Contract Address + +Find the Endpoint contract address at: + +``` +GET /query?type=contracts +``` + +### Function Interface + +#### Basic Deposit + +```solidity +function depositCollateral( + bytes12 subaccountName, // last 12 bytes of the subaccount bytes32 + uint32 productId, // product ID for the token + uint128 amount // raw token amount (see decimals below) +) external +``` + +**Parameters:** + +* `subaccountName`: The last 12 bytes of your subaccount identifier (e.g., `0x64656661756c740000000000` for "default") +* `productId`: The product ID for the token you're depositing +* `amount`: The raw amount in the token's smallest unit + * For USDT0 (6 decimals): 1 USDT0 = `1e6` = `1000000` + * For wETH (18 decimals): 1 wETH = `1e18` + * For wBTC (8 decimals): 1 wBTC = `1e8` + +#### Deposit with Referral Code + +```solidity +function depositCollateralWithReferral( + bytes32 subaccount, // full 32-byte subaccount identifier + uint32 productId, // product ID for the token + uint128 amount, // raw token amount + string memory referralCode // referral code (optional) +) public +``` + +### Prerequisites + +Before depositing via contract call, you must: + +1. **Approve Token Allowance** + + ```solidity + // Give the Endpoint contract permission to transfer your tokens + IERC20(tokenAddress).approve(endpointAddress, amount); + ``` +2. **Get Product Information** + * Use [All Products](https://docs.nado.xyz/developer-resources/api/gateway/queries/all-products) query to find: + * Product ID for your token + * Token contract address + * Token decimals + +### Example: Depositing 100 USDT0 + +Assuming USDT0 has product ID `0` and 6 decimals: + +```typescript +// 1. Approve allowance (one-time or as needed) +await usdtContract.approve(endpointAddress, ethers.constants.MaxUint256); + +// 2. Deposit 100 USDT0 +const subaccountName = ethers.utils.formatBytes32String("default").slice(0, 26); // bytes12 +const productId = 0; +const amount = 100 * 1e6; // 100 USDT0 with 6 decimals + +await endpointContract.depositCollateral(subaccountName, productId, amount); +``` + +### Processing Time + +Deposits may take a few seconds to process after transaction confirmation. You can monitor your balance via: + +* [Subaccount Info](https://docs.nado.xyz/developer-resources/api/gateway/queries/subaccount-info) query +* WebSocket subscriptions for real-time updates + +*** + +## Important Notes + +* **Use Correct Product ID**: Each token has a specific product ID. Using the wrong ID will cause the transaction to fail. +* **Check Token Decimals**: Always multiply by the correct decimal factor (6 for USDT0, 18 for wETH, etc.) +* **Minimum Deposit**: Some products may have minimum deposit amounts +* **Only Supported Tokens**: Only deposit tokens that are listed via the All Products query + +*** + +## Getting Token Information + +Use the [All Products](https://docs.nado.xyz/developer-resources/api/gateway/queries/all-products) query to get: + +```json +{ + "product_id": 0, + "symbol": "USDT0", + "token": "0x...", // token contract address + "decimals": 6 +} +``` + +This information is essential for: + +* Finding the correct `productId` +* Getting the token contract for approvals (Method 2 only) +* Calculating the correct `amount` with proper decimals diff --git a/docs/nado/developer-resources/api/endpoints.md b/docs/nado/developer-resources/api/endpoints.md new file mode 100644 index 0000000..65f74ea --- /dev/null +++ b/docs/nado/developer-resources/api/endpoints.md @@ -0,0 +1,25 @@ +# Endpoints + +## Mainnet + +### Ink Mainnet + +* **Gateway Websocket**: `wss://gateway.prod.nado.xyz/v1/ws` +* **Gateway REST:** `https://gateway.prod.nado.xyz/v1` +* **Gateway V2:** `https://gateway.prod.nado.xyz/v2` +* **Subscriptions**: `wss://gateway.prod.nado.xyz/v1/subscribe` +* **Archive (Indexer):** `https://archive.prod.nado.xyz/v1` +* **Archive (Indexer) V2:** `https://archive.prod.nado.xyz/v2` +* **Trigger**: `https://trigger.prod.nado.xyz/v1` + +## Testnet + +### Ink Sepolia + +* **Gateway Websocket**: `wss://gateway.test.nado.xyz/v1/ws` +* **Gateway REST:** `https://gateway.test.nado.xyz/v1` +* **Gateway V2:** `https://gateway.test.nado.xyz/v2` +* **Subscriptions**: `wss://gateway.test.nado.xyz/v1/subscribe` +* **Archive (Indexer):** `https://archive.test.nado.xyz/v1` +* **Archive (Indexer) V2:** `https://archive.test.nado.xyz/v2` +* **Trigger**: `https://trigger.test.nado.xyz/v1` diff --git a/docs/nado/developer-resources/api/errors.md b/docs/nado/developer-resources/api/errors.md new file mode 100644 index 0000000..3dcc841 --- /dev/null +++ b/docs/nado/developer-resources/api/errors.md @@ -0,0 +1,61 @@ +# Errors + +List of possible `error` values in the API Response: + +### General + +
Error CodeError ValueDescription
1000/1015RateLimitToo Many Requests: You have exceeded the rate limit. Please reduce your request frequency and try again later.
1001BlacklistedAddressThis address has been blacklisted from accessing the sequencer due to a violation of the Terms of Service. If you believe this is an error, please reach out for assistance.
1002BlockedLocationAccess from your current location ({location}) is blocked. Please check your location and try again.
1003BlockedSubdivisionAccess from your current location ({location} - {subdivision}) is blocked. Please check your location and try again.
1004MaintenanceService is temporarily unavailable due to scheduled maintenance. Please try again later.
+ +### Execute / Query API + +
+ +Execute Error Response + +```json +{ + "status": "failure", + "signature": {signature}, + "error": "{error msg}", + "error_code": {error_code} +} +``` + +
+ +
+ +Query Error Response + +```json +{ + "status": "failure", + "error": "{error msg}", + "error_code": {error_code} +} +``` + +
+ +
Error CodeError ValueDescription
2000InvalidPriceIncrementInvalid order price: Order price, {order.price}, is not divisible by the price_increment_x18; price_increment_x18 for product {product_id}: {price_increment_x18}.
2001InvalidAmountIncrementInvalid order amount: Order amount, {order.amount}, must be divisible by the size_increment; size_increment for product {product_id}: {size_increment}.
2002ZeroAmountInvalid order amount: The provided amount is zero. Please specify a valid order amount.
2003OrderAmountTooSmallInvalid order amount: Order amount, {order.amount}, is too small. abs(amount) must be >= min_size; min_size for product {product_id}: {min_size}.
2004OrderExpiredInvalid order expiration: The order has already expired. Please ensure the expiration date is in the future.
2005MaxOrdersLimitReachedYou have reached the maximum number of open orders allowed for this market.
2006UnhealthyOrderInsufficient account health. The execution of this order would lower your account health below the required threshold. Please adjust your order size or manage your positions to maintain a healthy account balance.
2007OraclePriceDifferenceOrder price must be no less than 20% and no more than 500% of the determined oracle price.
2008PostOnlyOrderCrossesBookThe order cannot be placed as it is post-only and crosses the book. Please adjust your order parameters.
2009OrderTypeNotSupportedThe order type you are trying to use is not currently supported.
2010InvalidTakerInvalid taker: The order placement health checks were successfully passed; however, the health checks failed upon matching.
2011LateRecvExecutionExecute request received after ‘recv_time’. Ensure that your ‘recv_time’ allows adequate time for requests to be received.
2012EarlyRevcExecutionExecute request received more than 100 seconds before the 'recv_time'. Ensure that the request is sent no more than 100 seconds prior to the 'recv_time'.
2013DigestAlreadyExistsThe provided digest already exists. Ensure that the provided digest is unique.
2014UnauthorizedSubaccountCancellationOperation failed. You're attempting to cancel an order for a different subaccount. Please verify the subaccount.
2015MarketNotFoundThe market for the given product or ticker ID was not found. Please try again with a different product or ticker ID.
2016InvalidProductIdThe provided 'product_id' is invalid. Please verify and input a valid 'product_id'.
2017SpotExecuteExceedsBorrowLimitExecuting this action could result in exceeding your borrowing limit as your spot leverage is currently set to false. Please adjust your withdrawal amount or manage your borrowings to prevent potential risk.
2019InappropriateSpotLeverageSpot leverage cannot be applied to a non-spot product. Please ensure you're using the correct type of leverage for the product in question.
2020OrderNotFoundOrder with the provided digest ({digest}) could not be found. Please verify the order digest and try again.
2021AddressRiskTooHighThe risk associated with the provided address is too high. Please use a different address or mitigate the associated risk.
2022InvalidNonceThe provided nonce is invalid. Ensure the nonce is correct and try again.
2023AddressScreeningPendingRisk screening check for the provided address is still in progress. Please wait until the check is complete before proceeding.
2024NoPriorDepositThe provided address has no previous deposits. Ensure you're using an address with prior deposits.
2025SingleSignatureInsufficientAccountValueYour account must hold a minimum value of 5 USDT0 to enable single signature sessions. Please ensure your account balance meets this requirement.
2026DuplicateSignerLinkingYou cannot link a signer to the same address more than once. Please provide a unique address for each signer.
2027SignatureLengthThe provided signature does not meet the required length specifications. Please verify and provide a valid signature.
2028InvalidSignerThe provided signature does not match with the sender's or the linked signer's. Please verify and provide the correct signature.
2029InvalidSignerZeroSigner cannot be zero. Please provide a valid non-zero signer.
2030LinkedSignerUpdateLimitExceededLinked Signer update limit exceeded. Please wait for {{wait_time}} seconds before trying again.
2031FillOrKillNotFilledYour 'Fill or Kill' order could not be entirely filled. Slippage parameters may be too conservative or size too large.
2033NonceMissingInPayloadNo nonce provided in the request payload. Please ensure a valid nonce is included.
2034InvalidSignatureVInvalid Signature: The 'v' value of the signature you provided is not valid. Please verify your signature and try again.
2035SignatureErrorSignature error: {error_msg}
2036SubaccountHealthTooLowSubaccount health insufficient. Please ensure sufficient health level in your subaccount to proceed.
2037ExcessiveLPTokenBurnAttempt to burn more LP tokens than currently owned. Please adjust the amount to match or be less than your current LP token balance.
2038InvalidExecuteMessageThe execute message provided is invalid. Please verify and provide a valid execute message.
2039MismatchedDigestsAndProductIdsLength'digests' and 'productIds' arrays should have the same length. Please ensure their lengths match.
2040InvalidBoolThe value you entered is not a valid boolean. Please try again with a value of true or false.
2041RebateExecuteFormattingThe length of 'subaccounts' array does not match the length of 'amounts' array. Ensure that both arrays have the same number of elements and try again.
2042NotLiquidatableFailed to initiate liquidation: The account does not meet the requirements for liquidation.
2043LiquidatorHealthTooLowFailed to initiate liquidation: The liquidator's account health is too low.
2044PositiveInitialHealthLiquidationAttemptFailed to initiate liquidation: The account to be liquidated has positive initial health.
2045InvalidLiquidationParametersFailed to initiate liquidation: Attempted to liquidate quote or provided invalid liquidation parameters.
2046PerpLiquidationSizeIncrementMismatchFailed to initiate liquidation: Attempted to liquidate perpetual contract but the amount is not divisible by sizeIncrement.
2047InvalidLiquidationAmountFailed to initiate liquidation: Attempted to liquidate either too little, too much or the signs are different.
2048LiabilitiesBeforePerpsLiquidationAttemptFailed to initiate liquidation: Attempted to liquidate liabilities before perpetual contracts.
2049TransferFailedERC20 Transfer failed. Please verify the transaction details.
2050UnauthorizedActionUnauthorized action attempted. Please ensure you have the necessary permissions.
2051NotFinalizableSubaccountAttempted to finalize a subaccount which is not eligible for finalization. Ensure that the subaccount meets all the necessary conditions before proceeding.
2052InvalidMakerThe maker order subaccount is invalid or has failed the risk check. Please verify the subaccount and ensure it meets the necessary risk parameters.
2053OrdersCannotBeMatchedOrder failed to match due to an internal error. Please try again.
2054SlippageTooHighThe requested operation could not be completed due to excessive slippage. Please adjust your order to match market conditions.
2055InvalidPriceInvalid price provided. The price must be greater than 0. Please input a valid price.
2056ImmediateOrCancelDoesNotCrossYour 'Immediate or Cancel' order does not cross the book. Please review the market conditions or adjust your order.
2057MaxTriggerOrdersLimitReachedYou have reached the maximum number of trigger orders allowed for this subaccount.
2058TriggerOrderNotFoundTrigger order with the provided digest ({digest}) could not be found. Please verify the order digest and try again.
2059NotTriggerOrderSubmitted order is not a trigger order.
2060InvalidProductIdsThe provided 'product_ids' is invalid. Please verify input contains only valid products and no duplicates.
2061InvalidProductTypeInvalid product type {{product_type}}. 'product_type' must be 'spot' or 'perp'
2062MissingProductIdsThe 'product_ids' provided is empty. Please ensure you include a non-empty list of valid product_ids in your request.
2063InvalidQueryResponseInvalid query response. Expected {{Response}}.
2064ReduceOnlyIncreasesPositionReduce only order increases position.
2065InvalidExpirationBitsInvalid expiration bits: The 4th to 6th most significant bits are reserved and must be unset.
2066CancelAndPlaceDifferentSenderOrSignerSender or signer of cancel and place are not the same.
2067ReduceOnlyNotTakerOnly taker orders can be set as reduce only.
2068SystemUnderMaintenanceWe're currently performing maintenance on the system. Please try again later.
2069MarketTradingBlockedTrading is blocked for this market.
2070MarketMaxOpenInterestMarket has reached maximum open interest. Please only close positions at this time.
2071MaxUtilizationProduct at maximum utilization
2072OrderBatchExceedLimitThe number of specified 'orders' exceeds the limit. Please reduce the 'orders' to meet the defined limit.
2073SelfMatchNotAllowedSelf-match is not allowed.
2074MismatchedProductIdsProduct IDs do not match.
2075NonDefaultPrivateBatchOrderPrivate batch order types must all be default.
2076InvalidTriggerPrivateBatchOrderPrivate batch order cannot be trigger order.
2077TransferQuoteAmountTooSmallTransfer quote amount is too small. You must transfer a minimum of 5 USDT0.
2078TransferQuoteNewRecipientLimitExceededTransfer quote to new recipients limit exceeded. Please wait 24hrs before transferring quote to new recipients.
2079SelfTransferQuoteNotAllowedSelf-transfer quote is not allowed.
2080WebsocketCompressionRequiredSubscriptions require the header 'Sec-WebSocket-Extensions' with value 'permessage-deflate'.
2081IsolatedSubaccountCannotPlaceIsolatedOrderAn isolated subaccount cannot place an isolated order.
2082IsolatedSubaccountInvalidProductInvalid product_id for isolated subaccount.
2083InvalidIsolatedSpotOrderIsolated orders cannot be placed on spots.
2084InvalidIsolatedTriggerOrderIsolated orders cannot be trigger orders.
2085InvalidIsolatedReduceOnlyOrderIsolated orders cannot be reduce-only.
2086InvalidIsolatedMarginIsolated margin must be non-negative.
2087FailedToCreateIsolatedSubaccountFailed to create isolated subaccount.
2088InvalidOrderFromIsolatedSubaccountOrders from isolated subaccount must be reduce-only.
2089InvalidLinkSignerSenderCannot link signer to isolated subaccount.
2090MintNlpAmountTooSmallNlp minting amount is too small. You must mint a minimum of 1 USDT0.
2091AmountTooLargeAmount is too large.
2092NAccountHealthTooLowN_ACCOUNT health insufficient.
2093NotCanonicalChainCan not execute in non-canonical chains.
2094OrderSizeTooSmallInvalid order size: Order amount, {order.amount}, or price, {order.price}, is too small. abs(amount) * price must be >= min_size; min_size for product {product_id}: {min_size}.
2095InvalidOrderVersionInvalid Order Version: the order version in the appendix, {version}, does not match the expected version: {expected_version}
2096UnlockedNlpInsufficientDo not have enough unlocked NLP.
2097InvalidTriggerOrderInvalid trigger order appendix.
2098InvalidTwapInvalid TWAP order.
2099InvalidTwapOrderTypeTWAP order must be of type 'Immediate or Cancel'. Please ensure your TWAP order uses the correct order type.
2100InvalidTwapTimesInvalid TWAP times: {times}. TWAP times must be between 1 and 500.
2101InvalidTwapAmountDistributionInvalid TWAP amount distribution: amount {amount} is not evenly divisible by times {times}. For non-random TWAP orders, the total amount must be evenly divisible by the number of executions.
2102InvalidTwapExpirationInvalid TWAP expiration: expiration time {expiration} exceeds maximum allowed duration of 25 hours from current time {current_time}. Please adjust the expiration time.
2103InvalidTwapIntervalInvalid TWAP interval: interval {interval} seconds exceeds maximum allowed interval of 3600 seconds (1 hour).
2104InvalidTwapTotalDurationInvalid TWAP total duration: total duration {duration} seconds exceeds maximum allowed duration of 86400 seconds (24 hours).
2105InvalidTwapExpirationTimingInvalid TWAP expiration timing: expiration {expiration} is before the minimum required time {min_time} for the given interval and times.
2106InvalidTwapRandomConfigurationInvalid TWAP random configuration: amounts array presence {amounts_present} does not match is_twap_random flag {is_random}.
2107InvalidTwapAmountInvalid TWAP amount: amount {amount} is zero or has different sign than order amount {order_amount}.
2108InvalidTwapAmountsSumInvalid TWAP amounts sum: sum of amounts {sum} does not match order amount {order_amount}.
2109InvalidTwapTriggerAmountConfigurationInvalid TWAP trigger amount configuration: trigger_amount presence {trigger_amount_present} does not match is_twap_random flag {is_random}.
2110InvalidTwapIsolatedTWAP orders cannot be isolated. Please remove the isolated flag from your TWAP order.
2111MaxOrderLimitExceededCannot place more than 50 orders.
2112NlpPoolAccountsCannotPlaceIsolatedOrderNLP pool accounts cannot place isolated order.
2113NlpPoolAccountsCannotPlaceTriggerOrderNLP pool accounts cannot place trigger order.
2114BatchSenderMismatchAll orders in a batch must have the same sender. Please ensure all orders are from the same account.
2115LiquidationFrontrunByNlpLiquidation succeeded but was executed by the NLP account instead of the requested liquidator.
+ +### Indexer API + +
+ +Indexer Error Response + +```json +{ + "error": "{error_msg}, + "error_code": {error_code} +} +``` + +
+ +
Error CodeError ValueDescription
3000DigestsNotAllowedUnable to accept 'digests' in conjunction with 'subaccount' or 'product_ids'. Please make sure your request does not contain these fields simultaneously.
3001DigestsExceedLimitThe number of specified 'digests' exceeds the specified limit. Please reduce the 'digests' to meet the defined limit.
3002MissingSubaccountA 'subaccount' is required but not specified. Please ensure you include a 'subaccount' in your request.
3003InvalidIntervalInvalid interval: Please try again with a different 'max_timestamp', 'granularity', or 'count'.
3004InvalidWithdrawalIdxInvalid idx: withdrawal tx not found at idx {{idx}}. Check the input idx and try again later.
3005NotEnoughFastWithdrawalSignaturesNot enough signatures for tx at idx {{idx}}, try again later.
+ +### Others + +
Error CodeError ValueDescription
4000PerpTickFormattingThe length of the 'avg_price_diffs' array does not match the length of 'product_ids'. Ensure that the arrays are correctly formed and try again.
4001NotImplementedThe feature you are trying to use is not yet implemented. Please check back later.
4002TemporarilyDisabledMintLpMintLp operation is currently disabled. Please try again later.
4003EvmRevertA critical error occurred while attempting match. Reverted with: {revert message}
4004WithdrawRiskProtocol risk: {limit} withdrawal limit over 24 hours exceeded; Try again later
5000InternalErrorInternal error: {message}
diff --git a/docs/nado/developer-resources/api/gateway.md b/docs/nado/developer-resources/api/gateway.md new file mode 100644 index 0000000..d6369a6 --- /dev/null +++ b/docs/nado/developer-resources/api/gateway.md @@ -0,0 +1,33 @@ +# Gateway + +There are two types of actions. An `Execute` involves a modification to state, and a `Query` merely fetches information from state. + +All actions can be sent over websocket as json payloads at `WEBSOCKET [GATEWAY_WEBSOCKET_ENDPOINT]` + +Additionally, you can send executes and queries over HTTP, at `POST [GATEWAY_REST_ENDPOINT]/execute` and `GET/POST [GATEWAY_REST_ENDPOINT]/query` respectively. For executes, the request should be sent with a json payload, while for queries, the payload should be encoded into url query strings. + +`HTTP` requests must set the `Accept-Encoding` to include `gzip`, `br` or `deflate` + +## Endpoints + +### **Testnet**: + +* Websocket: `wss://gateway.test.nado.xyz/v1/ws` +* REST: `https://gateway.test.nado.xyz/v1` + +## Websocket + +{% hint style="info" %} +**Notes on *****keeping websocket connections alive*****:** + +* When interacting with our API via websocket, you must send ping frames every 30 seconds to keep the websocket connection alive. +* Ping / Pong frames are built into the websocket protocol and should be supported natively by your websocket library. See [Ping/Pong frames](https://datatracker.ietf.org/doc/html/rfc6455#section-5.5.2) for more info. + {% endhint %} + +{% content-ref url="gateway/executes" %} +[executes](https://docs.nado.xyz/developer-resources/api/gateway/executes) +{% endcontent-ref %} + +{% content-ref url="gateway/queries" %} +[queries](https://docs.nado.xyz/developer-resources/api/gateway/queries) +{% endcontent-ref %} diff --git a/docs/nado/developer-resources/api/gateway/executes.md b/docs/nado/developer-resources/api/gateway/executes.md new file mode 100644 index 0000000..aad61fd --- /dev/null +++ b/docs/nado/developer-resources/api/gateway/executes.md @@ -0,0 +1,63 @@ +# Executes + +## Overview + +All executes go through the following endpoint; the exact details of the execution are specified by the JSON payload. + +* **Websocket**: `WEBSOCKET [GATEWAY_WEBSOCKET_ENDPOINT]` +* **REST**: `POST [GATEWAY_REST_ENDPOINT]/execute` + +### **Signing** + +All executes are signed using [EIP712](https://eips.ethereum.org/EIPS/eip-712). Each execute request contains: + +1. A piece of structured data that includes the sender address +2. A signature of the hash of that structured data, signed by the sender + +You can check the SDK for some examples of how to generate these signatures. + +{% hint style="info" %} +See more info in the [signing](https://docs.nado.xyz/developer-resources/api/gateway/signing) page. +{% endhint %} + +### **Sender Field Structure** + +The sender field is a solidity `bytes32` . There are two components: + +* an `address` that is a `bytes20` +* a subaccount identifier that is a `bytes12` + +For example, if your address was `0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43`, and you wanted to use the default subaccount identifier (i.e: the word `default`) you can set `sender` to `0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c4364656661756c740000000000` , which sets the subaccount identifier to `64656661756c740000000000`. + +### **Amounts** + +For `DepositCollateral` and `WithdrawCollateral`, the amount specifies the physical token amount that you want to receive. `i.e.` if USDT0 has 6 decimals, and you want to deposit or withdraw 1 USDT0, you specify `amount = 1e6`. + +For all other transactions, amount is normalized to 18 decimals, so `1e18` == one unit of the underlying asset. For example, if you want to buy 1 wETH, regardless of the amount of decimals the wETH contract has on chain, you specify `1e18` in the amount field of the order. + +## API Response + +All `Execute` messages return the following information: + +#### Success + +```json +{ + "status": "success", + "signature": "{signature}", + "data"?: {data_obj}, + "request_type": "{request_type}" +} +``` + +#### Failure + +```json +{ + "status": "failure", + "signature": "{signature}", + "error": "{error_msg}", + "error_code": {error_code}, + "request_type": "{request_type}" +} +``` diff --git a/docs/nado/developer-resources/api/gateway/executes/burn-nlp.md b/docs/nado/developer-resources/api/gateway/executes/burn-nlp.md new file mode 100644 index 0000000..3c1aa6f --- /dev/null +++ b/docs/nado/developer-resources/api/gateway/executes/burn-nlp.md @@ -0,0 +1,116 @@ +# Burn NLP + +## Rate limits + +* 60 burns/min or 10 burns every 10 seconds per wallet. (**weight = 10**) + +{% hint style="info" %} +See more details in [API Rate limits](https://docs.nado.xyz/developer-resources/api/rate-limits). +{% endhint %} + +## Request + +{% tabs %} +{% tab title="Websocket" %} +**Connect** + +`WEBSOCKET [GATEWAY_WEBSOCKET_ENDPOINT]` + +**Message** + +```json +{ + "burn_nlp": { + "tx": { + "sender": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43746573743000000000000000", + "nlpAmount": "10001000000000000000000" + "nonce": "1" + }, + "signature": "0x" + } +} +``` + +{% endtab %} + +{% tab title="REST" %} `POST [GATEWAY_REST_ENDPOINT]/execute` + +**Body** + +```json +{ + "burn_lp": { + "tx": { + "sender": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43746573743000000000000000", + "productId": 1, + "amount": "10001000000000000000000" + "nonce": "1" + }, + "signature": "0x" + } +} +``` + +{% endtab %} +{% endtabs %} + +## Request Parameters + +
ParameterTypeRequiredDescription
txobjectYesBurn NLP transaction object. See Signing section for details on the transaction fields.
tx.senderstringYesHex string representing the subaccount's 32 bytes (address + subaccount name) of the tx sender.
tx.nlpAmountstringYesAmount of NLP tokens to burn multiplied by 1e18, sent as a string.
tx.noncestringYesThis is an incrementing nonce, can be obtained using the Nonces query.
signaturestringYesSigned transaction. See Signing section for more details.
+ +## Signing + +{% hint style="info" %} +See more details and examples in our [signing](https://docs.nado.xyz/developer-resources/api/gateway/signing) page. +{% endhint %} + +The solidity typed data struct that needs to be signed is: + +```solidity +struct BurnNlp { + bytes32 sender; + uint128 nlpAmount; + uint64 nonce; +} +``` + +`sender`: a `bytes32` sent as a hex string; includes the address and the subaccount identifier. + +`nlpAmount`: amount of NLP tokens to burn, sent as a string. This must be positive and must be specified with 18 decimals. + +`nonce`: the `tx_nonce`. This is an incrementing nonce, can be obtained using the [Nonces](https://docs.nado.xyz/developer-resources/api/gateway/queries/nonces) query. + +{% hint style="warning" %} +**Note**: for signing you should always use the data type specified in the solidity struct which might be different from the type sent in the request e.g: `nonce` should be an `uint64` for **Signing** but should be sent as a `string` in the final payload. +{% endhint %} + +## Response + +```json +{ + "status": "success", + +} +``` + +#### Success + +```json +{ + "status": "success", + "signature": {signature}, + "request_type": "execute_burn_nlp" +} +``` + +#### Failure + +```json +{ + "status": "failure", + "signature": {signature}, + "error": "{error_msg}", + "error_code": {error_code}, + "request_type": "execute_burn_nlp" +} +``` diff --git a/docs/nado/developer-resources/api/gateway/executes/cancel-and-place.md b/docs/nado/developer-resources/api/gateway/executes/cancel-and-place.md new file mode 100644 index 0000000..f661c3f --- /dev/null +++ b/docs/nado/developer-resources/api/gateway/executes/cancel-and-place.md @@ -0,0 +1,119 @@ +# Cancel And Place + +## Rate limits + +* The sum of [Cancel Orders](https://docs.nado.xyz/developer-resources/api/gateway/cancel-orders#rate-limits) + [Place Order](https://docs.nado.xyz/developer-resources/api/gateway/place-order#rate-limits) limits + +{% hint style="info" %} +See more details in [API Rate limits](https://docs.nado.xyz/developer-resources/api/rate-limits). +{% endhint %} + +## Request + +{% tabs %} +{% tab title="Websocket" %} +**Connect** + +`WEBSOCKET [GATEWAY_WEBSOCKET_ENDPOINT]` + +**Message** + +```json +{ + "cancel_and_place": { + "cancel_tx": { + "sender": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43746573743000000000000000", + "productIds": [2], + "digests": ["0x"], + "nonce": "1" + }, + "cancel_signature": "0x", + "place_order": { + "product_id": 1, + "order": { + "sender": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43746573743000000000000000", + "priceX18": "1000000000000000000", + "amount": "1000000000000000000", + "expiration": "4294967295", + "appendix": "1537", + "nonce": "1757062078359666688" + }, + "signature": "0x", + } + } +} +``` + +{% endtab %} + +{% tab title="REST" %} `POST [GATEWAY_REST_ENDPOINT]/execute` + +**Body** + +```json +{ + "cancel_and_place": { + "cancel_tx": { + "sender": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43746573743000000000000000", + "productIds": [2], + "digests": ["0x"], + "nonce": "1" + }, + "cancel_signature": "0x", + "place_order": { + "product_id": 1, + "order": { + "sender": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43746573743000000000000000", + "priceX18": "1000000000000000000", + "amount": "1000000000000000000", + "expiration": "4294967295", + "nonce": "1757062078359666688" + }, + "signature": "0x", + } + } +} +``` + +{% endtab %} +{% endtabs %} + +## Request Parameters + +
ParameterTypeRequiredDescription
cancel_txobjectYesCancel order transaction object. See Cancel order signing for details on the transaction fields.
cancel_tx.senderstringYesHex string representing the subaccount's 32 bytes (address + subaccount name) of the tx sender.
cancel_tx.productIdsnumber[]YesA list of product IDs, corresponding to the product ids of the orders in digests
cancel_tx.digestsstring[]YesA list of order digests, represented as hex strings.
cancel_tx.noncestringYesUsed to differentiate between the same cancellation multiple times. See Cancel order signing section for more details.
cancel_signaturestringYesSigned transaction. See SigningCancel order signing for more details.
place_orderobjectYesPayload of order to be placed. See Place order request parameters for payload details.
+ +## Signing + +{% hint style="warning" %} +**Note**: both `cancel_tx` and `place_order` objects must be signed using the same signer, otherwise the request will be rejected. +{% endhint %} + +* See [Cancel orders signing](https://docs.nado.xyz/developer-resources/api/gateway/cancel-orders#signing) for details on how to sign the order cancellation. +* See [Place order signing](https://docs.nado.xyz/developer-resources/api/gateway/place-order#signing) for details on how to sign the order placement. + +## Response + +#### Success + +```json +{ + "status": "success", + "signature": {signature}, + "data": { + "digest": {order digest} + }, + "request_type": "execute_cancel_and_place" +} +``` + +#### Failure + +```json +{ + "status": "failure", + "signature": {signature} + "error": "{error_msg}" + "error_code": {error_code} + "request_type": "execute_cancel_and_place" +} +``` diff --git a/docs/nado/developer-resources/api/gateway/executes/cancel-orders.md b/docs/nado/developer-resources/api/gateway/executes/cancel-orders.md new file mode 100644 index 0000000..550984c --- /dev/null +++ b/docs/nado/developer-resources/api/gateway/executes/cancel-orders.md @@ -0,0 +1,139 @@ +# Cancel Orders + +## Rate limits + +* When no **digests** are provided: 600 cancellations/min or 10 cancellations/sec per wallet. (**weight=1**) +* When **digests** are provided: 600/(total digests) cancellations per minute per wallet. (**weight=total digests**) + +{% hint style="info" %} +See more details in [API Rate limits](https://docs.nado.xyz/developer-resources/api/rate-limits). +{% endhint %} + +## Request + +{% tabs %} +{% tab title="Websocket" %} +**Connect** + +`WEBSOCKET [GATEWAY_WEBSOCKET_ENDPOINT]` + +**Message** + +```json + +{ + "cancel_orders": { + "tx": { + "sender": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43746573743000000000000000", + "productIds": [2], + "digests": ["0x"], + "nonce": "1" + }, + "signature": "0x" + } +} +``` + +{% endtab %} + +{% tab title="REST" %} `POST [GATEWAY_REST_ENDPOINT]/execute` + +**Body** + +```json + +{ + "cancel_orders": { + "tx": { + "sender": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43746573743000000000000000", + "productIds": [0], + "digests": ["0x"], + "nonce": "1" + }, + "signature": "0x" + } +} +``` + +{% endtab %} +{% endtabs %} + +## Request Parameters + +
ParameterTypeRequiredDescription
txobjectYesCancel order transaction object. See Signing section for details on the transaction fields.
tx.senderstringYesHex string representing the subaccount's 32 bytes (address + subaccount name) of the tx sender.
tx.productIdsnumber[]YesA list of product IDs, corresponding to the product ids of the orders in digests
tx.digestsstring[]YesA list of order digests, represented as hex strings.
tx.noncestringYesUsed to differentiate between the same cancellation multiple times. See Signing section for more details.
signaturestringYesSigned transaction. See Signing section for more details.
+ +## Signing + +{% hint style="info" %} +See more details and examples in our [signing](https://docs.nado.xyz/developer-resources/api/gateway/signing) page. +{% endhint %} + +The solidity typed data struct that needs to be signed is: + +```solidity +struct Cancellation { + bytes32 sender; + uint32[] productIds; + bytes32[] digests; + uint64 nonce; +} +``` + +`sender`: a `bytes32` sent as a hex string; includes the address and the subaccount identifier + +`productIds`: a list of product IDs, corresponding to the product ids of the orders in `digests` + +`digests`: a list of order digests, represented as hex strings, for the orders you want to cancel. + +`nonce`: used to differentiate between the same cancellation multiple times, and a user trying to place a cancellation with the same parameters twice. Sent as a string. Encodes two bit of information: + +* Most significant `44` bits encoding the `recv_time` in milliseconds after which the cancellation should be ignored by the matching engine; the engine will accept cancellations where `current_time < recv_time <= current_time + 100000` +* Least significant `20` bits are a random integer used to avoid hash collisions + + For example, to place a cancellation with a random integer of `1000`, and a discard time 50 ms from now, we would send a nonce of `(timestamp_ms() + 50) << 20 + 1000` + +{% hint style="warning" %} +**Note**: for signing you should always use the data type specified in the solidity struct which might be different from the type sent in the request e.g: `nonce` should be an `uint64` for **Signing** but should be sent as a `string` in the final payload. +{% endhint %} + +## Response + +#### Success + +```json +{ + "status": "success", + "signature": {signature}, + "data": { + "cancelled_orders": [ + { + "product_id": 2, + "sender": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43746573743000000000000000", + "price_x18": "20000000000000000000000", + "amount": "-100000000000000000", + "expiration": "1686332748", + "order_type": "post_only", + "nonce": "1768248100142339392", + "unfilled_amount": "-100000000000000000", + "digest": "0x3195a7929feb8307edecf9c045j5ced68925108f0aa305f0ee5773854159377c", + "appendix": "1537", + "placed_at": 1686332708 + }, + ... + ] + }, + "request_type": "execute_cancel_orders" +} +``` + +#### Failure + +```json +{ + "status": "failure", + "signature": {signature}, + "error": "{error_msg}", + "error_code": {error_code}, + "request_type": "execute_cancel_orders" +} +``` diff --git a/docs/nado/developer-resources/api/gateway/executes/cancel-product-orders.md b/docs/nado/developer-resources/api/gateway/executes/cancel-product-orders.md new file mode 100644 index 0000000..d5f4635 --- /dev/null +++ b/docs/nado/developer-resources/api/gateway/executes/cancel-product-orders.md @@ -0,0 +1,135 @@ +# Cancel Product Orders + +## Rate limits + +* When no **productIds** are provided**:** 12 cancellations/min or 2 cancellations/sec per wallet. (**weight=50**) +* When **productIds** are provided: 600 / (5 \* total productIds) cancellations per minute per wallet. (**weight=5\*total productIds**) + +{% hint style="info" %} +See more details in [API Rate limits](https://docs.nado.xyz/developer-resources/api/rate-limits). +{% endhint %} + +## Request + +{% tabs %} +{% tab title="Websocket" %} +**Connect** + +`WEBSOCKET [GATEWAY_WEBSOCKET_ENDPOINT]` + +**Message** + +```json + +{ + "cancel_product_orders": { + "tx": { + "sender": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43746573743000000000000000", + "productIds": [2], + "nonce": "1" + }, + "signature": "0x", + "digest": null + } +} +``` + +{% endtab %} + +{% tab title="REST" %} `POST [GATEWAY_REST_ENDPOINT]/execute` + +**Body** + +```json +{ + "cancel_product_orders": { + "tx": { + "sender": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43746573743000000000000000", + "productIds": [0], + "nonce": "1" + }, + "signature": "0x", + "digest": "0x" + } +} +``` + +{% endtab %} +{% endtabs %} + +## Request Parameters + +
ParameterTypeRequiredDescription
txobjectYesCancel product orders transaction object. See Signing section for details on transaction fields.
tx.senderstringYesHex string representing the subaccount's 32 bytes (address + subaccount name) of the tx sender.
tx.productIdsnumber[]YesA list of product IDs to cancel orders for.
tx.noncestringYesUsed to differentiate between the same cancellation multiple times. See Signing section for more details.
signaturestringYesSigned transaction. See Signing section for more details.
digeststringNoHex string representing a hash of the CancellationProducts object.
+ +## Signing + +{% hint style="info" %} +See more details and examples in our [signing](https://docs.nado.xyz/developer-resources/api/gateway/signing) page. +{% endhint %} + +The solidity typed data struct that needs to be signed is: + +```solidity +struct CancellationProducts { + bytes32 sender; + uint32[] productIds; + uint64 nonce; +} +``` + +`sender`: a `bytes32` sent as a hex string; includes the address and the subaccount identifier + +`productIds`: a list of product Ids for which to cancel all subaccount orders. When left empty, orders from all products will be cancelled. + +`nonce`: used to differentiate between the same cancellation multiple times, and a user trying to place a cancellation with the same parameters twice. Sent as a string. Encodes two bit of information: + +* Most significant `44` bits encoding the `recv_time` in milliseconds after which the cancellation should be ignored by the matching engine; the engine will accept cancellations where `current_time < recv_time <= current_time + 100000` +* Least significant `20` bits are a random integer used to avoid hash collisions + + For example, to place a cancellation with a random integer of `1000`, and a discard time 50 ms from now, we would send a nonce of `(timestamp_ms() + 50) << 20 + 1000` + +{% hint style="warning" %} +**Note**: for signing you should always use the data type specified in the solidity struct which might be different from the type sent in the request e.g: `nonce` should be an `uint64` for **Signing** but should be sent as a `string` in the final payload. +{% endhint %} + +## Response + +#### Success + +```json +{ + "status": "success", + "signature": {signature}, + "data": { + "cancelled_orders": [ + { + "product_id": 2, + "sender": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43746573743000000000000000", + "price_x18": "20000000000000000000000", + "amount": "-100000000000000000", + "expiration": "1686332748", + "order_type": "post_only", + "nonce": "1768248100142339392", + "unfilled_amount": "-100000000000000000", + "digest": "0x3195a7929feb8307edecf9c045j5ced68925108f0aa305f0ee5773854159377c", + "appendix": "1537", + "placed_at": 1686332708 + }, + ... + ] + }, + "request_type": "execute_cancel_product_orders" +} +``` + +#### Failure + +```json +{ + "status": "failure", + "signature": {signature}, + "error": "{error_msg}", + "error_code": {error_code}, + "request_type": "execute_cancel_product_orders" +} +``` diff --git a/docs/nado/developer-resources/api/gateway/executes/link-signer.md b/docs/nado/developer-resources/api/gateway/executes/link-signer.md new file mode 100644 index 0000000..ee91ef4 --- /dev/null +++ b/docs/nado/developer-resources/api/gateway/executes/link-signer.md @@ -0,0 +1,123 @@ +# Link Signer + +Each subaccount can have at most one linked signer at a time. A linked signer can perform any execute on behalf of the subaccount it is linked to. Use the [Linked Signer](https://docs.nado.xyz/developer-resources/api/gateway/queries/linked-signer) query to view your current linked signer. + +{% hint style="warning" %} +**Please note**: + +* To enable a linked signer, your subaccount must have a minimum of **5 USDT0** worth in account value. + {% endhint %} + +## Rate limits + +* A max of 50 link signer requests every 7 days per subaccount. (**weight=30**). Use the [Linked Signer Rate Limit](https://docs.nado.xyz/developer-resources/api/archive-indexer/linked-signer-rate-limit) query to check a subaccount's linked signer usage and remaining wait time. + +{% hint style="info" %} +See more general details in [API Rate limits](https://docs.nado.xyz/developer-resources/api/rate-limits). +{% endhint %} + +## Request + +{% tabs %} +{% tab title="Websocket" %} +**Connect** + +`WEBSOCKET [GATEWAY_WEBSOCKET_ENDPOINT]` + +**Message** + +```json +{ + "link_signer": { + "tx": { + "sender": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43746573743000000000000000", + "signer": "0xeae27ae6412147ed6d5692fd91709dad6dbfc34264656661756c740000000000", + "nonce": "1" + }, + "signature": "0x" + } +} +``` + +{% endtab %} + +{% tab title="REST" %} `POST [GATEWAY_REST_ENDPOINT]/execute` + +**Body** + +```json +{ + "link_signer": { + "tx": { + "sender": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43746573743000000000000000", + "signer": "0xeae27ae6412147ed6d5692fd91709dad6dbfc34264656661756c740000000000", + "nonce": "1" + }, + "signature": "0x" + } +} +``` + +{% endtab %} +{% endtabs %} + +## Request Parameters + +
ParameterTypeRequiredDescription
txobjectYesA link signer transaction object. See Signing section for details on the transaction fields.
tx.senderstringYesHex string representing the subaccount's 32 bytes (address + subaccount name) of the tx sender.
tx.signerstringYesA bytes32 sent as a hex string; includes the address (first 20 bytes) that'll be used as the sender's signer. the last 12 bytes can be set to anything.
tx.noncestringYesThis is an incrementing nonce, can be obtained using the Nonces query.
signaturestringYesSigned transaction. See Signing section for more details.
+ +## Signing + +{% hint style="info" %} +See more details and examples in our [signing](https://docs.nado.xyz/developer-resources/api/gateway/signing) page. +{% endhint %} + +The solidity typed data struct that needs to be signed is: + +```solidity +struct LinkSigner { + bytes32 sender; + bytes32 signer; + uint64 nonce; +} +``` + +`sender`: a `bytes32` sent as a hex string; includes the address and the subaccount identifier of the primary subaccount to add a signer to. + +`signer`: a `bytes32` sent as a hex string; includes the address (first 20 bytes) that'll be used as the `sender's` signer. + +{% hint style="info" %} +**Notes**: + +* the last 12 bytes of the `signer` field do not matter and can be set to anything. +* set `signer` to the zero address to revoke current signer on the provided `sender`. + {% endhint %} + +`nonce`: the `tx_nonce`. This is an incrementing nonce, can be obtained using the [Nonces](https://docs.nado.xyz/developer-resources/api/gateway/queries/nonces) query. + +{% hint style="warning" %} +**Note**: for signing you should always use the data type specified in the solidity struct which might be different from the type sent in the request e.g: `nonce` should be an `uint64` for **Signing** but should be sent as a `string` in the final payload. +{% endhint %} + +## Response + +#### Success + +```json +{ + "status": "success", + "signature": {signature}, + "request_type": "execute_link_signer" +} +``` + +#### Failure + +```json +{ + "status": "failure", + "signature": {signature}, + "error": "{error_msg}", + "error_code": {error_code}, + "request_type": "execute_link_signer" +} +``` diff --git a/docs/nado/developer-resources/api/gateway/executes/liquidate-subaccount.md b/docs/nado/developer-resources/api/gateway/executes/liquidate-subaccount.md new file mode 100644 index 0000000..4f42d9d --- /dev/null +++ b/docs/nado/developer-resources/api/gateway/executes/liquidate-subaccount.md @@ -0,0 +1,138 @@ +# Liquidate Subaccount + +## Rate limits + +* 30 liquidations/min or 5 liquidations every 10 seconds per wallet. (**weight=20**) + +{% hint style="info" %} +See more details in [API Rate limits](https://docs.nado.xyz/developer-resources/api/rate-limits). +{% endhint %} + +## Request + +{% tabs %} +{% tab title="Websocket" %} +**Connect** + +`WEBSOCKET [GATEWAY_WEBSOCKET_ENDPOINT]` + +**Message** + +```json + +{ + "liquidate_subaccount": { + "tx": { + "sender": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43746573743000000000000000", + "liquidatee": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43746573743000000000000000", + "productId": 1, + "isEncodedSpread": false, + "amount": "1000000000000000000", + "nonce": "1" + }, + "signature": "0x" + } +} +``` + +{% endtab %} + +{% tab title="REST" %} `POST [GATEWAY_REST_ENDPOINT]/execute` + +**Body** + +```json + +{ + "liquidate_subaccount": { + "tx": { + "sender": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43746573743000000000000000", + "liquidatee": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43746573743000000000000000", + "mode": 0, + "healthGroup": 1, + "amount": "1000000000000000000", + "nonce": "1" + }, + "signature": "0x" + } +} +``` + +{% endtab %} +{% endtabs %} + +## Request Parameters + +
ParameterTypeRequiredDescription
txobjectYesLiquidate subaccount transaction object. See Signing section for details on the transaction fields.
tx.senderstringYesHex string representing the subaccount's 32 bytes (address + subaccount name) of the tx sender.
tx.liquidateestringYesHex string representing the subaccount's 32 bytes (address + subaccount name) of the subaccount being liquidated.
tx.productIdnumberYes

Perp Liquidation:

  • A valid perp product Id.

Spot Liquidation:

  • A valid spot product Id.

Spread Liquidation:

  • An encoded perp / spot product Ids, where the lower 16 bits represent the spot product and the higher 16 bits represent the perp product. isEncodedSpread must be set to true for spread liquidation. See Signing section for more details.
tx.isEncodedSpreadboolYesWhen set to true, the productId is expected to encode a perp and spot product Ids as follows: (perp_id << 16) | spot_id
tx.amountstringYesThe amount to liquidate multiplied by 1e18, sent as a string.
tx.noncestringYesThis is an incrementing nonce, can be obtained using the Nonces query.
signaturestringYesSigned transaction. See Signing section for more details.
+ +## Signing + +{% hint style="info" %} +See more details and examples in our [signing](https://docs.nado.xyz/developer-resources/api/gateway/signing) page. +{% endhint %} + +The solidity typed data struct that needs to be signed is: + +```solidity +struct LiquidateSubaccount { + bytes32 sender; + bytes32 liquidatee; + uint32 productId; + bool isEncodedSpread; + int128 amount; + uint64 nonce; +} +``` + +`sender`: a `bytes32` sent as a hex string; includes the address and the subaccount identifier. + +`liquidatee`: a `bytes32` sent as a hex string; includes the address and the subaccount identifier. + +`productId`: The product to liquidate as well as the liquidation mode. + +* *Perp liquidation* ⇒ A valid `perp` product id is provided and `isEncodedSpread` is set to `false`. +* *Spot liquidation* ⇒ A valid `spot` product id is provided and `isEncodedSpread` is set to `false` +* *Spread Liquidation* => If there are perp and spot positions in different directions, liquidate both at the same time. Must be set to a 32 bits integer where the lower 16 bits represent the `spot` product and the higher 16 bits represent the `perp` product. `isEncodedSpread` must be set to `true`. + +***Computing\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* ****productId**** \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*for Spread Liquidation*** + +```python +btc_spot = 1 +btc_perp = 2 + +spread_product_id = (btc_perp << 16) | btc_spot +``` + +`isEncodedSpread`: indicates whether `productId` encodes both a `spot` and a `perp` product Id for spread liquidation. + +`amount`: the amount to liquidate multiplied by 1e18, sent as a string. Can be positive or negative, depending on if the user’s balance is positive or negative. + +`nonce`: the `tx_nonce`. This is an incrementing nonce, can be obtained using the [Nonces](https://docs.nado.xyz/developer-resources/api/gateway/queries/nonces) query. + +{% hint style="warning" %} +**Note**: for signing you should always use the data type specified in the solidity struct which might be different from the type sent in the request e.g: `nonce` should be an `uint64` for **Signing** but should be sent as a `string` in the final payload. +{% endhint %} + +## Response + +#### Success + +```json +{ + "status": "success", + "signature": {signature}, + "request_type": "execute_liquidate_subaccount" +} +``` + +#### Failure + +```json +{ + "status": "failure", + "signature": {signature}, + "error": "{error_msg}", + "error_code": {error_code}, + "request_type": "execute_liquidate_subaccount" +} +``` diff --git a/docs/nado/developer-resources/api/gateway/executes/mint-nlp.md b/docs/nado/developer-resources/api/gateway/executes/mint-nlp.md new file mode 100644 index 0000000..2f85675 --- /dev/null +++ b/docs/nado/developer-resources/api/gateway/executes/mint-nlp.md @@ -0,0 +1,111 @@ +# Mint NLP + +## Rate limits + +* Wallet weight = `10` - allows 60 mints/min or 10 mints every 10 seconds per wallet. + +{% hint style="info" %} +See more details in [API Rate limits](https://docs.nado.xyz/developer-resources/api/rate-limits). +{% endhint %} + +## Request + +{% tabs %} +{% tab title="Websocket" %} +**Connect** + +`WEBSOCKET [GATEWAY_WEBSOCKET_ENDPOINT]` + +**Message** + +```json +{ + "mint_nlp": { + "tx": { + "sender": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43746573743000000000000000", + "quoteAmount": "1000000000000000000", + "nonce": "1" + }, + "signature": "0x" + } +} +``` + +{% endtab %} + +{% tab title="REST" %} `POST [GATEWAY_REST_ENDPOINT]/execute` + +**Body** + +```json +{ + "mint_lp": { + "tx": { + "sender": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43746573743000000000000000", + "productId": 1, + "amountBase": "1000000000000000000", + "quoteAmountLow": "10000000000000000000000", + "quoteAmountHigh": "20000000000000000000000", + "nonce": "1" + }, + "signature": "0x" + } +} +``` + +{% endtab %} +{% endtabs %} + +## Request Parameters + +
ParameterTypeRequiredDescription
txobjectYesMint NLP transaction object. See Signing section for details on the transaction fields.
tx.senderstringYesHex string representing the subaccount's 32 bytes (address + subaccount name) of the tx sender.
tx.quoteAmountstringYesThis amount of quote to be consumed by minting NLPs multiplied by 1e18, sent as a string.
tx.noncestringYesThis is an incrementing nonce, can be obtained using the Nonces query.
signaturestringYesHex string representing hash of the signed transaction. See Signing section for more details.
spot_leveragebooleanNoIndicates whether leverage should be used; when set to false , the mint fails if the transaction causes a borrow on the subaccount. Defaults to true.
+ +## Signing + +{% hint style="info" %} +See more details and examples in our [signing](https://docs.nado.xyz/developer-resources/api/gateway/signing) page. +{% endhint %} + +The solidity typed data struct that needs to be signed is: + +```solidity +struct MintNlp { + bytes32 sender; + uint128 quoteAmount; + uint64 nonce; +} +``` + +`sender`: a `bytes32` sent as a hex string; includes the address and the subaccount identifier. + +`quoteAmount`: this is the amount of quote to be consumed by minting NLPs, sent as a string. This must be positive and must be specified with 18 decimals. + +`nonce`: the `tx_nonce`. This is an incrementing nonce, can be obtained using the [Nonces](https://docs.nado.xyz/developer-resources/api/gateway/queries/nonces) query. + +{% hint style="warning" %} +**Note**: for signing you should always use the data type specified in the solidity struct which might be different from the type sent in the request e.g: `nonce` should be an `uint64` for **Signing** but should be sent as a `string` in the final payload. +{% endhint %} + +## Response + +#### Success + +```json +{ + "status": "success", + "signature": {signature}, + "request_type": "execute_mint_nlp" +} +``` + +#### Failure + +```json +{ + "status": "failure", + "signature": {signature}, + "error": "{error_msg}", + "error_code": {error_code}, + "request_type": "execute_min_nlp" +} +``` diff --git a/docs/nado/developer-resources/api/gateway/executes/place-order.md b/docs/nado/developer-resources/api/gateway/executes/place-order.md new file mode 100644 index 0000000..8edb4dd --- /dev/null +++ b/docs/nado/developer-resources/api/gateway/executes/place-order.md @@ -0,0 +1,185 @@ +# Place Order + +## Rate limits + +* With spot leverage: 600 orders/minute or 10 orders/sec per wallet. (**weight=1**) +* Without spot leverage: 30 orders/min or 5 orders every 10 seconds per wallet. (**weight = 20**) + +{% hint style="info" %} +See more details in [API Rate limits](https://docs.nado.xyz/developer-resources/api/rate-limits). +{% endhint %} + +## Request + +{% tabs %} +{% tab title="Websocket" %} +**Connect** + +`WEBSOCKET [GATEWAY_WEBSOCKET_ENDPOINT]` + +**Message** + +```json + +{ + "place_order": { + "product_id": 1, + "order": { + "sender": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43746573743000000000000000", + "priceX18": "1000000000000000000", + "amount": "1000000000000000000", + "expiration": "4294967295", + "nonce": "1757062078359666688", + "appendix": "1" + }, + "signature": "0x", + "id": 100 + } +} +``` + +{% endtab %} + +{% tab title="REST" %} `POST [GATEWAY_REST_ENDPOINT]/execute` + +**Body** + +```json + +{ + "place_order": { + "product_id": 1, + "order": { + "sender": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43746573743000000000000000", + "priceX18": "1000000000000000000", + "amount": "1000000000000000000", + "expiration": "4294967295", + "nonce": "1757062078359666688" + }, + "signature": "0x", + "id": 100 + } +} +``` + +{% endtab %} +{% endtabs %} + +## Request Parameters + +
ParameterTypeRequiredDescription
product_idnumberYesId of spot / perp product for which to place order. Use All products query to retrieve all valid product ids.
orderobjectYesOrder object, see Signing section for details on each order field.
order.senderstringYesHex string representing the subaccount's 32 bytes (address + subaccount name) of the tx sender.
order.priceX18stringYesPrice of the order multiplied by 1e18.
order.amountstringYesQuantity of the order multiplied by 1e18.
order.expirationstringYesA time after which the order should automatically be cancelled, as a timestamp in seconds after the unix epoch.
order.noncestringYesUsed to differentiate between the same order multiple times. See Signing section for more details.
order.appendixstringYesEncodes various order properties including execution types, isolated positions, TWAP parameters, and trigger types. See order appendix section for more details.
signaturestringYesHex string representing hash of the signed order. See Signing section for more details.
digeststringNoHex string representing a hash of the order.
spot_leveragebooleanNoIndicates whether leverage should be used; when set to false , placing the order fails if the transaction causes a borrow on the subaccount. Defaults to true.
idnumberNoAn optional id that when provided is returned as part of Fill and OrderUpdate stream events. See subscriptions for more details.

NOTE: The client id should not be used to differentiate orders, as it is not included in the order hash (i.e., the order digest). Instead, use the last 20 bits of the order nonce to distinguish between similar orders. For more details, refer to Order Nonce.
+ +## Signing + +{% hint style="info" %} +See more details and examples in our [signing](https://docs.nado.xyz/developer-resources/api/gateway/signing) page. +{% endhint %} + +The solidity typed data struct that needs to be signed is: + +```solidity +struct Order { + bytes32 sender; + int128 priceX18; + int128 amount; + uint64 expiration; + uint64 nonce; + uint128 appendix; +} +``` + +`sender`: a `bytes32` sent as a hex string; includes the address and the subaccount identifier + +`priceX18`: an `int128` representing the price of the order multiplied by 1e18, sent as a string. For example, a price of 1 USDT0 would be sent as `"1000000000000000000"` + +`amount`: an `int128` representing the quantity of the order multiplied by 1e18, sent as a string. A positive amount means that this is a buy order, and a negative amount means this is a sell order. + +`expiration`: a time after which the order should automatically be cancelled, as a timestamp in seconds after the unix epoch, sent as a string. + +### Order Nonce + +`nonce`: used to differentiate between the same order multiple times, and a user trying to place an order with the same parameters twice. Sent as a string. Encodes two bit of information: + +* Most significant `44` bits encoding the time in milliseconds (a `recv_time`) after which the order should be ignored by the matching engine +* Least significant `20` bits are a random integer used to avoid hash collisions + + For example, to place an order with a random integer of `1000`, and a discard time 50 ms from now, we would send a nonce of `((timestamp_ms() + 50) << 20) + 1000)` + +```python +import time +unix_epoch_ms = int(time.time()) * 1000 +nonce = ((unix_epoch_ms + 50) << 20) + 1000 +``` + +{% hint style="warning" %} +**Note**: for signing you should always use the data type specified in the solidity struct which might be different from the type sent in the request e.g: `nonce` should be an `uint64` for **Signing** but should be sent as a `string` in the final payload. +{% endhint %} + +## Order Appendix + +{% hint style="info" %} +See more details and examples in our [Order Appendix](https://docs.nado.xyz/developer-resources/api/order-appendix) page. +{% endhint %} + +`appendix`: is a 128-bit integer that encodes extra order parameters like execution type, isolated margin, and trigger type. + +### Bit Layout + +```json +| value | reserved | trigger | reduce only | order type | isolated | version | +| 64 bits | 50 bits | 2 bits | 1 bit | 2 bits | 1 bit | 8 bits | +| 127..64 | 63..14 | 13..12 | 11 | 10..9 | 8 | 7..0 | +``` + +**Fields (from LSB to MSB):** + +* **Version (8 bits, 0–7)** – protocol version (currently `1`) +* **Isolated (1 bit, 8)** – whether the order uses isolated margin +* **Order Type (2 bits, 9–10)** – 0 = DEFAULT, 1 = IOC, 2 = FOK, 3 = POST\_ONLY + * `0` - `DEFAULT`: Standard limit order behavior + * `1` - `IOC (Immediate or Cancel)`: Execute immediately, cancel unfilled portion + * `2` - `FOK (Fill or Kill)`: Execute completely or cancel entire order + * `3` - `POST_ONLY`: Only add liquidity, reject if would take liquidity +* **Reduce Only (1 bit, 11)** – only decreases an existing position. +* **Trigger Type (2 bits, 12–13)** – 0 = NONE, 1 = PRICE, 2 = TWAP, 3 = TWAP\_CUSTOM\_AMOUNTS +* **Reserved (50 bits, 14–63)** – future use +* **Value (64 bits, 64–127)** – extra data (isolated margin or TWAP parameters) + * if `trigger` is `2` or `3``value` represents how many times the TWAP order will execute and the maximum acceptable slippage. Encoded as: + + ```json + | times | slippage_x6 | + | 32 bits| 32 bits | + ``` + + * `times` : Number of TWAP executions. + * `slippage_x6`: Maximum slippage × 1,000,000 (6 decimal precision). + * if `isolated` is `1``value` represents `margin_x6` (in x6 precision, 6 decimals) to be transferred to the isolated subaccount when the order gets its first match. + * otherwise, `value` is `0`. + +## Response + +#### Success + +```json +{ + "status": "success", + "signature": {signature}, + "data": { + "digest": {order digest} + }, + "request_type": "execute_place_order" + "id": 100 +} +``` + +#### Failure + +```json +{ + "status": "failure", + "signature": {signature}, + "error": "{error_msg}", + "error_code": {error_code}, + "request_type": "execute_place_order" +} +``` diff --git a/docs/nado/developer-resources/api/gateway/executes/place-orders.md b/docs/nado/developer-resources/api/gateway/executes/place-orders.md new file mode 100644 index 0000000..eea2f27 --- /dev/null +++ b/docs/nado/developer-resources/api/gateway/executes/place-orders.md @@ -0,0 +1,193 @@ +# Place Orders + +Place multiple orders in a single request. This is more efficient than placing orders individually and allows for better control over batch order placement. + +## Rate limits + +* With spot leverage: 600 orders/minute or 10 orders/sec per wallet. (**weight=1 per order**) +* Without spot leverage: 30 orders/min or 5 orders every 10 seconds per wallet. (**weight = 20 per order**) + +{% hint style="info" %} +See more details in [API Rate limits](https://docs.nado.xyz/developer-resources/api/rate-limits). +{% endhint %} + +{% hint style="warning" %} +**Note**: There is a 50ms processing penalty for each `place_orders` request to ensure fair sequencing and prevent gaming of the matching engine. +{% endhint %} + +## Request + +{% tabs %} +{% tab title="Websocket" %} +**Connect** + +`WEBSOCKET [GATEWAY_WEBSOCKET_ENDPOINT]` + +**Message** + +```json +{ + "place_orders": { + "orders": [ + { + "product_id": 2, + "order": { + "sender": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43746573743000000000000000", + "priceX18": "100000000000000000000000", + "amount": "1000000000000000000", + "expiration": "4294967295", + "nonce": "1757062078359666688", + "appendix": "1" + }, + "signature": "0x...", + "id": 100 + }, + { + "product_id": 3, + "order": { + "sender": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43746573743000000000000000", + "priceX18": "3800000000000000000000", + "amount": "2000000000000000000", + "expiration": "4294967295", + "nonce": "1757062078359666689", + "appendix": "1" + }, + "signature": "0x...", + "id": 101 + } + ], + "stop_on_failure": false + } +} +``` + +{% endtab %} + +{% tab title="REST" %} `POST [GATEWAY_REST_ENDPOINT]/execute` + +**Body** + +```json +{ + "place_orders": { + "orders": [ + { + "product_id": 2, + "order": { + "sender": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43746573743000000000000000", + "priceX18": "100000000000000000000000", + "amount": "1000000000000000000", + "expiration": "4294967295", + "nonce": "1757062078359666688", + "appendix": "1" + }, + "signature": "0x...", + "id": 100 + }, + { + "product_id": 3, + "order": { + "sender": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43746573743000000000000000", + "priceX18": "3800000000000000000000", + "amount": "2000000000000000000", + "expiration": "4294967295", + "nonce": "1757062078359666689", + "appendix": "1" + }, + "signature": "0x...", + "id": 101 + } + ], + "stop_on_failure": false + } +} +``` + +{% endtab %} +{% endtabs %} + +## Request Parameters + +
ParameterTypeRequiredDescription
ordersarrayYesArray of order objects to place. Each order follows the same structure as Place Order.
orders[].product_idnumberYesId of spot / perp product for which to place order.
orders[].orderobjectYesOrder object (same structure as single order placement).
orders[].signaturestringYesHex string representing hash of the signed order.
orders[].digeststringNoHex string representing a hash of the order.
orders[].spot_leveragebooleanNoIndicates whether leverage should be used for this order. Defaults to true.
orders[].idnumberNoAn optional id returned in Fill and OrderUpdate events.
stop_on_failurebooleanNoIf true, stops processing remaining orders when the first order fails. Already successfully placed orders are NOT cancelled. Defaults to false.
+ +## Response + +```json +{ + "status": "success", + "data": { + "place_orders": [ + { + "digest": "0x1234...", + "error": null + }, + { + "digest": null, + "error": "insufficient margin" + } + ] + } +} +``` + +### Response Fields + +
FieldDescription
digestOrder digest (32-byte hash) if successfully placed, null if failed.
errorError message if order failed, null if successful.
+ +## Behavior + +* **Partial Success**: By default, orders are processed independently. Some orders may succeed while others fail. +* **Stop on Failure**: Set `stop_on_failure: true` to stop processing remaining orders when the first order fails. Already successfully placed orders remain on the book. +* **Order Signing**: Each order must be individually signed using EIP712 (see [Signing](https://docs.nado.xyz/developer-resources/api/gateway/signing) for details). +* **Rate Limits**: Rate limit weight is calculated per order (1 per order with leverage, 20 per order without). + +## Use Cases + +* **Spread Trading**: Place both legs of a spread trade in one request +* **Multiple Markets**: Open positions across multiple products in one request + +## Example + +Placing BTC and ETH perp orders simultaneously: + +```javascript +const placeOrdersParams = { + orders: [ + { + product_id: 2, // BTC-PERP + order: { + sender: subaccount, + priceX18: toX18(100000), // $100k + amount: toX18(0.1), + expiration: getExpiration(OrderType.DEFAULT), + nonce: genOrderNonce(), + appendix: buildAppendix() + }, + signature: await signOrder(btcOrder), + id: 1 + }, + { + product_id: 3, // ETH-PERP + order: { + sender: subaccount, + priceX18: toX18(3800), // $3.8k + amount: toX18(1), + expiration: getExpiration(OrderType.DEFAULT), + nonce: genOrderNonce(), + appendix: buildAppendix() + }, + signature: await signOrder(ethOrder), + id: 2 + } + ], + stop_on_failure: false +}; + +const response = await client.execute({ place_orders: placeOrdersParams }); +``` + +## See Also + +* [Place Order](https://docs.nado.xyz/developer-resources/api/gateway/executes/place-order) - Single order placement +* [Cancel And Place](https://docs.nado.xyz/developer-resources/api/gateway/executes/cancel-and-place) - Atomic cancel and place +* [Signing](https://docs.nado.xyz/developer-resources/api/gateway/signing) - EIP712 order signing diff --git a/docs/nado/developer-resources/api/gateway/executes/transfer-quote.md b/docs/nado/developer-resources/api/gateway/executes/transfer-quote.md new file mode 100644 index 0000000..3853592 --- /dev/null +++ b/docs/nado/developer-resources/api/gateway/executes/transfer-quote.md @@ -0,0 +1,132 @@ +# Transfer Quote + +## Fees + +Transfers between subaccounts incur a network fee: + +* **Standard transfers**: 1 USDT0 +* **Isolated subaccount transfers**: 0.1 USDT0 (when either sender or recipient is an isolated subaccount) + +The fee is automatically deducted from the sender's balance. + +## Rate limits + +* 60 transfer quotes/min or 10 every 10 seconds per wallet. (**weight=10**) +* A max of 5 transfer quotes to new recipients (subaccounts) every 24hrs. + * **Note**: Transferring quote to a subaccount that doesn't exist, creates the subaccount. + +{% hint style="info" %} +See more details in [API Rate limits](https://docs.nado.xyz/developer-resources/api/rate-limits). +{% endhint %} + +## Request + +{% tabs %} +{% tab title="Websocket" %} +**Connect** + +`WEBSOCKET [GATEWAY_WEBSOCKET_ENDPOINT]` + +**Message** + +```json + +{ + "transfer_quote": { + "tx": { + "sender": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43746573743000000000000000", + "recipient": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43746573743100000000000000", + "amount": "10000000000000000000", + "nonce": "1" + }, + "signature": "0x" + } +} +``` + +{% endtab %} + +{% tab title="REST" %} `POST [GATEWAY_REST_ENDPOINT]/execute` + +**Body** + +```json +{ + "transfer_quote": { + "tx": { + "sender": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43746573743000000000000000", + "recipient": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43746573743100000000000000", + "amount": "10000000000000000000", + "nonce": "1" + }, + "signature": "0x" + } +} +``` + +{% endtab %} +{% endtabs %} + +## Request Parameters + +
ParameterTypeRequiredDescription
txobjectYesTransfer Quote transaction object. See Signing section for details on the transaction fields.
tx.senderstringYesHex string representing the subaccount's 32 bytes (address + subaccount name) of the tx sender.
tx.recipientstringYesHex string representing the subaccount's 32 bytes (address + subaccount name) of the quote recipient.
tx.amountstringYesThe amount of USDT0 to transfer, denominated in x18. Transfr amount must be >= 5 USDT0 . See Signing section for more details.
tx.noncestringYesThis is an incrementing nonce, can be obtained using the Nonces query.
signaturestringYesHex string representing hash of the signed transaction. See Signing section for more details.
+ +## Signing + +{% hint style="info" %} +See more details and examples in our [signing](https://docs.nado.xyz/developer-resources/api/gateway/signing) page. +{% endhint %} + +The solidity typed data struct that needs to be signed is: + +```solidity +struct TransferQuote { + bytes32 sender; + bytes32 recipient; + uint128 amount; + uint64 nonce; +} +``` + +`sender`: a `bytes32` sent as a hex string; includes the address and the subaccount identifier. + +`recipient`: a `bytes32` sent as a hex string; includes the address and the subaccount identifier. + +`amount`: the amount of quote to transfer, sent as an `x18` string. + +{% hint style="warning" %} +**Notes:** + +* If you are transferring `5 USDT0`, must specify `5000000000000000000` i.e 5 USDT0 \* 1e18. +* Transfer amount should be >= 5 USDT0. + {% endhint %} + +`nonce`: the `tx_nonce`. This is an incrementing nonce, can be obtained using the [Nonces](https://docs.nado.xyz/developer-resources/api/gateway/queries/nonces) query. + +{% hint style="warning" %} +**Note**: for signing you should always use the data type specified in the solidity struct which might be different from the type sent in the request e.g: `nonce` should be an `uint64` for **Signing** but should be sent as a `string` in the final payload. +{% endhint %} + +## Response + +#### Success + +```json +{ + "status": "success", + "signature": {signature}, + "request_type": "execute_transfer_quote" +} +``` + +#### Failure + +```json +{ + "status": "failure", + "signature": {signature}, + "error": "{error_msg}", + "error_code": {error_code}, + "request_type": "execute_transfer_quote" +} +``` diff --git a/docs/nado/developer-resources/api/gateway/executes/withdraw-collateral.md b/docs/nado/developer-resources/api/gateway/executes/withdraw-collateral.md new file mode 100644 index 0000000..e41bcd8 --- /dev/null +++ b/docs/nado/developer-resources/api/gateway/executes/withdraw-collateral.md @@ -0,0 +1,120 @@ +# Withdraw Collateral + +{% hint style="info" %} +**Note**: use the [max withdrawable](https://docs.nado.xyz/developer-resources/api/gateway/queries/max-withdrawable) query to determine the max amount you can withdraw for a given spot product. +{% endhint %} + +## Rate limits + +* With spot leverage: 60 withdrawals/min or 10 withdrawals every 10 seconds per wallet. (**weight = 10**) +* Without spot leverage: 30 withdrawals/min or 5 withdrawals every 10 seconds per wallet. (**weight=20**) + +{% hint style="info" %} +See more details in [API Rate limits](https://docs.nado.xyz/developer-resources/api/rate-limits). +{% endhint %} + +## Request + +{% tabs %} +{% tab title="Websocket" %} +**Connect** + +`WEBSOCKET [GATEWAY_WEBSOCKET_ENDPOINT]` + +**Message** + +```json + +{ + "withdraw_collateral": { + "tx": { + "sender": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43746573743000000000000000", + "productId": 1, + "amount": "1000000000000000000", + "nonce": "1" + }, + "signature": "0x" + } +} +``` + +{% endtab %} + +{% tab title="REST" %} `POST [GATEWAY_REST_ENDPOINT]/execute` + +**Body** + +```json + +{ + "withdraw_collateral": { + "tx": { + "sender": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43746573743000000000000000", + "productId": 1, + "amount": "1000000000000000000", + "nonce": "1" + }, + "signature": "0x" + } +} +``` + +{% endtab %} +{% endtabs %} + +## Request Parameters + +
ParameterTypeRequiredDescription
txobjectYesWithdraw collateral transaction object. See Signing section for details on the transaction fields.
tx.senderstringYesHex string representing the subaccount's 32 bytes (address + subaccount name) of the tx sender.
tx.productIdnumberYesA spot product ID to withdraw from.
tx.amountstringYesThe amount of the asset to withdraw, denominated in the base ERC20 token of the specified product e.g: USDT0 (product=0) has 6 decimals whereas wETH (product=3) has 18. See Signing section for more details.
tx.noncestringYesThis is an incrementing nonce, can be obtained using the Nonces query.
signaturestringYesHex string representing hash of the signed transaction. See Signing section for more details.
spot_leveragebooleanNoIndicates whether leverage should be used; when set to false , the withdrawal fails if the transaction causes a borrow on the subaccount. Defaults to true.
+ +## Signing + +{% hint style="info" %} +See more details and examples in our [signing](https://docs.nado.xyz/developer-resources/api/gateway/signing) page. +{% endhint %} + +The solidity typed data struct that needs to be signed is: + +```solidity +struct WithdrawCollateral { + bytes32 sender; + uint32 productId; + uint128 amount; + uint64 nonce; +} +``` + +`sender`: a `bytes32` sent as a hex string; includes the address and the subaccount identifier. + +`productId`: a `uint32` that specifies the product you’d like to withdraw collateral from; must be for a spot product. + +`amount`: the amount of asset to withdraw, sent as a string. Note that this is different from the amounts provided in transactions that aren’t `depositCollateral`. This is the raw amount of the ERC20 token you want to receive, i.e. if USDT0 has 6 decimals and you want to withdraw 1 USDT0, specify 1e6; if wETH has 18 decimals and you want to withdraw 1 wETH, specify 1e18. Use [all products](https://docs.nado.xyz/developer-resources/api/gateway/queries/all-products) query to view the token address of the corresponding product which can be used to determine the correct decimals to use. + +`nonce`: the `tx_nonce`. This is an incrementing nonce, can be obtained using the [Nonces](https://docs.nado.xyz/developer-resources/api/gateway/queries/nonces) query. + +{% hint style="warning" %} +**Note**: for signing you should always use the data type specified in the solidity struct which might be different from the type sent in the request e.g: `nonce` should be an `uint64` for **Signing** but should be sent as a `string` in the final payload. +{% endhint %} + +## Response + +#### Success + +```json +{ + "status": "success", + "signature": {signature}, + "request_type": "execute_withdraw_collateral" +} +``` + +#### Failure + +```json +{ + "status": "failure", + "signature": {signature}, + "error": "{error_msg}", + "error_code": {error_code}, + "request_type": "execute_withdraw_collateral" +} +``` diff --git a/docs/nado/developer-resources/api/gateway/queries.md b/docs/nado/developer-resources/api/gateway/queries.md new file mode 100644 index 0000000..8d351a1 --- /dev/null +++ b/docs/nado/developer-resources/api/gateway/queries.md @@ -0,0 +1,28 @@ +# Queries + +All queries go through the following endpoint; the exact details of the query are specified by query params or `Websocket` messages. + +* **Websocket**: `WEBSOCKET [GATEWAY_WEBSOCKET_ENDPOINT]` +* **REST**: `GET [GATEWAY_REST_ENDPOINT]/query` or `POST [GATEWAY_REST_ENDPOINT]/query` + +## Overview + +### **Amounts and Prices** + +In general, amounts come back normalized to 18 decimal places. Meaning that for a balance of 1 USDT0, regardless of the number of decimals USDT0 has on-chain, a value of 1e18 will be returned. + +Prices are in `x18`, so if the price of one wBTC is $20,000, regardless of the number of decimals wBTC has on-chain, the price will be returned as `20,000 * 1e18`. + +## API Response + +All `queries` return in the format: + +```json +{ + "status": "success" | "failure", + "data"?: {data}, + "error"?: "{error_msg}", + "error_code"?: {error_code}, + "request_type": "{request_type}" +} +``` diff --git a/docs/nado/developer-resources/api/gateway/queries/all-products.md b/docs/nado/developer-resources/api/gateway/queries/all-products.md new file mode 100644 index 0000000..e20e846 --- /dev/null +++ b/docs/nado/developer-resources/api/gateway/queries/all-products.md @@ -0,0 +1,155 @@ +# All Products + +## Rate limits + +* 480 requests/min or 8 requests/sec per IP address. (**weight = 5**) + +{% hint style="info" %} +See more details in [API Rate limits](https://docs.nado.xyz/developer-resources/api/rate-limits) +{% endhint %} + +## Request + +{% tabs %} +{% tab title="Websocket" %} +**Connect** + +`WEBSOCKET [GATEWAY_WEBSOCKET_ENDPOINT]` + +**Message** + +```json +{ + "type": "all_products" +} +``` + +{% endtab %} + +{% tab title="REST (GET)" %} **GET** `[GATEWAY_REST_ENDPOINT]/query?type=all_products` +{% endtab %} + +{% tab title="REST (POST)" %} `POST [GATEWAY_REST_ENDPOINT]/query` + +**Body** + +```json +{ + "type": "all_products" +} +``` + +{% endtab %} +{% endtabs %} + +## Response + +{% hint style="info" %} +**Note**: + +* A product is some asset / position an account can take on. +* A market is a venue for a product against USDT0. +* All products have a market quoted against USDT0, except for product 0. +* Product 0 is the USDT0 asset itself. +* You can retrieve product symbols via [symbols](https://docs.nado.xyz/developer-resources/api/symbols "mention") query.Body + {% endhint %} + +```json +{ + "status": "success", + "data": { + "spot_products": [ + { + "product_id": 0, + "oracle_price_x18": "1000000000000000000", + "risk": { + "long_weight_initial_x18": "1000000000000000000", + "short_weight_initial_x18": "1000000000000000000", + "long_weight_maintenance_x18": "1000000000000000000", + "short_weight_maintenance_x18": "1000000000000000000", + "price_x18": "1000000000000000000" + }, + "config": { + "token": "0x5f65358d61a9a281ea3bb930d05889aca21e3f4f", + "interest_inflection_util_x18": "800000000000000000", + "interest_floor_x18": "10000000000000000", + "interest_small_cap_x18": "40000000000000000", + "interest_large_cap_x18": "1000000000000000000", + "withdraw_fee_x18": "1000000000000000000", + "min_deposit_rate_x18": "0" + }, + "state": { + "cumulative_deposits_multiplier_x18": "1000000000025524653", + "cumulative_borrows_multiplier_x18": "1000347390837434279", + "total_deposits_normalized": "20001011744258817298755054194662", + "total_borrows_normalized": "1617724891363505323532211" + }, + "book_info": { + "size_increment": "0", + "price_increment_x18": "0", + "min_size": "0", + "collected_fees": "0" + } + }, + { + "product_id": 1, + "oracle_price_x18": "115575316424148798147115", + "risk": { + "long_weight_initial_x18": "900000000000000000", + "short_weight_initial_x18": "1100000000000000000", + "long_weight_maintenance_x18": "950000000000000000", + "short_weight_maintenance_x18": "1050000000000000000", + "price_x18": "115575316424148798147115" + }, + "config": { + "token": "0xc57c1c64561a37ac9e8f9039cb6deab7539d99fc", + "interest_inflection_util_x18": "800000000000000000", + "interest_floor_x18": "10000000000000000", + "interest_small_cap_x18": "40000000000000000", + "interest_large_cap_x18": "1000000000000000000", + "withdraw_fee_x18": "40000000000000", + "min_deposit_rate_x18": "0" + }, + "state": { + "cumulative_deposits_multiplier_x18": "1000000000000318713", + "cumulative_borrows_multiplier_x18": "1000347390679880473", + "total_deposits_normalized": "9000399823280682696107190850", + "total_borrows_normalized": "9580268570661550719" + }, + "book_info": { + "size_increment": "1000000000000000", + "price_increment_x18": "1000000000000000000", + "min_size": "4000000000000000", + "collected_fees": "0" + } + } + ], + "perp_products": [ + { + "product_id": 2, + "oracle_price_x18": "115432187703236794231754", + "risk": { + "long_weight_initial_x18": "950000000000000000", + "short_weight_initial_x18": "1050000000000000000", + "long_weight_maintenance_x18": "970000000000000000", + "short_weight_maintenance_x18": "1030000000000000000", + "price_x18": "115432187703236794231754" + }, + "state": { + "cumulative_funding_long_x18": "-394223711772447555304", + "cumulative_funding_short_x18": "-394223711772447555304", + "available_settle": "20092193239667417956947", + "open_interest": "113605000000000000000" + }, + "book_info": { + "size_increment": "1000000000000000", + "price_increment_x18": "1000000000000000000", + "min_size": "4000000000000000", + "collected_fees": "0" + } + } + ] + }, + "request_type": "query_all_products" +} +``` diff --git a/docs/nado/developer-resources/api/gateway/queries/contracts.md b/docs/nado/developer-resources/api/gateway/queries/contracts.md new file mode 100644 index 0000000..9131201 --- /dev/null +++ b/docs/nado/developer-resources/api/gateway/queries/contracts.md @@ -0,0 +1,62 @@ +# Contracts + +## Rate limits + +* 2400 requests/min or 40 requests/sec per IP address. (**weight = 1**) + +{% hint style="info" %} +See more details in [API Rate limits](https://docs.nado.xyz/developer-resources/api/rate-limits) +{% endhint %} + +## Request + +{% tabs %} +{% tab title="Websocket" %} +**Connect** + +`WEBSOCKET [GATEWAY_WEBSOCKET_ENDPOINT]` + +**Message** + +```json +{ + "type": "contracts" +} +``` + +{% endtab %} + +{% tab title="REST (GET)" %} **GET** `[GATEWAY_REST_ENDPOINT]/query?type=contracts` +{% endtab %} + +{% tab title="REST (POST)" %} `POST [GATEWAY_REST_ENDPOINT]/query` + +**Body** + +```json +{ + "type": "contracts" +} +``` + +{% endtab %} +{% endtabs %} + +## Response + +```json +{ + "status": "success", + "data": { + "chain_id": "763373", + "endpoint_addr": "0xf8963f7860af7de9b94893edb9a3b5c155e1fc0c" + }, + "request_type": "query_contracts" +} +``` + +{% hint style="info" %} +**Note:** + +* `endpoint_addr` is the address of the Nado endpoint contracts. Deposits are sent to the endpoint address; **this to used sign every request except ****`PlaceOrder`** + {% endhint %} diff --git a/docs/nado/developer-resources/api/gateway/queries/edge-all-products.md b/docs/nado/developer-resources/api/gateway/queries/edge-all-products.md new file mode 100644 index 0000000..fbed7fd --- /dev/null +++ b/docs/nado/developer-resources/api/gateway/queries/edge-all-products.md @@ -0,0 +1,160 @@ +# Edge All Products + +## Rate limits + +* 480 requests/min or 8 requests/sec per IP address. (**weight = 5**) + +{% hint style="info" %} +See more details in [API Rate limits](https://docs.nado.xyz/developer-resources/api/rate-limits) +{% endhint %} + +## Request + +{% tabs %} +{% tab title="Websocket" %} +**Connect** + +`WEBSOCKET [GATEWAY_WEBSOCKET_ENDPOINT]` + +**Message** + +```json +{ + "type": "edge_all_products" +} +``` + +{% endtab %} + +{% tab title="REST (GET)" %} **GET** `[GATEWAY_REST_ENDPOINT]/query?type=edge_all_products` +{% endtab %} + +{% tab title="REST (POST)" %} `POST [GATEWAY_REST_ENDPOINT]/query` + +**Body** + +```json +{ + "type": "edge_all_products" +} +``` + +{% endtab %} +{% endtabs %} + +## Response + +{% hint style="info" %} +**Note**: + +* A product is some asset / position an account can take on. +* A market is a venue for a product against USDT0. +* All products have a market quoted against USDT0, except for product 0. +* Product 0 is the USDT0 asset itself. +* You can retrieve product symbols via [symbols](https://docs.nado.xyz/developer-resources/api/symbols "mention") query.Body +* Returns a mapping of `chain_id -> all_products` + {% endhint %} + +```json +{ + "status": "success", + "data": { + "edge_all_products": { + "763373": { + "spot_products": [ + { + "product_id": 0, + "oracle_price_x18": "1000000000000000000", + "risk": { + "long_weight_initial_x18": "1000000000000000000", + "short_weight_initial_x18": "1000000000000000000", + "long_weight_maintenance_x18": "1000000000000000000", + "short_weight_maintenance_x18": "1000000000000000000", + "price_x18": "1000000000000000000" + }, + "config": { + "token": "0x5f65358d61a9a281ea3bb930d05889aca21e3f4f", + "interest_inflection_util_x18": "800000000000000000", + "interest_floor_x18": "10000000000000000", + "interest_small_cap_x18": "40000000000000000", + "interest_large_cap_x18": "1000000000000000000", + "withdraw_fee_x18": "1000000000000000000", + "min_deposit_rate_x18": "0" + }, + "state": { + "cumulative_deposits_multiplier_x18": "1000000000025524653", + "cumulative_borrows_multiplier_x18": "1000347390837434279", + "total_deposits_normalized": "20001011744258817298755054194662", + "total_borrows_normalized": "1617724891363505323532211" + }, + "book_info": { + "size_increment": "0", + "price_increment_x18": "0", + "min_size": "0", + "collected_fees": "0" + } + }, + { + "product_id": 1, + "oracle_price_x18": "115575316424148798147115", + "risk": { + "long_weight_initial_x18": "900000000000000000", + "short_weight_initial_x18": "1100000000000000000", + "long_weight_maintenance_x18": "950000000000000000", + "short_weight_maintenance_x18": "1050000000000000000", + "price_x18": "115575316424148798147115" + }, + "config": { + "token": "0xc57c1c64561a37ac9e8f9039cb6deab7539d99fc", + "interest_inflection_util_x18": "800000000000000000", + "interest_floor_x18": "10000000000000000", + "interest_small_cap_x18": "40000000000000000", + "interest_large_cap_x18": "1000000000000000000", + "withdraw_fee_x18": "40000000000000", + "min_deposit_rate_x18": "0" + }, + "state": { + "cumulative_deposits_multiplier_x18": "1000000000000318713", + "cumulative_borrows_multiplier_x18": "1000347390679880473", + "total_deposits_normalized": "9000399823280682696107190850", + "total_borrows_normalized": "9580268570661550719" + }, + "book_info": { + "size_increment": "1000000000000000", + "price_increment_x18": "1000000000000000000", + "min_size": "4000000000000000", + "collected_fees": "0" + } + } + ], + "perp_products": [ + { + "product_id": 2, + "oracle_price_x18": "115432187703236794231754", + "risk": { + "long_weight_initial_x18": "950000000000000000", + "short_weight_initial_x18": "1050000000000000000", + "long_weight_maintenance_x18": "970000000000000000", + "short_weight_maintenance_x18": "1030000000000000000", + "price_x18": "115432187703236794231754" + }, + "state": { + "cumulative_funding_long_x18": "-394223711772447555304", + "cumulative_funding_short_x18": "-394223711772447555304", + "available_settle": "20092193239667417956947", + "open_interest": "113605000000000000000" + }, + "book_info": { + "size_increment": "1000000000000000", + "price_increment_x18": "1000000000000000000", + "min_size": "4000000000000000", + "collected_fees": "0" + } + } + ] + } + } + }, + "request_type": "query_edge_all_products" +} +``` diff --git a/docs/nado/developer-resources/api/gateway/queries/fee-rates.md b/docs/nado/developer-resources/api/gateway/queries/fee-rates.md new file mode 100644 index 0000000..04477b1 --- /dev/null +++ b/docs/nado/developer-resources/api/gateway/queries/fee-rates.md @@ -0,0 +1,93 @@ +# Fee Rates + +## Rate limits + +* 1200 requests/min or 20 requests/sec per IP address. (**weight = 2**) + +{% hint style="info" %} +See more details in [API Rate limits](https://docs.nado.xyz/developer-resources/api/rate-limits) +{% endhint %} + +## Request + +{% tabs %} +{% tab title="Websocket" %} +**Connect** + +`WEBSOCKET [GATEWAY_WEBSOCKET_ENDPOINT]` + +**Message** + +```json +{ + "type": "fee_rates", + "sender": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43000000000000000000000000" +} +``` + +{% endtab %} + +{% tab title="REST (GET)" %} **GET** `[GATEWAY_REST_ENDPOINT]/query?type=fee_rates&sender={sender}` +{% endtab %} + +{% tab title="REST (POST)" %} `POST [GATEWAY_REST_ENDPOINT]/query` + +**Message** + +```json +{ + "type": "fee_rates", + "sender": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43000000000000000000000000" +} +``` + +{% endtab %} +{% endtabs %} + +## Request Parameters + +
ParameterTypeRequiredDescription
senderstringYesA bytes32 sent as a hex string; includes the address and the subaccount identifier.
+ +## Response + +```json +{ + "status": "success", + "data": { + "taker_fee_rates_x18": [ + "0", + "300000000000000", + "200000000000000", + "300000000000000", + "200000000000000" + ], + "maker_fee_rates_x18": [ + "0", + "0", + "0", + "0", + "0" + ], + "liquidation_sequencer_fee": "250000000000000000", + "health_check_sequencer_fee": "100000000000000000", + "taker_sequencer_fee": "25000000000000000", + "withdraw_sequencer_fees": [ + "10000000000000000", + "40000000000000", + "0", + "600000000000000", + "0" + ] + }, + "request_type": "query_fee_rates", +} +``` + +{% hint style="info" %} + +* `taker_fee_rates_x18`: taker fee associated with a given product indexed by `product_id`. **Note**: this fee represents the basis point (BPS) on a taker order in `x18`. +* `maker_fee_rates_x18`: maker fee associated with a given produced indexed by `product_id``.` +* `withdraw_sequencer_fees`: withdraw fees associated with a given product indexed by `product_id`. **Note**: this fee represents a fixed amount of product to be deducted as fee in `x18`. + {% endhint %} + +See our [fees](https://github.com/nadohq/nado-docs/blob/main/docs/basics/fees.md) page for details about current fee rates. diff --git a/docs/nado/developer-resources/api/gateway/queries/health-groups.md b/docs/nado/developer-resources/api/gateway/queries/health-groups.md new file mode 100644 index 0000000..664799a --- /dev/null +++ b/docs/nado/developer-resources/api/gateway/queries/health-groups.md @@ -0,0 +1,69 @@ +# Health Groups + +{% hint style="info" %} +**Note**: a health group is a perp and spot product whose health is calculated together (e.g. BTC and BTC-PERP). +{% endhint %} + +## Rate limits + +* 1200 requests/min or 20 requests/sec per IP address. (**weight = 2**) + +{% hint style="info" %} +See more details in [API Rate limits](https://docs.nado.xyz/developer-resources/api/rate-limits) +{% endhint %} + +## Request + +{% tabs %} +{% tab title="Websocket" %} +**Connect** + +`WEBSOCKET [GATEWAY_WEBSOCKET_ENDPOINT]` + +**Message** + +```json +{ + "type": "health_groups" +} +``` + +{% endtab %} + +{% tab title="REST (GET)" %} **GET** `[GATEWAY_REST_ENDPOINT]/query?type=health_groups` +{% endtab %} + +{% tab title="REST (POST)" %} `POST [GATEWAY_REST_ENDPOINT]/query` + +**Message** + +```json +{ + "type": "health_groups" +} +``` + +{% endtab %} +{% endtabs %} + +## Response + +```json +{ + "status": "success", + "data": { + "health_groups": [ + [ + 1, + 2 + ] + ] + }, + "request_type": "query_health_groups" +} +``` + +{% hint style="info" %} + +* `health_groups`: list of all available health groups. **Note**: `health_groups[i]` is the spot / perp product pair of health group `i` where `health_groups[i][0]` is the spot `product_id` and `health_groups[i][1]` is the perp `product_id`. Additionally, it is possible for a health group to only have either a spot or perp product, in which case, the product that doesn’t exist is set to `0`. + {% endhint %} diff --git a/docs/nado/developer-resources/api/gateway/queries/insurance.md b/docs/nado/developer-resources/api/gateway/queries/insurance.md new file mode 100644 index 0000000..d629cd9 --- /dev/null +++ b/docs/nado/developer-resources/api/gateway/queries/insurance.md @@ -0,0 +1,55 @@ +# Insurance + +## Rate limits + +* 1200 requests/min or 20 requests/sec per IP address. (**weight = 2**) + +{% hint style="info" %} +See more details in [API Rate limits](https://docs.nado.xyz/developer-resources/api/rate-limits) +{% endhint %} + +## Request + +{% tabs %} +{% tab title="Websocket" %} +**Connect** + +`WEBSOCKET [GATEWAY_WEBSOCKET_ENDPOINT]` + +**Message** + +```json +{ + "type": "insurance" +} +``` + +{% endtab %} + +{% tab title="REST (GET)" %} **GET** `[GATEWAY_REST_ENDPOINT]/query?type=insurance` +{% endtab %} + +{% tab title="REST (POST)" %} `POST [GATEWAY_REST_ENDPOINT]/query` + +**Message** + +```json +{ + "type": "insurance" +} +``` + +{% endtab %} +{% endtabs %} + +## Response + +```json +{ + "status": "success", + "data": { + "insurance": "552843342443351553629462" + }, + "request_type": "query_insurance" +} +``` diff --git a/docs/nado/developer-resources/api/gateway/queries/isolated-positions.md b/docs/nado/developer-resources/api/gateway/queries/isolated-positions.md new file mode 100644 index 0000000..186690d --- /dev/null +++ b/docs/nado/developer-resources/api/gateway/queries/isolated-positions.md @@ -0,0 +1,171 @@ +# Isolated Positions + +## Rate limits + +* 240 requests/min or 40 requests every 10 seconds per IP address. (**weight = 10**) + +{% hint style="info" %} +See more details in [API Rate limits](https://docs.nado.xyz/developer-resources/api/rate-limits) +{% endhint %} + +## Request + +{% tabs %} +{% tab title="Websocket" %} +**Connect** + +`WEBSOCKET [GATEWAY_WEBSOCKET_ENDPOINT]` + +**Message** + +```json +{ + "type": "isolated_positions", + "subaccount": "0xeae27ae6412147ed6d5692fd91709dad6dbfc34264656661756c740000000000" +} +``` + +{% endtab %} + +{% tab title="REST (GET)" %} **GET** `[GATEWAY_REST_ENDPOINT]/query?type=isolated_positions&subaccount={subaccount}` +{% endtab %} + +{% tab title="REST (POST)" %} `POST [GATEWAY_REST_ENDPOINT]/query` + +**Message** + +```json +{ + "type": "isolated_positions", + "subaccount": "0xeae27ae6412147ed6d5692fd91709dad6dbfc34264656661756c740000000000" +} +``` + +{% endtab %} +{% endtabs %} + +## Request Parameters + +
ParameterTypeRequiredDescription
subaccountstringYesA bytes32 sent as a hex string; includes the address and the subaccount identifier. See sender field structure for details.
+ +## Response + +{% hint style="info" %} +**Note**: + +* `isolated_positions[i].subaccount`: is the isolated subaccount for the base product. +* `healths`: + * `healths[0]`: info about your initial health, which is weighted by `long_weight_initial_x18` and `short_weight_initial_x18.` + * `healths[1]`: info about your maintenance health, which is weighted by `long_weight_maintenance_x18` and `short_weight_maintenance_x18.` + * `healths[2]`: info about your unweighted health. + {% endhint %} + +```json +{ + "status": "success", + "data": { + "isolated_positions": [ + { + "subaccount": "0xeae27ae6412147ed6d5692fd91709dad6dbfc34200000000000000280269736f", + "quote_balance": { + "product_id": 0, + "balance": { + "amount": "200044412311089295472" + } + }, + "base_balance": { + "product_id": 40, + "balance": { + "amount": "1720000000000000000000", + "v_quote_balance": "-800854578334374649165", + "last_cumulative_funding_x18": "85496772388082947" + } + }, + "quote_product": { + "product_id": 0, + "oracle_price_x18": "1000000000000000000", + "risk": { + "long_weight_initial_x18": "1000000000000000000", + "short_weight_initial_x18": "1000000000000000000", + "long_weight_maintenance_x18": "1000000000000000000", + "short_weight_maintenance_x18": "1000000000000000000", + "price_x18": "1000000000000000000" + }, + "config": { + "token": "0x5f65358d61a9a281ea3bb930d05889aca21e3f4f", + "interest_inflection_util_x18": "800000000000000000", + "interest_floor_x18": "10000000000000000", + "interest_small_cap_x18": "40000000000000000", + "interest_large_cap_x18": "1000000000000000000", + "withdraw_fee_x18": "1000000000000000000", + "min_deposit_rate_x18": "0" + }, + "state": { + "cumulative_deposits_multiplier_x18": "1000000000025524653", + "cumulative_borrows_multiplier_x18": "1000347390837434279", + "total_deposits_normalized": "20001011744258817298755054194662", + "total_borrows_normalized": "1617724891363505323532211" + }, + "book_info": { + "size_increment": "0", + "price_increment_x18": "0", + "min_size": "0", + "collected_fees": "0" + } + }, + "base_product": { + "product_id": 2, + "oracle_price_x18": "115596528090565357611177", + "risk": { + "long_weight_initial_x18": "950000000000000000", + "short_weight_initial_x18": "1050000000000000000", + "long_weight_maintenance_x18": "970000000000000000", + "short_weight_maintenance_x18": "1030000000000000000", + "price_x18": "115596528090565357611177" + }, + "state": { + "cumulative_funding_long_x18": "-394223711772447555304", + "cumulative_funding_short_x18": "-394223711772447555304", + "available_settle": "20092193239667417956947", + "open_interest": "113605000000000000000" + }, + "book_info": { + "size_increment": "1000000000000000", + "price_increment_x18": "1000000000000000000", + "min_size": "4000000000000000", + "collected_fees": "0" + } + }, + "quote_healths": [ + "200044412311089295472", + "200044412311089295472", + "200044412311089295472" + ], + "base_healths": [ + "-109839746873700492625", + "-71450034014774150595", + "-33060321155847808565" + ], + "healths": [ + { + "assets": "200044412311089295472", + "liabilities": "109839746873700492625", + "health": "90204665437388802847" + }, + { + "assets": "200044412311089295472", + "liabilities": "71450034014774150595", + "health": "128594378296315144877" + }, + { + "assets": "200044412311089295472", + "liabilities": "33060321155847808565", + "health": "166984091155241486907" + } + ] + } + ] + }, + "request_type": "query_isolated_positions" +} +``` diff --git a/docs/nado/developer-resources/api/gateway/queries/linked-signer.md b/docs/nado/developer-resources/api/gateway/queries/linked-signer.md new file mode 100644 index 0000000..157df1e --- /dev/null +++ b/docs/nado/developer-resources/api/gateway/queries/linked-signer.md @@ -0,0 +1,63 @@ +# Linked Signer + +## Rate limits + +* 480 requests/min or 8 requests/sec per IP address. (**weight = 5**) + +{% hint style="info" %} +See more details in [API Rate limits](https://docs.nado.xyz/developer-resources/api/rate-limits) +{% endhint %} + +## Request + +{% tabs %} +{% tab title="Websocket" %} +**Connect** + +`WEBSOCKET [GATEWAY_WEBSOCKET_ENDPOINT]` + +**Message** + +```json +{ + "type": "linked_signer", + "subaccount": "0x9b9989a4E0b260B84a5f367d636298a8bfFb7a9b42544353504f540000000000" +} +``` + +{% endtab %} + +{% tab title="REST (GET)" %} **GET** `[GATEWAY_REST_ENDPOINT]/query?type=linked_signer&subaccount=0x9b9989a4E0b260B84a5f367d636298a8bfFb7a9b42544353504f540000000000` +{% endtab %} + +{% tab title="REST (POST)" %} `POST [GATEWAY_REST_ENDPOINT]/query` + +**Message** + +```json +{ + "type": "linked_signer", + "subaccount": "0x9b9989a4E0b260B84a5f367d636298a8bfFb7a9b42544353504f540000000000" +} +``` + +{% endtab %} +{% endtabs %} + +## Response + +```json +{ + "status": "success", + "data": { + "linked_signer": "0x0000000000000000000000000000000000000000" + }, + "request_type": "query_linked_signer", +} +``` + +{% hint style="info" %} +**Notes**: + +* `linked_signer`: the current linked signer address (20 bytes) associated to the provided `subaccount`. It returns the zero address when no signer is linked. + {% endhint %} diff --git a/docs/nado/developer-resources/api/gateway/queries/market-liquidity.md b/docs/nado/developer-resources/api/gateway/queries/market-liquidity.md new file mode 100644 index 0000000..ac0fb89 --- /dev/null +++ b/docs/nado/developer-resources/api/gateway/queries/market-liquidity.md @@ -0,0 +1,91 @@ +# Market Liquidity + +## Rate limits + +* 2400 requests/min or 40 requests/sec per IP address. (**weight = 1**) + +{% hint style="info" %} +See more details in [API Rate limits](https://docs.nado.xyz/developer-resources/api/rate-limits) +{% endhint %} + +## Request + +{% tabs %} +{% tab title="Websocket" %} +**Connect** + +`WEBSOCKET [GATEWAY_WEBSOCKET_ENDPOINT]` + +**Message** + +```json +{ + "type": "market_liquidity", + "product_id": 1, + "depth": 10 +} +``` + +{% endtab %} + +{% tab title="REST (GET)" %} **GET** `[GATEWAY_REST_ENDPOINT]/query?type=market_liquidity&product_id={product_id}&depth={depth}` +{% endtab %} + +{% tab title="REST (POST)" %} `POST [GATEWAY_REST_ENDPOINT]/query` + +**Body** + +```json +{ + "type": "market_liquidity", + "product_id": 1, + "depth": 10 +} +``` + +{% endtab %} +{% endtabs %} + +## Request Parameters + +
ParameterTypeRequiredDescription
product_idnumberYesId of spot / perp product for which to retrieve market liquidity.
depthnumberYesNumber of price levels to retrieve. (max: 100)
+ +## Response + +```json +{ + "status": "success", + "data": { + "bids": [ + [ + "30234000000000000000000", + "663000000000000000" + ], + [ + "30170000000000000000000", + "24623000000000000000" + ] + ], + "asks": [ + [ + "30245000000000000000000", + "664000000000000000" + ], + [ + "30252000000000000000000", + "4646000000000000000" + ] + ], + "timestamp": "1681850046966693400", + "product_id": 1 + }, + "request_type": "query_market_liquidity" +} +``` + +{% hint style="info" %} +**Note:** + +* Each entry inside bids and asks is an array of price and size respectively. **Note**: that price is represented using fixed point, so it is `1e18` times greater than the decimal price. +* `timestamp` is in nanoseconds. + {% endhint %} diff --git a/docs/nado/developer-resources/api/gateway/queries/market-prices.md b/docs/nado/developer-resources/api/gateway/queries/market-prices.md new file mode 100644 index 0000000..d677f61 --- /dev/null +++ b/docs/nado/developer-resources/api/gateway/queries/market-prices.md @@ -0,0 +1,131 @@ +# Market Prices + +## Rate limits + +* 2400 requests/min or 40 requests/sec per IP address. (**weight = 1**) or length of `product_ids` for [multi-product markets](#multiple-products) query. + +{% hint style="info" %} +See more details in [API Rate limits](https://docs.nado.xyz/developer-resources/api/rate-limits) +{% endhint %} + +## Single Product + +### Request + +{% tabs %} +{% tab title="Websocket" %} +**Connect** + +`WEBSOCKET [GATEWAY_WEBSOCKET_ENDPOINT]` + +**Message** + +```json +{ + "type": "market_price", + "product_id": 1 +} +``` + +{% endtab %} + +{% tab title="REST (GET)" %} **GET** `[GATEWAY_REST_ENDPOINT]/query?type=market_price&product_id={product_id}` +{% endtab %} + +{% tab title="REST (POST)" %} `POST [GATEWAY_REST_ENDPOINT]/query` + +**Body** + +```json +{ + "type": "market_price", + "product_id": 1 +} +``` + +{% endtab %} +{% endtabs %} + +### Request Parameters + +
ParameterTypeRequiredDescription
product_idnumberYesId of spot / perp product for which to retrieve market price data.
+ +### Response + +```json +{ + "status": "success", + "data": { + "product_id": 1, + "bid_x18": "24224000000000000000000", + "ask_x18": "24243000000000000000000" + }, + "request_type": "query_market_price", +} +``` + +{% hint style="info" %} +**Note**: that price is represented using fixed point, so it is `1e18` times greater than the decimal price. +{% endhint %} + +## Multiple Products + +### Request + +{% tabs %} +{% tab title="Websocket" %} +**Connect** + +`WEBSOCKET [CORE_WEBSOCKET_ENDPOINT]` + +**Message** + +```json +{ + "type": "market_prices", + "product_ids": [1, 2] +} +``` + +{% endtab %} + +{% tab title="REST" %} `POST /query` + +**Body** + +```json +{ + "type": "market_prices", + "product_ids": [1, 2] +} +``` + +{% endtab %} +{% endtabs %} + +### Request Parameters + +
ParameterTypeRequiredDescription
product_idsnumber[]YesList of spot / perp products for which to retrieve market price data.
+ +### Response + +```json +{ + "status": "success", + "data": { + "market_prices": [ + { + "product_id": 1, + "bid_x18": "31315000000000000000000", + "ask_x18": "31326000000000000000000" + }, + { + "product_id": 2, + "bid_x18": "31291000000000000000000", + "ask_x18": "31301000000000000000000" + }, + ] + }, + "request_type": "query_market_prices" +} +``` diff --git a/docs/nado/developer-resources/api/gateway/queries/max-nlp-burnable.md b/docs/nado/developer-resources/api/gateway/queries/max-nlp-burnable.md new file mode 100644 index 0000000..6432254 --- /dev/null +++ b/docs/nado/developer-resources/api/gateway/queries/max-nlp-burnable.md @@ -0,0 +1,61 @@ +# Max NLP Burnable + +## Rate limits + +* 120 requests/min or 20 requests every 10 seconds per IP address. (**weight = 20**) + +{% hint style="info" %} +See more details in [API Rate limits](https://docs.nado.xyz/developer-resources/api/rate-limits) +{% endhint %} + +## Request + +{% tabs %} +{% tab title="Websocket" %} +**Connect** + +`WEBSOCKET [GATEWAY_WEBSOCKET_ENDPOINT]` + +**Message** + +```json +{ + "type": "max_nlp_burnable", + "sender": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43000000000000000000000000" +} +``` + +{% endtab %} + +{% tab title="REST (GET)" %} **GET** `[GATEWAY_REST_ENDPOINT]/query?type=max_nlp_burnable&sender={sender}` +{% endtab %} + +{% tab title="REST (POST)" %} `POST [GATEWAY_REST_ENDPOINT]/query` + +**Body** + +```json +{ + "type": "max_nlp_burnable", + "sender": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43000000000000000000000000" +} +``` + +{% endtab %} +{% endtabs %} + +## Request Parameters + +
ParameterTypeRequiredDescription
senderstringYesA bytes32 sent as a hex string; includes the address and the subaccount identifier.
+ +## Response + +```json +{ + "status": "success", + "data": { + "max_nlp_amount": "34250782930221490366619" + }, + "request_type": "query_max_nlp_burnable", +} +``` diff --git a/docs/nado/developer-resources/api/gateway/queries/max-nlp-mintable.md b/docs/nado/developer-resources/api/gateway/queries/max-nlp-mintable.md new file mode 100644 index 0000000..b41aa6a --- /dev/null +++ b/docs/nado/developer-resources/api/gateway/queries/max-nlp-mintable.md @@ -0,0 +1,63 @@ +# Max NLP Mintable + +## Rate limits + +* 120 requests/min or 20 requests every 10 seconds per IP address. (**weight = 20**) + +{% hint style="info" %} +See more details in [API Rate limits](https://docs.nado.xyz/developer-resources/api/rate-limits) +{% endhint %} + +## Request + +{% tabs %} +{% tab title="Websocket" %} +**Connect** + +`WEBSOCKET [GATEWAY_WEBSOCKET_ENDPOINT]` + +**Message** + +```json +{ + "type": "max_nlp_mintable", + "sender": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43000000000000000000000000", + "spot_leverage": "true" +} +``` + +{% endtab %} + +{% tab title="REST (GET)" %} **GET** `[GATEWAY_REST_ENDPOINT]/query?type=max_nlp_mintable&sender={sender}` +{% endtab %} + +{% tab title="REST (POST)" %} `POST [GATEWAY_REST_ENDPOINT]/query` + +**Body** + +```json +{ + "type": "max_nlp_mintable", + "sender": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43000000000000000000000000", + "spot_leverage": "true" +} +``` + +{% endtab %} +{% endtabs %} + +## Request Parameters + +
ParameterTypeRequiredDescription
senderstringYesA bytes32 sent as a hex string; includes the address and the subaccount identifier.
spot_leveragebooleanNoBoolean sent as a string. indicates whether leverage should be used; when set to false , returns the max amount of base LP mintable possible without borrow. Defaults to true
+ +## Response + +```json +{ + "status": "success", + "data": { + "max_quote_amount": "34250782930221490366619" + }, + "request_type": "query_max_nlp_mintable", +} +``` diff --git a/docs/nado/developer-resources/api/gateway/queries/max-order-size.md b/docs/nado/developer-resources/api/gateway/queries/max-order-size.md new file mode 100644 index 0000000..d2e400a --- /dev/null +++ b/docs/nado/developer-resources/api/gateway/queries/max-order-size.md @@ -0,0 +1,73 @@ +# Max Order Size + +## Rate limits + +* 480 requests/min or 80 requests every 10 seconds per IP address. (**weight = 5**) + +{% hint style="info" %} +See more details in [API Rate limits](https://docs.nado.xyz/developer-resources/api/rate-limits) +{% endhint %} + +## Request + +{% tabs %} +{% tab title="Websocket" %} +**Connect** + +`WEBSOCKET [GATEWAY_WEBSOCKET_ENDPOINT]` + +**Message** + +```json +{ + "type": "max_order_size", + "product_id": 1, + "sender": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43000000000000000000000000", + "price_x18": "23000000000000000000000", + "direction": "short", + "spot_leverage": "true", + "reduce_only": "false", + "isolated": "false" +} +``` + +{% endtab %} + +{% tab title="REST (GET)" %} **GET** `[GATEWAY_REST_ENDPOINT]/query?type=max_order_size&product_id={product_id}&sender={sender}&price_x18={price_x18}&direction={direction}` +{% endtab %} + +{% tab title="REST (POST)" %} `POST [GATEWAY_REST_ENDPOINT]/query` + +**Body** + +```json +{ + "type": "max_order_size", + "product_id": 1, + "sender": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43000000000000000000000000", + "price_x18": "23000000000000000000000", + "direction": "short", + "spot_leverage": "true", + "reduce_only": "false", + "isolated": "false" +} +``` + +{% endtab %} +{% endtabs %} + +## Request Parameters + +
ParameterTypeRequiredDescription
senderstringYesA bytes32 sent as a hex string; includes the address and the subaccount identifier.
product_idnumberYesId of spot / perp product for which to retrieve max order size.
price_x18stringYesAn int128 representing the price of the order multiplied by 1e18, sent as a string. For example, a price of 1 USDT0 would be sent as "1000000000000000000"
directionstringYeslong for max bid or short for max ask.
spot_leveragestringNoBoolean sent as a string. Indicates whether leverage should be used; when set to false , returns the max order possible without borrow. Defaults to true
reduce_onlystringNoBoolean sent as a string. Indicates wether to retrieve the max order size to close / reduce a position. Defaults to false
isolatedstringNoBoolean sent as a string. When set to true, calculates max order size for an isolated margin position. Defaults to false. See Isolated Margin to learn more.
+ +## Response + +```json +{ + "status": "success", + "data": { + "max_order_size": "137847520631947079935" + }, + "request_type": "query_max_order_size", +} +``` diff --git a/docs/nado/developer-resources/api/gateway/queries/max-withdrawable.md b/docs/nado/developer-resources/api/gateway/queries/max-withdrawable.md new file mode 100644 index 0000000..0c62f08 --- /dev/null +++ b/docs/nado/developer-resources/api/gateway/queries/max-withdrawable.md @@ -0,0 +1,65 @@ +# Max Withdrawable + +## Rate limits + +* 480 requests/min or 80 requests every 10 seconds per IP address. (**weight = 5**) + +{% hint style="info" %} +See more details in [API Rate limits](https://docs.nado.xyz/developer-resources/api/rate-limits) +{% endhint %} + +## Request + +{% tabs %} +{% tab title="Websocket" %} +**Connect** + +`WEBSOCKET [GATEWAY_WEBSOCKET_ENDPOINT]` + +**Message** + +```json +{ + "type": "max_withdrawable", + "product_id": 1, + "sender": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43000000000000000000000000", + "spot_leverage": "true" +} +``` + +{% endtab %} + +{% tab title="REST (GET)" %} **GET** `[GATEWAY_REST_ENDPOINT]/query?type=max_withdrawable&product_id={product_id}&sender={sender}` +{% endtab %} + +{% tab title="REST (POST)" %} `POST [GATEWAY_REST_ENDPOINT]/query` + +**Message** + +```json +{ + "type": "max_withdrawable", + "product_id": 1, + "sender": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43000000000000000000000000", + "spot_leverage": "true" +} +``` + +{% endtab %} +{% endtabs %} + +## Request Parameters + +
ParameterTypeRequiredDescription
senderstringYesA bytes32 sent as a hex string; includes the address and the subaccount identifier.
product_idnumberYesId of spot / perp product for which to retrieve max withdrawable amount.
spot_leveragestringNoBoolean sent as a string. Indicates whether leverage should be used; when set to false , returns the max withdrawable amount possible without borrow. Defaults to true
+ +## Response + +```json +{ + "status": "success", + "data": { + "max_withdrawable": "7968557932297078268650" + }, + "request_type": "query_max_withdrawable", +} +``` diff --git a/docs/nado/developer-resources/api/gateway/queries/nlp-locked-balances.md b/docs/nado/developer-resources/api/gateway/queries/nlp-locked-balances.md new file mode 100644 index 0000000..547aec2 --- /dev/null +++ b/docs/nado/developer-resources/api/gateway/queries/nlp-locked-balances.md @@ -0,0 +1,117 @@ +# NLP Locked Balances + +## Rate limits + +* 120 requests/min or 20 requests every 10 seconds per IP address. (**weight = 20**) + +{% hint style="info" %} +See more details in [API Rate limits](https://docs.nado.xyz/developer-resources/api/rate-limits) +{% endhint %} + +## Request + +{% tabs %} +{% tab title="Websocket" %} +**Connect** + +`WEBSOCKET [GATEWAY_WEBSOCKET_ENDPOINT]` + +**Message** + +```json +{ + "type": "nlp_locked_balances", + "subaccount": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43000000000000000000000000" +} +``` + +{% endtab %} + +{% tab title="REST (GET)" %} **GET** `[GATEWAY_REST_ENDPOINT]/query?type=nlp_locked_balances&subaccount={subaccount}` +{% endtab %} + +{% tab title="REST (POST)" %} `POST [GATEWAY_REST_ENDPOINT]/query` + +**Body** + +```json +{ + "type": "nlp_locked_balances", + "subaccount": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43000000000000000000000000" +} +``` + +{% endtab %} +{% endtabs %} + +## Request Parameters + +
ParameterTypeRequiredDescription
subaccountstringYesA bytes32 sent as a hex string; includes the address and the subaccount identifier.
+ +## Response + +```json +{ + "status": "success", + "data": { + "balance_locked": { + "product_id": 0, + "balance": { + "amount": "1000000000000000000000", + "last_cumulative_funding_x18": "0" + } + }, + "balance_unlocked": { + "product_id": 0, + "balance": { + "amount": "500000000000000000000", + "last_cumulative_funding_x18": "0" + } + }, + "locked_balances": [ + { + "balance": { + "product_id": 0, + "balance": { + "amount": "250000000000000000000", + "last_cumulative_funding_x18": "0" + } + }, + "unlocked_at": "1735689600" + }, + { + "balance": { + "product_id": 0, + "balance": { + "amount": "750000000000000000000", + "last_cumulative_funding_x18": "0" + } + }, + "unlocked_at": "1736035200" + } + ] + }, + "request_type": "query_nlp_locked_balances" +} +``` + +## Response Fields + +### NLP Locked Balances Response + +
Field nameDescription
balance_lockedTotal balance that is currently locked (SpotBalance object)
balance_unlockedTotal balance that is currently unlocked and available (SpotBalance object)
locked_balancesArray of individual locked balance entries with their unlock times
+ +### Locked Balance Entry + +
Field nameDescription
balanceSpotBalance object containing the locked amount
unlocked_atUnix epoch timestamp (in seconds) when this balance will unlock
+ +### SpotBalance Object + +
Field nameDescription
product_idThe product ID (typically 0 for USDT0/quote asset)
balanceBalance details object
balance.amountThe balance amount in x18 format (string)
balance.last_cumulative_funding_x18Last cumulative funding value in x18 format (string)
+ +## Notes + +* NLP positions have a 4-day lock period after minting before they can be burned (withdrawn) +* The `locked_balances` array shows individual lock entries, each with their own unlock timestamp +* `balance_locked` is the sum of all locked balances +* `balance_unlocked` represents balances that have passed their lock period and can be withdrawn diff --git a/docs/nado/developer-resources/api/gateway/queries/nlp-pool-info.md b/docs/nado/developer-resources/api/gateway/queries/nlp-pool-info.md new file mode 100644 index 0000000..9d6f693 --- /dev/null +++ b/docs/nado/developer-resources/api/gateway/queries/nlp-pool-info.md @@ -0,0 +1,89 @@ +# NLP Pool Info + +## Rate limits + +* 120 requests/min or 20 requests every 10 seconds per IP address. (**weight = 20**) + +{% hint style="info" %} +See more details in [API Rate limits](https://docs.nado.xyz/developer-resources/api/rate-limits) +{% endhint %} + +## Request + +{% tabs %} +{% tab title="Websocket" %} +**Connect** + +`WEBSOCKET [GATEWAY_WEBSOCKET_ENDPOINT]` + +**Message** + +```json +{ + "type": "nlp_pool_info" +} +``` + +{% endtab %} + +{% tab title="REST (GET)" %} **GET** `[GATEWAY_REST_ENDPOINT]/query?type=nlp_pool_info` +{% endtab %} + +{% tab title="REST (POST)" %} `POST [GATEWAY_REST_ENDPOINT]/query` + +**Body** + +```json +{ + "type": "nlp_pool_info" +} +``` + +{% endtab %} +{% endtabs %} + +## Request Parameters + +This query does not require any parameters. + +## Response + +```json +{ + "status": "success", + "data": { + "nlp_pools": [ + { + "pool_id": 1, + "subaccount": "0x0000000000000000000000000000000000000000000000000000000000000002", + "owner": "0x1234567890123456789012345678901234567890", + "balance_weight_x18": "500000000000000000", + "subaccount_info": { + "subaccount": "0x0000000000000000000000000000000000000000000000000000000000000002", + "exists": true, + "health": { + "assets": "1000000000000000000000", + "liabilities": "500000000000000000000", + "initial_health": "250000000000000000000", + "maintenance_health": "100000000000000000000" + }, + "spot_balances": [], + "perp_balances": [] + }, + "open_orders": [] + } + ] + }, + "request_type": "query_nlp_pool_info" +} +``` + +## Response Fields + +### NLP Pool Info + +
Field nameDescription
nlp_poolsArray of NLP pool objects
+ +### NLP Pool Object + +
Field nameDescription
pool_idUnique identifier for the pool
subaccountThe subaccount address associated with this pool (bytes32 hex string)
ownerThe owner address of the pool (bytes20 hex string)
balance_weight_x18Weight of this pool's balance in x18 format (string representation of u128)
subaccount_infoComplete subaccount information including health, balances, and positions
open_ordersArray of currently open orders for this pool
diff --git a/docs/nado/developer-resources/api/gateway/queries/nonces.md b/docs/nado/developer-resources/api/gateway/queries/nonces.md new file mode 100644 index 0000000..cea3996 --- /dev/null +++ b/docs/nado/developer-resources/api/gateway/queries/nonces.md @@ -0,0 +1,66 @@ +# Nonces + +## Rate limits + +* 1200 requests/min or 20 requests/sec per IP address. (**weight = 2**) + +{% hint style="info" %} +See more details in [API Rate limits](https://docs.nado.xyz/developer-resources/api/rate-limits) +{% endhint %} + +## Request + +{% tabs %} +{% tab title="Websocket" %} +**Connect** + +`WEBSOCKET [GATEWAY_WEBSOCKET_ENDPOINT]` + +**Message** + +```json +{ + "type": "nonces", + "address": "0x0000000000000000000000000000000000000000" +} +``` + +{% endtab %} + +{% tab title="REST (GET)" %} **GGET** `[GATEWAY_REST_ENDPOINT]/query?type=nonces&address={address}` +{% endtab %} + +{% tab title="REST (POST)" %} `POST [GATEWAY_REST_ENDPOINT]/query` + +**Body** + +```json +{ + "type": "nonces", + "address": "0x0000000000000000000000000000000000000000" +} +``` + +{% endtab %} +{% endtabs %} + +## Request Parameters + +
ParameterTypeRequiredDescription
addressstringYesA bytes20 sent as a hex string representing the wallet address.
+ +## Response + +```json +{ + "status":"success", + "data":{ + "tx_nonce": 0, + "order_nonce": 1753048133299863552 + }, + "request_type": "query_nonces", +} +``` + +{% hint style="info" %} +**Note**: when doing any execute that is not `place_orders`, i.e. `withdraw_collateral`, `liquidate_subaccount`, you want to use `tx_nonce` as the nonce. `tx_nonce` increments by one each time a successful execute goes through. `order_nonce` is a historical artifact for the frontend, and simply returns the current timestamp in milliseconds plus 100000 multiplied by 2\*\*20. +{% endhint %} diff --git a/docs/nado/developer-resources/api/gateway/queries/order.md b/docs/nado/developer-resources/api/gateway/queries/order.md new file mode 100644 index 0000000..68b3f51 --- /dev/null +++ b/docs/nado/developer-resources/api/gateway/queries/order.md @@ -0,0 +1,77 @@ +# Order + +## Rate limits + +* 2400 requests/min or 40 requests/sec per IP address. (**weight = 1**) + +{% hint style="info" %} +See more details in [API Rate limits](https://docs.nado.xyz/developer-resources/api/rate-limits) +{% endhint %} + +## Request + +{% tabs %} +{% tab title="Websocket" %} +**Connect** + +`WEBSOCKET [GATEWAY_WEBSOCKET_ENDPOINT]` + +**Message** + +```json +{ + "type": "order", + "product_id": 1, + "digest": "0x0000000000000000000000000000000000000000000000000000000000000000" +} +``` + +{% endtab %} + +{% tab title="REST (GET)" %} **GET** `[GATEWAY_REST_ENDPOINT]/query?type=order&product_id={product_id}&digest={digest}` +{% endtab %} + +{% tab title="REST (POST)" %} `POST [GATEWAY_REST_ENDPOINT]/query` + +**Body** + +```json +{ + "type": "order", + "product_id": 1, + "digest": "0x0000000000000000000000000000000000000000000000000000000000000000" +} +``` + +{% endtab %} +{% endtabs %} + +## Request Parameters + +
ParameterTypeRequiredDescription
product_idnumberYesId of spot / perp product for which to retrieve order.
digeststringYesOrder digest to retrieve.
+ +## Response + +```json +{ + "status": "success", + "data": { + "product_id": 1, + "sender": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43000000000000000000000000", + "price_x18": "1000000000000000000", + "amount": "1000000000000000000", + "expiration": "2000000000", + "nonce": "1", + "unfilled_amount": "1000000000000000000", + "digest": "0x0000000000000000000000000000000000000000000000000000000000000000", + "placed_at": 1681951347, + "appendix": "1537", + "order_type": "ioc" + }, + "request_type": "query_order", +} +``` + +{% hint style="info" %} +**Note**: that side of the order (buy/sell) is included in the sign of `amount` and `unfilled_amount` . They are positive if the order is a buy order, otherwise negative. +{% endhint %} diff --git a/docs/nado/developer-resources/api/gateway/queries/orders.md b/docs/nado/developer-resources/api/gateway/queries/orders.md new file mode 100644 index 0000000..1de8067 --- /dev/null +++ b/docs/nado/developer-resources/api/gateway/queries/orders.md @@ -0,0 +1,162 @@ +# Orders + +## Rate limits + +* 1200 requests/min or 20 requests/sec per IP address. (**weight = 2**) or 2 \* length of `product_ids` for [multi-product orders](#multiple-products) query. + +{% hint style="info" %} +See more details in [API Rate limits](https://docs.nado.xyz/developer-resources/api/rate-limits) +{% endhint %} + +## Single Product + +### Request + +{% tabs %} +{% tab title="Websocket" %} +**Connect** + +`WEBSOCKET [GATEWAY_WEBSOCKET_ENDPOINT]` + +**Message** + +```json +{ + "type": "subaccount_orders", + "sender": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43000000000000000000000000", + "product_id": 1 +} +``` + +{% endtab %} + +{% tab title="REST (GET)" %} **GET** `[GATEWAY_REST_ENDPOINT]/query?type=subaccount_orders&sender={sender}&product_id={product_id}` +{% endtab %} + +{% tab title="REST (POST)" %} `POST [GATEWAY_REST_ENDPOINT]/query` + +**Body** + +```json +{ + "type": "subaccount_orders", + "sender": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43000000000000000000000000", + "product_id": 1 +} +``` + +{% endtab %} +{% endtabs %} + +### Request Parameters + +
ParameterTypeRequiredDescription
senderstringYesA bytes32 sent as a hex string; includes the address and the subaccount identifier.
product_idnumberYesId of spot / perp product for which to retrieve subaccount orders.
+ +### Response + +```json +{ + "status": "success", + "data": { + "sender": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43000000000000000000000000", + "product_id": 1, + "orders": [ + { + "product_id": 1, + "sender": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43000000000000000000000000", + "price_x18": "1000000000000000000", + "amount": "1000000000000000000", + "expiration": "2000000000", + "nonce": "1", + "unfilled_amount": "1000000000000000000", + "digest": "0x0000000000000000000000000000000000000000000000000000000000000000", + "placed_at": 1682437739, + "appendix": "1537", + "order_type": "ioc" + } + ] + }, + "request_type": "query_subaccount_orders" +} +``` + +{% hint style="info" %} +**Note**: that side of the order (buy/sell) is included in the sign of `amount` and `unfilled_amount` . They are positive if the order is a buy order, otherwise negative. +{% endhint %} + +## Multiple Products + +### Request + +{% tabs %} +{% tab title="Websocket" %} +**Connect** + +`WEBSOCKET [CORE_WEBSOCKET_ENDPOINT]` + +**Message** + +```json +{ + "type": "orders", + "sender": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43000000000000000000000000", + "product_ids": [1, 2, 3] +} +``` + +{% endtab %} + +{% tab title="REST (POST)" %} `POST /query` + +**Body** + +```json +{ + "type": "orders", + "sender": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43000000000000000000000000", + "product_ids": [1, 2, 3] +} +``` + +{% endtab %} +{% endtabs %} + +### Request Parameters + +
ParameterTypeRequiredDescription
senderstringYesA bytes32 sent as a hex string; includes the address and the subaccount identifier.
product_idsnumber[]YesList of spot / perp products for which to retrieve open orders.
+ +### Response + +```json +{ + "status": "success", + "data": { + "sender": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43000000000000000000000000", + "product_orders": [ + { + "product_id": 1, + "orders": [ + { + "product_id": 1, + "sender": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43000000000000000000000000", + "price_x18": "1000000000000000000", + "amount": "1000000000000000000", + "expiration": "2000000000", + "nonce": "1", + "unfilled_amount": "1000000000000000000", + "digest": "0x0000000000000000000000000000000000000000000000000000000000000000", + "appendix": "1537", + "placed_at": 1682437739, + "order_type": "ioc" + } + ] + }, + { + "product_id": 2, + "orders": [] + } + ] + }, + "request_type": "query_orders" +} +``` diff --git a/docs/nado/developer-resources/api/gateway/queries/status.md b/docs/nado/developer-resources/api/gateway/queries/status.md new file mode 100644 index 0000000..16eb658 --- /dev/null +++ b/docs/nado/developer-resources/api/gateway/queries/status.md @@ -0,0 +1,60 @@ +# Status + +## Rate limits + +* 2400 requests/min or 40 requests/sec per IP address. (**weight = 1**) + +{% hint style="info" %} +See more details in [API Rate limits](https://docs.nado.xyz/developer-resources/api/rate-limits) +{% endhint %} + +## Request + +{% tabs %} +{% tab title="Websocket" %} +**Connect** + +`WEBSOCKET [GATEWAY_WEBSOCKET_ENDPOINT]` + +**Message** + +```json +{ + "type": "status" +} +``` + +{% endtab %} + +{% tab title="REST (GET)" %} **GET** `[GATEWAY_REST_ENDPOINT]/query?type=status` +{% endtab %} + +{% tab title="REST (POST)" %} `POST [GATEWAY_REST_ENDPOINT]/query` + +**Body** + +```json +{ + "type": "status" +} +``` + +{% endtab %} +{% endtabs %} + +## Response + +```json +{ + "status": "success", + "data": "active", + "request_type": "query_status", +} +``` + +{% hint style="info" %} +The offchain sequencer could be in any of the following statuses: + +* `active`: accepting incoming executes. +* `failed`: sequencer is in a failed state. + {% endhint %} diff --git a/docs/nado/developer-resources/api/gateway/queries/subaccount-info.md b/docs/nado/developer-resources/api/gateway/queries/subaccount-info.md new file mode 100644 index 0000000..313f857 --- /dev/null +++ b/docs/nado/developer-resources/api/gateway/queries/subaccount-info.md @@ -0,0 +1,353 @@ +# Subaccount Info + +## Rate limits + +The rate limit weight varies based on the request parameters: + +* **Basic query** (no `txns`): **weight = 2** + * 1200 requests/min or 200 requests every 10 seconds per IP address +* **With simulation** (`txns` provided): **weight = 10** + * 240 requests/min or 40 requests every 10 seconds per IP address +* **With simulation + pre\_state** (`txns` and `pre_state="true"`): **weight = 15** + * 160 requests/min or \~26 requests every 10 seconds per IP address + +{% hint style="info" %} +See more details in [API Rate limits](https://docs.nado.xyz/developer-resources/api/rate-limits) +{% endhint %} + +## Request + +{% tabs %} +{% tab title="Websocket" %} +**Connect** + +`WEBSOCKET [GATEWAY_WEBSOCKET_ENDPOINT]` + +**Message** + +```json +{ + "type": "subaccount_info", + "subaccount": "0xeae27ae6412147ed6d5692fd91709dad6dbfc34264656661756c740000000000", + "txns": "[{\"apply_delta\":{\"product_id\":4,\"subaccount\":\"0xeae27ae6412147ed6d5692fd91709dad6dbfc34264656661756c740000000000\",\"amount_delta\":\"10790000000000000000\",\"v_quote_delta\":\"-35380410000000000000000\"}}]" +} +``` + +{% endtab %} + +{% tab title="REST (GET)" %} **GET** `[GATEWAY_REST_ENDPOINT]/query?type=subaccount_info&subaccount={subaccount}&txns=[{"apply_delta":{"product_id":2,"subaccount":"0xeae27ae6412147ed6d5692fd91709dad6dbfc34264656661756c740000000000","amount_delta":"100000000000000000","v_quote_delta":"3033500000000000000000"}}]` +{% endtab %} + +{% tab title="REST (POST)" %} `POST [GATEWAY_REST_ENDPOINT]/query` + +**Message** + +```json +{ + "type": "subaccount_info", + "subaccount": "0xeae27ae6412147ed6d5692fd91709dad6dbfc34264656661756c740000000000", + "txns": "[{\"apply_delta\":{\"product_id\":4,\"subaccount\":\"0xeae27ae6412147ed6d5692fd91709dad6dbfc34264656661756c740000000000\",\"amount_delta\":\"10790000000000000000\",\"v_quote_delta\":\"-35380410000000000000000\"}}]" +} +``` + +{% endtab %} +{% endtabs %} + +## Request Parameters + +
ParameterTypeRequiredDescription
subaccountstringYesA bytes32 sent as a hex string; includes the address and the subaccount identifier. See sender field structure for details.
txnsstringnoA list of transactions to get an estimated/simulated view. see more info below.
pre_statestringnoWhen "true" and txns are provided, returns the subaccount state before the transactions were applied in the pre_state field. Defaults to "false".
+ +### Supported txs for an estimated subaccount info + +The following are the supported `txns` you can provide to get an estimated view of your subaccount. + +{% hint style="info" %} +**Note**: these `txns` are only used to simulate what your subaccount would look like if they were executed. +{% endhint %} + +#### ApplyDelta + +Updates internal balances for the `product_id` and amount deltas provided. + +```json +{ + "apply_delta": { + "product_id": 2, + "subaccount": "0xeae27ae6412147ed6d5692fd91709dad6dbfc34264656661756c740000000000", + "amount_delta": "100000000000000000", + "v_quote_delta": "3033500000000000000000" + } +} +``` + +## Response + +{% hint style="info" %} +**Note**: + +* `healths`: + * `healths[0]`: info about your initial health, which is weighted by `long_weight_initial_x18` and `short_weight_initial_x18.` + * `healths[1]`: info about your maintenance health, which is weighted by `long_weight_maintenance_x18` and `short_weight_maintenance_x18.` + * `healths[2]`: info about your unweighted health. +* `health_contributions` is indexed by product\_id and represents the contribution of the corresponding product to the final health. + * `health_contributions[product_id][0]``: contribution to healths[0]` + * `health_contributions[product_id][1]``: contribution to healths[1]` + * `health_contributions[product_id][2]``: contribution to healths[2]` +* `pre_state`: (Optional) When `pre_state="true"` is provided with `txns`, this field contains the subaccount state **before** the simulated transactions were applied. This allows you to compare the before/after states when simulating transactions. + * `pre_state.healths`: Same structure as the main `healths` field, but reflecting the state before transactions + * `pre_state.health_contributions`: Health contributions before transactions + * `pre_state.spot_balances`: Spot balances before transactions + * `pre_state.perp_balances`: Perpetual balances before transactions + {% endhint %} + +```json +{ + "status": "success", + "data": { + "subaccount": "0x8d7d64d6cf1d4f018dd101482ac71ad49e30c56064656661756c740000000000", + "exists": true, + "healths": [ + { + "assets": "456895621098158389211471", + "liabilities": "76286259844766495292488", + "health": "380609361253391893918983" + }, + { + "assets": "456895621098158389211471", + "liabilities": "72818702579095290924243", + "health": "384076918519063098287228" + }, + { + "assets": "456895621098158389211471", + "liabilities": "69351145313424086671554", + "health": "387544475784734302539917" + } + ], + "health_contributions": [ + [ + "456895621098158389211471", + "456895621098158389211471", + "456895621098158389211471" + ], + [ + "-76286259844766495292488", + "-72818702579095290924243", + "-69351145313424086671554" + ], + [ + "0", + "0", + "0" + ] + ], + "spot_count": 2, + "perp_count": 1, + "spot_balances": [ + { + "product_id": 0, + "balance": { + "amount": "456895621098158389211471" + } + }, + { + "product_id": 1, + "balance": { + "amount": "-600152323366021154" + } + } + ], + "perp_balances": [ + { + "product_id": 2, + "balance": { + "amount": "0", + "v_quote_balance": "0", + "last_cumulative_funding_x18": "-394223711772447555304" + } + } + ], + "spot_products": [ + { + "product_id": 0, + "oracle_price_x18": "1000000000000000000", + "risk": { + "long_weight_initial_x18": "1000000000000000000", + "short_weight_initial_x18": "1000000000000000000", + "long_weight_maintenance_x18": "1000000000000000000", + "short_weight_maintenance_x18": "1000000000000000000", + "price_x18": "1000000000000000000" + }, + "config": { + "token": "0x5f65358d61a9a281ea3bb930d05889aca21e3f4f", + "interest_inflection_util_x18": "800000000000000000", + "interest_floor_x18": "10000000000000000", + "interest_small_cap_x18": "40000000000000000", + "interest_large_cap_x18": "1000000000000000000", + "withdraw_fee_x18": "1000000000000000000", + "min_deposit_rate_x18": "0" + }, + "state": { + "cumulative_deposits_multiplier_x18": "1000000000025524653", + "cumulative_borrows_multiplier_x18": "1000347390837434279", + "total_deposits_normalized": "20001011744258817298755054194662", + "total_borrows_normalized": "1617724891363505323532211" + }, + "book_info": { + "size_increment": "0", + "price_increment_x18": "0", + "min_size": "0", + "collected_fees": "0" + } + }, + { + "product_id": 1, + "oracle_price_x18": "115555905748161505821744", + "risk": { + "long_weight_initial_x18": "900000000000000000", + "short_weight_initial_x18": "1100000000000000000", + "long_weight_maintenance_x18": "950000000000000000", + "short_weight_maintenance_x18": "1050000000000000000", + "price_x18": "115555905748161505821744" + }, + "config": { + "token": "0xc57c1c64561a37ac9e8f9039cb6deab7539d99fc", + "interest_inflection_util_x18": "800000000000000000", + "interest_floor_x18": "10000000000000000", + "interest_small_cap_x18": "40000000000000000", + "interest_large_cap_x18": "1000000000000000000", + "withdraw_fee_x18": "40000000000000", + "min_deposit_rate_x18": "0" + }, + "state": { + "cumulative_deposits_multiplier_x18": "1000000000000318713", + "cumulative_borrows_multiplier_x18": "1000347390679880473", + "total_deposits_normalized": "9000399823280682696107190850", + "total_borrows_normalized": "9580268570661550719" + }, + "book_info": { + "size_increment": "1000000000000000", + "price_increment_x18": "1000000000000000000", + "min_size": "4000000000000000", + "collected_fees": "0" + } + } + ], + "perp_products": [ + { + "product_id": 2, + "oracle_price_x18": "115596528090565357611177", + "risk": { + "long_weight_initial_x18": "950000000000000000", + "short_weight_initial_x18": "1050000000000000000", + "long_weight_maintenance_x18": "970000000000000000", + "short_weight_maintenance_x18": "1030000000000000000", + "price_x18": "115596528090565357611177" + }, + "state": { + "cumulative_funding_long_x18": "-394223711772447555304", + "cumulative_funding_short_x18": "-394223711772447555304", + "available_settle": "20092193239667417956947", + "open_interest": "113605000000000000000" + }, + "book_info": { + "size_increment": "1000000000000000", + "price_increment_x18": "1000000000000000000", + "min_size": "4000000000000000", + "collected_fees": "0" + } + } + ] + }, + "request_type": "query_subaccount_info" +} +``` + +### Example with `pre_state` + +When you want to simulate transactions and compare the before/after states, you can use the `pre_state` parameter: + +#### Request + +{% tabs %} +{% tab title="REST (GET)" %} **GET** `[GATEWAY_REST_ENDPOINT]/query?type=subaccount_info&subaccount={subaccount}&txns=[{"apply_delta":{"product_id":2,"subaccount":"0xeae27ae6412147ed6d5692fd91709dad6dbfc34264656661756c740000000000","amount_delta":"100000000000000000","v_quote_delta":"3033500000000000000000"}}]&pre_state="true"` +{% endtab %} + +{% tab title="REST (POST)" %} `POST [GATEWAY_REST_ENDPOINT]/query` + +```json +{ + "type": "subaccount_info", + "subaccount": "0xeae27ae6412147ed6d5692fd91709dad6dbfc34264656661756c740000000000", + "txns": "[{\"apply_delta\":{\"product_id\":2,\"subaccount\":\"0xeae27ae6412147ed6d5692fd91709dad6dbfc34264656661756c740000000000\",\"amount_delta\":\"100000000000000000\",\"v_quote_delta\":\"3033500000000000000000\"}}]", + "pre_state": "true" +} +``` + +{% endtab %} +{% endtabs %} + +#### Response + +The response will now include a `pre_state` field showing the state before the simulated transactions: + +```json +{ + "status": "success", + "data": { + "subaccount": "0x8d7d64d6cf1d4f018dd101482ac71ad49e30c56064656661756c740000000000", + "exists": true, + "healths": [ + { + "assets": "460000000000000000000000", + "liabilities": "80000000000000000000000", + "health": "380000000000000000000000" + } + ], + "health_contributions": [...], + "spot_balances": [...], + "perp_balances": [ + { + "product_id": 2, + "balance": { + "amount": "100000000000000000", + "v_quote_balance": "3033500000000000000000", + "last_cumulative_funding_x18": "-394223711772447555304" + } + } + ], + "spot_products": [...], + "perp_products": [...], + "pre_state": { + "healths": [ + { + "assets": "456895621098158389211471", + "liabilities": "76286259844766495292488", + "health": "380609361253391893918983" + } + ], + "health_contributions": [...], + "spot_balances": [...], + "perp_balances": [ + { + "product_id": 2, + "balance": { + "amount": "0", + "v_quote_balance": "0", + "last_cumulative_funding_x18": "-394223711772447555304" + } + } + ] + } + }, + "request_type": "query_subaccount_info" +} +``` + +{% hint style="success" %} +**Use Case**: The `pre_state` feature is particularly useful for: + +* **Position Simulation**: Preview how a potential trade would affect your health and balances +* **Risk Analysis**: Compare health metrics before and after simulated transactions +* **UI/UX**: Display "before → after" views to users when they're about to execute trades +* **Testing**: Validate transaction impacts without executing them on-chain + {% endhint %} diff --git a/docs/nado/developer-resources/api/gateway/queries/symbols.md b/docs/nado/developer-resources/api/gateway/queries/symbols.md new file mode 100644 index 0000000..23981ec --- /dev/null +++ b/docs/nado/developer-resources/api/gateway/queries/symbols.md @@ -0,0 +1,103 @@ +# Symbols + +## Rate limits + +* 1200 requests/min or 20 requests/sec per IP address. (**weight = 2**) + +{% hint style="info" %} +See more details in [API Rate limits](https://docs.nado.xyz/developer-resources/api/rate-limits) +{% endhint %} + +## Request + +{% tabs %} +{% tab title="Websocket" %} +**Connect** + +`WEBSOCKET [GATEWAY_WEBSOCKET_ENDPOINT]` + +**Message** + +```json +{ + "type": "symbols", + "product_ids": [1, 2] +} +``` + +{% endtab %} + +{% tab title="REST (GET)" %} **GET** `[GATEWAY_REST_ENDPOINT]/query?type=symbols&product_type=spot` +{% endtab %} + +{% tab title="REST (POST)" %} `POST [GATEWAY_REST_ENDPOINT]/query` + +**Message** + +```json +{ + "type": "symbols", + "product_ids": [1, 2, 3, 4], + "product_type": "spot" +} +``` + +{% endtab %} +{% endtabs %} + +## Request Parameters + +
ParameterTypeRequiredDescription
product_idsnumber[]NoAn array of product ids. Only available for POST and WS requests.
product_typestringNoType of products to return, must be:
"spot" | "perp".
+ +## Response + +{% hint style="info" %} +**Note**: + +* All products have are quoted against USDT0, except for product 0. + {% endhint %} + +```json +{ + "status": "success", + "data": { + "symbols": { + "WBTC": { + "type": "spot", + "product_id": 1, + "symbol": "WBTC", + "price_increment_x18": "1000000000000000000", + "size_increment": "1000000000000000", + "min_size": "4000000000000000", + "maker_fee_rate_x18": "0", + "taker_fee_rate_x18": "200000000000000", + "long_weight_initial_x18": "900000000000000000", + "long_weight_maintenance_x18": "950000000000000000", + "max_open_interest_x18": null + }, + "BTC-PERP": { + "type": "perp", + "product_id": 2, + "symbol": "BTC-PERP", + "price_increment_x18": "1000000000000000000", + "size_increment": "1000000000000000", + "min_size": "4000000000000000", + "maker_fee_rate_x18": "0", + "taker_fee_rate_x18": "200000000000000", + "long_weight_initial_x18": "950000000000000000", + "long_weight_maintenance_x18": "970000000000000000", + "max_open_interest_x18": null + } + } + }, + "request_type": "query_symbols" +} +``` + +## Response fields + +### Symbols + +All numerical values are returned as strings and scaled by 1e18. + +
Field nameDescription
typeProduct type, "spot" or "perp"
product_idProduct id
symbolProduct symbol
price_increment_x18Price increment, a.k.a tick size
size_incrementSize increment, in base units
min_sizeMinimum order size, in base units
maker_fee_rate_x18Maker fee rate, given as decimal rate
taker_fee_rate_x18Taker fee rate, given as decimal rate
long_weight_initial_x18Long initial margin weight, given as decimal
long_weight_maintenance_x18Long maintenance margin weight, given as decimal
max_open_interest_x18Maximum open interest, null if no limit
diff --git a/docs/nado/developer-resources/api/gateway/signing.md b/docs/nado/developer-resources/api/gateway/signing.md new file mode 100644 index 0000000..5f8b455 --- /dev/null +++ b/docs/nado/developer-resources/api/gateway/signing.md @@ -0,0 +1,326 @@ +# Signing + +All executes are signed using [EIP712](https://eips.ethereum.org/EIPS/eip-712). Each execute request contains: + +1. A piece of structured data that includes the sender address i.e: the `primaryType` that needs to be signed. +2. A signature of the hash of that structured data, signed by the sender. + +## Domain + +The following is the domain required as part of the EIP712 structure: + +```json +{ + name: 'Nado', + version: '0.0.1', + chainId: chainId, + verifyingContract: contractAddress +} +``` + +You can retrieve the corresponding chain id and verifying contract via the [contracts](https://docs.nado.xyz/developer-resources/api/gateway/queries/contracts) query. + +{% hint style="warning" %} +**Note**: make sure to use the correct verifying contract for each execute: + +* For place order: should use `address(producId)` i.e: the 20 bytes hex representation of the `productId` for the order. For example, the verify contract of product `18` is `0x0000000000000000000000000000000000000012` . +* For everything else: should use the endpoint address. + +See more details in the [contracts](https://docs.nado.xyz/developer-resources/api/gateway/queries/contracts) query page. +{% endhint %} + +```python +def gen_order_verifying_contract(product_id: int) -> str: + """ + Generates the order verifying contract address based on the product ID. + + Args: + product_id (int): The product ID for which to generate the verifying contract address. + + Returns: + str: The generated order verifying contract address in hexadecimal format. + """ + be_bytes = product_id.to_bytes(20, byteorder="big", signed=False) + return "0x" + be_bytes.hex() +``` + +## EIP712 Types + +See below the EIP712 type for each execute: + +{% hint style="info" %} +See more details in the **Signing** section of each execute's page. +{% endhint %} + +### [Place Order](https://docs.nado.xyz/developer-resources/api/gateway/executes/place-order) + +**Primary Type**: `Order` + +Solidity struct that needs to be signed: + +```solidity +struct Order { + bytes32 sender; + int128 priceX18; + int128 amount; + uint64 expiration; + uint64 nonce; + uint128 appendix; +} +``` + +**JSON representation:** + +```typescript +{ + Order: [ + { name: 'sender', type: 'bytes32' }, + { name: 'priceX18', type: 'int128' }, + { name: 'amount', type: 'int128' }, + { name: 'expiration', type: 'uint64' }, + { name: 'nonce', type: 'uint64' }, + { name: 'appendix', type: 'uint128' } + ], +} +``` + +### [Cancel Orders](https://docs.nado.xyz/developer-resources/api/gateway/executes/cancel-orders) + +**Primary Type:** `Cancellation` + +Solidity struct that needs to be signed: + +```solidity +struct Cancellation { + bytes32 sender; + uint32[] productIds; + bytes32[] digests; + uint64 nonce; +} +``` + +**JSON representation:** + +```typescript +{ + Cancellation: [ + { name: 'sender', type: 'bytes32' }, + { name: 'productIds', type: 'uint32[]' }, + { name: 'digests', type: 'bytes32[]' }, + { name: 'nonce', type: 'uint64' }, + ], +} +``` + +### [Cancel Product Orders](https://docs.nado.xyz/developer-resources/api/gateway/executes/cancel-product-orders) + +**Primary Type**: `CancellationProducts` + +Solidity struct that needs to be signed: + +```solidity +struct CancellationProducts { + bytes32 sender; + uint32[] productIds; + uint64 nonce; +} +``` + +**JSON representation:** + +```typescript +{ + CancellationProducts: [ + { name: 'sender', type: 'bytes32' }, + { name: 'productIds', type: 'uint32[]' }, + { name: 'nonce', type: 'uint64' }, + ], +} +``` + +### [Withdraw Collateral](https://docs.nado.xyz/developer-resources/api/gateway/executes/withdraw-collateral) + +**Primary Type:** `WithdrawCollateral` + +Solidity struct that needs to be signed: + +```solidity +struct WithdrawCollateral { + bytes32 sender; + uint32 productId; + uint128 amount; + uint64 nonce; +} +``` + +**JSON representation:** + +```typescript +{ + WithdrawCollateral: [ + { name: 'sender', type: 'bytes32' }, + { name: 'productId', type: 'uint32' }, + { name: 'amount', type: 'uint128' }, + { name: 'nonce', type: 'uint64' }, + ], +} +``` + +### [Liquidate Subaccount](https://docs.nado.xyz/developer-resources/api/gateway/executes/liquidate-subaccount) + +**Primary Type:** `LiquidateSubaccount` + +Solidity struct that needs to be signed: + +```solidity +struct LiquidateSubaccount { + bytes32 sender; + bytes32 liquidatee; + uint32 productId; + bool isEncodedSpread; + int128 amount; + uint64 nonce; +} +``` + +**JSON representation:** + +```typescript +{ + LiquidateSubaccount: [ + { name: 'sender', type: 'bytes32' }, + { name: 'liquidatee', type: 'bytes32' }, + { name: 'productId', type: 'uint32' }, + { name: 'isEncodedSpread', type: 'bool' }, + { name: 'amount', type: 'int128' }, + { name: 'nonce', type: 'uint64' }, + ], +} +``` + +### [Mint NLP](https://docs.nado.xyz/developer-resources/api/gateway/executes/mint-nlp) + +**Primary Type**: `MintNlp` + +Solidity struct that needs to be signed: + +```solidity +struct MintNlp { + bytes32 sender; + uint32 productId; + uint128 quoteAmount; + uint64 nonce; +} +``` + +**JSON representation:** + +```typescript +{ + MintLp: [ + { name: 'sender', type: 'bytes32' }, + { name: 'quoteAmount', type: 'uint128' }, + { name: 'nonce', type: 'uint64' }, + ], +} +``` + +## [Burn NLP](https://docs.nado.xyz/developer-resources/api/gateway/executes/burn-nlp) + +**Primary Type:** `BurnNlp` + +Solidity struct that needs to be signed: + +```solidity +struct BurnLp { + bytes32 sender; + uint128 nlpAmount; + uint64 nonce; +} +``` + +**JSON representation:** + +```typescript +{ + BurnLp: [ + { name: 'sender', type: 'bytes32' }, + { name: 'nlpAmount', type: 'uint128' }, + { name: 'nonce', type: 'uint64' }, + ], +} +``` + +## [Link Signer](https://docs.nado.xyz/developer-resources/api/gateway/executes/link-signer) + +**Primary Type**: `LinkSigner` + +Solidity struct that needs to be signed: + +```solidity +struct LinkSigner { + bytes32 sender; + bytes32 signer; + uint64 nonce; +} +``` + +**JSON representation:** + +```typescript +{ + LinkSigner: [ + { name: 'sender', type: 'bytes32' }, + { name: 'signer', type: 'bytes32' }, + { name: 'nonce', type: 'uint64' }, + ], +} +``` + +## [List Trigger Orders](https://docs.nado.xyz/developer-resources/api/trigger/queries/list-trigger-orders) + +**Primary Type**: `ListTriggerOrders` + +Solidity struct that needs to be signed: + +```solidity +struct ListTriggerOrders { + bytes32 sender; + uint64 recvTime; +} +``` + +**JSON representation:** + +```typescript +{ + ListTriggerOrders: [ + { name: 'sender', type: 'bytes32' }, + { name: 'recvTime', type: 'uint64' }, + ], +} +``` + +## [Authenticate Subscription Streams](https://docs.nado.xyz/developer-resources/subscriptions#authentication) + +**Primary Type**: `StreamAuthentication` + +Struct that needs to be signed: + +```solidity +struct StreamAuthentication { + bytes32 sender; + uint64 expiration; +} +``` + +**JSON representation:** + +```typescript +{ + StreamAuthentication: [ + { name: 'sender', type: 'bytes32' }, + { name: 'expiration', type: 'uint64' }, + ], +} +``` diff --git a/docs/nado/developer-resources/api/gateway/signing/examples.md b/docs/nado/developer-resources/api/gateway/signing/examples.md new file mode 100644 index 0000000..baa8d35 --- /dev/null +++ b/docs/nado/developer-resources/api/gateway/signing/examples.md @@ -0,0 +1,376 @@ +# Examples + +The following are full examples of EIP12 typed data for each of Nado's executes. Each execute includes a `sender` field which is a solidity `bytes32` . There are two components to this field: + +* an `address` that is a `bytes20` +* a subaccount identifier that is a `bytes12` + +For example, if your address was `0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43`, and you wanted to use the default subaccount identifier (i.e: an empty identifier `""`) you can set `sender` to `0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43000000000000000000000000` , which sets all bytes of the subaccount identifier to `0`. + +{% hint style="info" %} +**Note**: a `bytes32` representation of the sender must used when signing the request. +{% endhint %} + +See below a sample util to convert a hex to a **bytes32**: + +{% tabs %} +{% tab title="Python" %} + +```python +def hex_to_bytes32(hex_string): + if hex_string.startswith("0x"): + hex_string = hex_string[2:] + data_bytes = bytes.fromhex(hex_string) + padded_data = data_bytes + b"\x00" * (32 - len(data_bytes)) + return padded_data + +sender = hex_to_bytes32('0x841fe4876763357975d60da128d8a54bb045d76a64656661756c740000000000') +``` + +{% endtab %} + +{% tab title="Typescript" %} + +```typescript +import { arrayify } from 'ethers/lib/utils'; + +export function hexToBytes32(subaccount: string) { + const subaccountBytes = arrayify(subaccount); + const bytes32 = new Uint8Array(32); + for (let i = 0; i < Math.min(subaccountBytes.length, 32); i++) { + bytes32[i] = subaccountBytes[i]; + } + return bytes32; +} + +const sender = hexToBytes32('0x841fe4876763357975d60da128d8a54bb045d76a64656661756c740000000000') +``` + +{% endtab %} +{% endtabs %} + +## EIP712 Typed data examples + +{% tabs %} +{% tab title="Place Order" %} + +```python +{ + 'types': { + 'EIP712Domain': [ + {'name': 'name', 'type': 'string'}, + {'name': 'version', 'type': 'string'}, + {'name': 'chainId', 'type': 'uint256'}, + {'name': 'verifyingContract', 'type': 'address'} + ], + 'Order': [ + {'name': 'sender', 'type': 'bytes32'}, + {'name': 'priceX18', 'type': 'int128'}, + {'name': 'amount', 'type': 'int128'}, + {'name': 'expiration', 'type': 'uint64'}, + {'name': 'nonce', 'type': 'uint64'}, + {'name': 'appendix', 'type': 'uint128'}, + ], + }, + 'primaryType': 'Order', + 'domain': { + 'name': 'Nado', + 'version': '0.0.1', + 'chainId': 763373, + 'verifyingContract': '0x0000000000000000000000000000000000000001' + }, + 'message': { + 'sender': hex_to_bytes32('0x841fe4876763357975d60da128d8a54bb045d76a64656661756c740000000000'), + 'priceX18': 28898000000000000000000, + 'amount': -10000000000000000, + 'expiration': 4611687701117784255, + 'appendix': 1537, # Version 1, POST_ONLY order + 'nonce': 1764428860167815857, + }, +} +``` + +{% endtab %} + +{% tab title="Cancel Orders" %} + +```python +{ + 'types': { + 'EIP712Domain': [ + {'name': 'name', 'type': 'string'}, + {'name': 'version', 'type': 'string'}, + {'name': 'chainId', 'type': 'uint256'}, + {'name': 'verifyingContract', 'type': 'address'} + ], + 'Cancellation': [ + { 'name': 'sender', 'type': 'bytes32' }, + { 'name': 'productIds', 'type': 'uint32[]'}, + { 'name': 'digests', 'type': 'bytes32[]'}, + { 'name': 'nonce', 'type': 'uint64'}, + ], + }, + 'primaryType': 'Cancellation', + 'domain': { + 'name': 'Nado', + 'version': '0.0.1', + 'chainId': 763373, + 'verifyingContract': '0xbf16e41fb4ac9922545bfc1500f67064dc2dcc3b' + }, + 'message': { + 'sender': hex_to_bytes32('0x841fe4876763357975d60da128d8a54bb045d76a64656661756c740000000000'), + 'productIds': [4], + 'digests': [hex_to_bytes32('0x51ba8762bc5f77957a4e896dba34e17b553b872c618ffb83dba54878796f2821')], + 'nonce': 1, + }, +} +``` + +{% endtab %} + +{% tab title="Cancel Product orders" %} + +```python +{ + 'types': { + 'EIP712Domain': [ + {'name': 'name', 'type': 'string'}, + {'name': 'version', 'type': 'string'}, + {'name': 'chainId', 'type': 'uint256'}, + {'name': 'verifyingContract', 'type': 'address'} + ], + 'CancellationProducts': [ + {'name': 'sender', 'type': 'bytes32'}, + {'name': 'productIds', 'type': 'uint32[]'}, + {'name': 'nonce', 'type': 'uint64'}, + ], + }, + 'primaryType': 'CancellationProducts', + 'domain': { + 'name': 'Nado', + 'version': '0.0.1', + 'chainId': 763373, + 'verifyingContract': '0xbf16e41fb4ac9922545bfc1500f67064dc2dcc3b' + }, + 'message': { + 'sender': hex_to_bytes32('0x841fe4876763357975d60da128d8a54bb045d76a64656661756c740000000000'), + 'productIds': [1, 2], + 'nonce': 1, + }, +} +``` + +{% endtab %} + +{% tab title="Link Signer" %} + +```python +{ + 'types': { + 'EIP712Domain': [ + {'name': 'name', 'type': 'string'}, + {'name': 'version', 'type': 'string'}, + {'name': 'chainId', 'type': 'uint256'}, + {'name': 'verifyingContract', 'type': 'address'} + ], + 'LinkSigner': [ + {'name': 'sender', 'type': 'bytes32'}, + {'name': 'signer', 'type': 'bytes32'}, + {'name': 'nonce', 'type': 'uint64'}, + ], + }, + 'primaryType': 'LinkSigner', + 'domain': { + 'name': 'Nado', + 'version': '0.0.1', + 'chainId': 763373, + 'verifyingContract': '0xbf16e41fb4ac9922545bfc1500f67064dc2dcc3b' + }, + 'message': { + 'sender': hex_to_bytes32('0x841fe4876763357975d60da128d8a54bb045d76a64656661756c740000000000'), + 'signer': hex_to_bytes32('0x12a0b4888021576eb10a67616dd3dd3d9ce206b664656661756c740000000000'), + 'nonce': 1, + }, +} +``` + +{% endtab %} +{% endtabs %} + +{% tabs %} +{% tab title="Withdraw Collateral" %} + +```python +{ + 'types': { + 'EIP712Domain': [ + {'name': 'name', 'type': 'string'}, + {'name': 'version', 'type': 'string'}, + {'name': 'chainId', 'type': 'uint256'}, + {'name': 'verifyingContract', 'type': 'address'} + ], + 'WithdrawCollateral': [ + {'name': 'sender', 'type': 'bytes32'}, + {'name': 'productId', 'type': 'uint32'}, + {'name': 'amount', 'type': 'uint128'}, + {'name': 'nonce', 'type': 'uint64'}, + ], + }, + 'primaryType': 'WithdrawCollateral', + 'domain': { + 'name': 'Nado', + 'version': '0.0.1', + 'chainId': 763373, + 'verifyingContract': '0xbf16e41fb4ac9922545bfc1500f67064dc2dcc3b' + }, + 'message': { + 'sender': hex_to_bytes32('0x841fe4876763357975d60da128d8a54bb045d76a64656661756c740000000000'), + 'productId': 2, + 'amount': 10000000000000000, + 'nonce': 1 + }, +} +``` + +{% endtab %} + +{% tab title="Liquidate Subaccount" %} + +```python +{ + 'types': { + 'EIP712Domain': [ + {'name': 'name', 'type': 'string'}, + {'name': 'version', 'type': 'string'}, + {'name': 'chainId', 'type': 'uint256'}, + {'name': 'verifyingContract', 'type': 'address'} + ], + 'LiquidateSubaccount': [ + {'name': 'sender', 'type': 'bytes32'}, + {'name': 'liquidatee', 'type': 'bytes32'}, + {'name': 'productId', 'type': 'uint32'}, + {'name': 'isEncodedSpread', 'type': 'bool'}, + {'name': 'amount', 'type': 'int128'}, + {'name': 'nonce', 'type': 'uint64'}, + ], + }, + 'primaryType': 'LiquidateSubaccount', + 'domain': { + 'name': 'Nado', + 'version': '0.0.1', + 'chainId': 763373, + 'verifyingContract': '0xbf16e41fb4ac9922545bfc1500f67064dc2dcc3b' + }, + 'message': { + 'sender': hex_to_bytes32('0x841fe4876763357975d60da128d8a54bb045d76a64656661756c740000000000'), + 'liquidatee': hex_to_bytes32('0x12a0b4888021576eb10a67616dd3dd3d9ce206b664656661756c740000000000'), + 'productId': 1, + 'isEncodedSpread': false, + 'amount': 10000000000000000, + 'nonce': 1, + }, +} +``` + +{% endtab %} + +{% tab title="Mint NLP" %} + +```python +{ + 'types': { + 'EIP712Domain': [ + {'name': 'name', 'type': 'string'}, + {'name': 'version', 'type': 'string'}, + {'name': 'chainId', 'type': 'uint256'}, + {'name': 'verifyingContract', 'type': 'address'} + ], + 'MintLp': [ + {'name': 'sender', 'type': 'bytes32' }, + {'name': 'quoteAmount', 'type': 'uint128'}, + {'name': 'nonce', 'type': 'uint64' }, + ], + }, + 'primaryType': 'MintNlp', + 'domain': { + 'name': 'Nado', + 'version': '0.0.1', + 'chainId': 763373, + 'verifyingContract': '0xbf16e41fb4ac9922545bfc1500f67064dc2dcc3b' + }, + 'message': { + 'sender': hex_to_bytes32('0x841fe4876763357975d60da128d8a54bb045d76a64656661756c740000000000'), + 'quoteAmount': 20000000000000000000000, + 'nonce': 1, + }, +} +``` + +{% endtab %} + +{% tab title="Burn NLP" %} + +```python +{ + 'types': { + 'EIP712Domain': [ + {'name': 'name', 'type': 'string'}, + {'name': 'version', 'type': 'string'}, + {'name': 'chainId', 'type': 'uint256'}, + {'name': 'verifyingContract', 'type': 'address'} + ], + 'BurnLp': [ + {'name': 'sender', 'type': 'bytes32'}, + {'name': 'nlpAmount', 'type': 'uint128'}, + {'name': 'nonce', 'type': 'uint64'}, + ], + }, + 'primaryType': 'BurnNlp', + 'domain': { + 'name': 'Nado', + 'version': '0.0.1', + 'chainId': 763373, + 'verifyingContract': '0xbf16e41fb4ac9922545bfc1500f67064dc2dcc3b' + }, + 'message': { + 'sender': hex_to_bytes32('0x841fe4876763357975d60da128d8a54bb045d76a64656661756c740000000000'), + 'nlpAmount': 1000000000000000000, + 'nonce': 1, + }, +} +``` + +{% endtab %} + +{% tab title="List Trigger Orders" %} + +```python +{ + 'types': { + 'EIP712Domain': [ + {'name': 'name', 'type': 'string'}, + {'name': 'version', 'type': 'string'}, + {'name': 'chainId', 'type': 'uint256'}, + {'name': 'verifyingContract', 'type': 'address'} + ], + 'ListTriggerOrders': [ + {'name': 'sender', 'type': 'bytes32' }, + {'name': 'recvTime', 'type': 'uint64' } + ], + }, + 'primaryType': 'WithdrawCollateral', + 'domain': { + 'name': 'Nado', + 'version': '0.0.1', + 'chainId': 763373, + 'verifyingContract': '0xbf16e41fb4ac9922545bfc1500f67064dc2dcc3b' + }, + 'message': { + 'sender': hex_to_bytes32('0x841fe4876763357975d60da128d8a54bb045d76a64656661756c740000000000'), + 'recvTime': 1688939576000 + }, +} +``` + +{% endtab %} +{% endtabs %} diff --git a/docs/nado/developer-resources/api/gateway/signing/q-and-a.md b/docs/nado/developer-resources/api/gateway/signing/q-and-a.md new file mode 100644 index 0000000..667f6dd --- /dev/null +++ b/docs/nado/developer-resources/api/gateway/signing/q-and-a.md @@ -0,0 +1,49 @@ +# Q\&A + +### Q: **What is Nado's EIP712 domain?** + +```json +{ + name: 'Nado', + version: '0.0.1', + chainId: chainId, + verifyingContract: contractAddress +} +``` + +{% hint style="info" %} +See [signing](https://docs.nado.xyz/developer-resources/api/gateway/signing/..#domain) for more details. +{% endhint %} + +### Q: How can i retrieve the verifying contracts to use? + +* Via the [contracts](https://docs.nado.xyz/developer-resources/api/gateway/queries/contracts) query for all executes except place orders. + +### Q: Which contract should I use for each execute? + +* For place orders: must be computed as `address(productId)`. For example, the verify contract of product `18` is `0x0000000000000000000000000000000000000012`. +* For everything else: use the endpoint contract from the contracts query. + +{% hint style="info" %} +See the [contracts](https://docs.nado.xyz/developer-resources/api/queries/contracts#response) query for more details. +{% endhint %} + +### Q: I am running into signature errors, how to fix? + +Signature errors can arise for several reasons: + +* **An invalid struct**: confirm you are signing the correct struct. See the [Signing](https://docs.nado.xyz/developer-resources/api/gateway/signing) page to verify the struct of each execute request. +* **An invalid chain id**: confirm you have the correct chain id for the network you are on. +* **An invalid verifying contract**: confirm you have the correct verifying contract address for the network and execute you are signing. i.e: confirm you are using the correct orderbook address for place orders and endpoint address for everything else. + +### Q: Is any other signing standard supported? + +No, only [EIP712](https://eips.ethereum.org/EIPS/eip-712). + +### Q: Are there any examples you can provide? + +See [examples](https://docs.nado.xyz/developer-resources/api/gateway/signing/examples). + +### Q: What is the PrimaryType of execute X? + +All primary types are listed in our [signing](https://docs.nado.xyz/developer-resources/api/gateway/signing) page. diff --git a/docs/nado/developer-resources/api/integrate-via-smart-contracts.md b/docs/nado/developer-resources/api/integrate-via-smart-contracts.md new file mode 100644 index 0000000..d547803 --- /dev/null +++ b/docs/nado/developer-resources/api/integrate-via-smart-contracts.md @@ -0,0 +1,64 @@ +# Integrate via Smart Contracts + +Smart contracts can use the `LinkSigner` transaction type (see [Link Signer](https://docs.nado.xyz/developer-resources/api/gateway/executes/link-signer)) to perform the following: + +1. Deposit into Nado. +2. LinkSigner an externally owned account ([EOA](https://ethereum.org/en/developers/docs/accounts/#externally-owned-accounts-and-key-pairs)). +3. Have the externally owned account trade using the smart contract's assets on Nado. + +## Setup: Depositing into Nado + Linking an EOA + +1. Deposits are always on-chain, as such, users can simply have their smart contract call `depositCollateral` on our `Endpoint` contract (see [Contracts](https://docs.nado.xyz/developer-resources/api/broken-reference) for addresses). +2. The contract needs to have 1 USDT0 available to pay for slow-mode fee and approve the endpoint contract, assemble the bytes for a slow mode linked signer transaction, and submit it via [submitSlowModeTransaction](https://github.com/vertex-protocol/vertex-contracts/blob/1ab8c8ba11e9ddf82c4210a826aad0b899f342aa/contracts/Endpoint.sol#L258). + +{% hint style="info" %} +You can find the requisite parsing logic in the [Endpoint](https://github.com/vertex-protocol/vertex-contracts/blob/1ab8c8ba11e9ddf82c4210a826aad0b899f342aa/contracts/Endpoint.sol#L429) contract. +{% endhint %} + +### Example + +```solidity +struct LinkSigner { + bytes32 sender; + bytes32 signer; + uint64 nonce; +} + +function linkNadoSigner( + address nadoEndpoint, + address externalAccount, + address usdt0Address + ) external { + // 1. a slow mode fee of 1 USDT0 needs to be avaliable and approved + ERC20 usdt0Token = ERC20(usdt0Address); + + // NOTE: should double check the USDT0 decimals in the corresponding chain. + // e.g: it's 1e6 on arbitrum, whereas it's 1e18 on blast, etc. + uint256 SLOW_MODE_FEE = 1e6; + usdt0Token.transferFrom(msg.sender, address(this), SLOW_MODE_FEE); + usdt0Token.approve(nadoEndpoint, SLOW_MODE_FEE); + + // 2. assamble the link signer slow mode transaction + bytes12 defaultSubaccountName = bytes12(abi.encodePacked("default")); + bytes32 contractSubaccount = bytes32( + abi.encodePacked(uint160(address(this)), defaultSubaccountName) + ); + bytes32 externalSubaccount = bytes32( + uint256(uint160(externalAccount)) << 96 + ); + LinkSigner memory linkSigner = LinkSigner( + contractSubaccount, + externalSubaccount, + IEndpoint(nadoEndpoint).getNonce(externalAccount) + ); + bytes memory txs = abi.encodePacked( + uint8(19), + abi.encode(linkSigner) + ); + + // 3. submit slow mode transaction + IEndpoint(nadoEndpoint).submitSlowModeTransaction(txs); +} +``` + +Once the transaction is confirmed, it may take a few seconds for it to make its way into the Nado offchain sequencer. Afterwards, you can sign transactions that have sender `contractSubaccount` using `externalSubaccount`, and they will be accepted by the sequencer and the blockchain. diff --git a/docs/nado/developer-resources/api/order-appendix.md b/docs/nado/developer-resources/api/order-appendix.md new file mode 100644 index 0000000..fe0b4f7 --- /dev/null +++ b/docs/nado/developer-resources/api/order-appendix.md @@ -0,0 +1,294 @@ +# Order Appendix + +The **Order Appendix** is a 128-bit integer that encodes extra order parameters like execution type, isolated margin, and trigger configurations. + +## Bit Layout + +```json +| value | reserved | trigger | reduce only | order type | isolated | version | +| 64 bits | 50 bits | 2 bits | 1 bit | 2 bits | 1 bit | 8 bits | +| 127..64 | 63..14 | 13..12 | 11 | 10..9 | 8 | 7..0 | +``` + +## Fields (from LSB to MSB) + +### Version + +**8-bits (0-7)**. Protocol version identifier. Currently `1`. May increment when encoding structure updates. + +### Isolated + +**1-bit (8)**. Indicates whether the order uses isolated margin. Isolated positions have dedicated margin for a specific product, creating a separate isolated subaccount. The original account becomes the "parent subaccount" that can manage the isolated position. + +*Key Properties:* + +* Creates isolated subaccount with dedicated margin +* Only quote transfers allowed between isolated and parent subaccounts +* Parent account can sign orders for isolated subaccount +* Cannot be combined with TWAP orders + +*Example:* + +```python +from nado_protocol.utils.appendix import build_appendix +from nado_protocol.utils.math import to_x6 + +# Create isolated order with 1000 USDT0 margin +appendix = build_appendix( + order_type=OrderType.DEFAULT, + isolated=True, + isolated_margin=to_x6(1000) # 1000 USDT0 (x6 precision) +) +``` + +### Order Type + +**2-bits (9-10)**. Execution behavior for the order. + +*Values:* + +* `0` - `DEFAULT`: Standard limit order behavior. +* `1` - `IOC (Immediate or Cancel)`: Execute immediately, cancel unfilled portion. +* `2` - `FOK (Fill or Kill)`: Execute completely or cancel entire order. +* `3` - `POST_ONLY`: Only add liquidity, reject if would take liquidity. + +*Example:* + +```python +from nado_protocol.utils.appendix import build_appendix + +# Post-only order that only provides liquidity +appendix = build_appendix( + order_type=OrderType.POST_ONLY +) +``` + +### Reduce Only + +**1-bit (11)**. Restricts order to only decrease existing positions. Prevent accidentally increasing position size. Order will be rejected if it would increase the position in the same direction. + +*Use Cases:* + +* Risk management when closing positions. +* Taking profits without adding exposure. +* Automated position reduction strategies. + +*Example:* + +```python +from nado_protocol.utils.appendix import build_appendix + +# Reduce-only order to close part of existing position +appendix = build_appendix( + order_type=OrderType.DEFAULT, + reduce_only=True +) +``` + +### Trigger Type + +**2-bits (12-13)**. Conditional execution behavior. + +*Values:* + +* `0` - `NONE`: Execute immediately (regular order). +* `1` - `PRICE`: Price-based conditional order. +* `2` - `TWAP`: Time-Weighted Average Price execution. +* `3` - `TWAP_CUSTOM_AMOUNTS`: TWAP with randomized amounts. + +*Example:* + +```python +from nado_protocol.utils.appendix import build_appendix + +# TWAP order executing 5 times with 0.5% max slippage +appendix = build_appendix( + order_type=OrderType.DEFAULT, + trigger_type=OrderAppendixTriggerType.TWAP, + twap_times=5, + twap_slippage_frac=0.005 # 0.5% +) +``` + +### Reserved + +**50-bits (14-63)**. Reserved for future protocol extensions. Must be set to `0`. + +### Value + +**64-bits (64-127)**. Context-dependent data based on other flags. + +#### **TWAP Configuration (when trigger = 2 or 3)** + +Encodes TWAP execution parameters in the 64-bit value field: + +```json +| times | slippage_x6 | +| 32 bits| 32 bits | +``` + +**Fields:** + +* `times`: Number of TWAP executions. +* `slippage_x6`: Maximum slippage × 1\_000\_000 (6 decimal precision). + +**Example:** + +```python +from nado_protocol.utils.appendix import build_appendix, order_twap_data + +# TWAP: 10 executions, 1% max slippage +appendix = build_appendix( + order_type=OrderType.DEFAULT, + trigger_type=OrderAppendixTriggerType.TWAP, + twap_times=10, + twap_slippage_frac=0.01 # 1% +) + +# Extract TWAP data +times, slippage = order_twap_data(appendix) +# times = 10, slippage = 0.01 +``` + +#### **Isolated Margin (when isolated = 1)** + +Amount of quote (margin\_x6) to transfer to isolated subaccount on first fill, stored in the 64-bit value field. + +{% hint style="warning" %} +**Important:** Isolated margin is stored in **x6 precision** (6 decimals) in the appendix value field. + +* Stored as `margin_x6` (6 decimal places) +* Takes up 64 bits (bits 64-127 of the appendix) + {% endhint %} + +*Example:* + +```python +from nado_protocol.utils.appendix import build_appendix, order_isolated_margin +from nado_protocol.utils.math import to_x6 + +# Isolated order with 500 USDT0 margin +appendix = build_appendix( + order_type=OrderType.DEFAULT, + isolated=True, + isolated_margin=to_x6(500) # 500 USDT0 (x6 precision) +) + +# Extract isolated margin +margin = order_isolated_margin(appendix) +# Returns: 500000000 (500 * 10^6 in x6 precision) +``` + +## Constraints + +* **Isolated + TWAP**: Cannot combine isolated orders with TWAP (trigger types 2 or 3). +* **TWAP Requirements**: TWAP orders must specify both `twap_times` and `twap_slippage_frac` . +* **Isolated Margin**: Can only set `isolated_margin` when `isolated=True` . + +## Migration from Legacy Format + +**Before (deprecated):** + +* Order type encoded in `expiration` field. +* Reduce-only flag encoded in `nonce` field. +* Limited trigger functionality. + +**After (current):** + +* All flags consolidated in 128-bit `appendix` . +* `expiration` is pure timestamp. +* `nonce` encodes `recv_time` only. +* Enhanced trigger and isolated margin support. + +## Building Appendix Values + +#### Using Python SDK (Recommended) + +```python +from nado_protocol.utils.expiration import OrderType +from nado_protocol.utils.appendix import build_appendix, OrderAppendixTriggerType + +# Simple market order +appendix = build_appendix(order_type=OrderType.DEFAULT) + +# Post-only reduce order +appendix = build_appendix( + order_type=OrderType.POST_ONLY, + reduce_only=True +) + +# Isolated order with margin +appendix = build_appendix( + order_type=OrderType.DEFAULT, + isolated=True, + isolated_margin=to_x6(1000) # 1000 USDT0 (x6 precision) +) + +# TWAP order +appendix = build_appendix( + order_type=OrderType.DEFAULT, + trigger_type=OrderAppendixTriggerType.TWAP, + twap_times=5, + twap_slippage_frac=0.01 # 1% +) +``` + +#### Manual Bit Manipulation (Advanced) + +{% hint style="info" %} +Refer to [nado\_protocol.utils.order](https://nadohq.github.io/nado-python-sdk/_modules/nado_protocol/utils/order.html) for a detailed implementation. +{% endhint %} + +```python +# Build appendix manually +def build_manual_appendix(order_type=0, isolated=False, reduce_only=False, + trigger_type=0, value=0): + appendix = 0 + + # Version (bits 0-7) + appendix |= 1 # Version 1 + + # Isolated (bit 8) + if isolated: + appendix |= 1 << 8 + + # Order type (bits 9-10) + appendix |= (order_type & 0b11) << 9 + + # Reduce only (bit 11) + if reduce_only: + appendix |= 1 << 11 + + # Trigger type (bits 12-13) + appendix |= (trigger_type & 0b11) << 12 + + # Reserved bits 14-63 (set to 0) + + # Value (bits 64-127) + # Note: Value is stored in x6 precision for isolated margin + appendix |= (value & ((1 << 64) - 1)) << 64 + + return appendix + +# Example: Post-only reduce order +appendix = build_manual_appendix( + order_type=3, # POST_ONLY + reduce_only=True +) +``` + +### Utility Functions + +```python +# Check order properties +is_reduce_only = order_reduce_only(appendix) +is_trigger = order_is_trigger_order(appendix) +is_isolated = order_is_isolated(appendix) +version = order_version(appendix) +order_type = order_execution_type(appendix) +trigger_type = order_trigger_type(appendix) + +# Extract context data +twap_data = order_twap_data(appendix) # Returns (times, slippage) or None +isolated_margin = order_isolated_margin(appendix) # Returns margin or None +``` diff --git a/docs/nado/developer-resources/api/rate-limits.md b/docs/nado/developer-resources/api/rate-limits.md new file mode 100644 index 0000000..208496d --- /dev/null +++ b/docs/nado/developer-resources/api/rate-limits.md @@ -0,0 +1,120 @@ +# Rate limits + +## Overview + +* Nado uses a weight-based rate-limiting system across queries and executes. We limit based on `IP address`, `Wallet address`, and a global `max # of orders per subaccount per market`. +* These limits equally apply to both `http` requests and `Websocket` messages. +* Limits are applied on a `1 minute` and `10 seconds` basis. + +## Limits + +* IP addresses have a max weight limit of `2400` per minute or `400` every 10 seconds applied only to queries. +* Wallet addresses have a max weight limit of `600` per minute or `100` every 10 seconds applied only to executes. +* Users can have up to `500` open orders per subaccount per market. +* Orders have the following additional limits: + * Place orders (with spot leverage): up to `600` per minute or `100` every 10 seconds across all markets. + * Place orders (without spot leverage): up to `30` per minute or `5` every 10 seconds across all markets. **Note**: orders without spot leverage are `20x` more expensive to place due to additional health checks needed. + * Order cancellations: up to `600` per minute or `100` every 10 seconds. + +## Query Weights + +Queries are rate-limited based on IP. The following weights are applied per query: + +* [**Status**](https://docs.nado.xyz/developer-resources/api/gateway/queries/status): `IP weight = 1` +* [**Contracts**](https://docs.nado.xyz/developer-resources/api/gateway/queries/contracts): `IP weight = 1` +* [**Nonces**](https://docs.nado.xyz/developer-resources/api/gateway/queries/nonces): `IP weight = 2` +* [**Order**](https://docs.nado.xyz/developer-resources/api/gateway/queries/order)**:** `IP weight = 1` +* [**Orders**](https://docs.nado.xyz/developer-resources/api/gateway/queries/orders): `IP weight = 2 * product_ids.length` +* [**Subaccount Info**](https://docs.nado.xyz/developer-resources/api/gateway/queries/subaccount-info): `IP weight = 2` (or `10` with `txns`, or `15` with `txns` + `pre_state="true"`) +* [**Isolated Positions**](https://docs.nado.xyz/developer-resources/api/gateway/queries/isolated-positions): `IP weight = 10` +* [**Market Liquidity**](https://docs.nado.xyz/developer-resources/api/gateway/queries/market-liquidity): `IP weight = 1` +* [**Symbols**](https://docs.nado.xyz/developer-resources/api/gateway/queries/symbols): `IP weight = 2` +* [**All Products**](https://docs.nado.xyz/developer-resources/api/gateway/queries/all-products): `IP weight = 5` +* [**Edge All Products**](https://docs.nado.xyz/developer-resources/api/gateway/queries/edge-all-products): `IP weight = 5` +* [**Market Prices**](https://docs.nado.xyz/developer-resources/api/gateway/queries/market-prices)**:** `IP weight = product_ids.length` +* [**Max Order Size**](https://docs.nado.xyz/developer-resources/api/gateway/queries/max-order-size)**:** `IP weight = 5` +* [**Max Withdrawable**](https://docs.nado.xyz/developer-resources/api/gateway/queries/max-withdrawable)**:** `IP weight = 5` +* [**Max NLP Mintable**](https://docs.nado.xyz/developer-resources/api/gateway/queries/max-nlp-mintable)**:** `IP weight = 20` +* [**Max NLP Burnable**](https://docs.nado.xyz/developer-resources/api/gateway/queries/max-nlp-burnable)**:** `IP weight = 20` +* [**NLP Pool Info**](https://docs.nado.xyz/developer-resources/api/gateway/queries/nlp-pool-info)**:** `IP weight = 20` +* [**NLP Locked Balances**](https://docs.nado.xyz/developer-resources/api/gateway/queries/nlp-locked-balances)**:** `IP weight = 20` +* [**Health Groups**](https://docs.nado.xyz/developer-resources/api/gateway/queries/health-groups)**:** `IP weight = 2` +* [**Linked Signer**](https://docs.nado.xyz/developer-resources/api/gateway/queries/linked-signer)**:** `IP weight = 5` +* [**Insurance**](https://docs.nado.xyz/developer-resources/api/gateway/queries/insurance): `IP weight = 2` +* [**Fee Rates**](https://docs.nado.xyz/developer-resources/api/gateway/queries/fee-rates)**:** `IP weight = 2` +* [**Assets**](https://docs.nado.xyz/developer-resources/api/v2/assets): `IP weight = 2` +* [**Orderbook**](https://docs.nado.xyz/developer-resources/api/v2/orderbook): `IP weight = 1` + +## Archive (indexer) Weights + +* Archive (indexer) queries are rate-limited based on IP. +* IP addresses have a max weight limit of `2400` per minute or `400` every 10 seconds. + +The following weights are applied per query: + +* [**Orders**](https://docs.nado.xyz/developer-resources/api/archive-indexer/orders)**:** `IP Weight = 2 + (limit * subaccounts.length / 20)`; where `limit` and `subaccounts` are query params. +* [**Matches**](https://docs.nado.xyz/developer-resources/api/archive-indexer/matches)**:** `IP Weight = 2 + (limit * subaccounts.length / 10)`; where `limit` and `subaccounts` are query params. +* [**Events**](https://docs.nado.xyz/developer-resources/api/archive-indexer/events)**:** `IP Weight = 2 + (limit * subaccounts.length / 10)`; where `limit` and `subaccounts` are query params. +* [**Candlesticks**](https://docs.nado.xyz/developer-resources/api/archive-indexer/candlesticks)**:** `IP Weight = 1 + limit / 20`; where `limit` is a query param. +* [**Edge Candlesticks**](https://docs.nado.xyz/developer-resources/api/archive-indexer/edge-candlesticks): `IP Weight = 1 + limit / 20`; where `limit` is a query param. +* [**Product Snapshots**](https://docs.nado.xyz/developer-resources/api/archive-indexer/product-snapshots)**:** `IP Weight = 10` for single `products` query, or `10 * timestamps.length` for multiple `product_snapshots` query with max\_time parameter +* [**Funding Rate**](https://docs.nado.xyz/developer-resources/api/archive-indexer/funding-rate)**:** `IP Weight = 2` +* [**Interest & funding payments**](https://docs.nado.xyz/developer-resources/api/archive-indexer/interest-and-funding-payments)**:** `IP Weight = 5` +* [**Oracle Price**](https://docs.nado.xyz/developer-resources/api/archive-indexer/oracle-price)**:** `IP Weight = 2` +* [**Oracle Snapshots**](https://docs.nado.xyz/developer-resources/api/archive-indexer/oracle-snapshots): `IP Weight = max((snapshot_count * product_ids.length / 100), 2)`; where snapshot\_count is `interval.count.min(500)` +* [**Perp Prices**](https://docs.nado.xyz/developer-resources/api/archive-indexer/perp-prices)**:** `IP Weight = 2` (includes both single `price` and multiple `perp_prices` queries) +* [**Market Snapshots**](https://docs.nado.xyz/developer-resources/api/archive-indexer/market-snapshots)**:** `IP Weight = max((snapshot_count * product_ids.length / 100), 2)`; where snapshot\_count is `interval.count.min(500)` +* [**Edge Market Snapshots**](https://docs.nado.xyz/developer-resources/api/archive-indexer/edge-market-snapshots): `IP weight = (interval.count.min(500) / 20) + (interval.count.clamp(2, 20) * 2)` +* [**Subaccounts**](https://docs.nado.xyz/developer-resources/api/archive-indexer/subaccounts)**:** `IP Weight = 2` +* [**Subaccount Snapshots**](https://docs.nado.xyz/developer-resources/api/archive-indexer/subaccount-snapshots): `IP Weight = 2 + (limit * subaccounts.length / 10)`; where `limit` and `subaccounts` are query params. +* [**Linked Signers**](https://docs.nado.xyz/developer-resources/api/archive-indexer/linked-signers): `IP Weight = 2` +* [**Linked Signer Rate Limit**](https://docs.nado.xyz/developer-resources/api/archive-indexer/linked-signer-rate-limit)**:** `IP Weight = 2` +* [**Isolated Subaccounts**](https://docs.nado.xyz/developer-resources/api/archive-indexer/isolated-subaccounts): `IP Weight = 2` +* [**Signatures**](https://docs.nado.xyz/developer-resources/api/archive-indexer/signatures): `IP Weight = 2 + len(digests) / 10`; where `digests` is a query param. +* [**Fast Withdrawal Signature**](https://docs.nado.xyz/developer-resources/api/archive-indexer/fast-withdrawal-signature): `IP Weight = 10` +* [**NLP Funding Payments**](https://docs.nado.xyz/developer-resources/api/archive-indexer/nlp-funding-payments): `IP Weight = 5` +* [**NLP Interest Payments**](https://docs.nado.xyz/developer-resources/api/archive-indexer/nlp-interest-payments): `IP Weight = 5` +* [**NLP Snapshots**](https://docs.nado.xyz/developer-resources/api/archive-indexer/nlp-snapshots): `IP Weight = limit.min(500) / 100`; where `limit` is a query param. +* [**Tx Hashes**](https://docs.nado.xyz/developer-resources/api/archive-indexer/tx-hashes): `IP Weight = idxs.length * 2`; where `idxs` is an array of submission indices (max 100). +* [**Liquidation Feed**](https://docs.nado.xyz/developer-resources/api/archive-indexer/liquidation-feed)**:** `IP Weight = 2` +* [**Sequencer Backlog**](https://docs.nado.xyz/developer-resources/api/archive-indexer/sequencer-backlog): `IP Weight = 1` +* [**Direct Deposit Address**](https://docs.nado.xyz/developer-resources/api/archive-indexer/direct-deposit-address): `IP Weight = 10` +* [**Quote Price**](https://docs.nado.xyz/developer-resources/api/archive-indexer/quote-price): `IP Weight = 2` +* [**Ink Airdrop**](https://docs.nado.xyz/developer-resources/api/archive-indexer/ink-airdrop): `IP Weight = 2` + +## Execute Weights + +Executes are rate-limited based on Wallet address. The following weights are applied per execute: + +* [**Place order**](https://docs.nado.xyz/developer-resources/api/gateway/executes/place-order)**:** + * With spot leverage: `Wallet weight = 1` + * Without spot leverage: `Wallet weight = 20` +* [**Place orders**](https://docs.nado.xyz/developer-resources/api/gateway/executes/place-orders)**:** + * With spot leverage: `Wallet weight = 1 per order` + * Without spot leverage: `Wallet weight = 20 per order` + * **Note**: 50ms processing penalty per request +* [**Cancel orders**](https://docs.nado.xyz/developer-resources/api/gateway/executes/cancel-orders)**:** + * When no **digests** are provided: `Wallet weight = 1` + * When **digests** are provided: `Wallet weight = total digests` +* [**Cancel Product Orders**](https://docs.nado.xyz/developer-resources/api/gateway/executes/cancel-product-orders)**:** + * When no **productIds** are provided**:** `Wallet weight = 50` + * When **productIds** are provided: `Wallet weight = 5 * total productIds` +* [**Cancel And Place**](https://docs.nado.xyz/developer-resources/api/gateway/executes/cancel-and-place): + * The sum of [Cancel orders](https://docs.nado.xyz/developer-resources/api/gateway/executes/cancel-orders) + [Place order](https://docs.nado.xyz/developer-resources/api/gateway/executes/place-order) limits +* [**Withdraw Collateral**](https://docs.nado.xyz/developer-resources/api/gateway/executes/withdraw-collateral)**:** + * With spot leverage: `Wallet weight = 10` + * Without spot leverage: `Wallet weight = 20` +* [**Liquidate Subaccount**](https://docs.nado.xyz/developer-resources/api/gateway/executes/liquidate-subaccount): `Wallet weight = 20` +* [**Mint NLP**](https://docs.nado.xyz/developer-resources/api/gateway/executes/mint-nlp): `Wallet weight = 10` +* [**Burn NLP**](https://docs.nado.xyz/developer-resources/api/gateway/executes/burn-nlp): `Wallet weight = 10` +* [**Link Signer**](https://docs.nado.xyz/developer-resources/api/gateway/executes/link-signer): `Wallet weight = 30` + * Can only perform a max of 50 link signer requests every 7 days per subaccount. +* [**Transfer Quote:**](https://docs.nado.xyz/developer-resources/api/gateway/executes/transfer-quote) `Wallet weight = 10` + * Can only transfer to a max of 5 new recipients within 24hrs. + +## Trigger Service Limits + +The trigger service has additional limits specific to conditional orders: + +* **Pending trigger orders**: Max of `25` pending trigger orders per product per subaccount +* **TWAP orders**: Must use IOC execution type and cannot be combined with isolated margin diff --git a/docs/nado/developer-resources/api/subscriptions.md b/docs/nado/developer-resources/api/subscriptions.md new file mode 100644 index 0000000..844ea52 --- /dev/null +++ b/docs/nado/developer-resources/api/subscriptions.md @@ -0,0 +1,33 @@ +# Subscriptions + +## Overview + +To interact with the subscription API, send websocket messages to `WEBSOCKET [SUBSCRIPTIONS_ENDPOINT]`. + +Subscription connections must set the `Sec-WebSocket-Extensions` header to include `permessage-deflate`. + +## Endpoints + +### Testnet: + +* `wss://gateway.test.nado.xyz/v1/subscribe` + +{% hint style="info" %} +**Note**: You must send ping frames every 30 seconds to keep the websocket connection alive. +{% endhint %} + +{% content-ref url="subscriptions/authentication" %} +[authentication](https://docs.nado.xyz/developer-resources/api/subscriptions/authentication) +{% endcontent-ref %} + +{% content-ref url="subscriptions/streams" %} +[streams](https://docs.nado.xyz/developer-resources/api/subscriptions/streams) +{% endcontent-ref %} + +{% content-ref url="subscriptions/events" %} +[events](https://docs.nado.xyz/developer-resources/api/subscriptions/events) +{% endcontent-ref %} + +{% content-ref url="subscriptions/rate-limits" %} +[rate-limits](https://docs.nado.xyz/developer-resources/api/subscriptions/rate-limits) +{% endcontent-ref %} diff --git a/docs/nado/developer-resources/api/subscriptions/authentication.md b/docs/nado/developer-resources/api/subscriptions/authentication.md new file mode 100644 index 0000000..3de6143 --- /dev/null +++ b/docs/nado/developer-resources/api/subscriptions/authentication.md @@ -0,0 +1,82 @@ +# Authentication + +### Rate limits + +A **single wallet address** can be authenticated by up to 5 websocket connections, regardless of the originating IP address. Connections exceeding these limits will be automatically disconnected. + +{% hint style="info" %} +See [rate limits](https://docs.nado.xyz/developer-resources/api/subscriptions/rate-limits) for more details. +{% endhint %} + +### Request + +To access streams that require authentication, submit a request with the `method` field set to `authenticate`. + +{% tabs %} +{% tab title="Authenticate" %} +**Connect** + +`WEBSOCKET [SUBSCRIPTIONS_ENDPOINT]` + +**Message** + +```json +{ + "method": "authenticate", + "id": 0, + "tx": { + "sender": "0x...", + "expiration": "1..." + }, + "signature": "0x..." +} +``` + +{% endtab %} +{% endtabs %} + +### Request Parameters + +
ParameterTypeRequiredDescription
methodstringYesauthenticate
idnumberYesCan be set to any positive integer. Can be used to identify the websocket request / response.
txobjectYesStreamAuthentication object that needs to be signed. See Signing section for more details.
tx.senderstringYesA hex string representing a bytes32 of a specific subaccount.
tx.expirationstringYesRepresents the expiration time in milliseconds since the Unix epoch.
signaturestringYesHex string representing hash of the signed StreamAuthentication object.See Signing section for more details.
+ +{% hint style="info" %} +**Notes**: + +* Although sender specifies a specific subaccount, authentication applies to the entire wallet address, enabling access to authenticated streams for different subaccounts under that address. +* Once authenticated, the authentication status of that websocket connection cannot be changed and stays for the duration of the connection. + {% endhint %} + +## Signing + +{% hint style="info" %} +See more details and examples in our [signing](https://docs.nado.xyz/developer-resources/api/gateway/signing) page. +{% endhint %} + +The typed data struct that needs to be signed is: + +```solidity +struct StreamAuthentication { + bytes32 sender; + uint64 expiration; +} +``` + +`sender`: A hex string representing a `bytes32` of a specific subaccount. The signature must be signed by the wallet address specified by sender. + +`expiration`: Represents the expiration time in milliseconds since the Unix epoch. Requests will be denied if the expiration is either smaller than the current time or more than 100 seconds ahead of it. + +{% hint style="info" %} +**Notes**: + +* Should use the endpoint address as `verifyingContract`. +* For signing, you should always use the data type specified in the typed data struct which might be different from the type sent in the request e.g: `expiration` should be an `uint64` for **Signing** but should be sent as a `string` in the final payload. + {% endhint %} + +### **Response** + +```json +{ + "result": null, + "id": 10 +} +``` diff --git a/docs/nado/developer-resources/api/subscriptions/events.md b/docs/nado/developer-resources/api/subscriptions/events.md new file mode 100644 index 0000000..5070771 --- /dev/null +++ b/docs/nado/developer-resources/api/subscriptions/events.md @@ -0,0 +1,284 @@ +# Events + +## **Order Update** + +**Update speed: real-time** + +```json +{ + "type": "order_update", + // timestamp of the event in nanoseconds + "timestamp": "1695081920633151000", + "product_id": 1, + // order digest + "digest": "0xf7712b63ccf70358db8f201e9bf33977423e7a63f6a16f6dab180bdd580f7c6c", + // remaining amount to be filled. + // will be `0` if the order is either fully filled or cancelled. + "amount": "82000000000000000", + // any of: "filled", "cancelled", "placed" + "reason": "filled", + // an optional `order id` that can be provided when placing an order + "id": 100 +} +``` + +### Example Scenarios: + +Let's assume your initial order amount is 100 units and each match occurs for an amount of 10 units. + +{% hint style="info" %} +**Note**: The following events only include `amount` and `reason` for simplicity. +{% endhint %} + +#### Scenario 1: Limit Order Partially Fills and Gets Placed + +Your limit order matches against existing orders in the book. + +You will receive the following events over websocket, each with the same timestamp but in sequential order: + +* Event 1: `(90, "filled")` — 10 units of your order are filled. +* Event 2: `(80, "filled")` — Another 10 units are filled. +* Event 3: `(80, "placed")` — The remaining 80 units are placed on the book. + +#### Scenario 2: Immediate-Or-Cancel (IOC) Order Partially Fills + +Your IOC order matches against existing orders but is not completely filled. + +The events you will receive are as follows: + +* Event 1: `(90, "filled")` — 10 units are filled. +* Event 2: `(80, "filled")` — Another 10 units are filled. +* Event 3: `(0, "cancelled")` — The remaining order is cancelled. **Note**: If your IOC order is fully filled, the last event you will receive is `(0, "filled")`. + +#### Scenario 3: Resting Limit Order Gets Matched + +Your existing, or "resting," limit order matches against an incoming order. + +You will receive the following event: `(90, "filled")` — 10 units of your resting limit order are filled. + +#### Scenario 4: Resting Limit Order Gets Cancelled + +Your resting limit order could be cancelled for various reasons, such as manual cancellation, expiration, failing health checks, or self-trade prevention. + +In any of these cases, you will receive: `(0, "cancelled")` + +#### **Scenario 5: IOC order doesn't cross the book or FOK order fails to be entirely filled** + +In any of these cases, you will receive: `(0, "cancelled")` + +## **Trade** + +**Update speed: real-time** + +```json +{ + "type": "trade", + "timestamp": "1676151190656903000", // timestamp of the event in nanoseconds + "product_id": 1, + "price": "1000", // price the trade happened at, multiplied by 1e18 + // both taker_qty and maker_qty have the same value; + // set to filled amount (min amount of taker and maker) when matching against book + "taker_qty": "1000", + "maker_qty": "1000", + "is_taker_buyer": true +} +``` + +## **Best Bid Offer** + +**Update speed: real-time** + +```json +{ + "type": "best_bid_offer", + "timestamp": "1676151190656903000", // timestamp of the event in nanoseconds + "product_id": 1, + "bid_price": "1000", // the highest bid price, multiplied by 1e18 + "bid_qty": "1000", // quantity at the highest bid, multiplied by 1e18. + // i.e. if this is USDT0 with 6 decimals, one USDT0 + // would be 1e12 + "ask_price": "1000", // lowest ask price + "ask_qty": "1000" // quantity at the lowest ask +} +``` + +## **Fill** + +**Update speed: real-time** + +```json +{ + "type": "fill", + "timestamp": "1676151190656903000", // timestamp of the event in nanoseconds + "product_id": 1, + // the subaccount that placed this order + "subaccount": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43746573743000000000000000", + // hash of the order that uniquely identifies it + "order_digest": "0xf4f7a8767faf0c7f72251a1f9e5da590f708fd9842bf8fcdeacbaa0237958fff", + // order appendix containing execution type, reduce-only flag, etc. + "appendix": "4096", + // the amount filled, multiplied by 1e18 + "filled_qty": "1000", + // the amount outstanding unfilled, multiplied by 1e18 + "remaining_qty": "2000", + // the original order amount, multiplied by 1e18 + "original_qty": "3000", + // fill price + "price": "24991000000000000000000", + // true for `taker`, false for `maker` + "is_taker": true, + "is_bid": true, + // the amount of fee paid, multiplied by 1e18 + "fee": "100", + // the submission_idx of the transaction (n_submissions - 1) + // can use to map `fills` to historical `matches`. + "submission_idx": 100, + // an optional `order id` that can be provided when placing an order + "id": 100 +} +``` + +## **Position Change** + +**Update speed: real-time** + +```json +{ + "type":"position_change", + "timestamp": "1676151190656903000", // timestamp of event in nanoseconds + "product_id":1, + // subaccount who's position changed + "subaccount":"0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43706d00000000000000000000", + // whether this position change is for an isolated margin position + "isolated": false, + // new amount for this product + "amount":"51007390115411548", + // new quote balance for this product; zero for everything except non lp perps + // the negative of the entry cost of the perp + "v_quote_amount":"0", + // any of: "deposit_collateral", "match_orders", "withdraw_collateral", "transfer_quote", "settle_pnl", "mint_nlp", "burn_nlp", "liquidate_subaccount" + "reason": "deposit_collateral" +} +``` + +{% hint style="info" %} +**Note:** that it is possible that back to back `position_change` events have the same fields except for `timestamp`. Additionally, `position_change` events are not sent on interest and funding payments, and also are not sent on actions done through slow mode (except deposits). The full list of actions that will trigger a `PositionChange` event are: + +* Minting or burning NLP tokens +* Liquidating a subaccount +* Matching orders +* Depositing or withdrawing spot +* Settling PNL + {% endhint %} + +## **Book Depth** + +**Update speed: once every 50ms** + +```json +{ + "type":"book_depth", + // book depth aggregates a number of events once every 50ms + // these are the minimum and maximum timestamps from + // events that contributed to this response + "min_timestamp": "1683805381879572835", + "max_timestamp": "1683805381879572835", + // the max_timestamp of the last book_depth event for this product + "last_max_timestamp": "1683805381771464799", + "product_id":1, + // changes to the bid side of the book in the form of [[price, new_qty]] + "bids":[["21594490000000000000000","51007390115411548"]], + // changes to the ask side of the book in the form of [[price, new_qty]] + "asks":[["21694490000000000000000","0"],["21695050000000000000000","0"]] +} +``` + +{% hint style="info" %} +**Note**: To keep an updated local orderbook, do the following: + +1. Subscribe to the `book_depth` stream and queue up events. +2. Get a market data snapshot by calling [MarketLiquidity](https://docs.vertexprotocol.com/developer-resources/api/gateway/queries/market-liquidity). The snapshot contains a `timestamp` in the response. +3. Apply events with `max_timestamp` > snapshot `timestamp`. +4. When you receive an event where its `last_max_timestamp` is not equal to the `max_timestamp` of the last event you've received, it means some events were lost and you should repeat 1-3 again. + {% endhint %} + +## **Liquidation** + +**Update speed: real-time** + +```json +{ + "type": "liquidation", + "timestamp": "1234567890000", // timestamp of the event in nanoseconds + // single element for regular liquidations, two elements for spread liquidations (spot_id, perp_id) + "product_ids": [1], + // liquidator subaccount (32 bytes) + "liquidator": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43746573743000000000000000", + // liquidatee subaccount (32 bytes) + "liquidatee": "0x8b6fd3859f7065794491a8d29dcf3f9edb8d7c43746573743000000000000000", + // amount liquidated (positive for long, negative for short) + "amount": "1000000000000000000", + // price at which liquidation occurred + "price": "50000000000000000000" +} +``` + +## Latest Candlestick + +**Update speed: real-time** + +```json +{ + "type": "latest_candlestick", + "timestamp": 1234567890000, + "product_id": 1, + "granularity": 60, + "open_x18": "50000000000000000000", + "high_x18": "51000000000000000000", + "low_x18": "49000000000000000000", + "close_x18": "50500000000000000000", + "volume": "1000000000000000000" +} +``` + +## Funding Payment + +**Update speed: real-time at the time of payment (payments happen hourly).** + +```json +{ + "type": "funding_payment", + "timestamp": 1234567890000, + "product_id": 1, + // funding payment amount (positive = receive, negative = pay) + "payment_amount": "1000000000000000000", + // open interest at time of funding + "open_interest": "50000000000000000000", + // cumulative funding values, multiplied by 1e18 + "cumulative_funding_long_x18": "100000000000000000", + "cumulative_funding_short_x18": "-100000000000000000", + // time delta over which the funding payment was calculated + "dt": 3600000 +} +``` + +## Funding Rate + +**Update speed: real-time (updates occur every 20 seconds).** + +{% hint style="info" %} +**Note**: The `funding_rate_x18` and `update_time` values are identical to those returned by the [Funding Rate](https://docs.nado.xyz/developer-resources/api/archive-indexer/funding-rate) indexer endpoint. +{% endhint %} + +```json +{ + "type": "funding_rate", + // timestamp when the event was generated, in nanoseconds + "timestamp": "1234567890123456789", + "product_id": 1, + // latest 24hr funding rate, multiplied by 1e18 + "funding_rate_x18": "50000000000000000", + // timestamp when the funding rate was updated, in seconds + "update_time": "1234567890" +} +``` diff --git a/docs/nado/developer-resources/api/subscriptions/rate-limits.md b/docs/nado/developer-resources/api/subscriptions/rate-limits.md new file mode 100644 index 0000000..c992cdd --- /dev/null +++ b/docs/nado/developer-resources/api/subscriptions/rate-limits.md @@ -0,0 +1,3 @@ +# Rate limits + +Each IP address is restricted to a maximum of **100** active websocket connections. Additionally, a **single wallet address** can be authenticated by up to **5** websocket connections, regardless of the originating IP address. Connections exceeding these limits will be automatically disconnected. diff --git a/docs/nado/developer-resources/api/subscriptions/streams.md b/docs/nado/developer-resources/api/subscriptions/streams.md new file mode 100644 index 0000000..1c2c35a --- /dev/null +++ b/docs/nado/developer-resources/api/subscriptions/streams.md @@ -0,0 +1,538 @@ +# Streams + +## Available Streams + +See below the available streams you can subscribe to: + +```rust +pub enum StreamSubscription { + // pass `null` product_id to subscribe to all products + OrderUpdate { product_id: Option, subaccount: H256 }, + Trade { product_id: u32 }, + BestBidOffer { product_id: u32 }, + // pass `null` product_id to subscribe to all products + Fill { product_id: Option, subaccount: H256 }, + // pass `null` product_id to subscribe to all products + PositionChange { product_id: Option, subaccount: H256}, + BookDepth { product_id: u32 }, + // pass `null` product_id to subscribe to all products + Liquidation { product_id: Option }, + LatestCandlestick { + product_id: u32, + // time interval in seconds (e.g., 60, 300, 900, 3600) + granularity: i32 + }, + FundingPayment { product_id: u32 }, + // pass `null` product_id to subscribe to all products + FundingRate { product_id: Option } +} +``` + +## **Subscribing to a stream** + +{% tabs %} +{% tab title="Order Update" %} +**Connect** + +`WEBSOCKET [SUBSCRIPTIONS_ENDPOINT]` + +**Message** + +*Requires Authentication: Yes.* + +{% hint style="info" %} +**Note**: Set `product_id` to `null` to subscribe to order updates across all products for the subaccount. +{% endhint %} + +```json +{ + "method": "subscribe", + "stream": { + "type": "order_update", + "subaccount": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43746573743000000000000000", + "product_id": 1 + }, + "id": 10 +} +``` + +**Subscribe to all products:** + +```json +{ + "method": "subscribe", + "stream": { + "type": "order_update", + "subaccount": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43746573743000000000000000", + "product_id": null + }, + "id": 10 +} +``` + +{% endtab %} + +{% tab title="Trade" %} +**Connect** + +`WEBSOCKET [SUBSCRIPTIONS_ENDPOINT]` + +**Message** + +*Requires Authentication: No.* + +```json +{ + "method": "subscribe", + "stream": { + "type": "trade", + "product_id": 0 + }, + "id": 10 +} +``` + +{% endtab %} + +{% tab title="Best Bid Offer" %} +**Connect** + +`WEBSOCKET [SUBSCRIPTIONS_ENDPOINT]` + +**Message** + +*Requires Authentication: No.* + +```json +{ + "method": "subscribe", + "stream": { + "type": "best_bid_offer", + "product_id": 0 + }, + "id": 10 +} +``` + +{% endtab %} +{% endtabs %} + +{% tabs %} +{% tab title="Fill" %} +**Connect** + +`WEBSOCKET [SUBSCRIPTIONS_ENDPOINT]` + +**Message** + +*Requires Authentication: No.* + +{% hint style="info" %} +**Note**: Set `product_id` to `null` to subscribe to fills across all products for the subaccount. +{% endhint %} + +```json +{ + "method": "subscribe", + "stream": { + "type": "fill", + "product_id": 1, + "subaccount": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43746573743000000000000000" + }, + "id": 10 +} +``` + +**Subscribe to all products:** + +```json +{ + "method": "subscribe", + "stream": { + "type": "fill", + "product_id": null, + "subaccount": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43746573743000000000000000" + }, + "id": 10 +} +``` + +{% endtab %} + +{% tab title="Position Change" %} +**Connect** + +`WEBSOCKET [SUBSCRIPTIONS_ENDPOINT]` + +**Message** + +*Requires Authentication: No.* + +{% hint style="info" %} +**Note**: Set `product_id` to `null` to subscribe to position changes across all products for the subaccount. +{% endhint %} + +```json +{ + "method": "subscribe", + "stream": { + "type": "position_change", + "product_id": 0, + "subaccount": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43746573743000000000000000" + }, + "id": 10 +} +``` + +**Subscribe to all products:** + +```json +{ + "method": "subscribe", + "stream": { + "type": "position_change", + "product_id": null, + "subaccount": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43746573743000000000000000" + }, + "id": 10 +} +``` + +{% endtab %} + +{% tab title="Book Depth" %} +**Connect** + +`WEBSOCKET [SUBSCRIPTIONS_ENDPOINT]` + +**Message** + +*Requires Authentication: No.* + +```json +{ + "method": "subscribe", + "stream": { + "type": "book_depth", + "product_id": 0 + }, + "id": 10 +} +``` + +{% endtab %} +{% endtabs %} + +{% tabs %} +{% tab title="Liquidation" %} +**Connect** + +`WEBSOCKET [SUBSCRIPTIONS_ENDPOINT]` + +**Message** + +*Requires Authentication: No.* + +{% hint style="info" %} +**Note**: Set `product_id` to `null` to subscribe to liquidations across all products. +{% endhint %} + +```json +{ + "method": "subscribe", + "stream": { + "type": "liquidation", + "product_id": 1 + }, + "id": 10 +} +``` + +**Subscribe to all products:** + +```json +{ + "method": "subscribe", + "stream": { + "type": "liquidation", + "product_id": null + }, + "id": 10 +} +``` + +{% endtab %} + +{% tab title="Latest Candlestick" %} +**Connect** + +`WEBSOCKET [SUBSCRIPTIONS_ENDPOINT]` + +**Message** + +{% hint style="info" %} +See all supportes `granularity` values in [Available Granularities](https://docs.nado.xyz/developer-resources/archive-indexer/candlesticks#available-granularities) +{% endhint %} + +*Requires Authentication: No.* + +```json +{ + "method": "subscribe", + "stream": { + "type": "latest_candlestick", + "product_id": 1, + "granularity": 60 + }, + "id": 10 +} +``` + +{% endtab %} + +{% tab title="Funding Payment" %} +**Connect** + +`WEBSOCKET [SUBSCRIPTIONS_ENDPOINT]` + +**Message** + +*Requires Authentication: No.* + +```json +{ + "method": "subscribe", + "stream": { + "type": "funding_payment", + "product_id": 2 + }, + "id": 10 +} +``` + +{% endtab %} + +{% tab title="Funding Rate" %} +**Connect** + +`WEBSOCKET [SUBSCRIPTIONS_ENDPOINT]` + +**Message** + +*Requires Authentication: No.* + +{% hint style="info" %} +**Note**: Set `product_id` to `null` to subscribe to funding rate updates across all products. +{% endhint %} + +```json +{ + "method": "subscribe", + "stream": { + "type": "funding_rate", + "product_id": 2 + }, + "id": 10 +} +``` + +**Subscribe to all products:** + +```json +{ + "method": "subscribe", + "stream": { + "type": "funding_rate", + "product_id": null + }, + "id": 10 +} +``` + +{% endtab %} +{% endtabs %} + +### **Response** + +```json +{ + "result": null, + "id": 10 +} +``` + +## **Unsubscribing** + +{% tabs %} +{% tab title="Order Update" %} +**Connect** + +`WEBSOCKET [SUBSCRIPTIONS_ENDPOINT]` + +**Message** + +```json +{ + "method": "unsubscribe", + "stream": { + "type": "order_update", + "subaccount": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43746573743000000000000000", + "product_id": 1 + }, + "id": 10 +} +``` + +**Unsubscribe from all products:** + +```json +{ + "method": "unsubscribe", + "stream": { + "type": "order_update", + "subaccount": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43746573743000000000000000", + "product_id": null + }, + "id": 10 +} +``` + +{% endtab %} + +{% tab title="Trade" %} +**Connect** + +`WEBSOCKET [SUBSCRIPTIONS_ENDPOINT]` + +**Message** + +```json +{ + "method": "unsubscribe", + "stream": { + "type": "trade", + "product_id": 0 + }, + "id": 10 +} +``` + +{% endtab %} + +{% tab title="Best Bid Offer" %} +**Connect** + +`WEBSOCKET [SUBSCRIPTIONS_ENDPOINT]` + +**Message** + +```json +{ + "method": "unsubscribe", + "stream": { + "type": "best_bid_offer", + "product_id": 0 + }, + "id": 10 +} +``` + +{% endtab %} + +{% tab title="Fill" %} +**Connect** + +`WEBSOCKET [SUBSCRIPTIONS_ENDPOINT]` + +**Message** + +```json +{ + "method": "unsubscribe", + "stream": { + "type": "fill", + "product_id": 0, + "subaccount": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43746573743000000000000000" + }, + "id": 10 +} +``` + +{% endtab %} + +{% tab title="Position Change" %} +**Connect** + +`WEBSOCKET [SUBSCRIPTIONS_ENDPOINT]` + +**Message** + +```json +{ + "method": "unsubscribe", + "stream": { + "type": "position_change", + "product_id": 0, + "subaccount": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43746573743000000000000000" + }, + "id": 10 +} +``` + +{% endtab %} + +{% tab title="Book Depth" %} +**Connect** + +`WEBSOCKET [SUBSCRIPTIONS_ENDPOINT]` + +**Message** + +```json +{ + "method": "unsubscribe", + "stream": { + "type": "book_depth", + "product_id": 0, + }, + "id": 10 +} +``` + +{% endtab %} +{% endtabs %} + +### **Response** + +```json +{ + "result": null, + "id": 10 +} +``` + +## **Listing subscribed streams** + +```json +{ + "method": "list", + "id": 10 +} +``` + +### Response + +```json +{ + "result": [ + { + "type": "default" + }, + { + "type": "trade", + "product_id": 0 + } + ], + "id": 10 +} +``` diff --git a/docs/nado/developer-resources/api/symbols.md b/docs/nado/developer-resources/api/symbols.md new file mode 100644 index 0000000..b4fed74 --- /dev/null +++ b/docs/nado/developer-resources/api/symbols.md @@ -0,0 +1,80 @@ +# Symbols + +## Request + +{% tabs %} +{% tab title="REST" %} **GET** `[GATEWAY_REST_ENDPOINT]/symbols` +{% endtab %} +{% endtabs %} + +## Response + +```json +[ + { + "product_id": 0, + "symbol": "USDT0", + "delisted": false + }, + { + "product_id": 1, + "symbol": "KBTC", + "delisted": false + }, + { + "product_id": 2, + "symbol": "BTC-PERP", + "delisted": false + }, + { + "product_id": 3, + "symbol": "WETH", + "delisted": false + }, + { + "product_id": 4, + "symbol": "ETH-PERP", + "delisted": false + }, + { + "product_id": 5, + "symbol": "USDC", + "delisted": false + }, + { + "product_id": 8, + "symbol": "SOL-PERP", + "delisted": false + }, + { + "product_id": 10, + "symbol": "XRP-PERP", + "delisted": false + }, + { + "product_id": 14, + "symbol": "BNB-PERP", + "delisted": false + }, + { + "product_id": 16, + "symbol": "HYPE-PERP", + "delisted": false + }, + { + "product_id": 18, + "symbol": "ZEC-PERP", + "delisted": false + }, + { + "product_id": 20, + "symbol": "MON-PERP", + "delisted": false + }, + { + "product_id": 22, + "symbol": "FARTCOIN-PERP", + "delisted": false + } +] +``` diff --git a/docs/nado/developer-resources/api/trigger.md b/docs/nado/developer-resources/api/trigger.md new file mode 100644 index 0000000..a092c3c --- /dev/null +++ b/docs/nado/developer-resources/api/trigger.md @@ -0,0 +1,65 @@ +# Trigger + +The trigger service enables sophisticated order execution strategies through conditional triggers: + +## Order Types + +### **Price Triggers** + +Execute orders when price conditions are met: + +* **Stop orders**: Trigger when price moves above or below a threshold +* **Take profit/Stop loss**: Automated position management +* **Support multiple price sources**: Oracle price, last trade price, or mid-book price + +### **Time Triggers (TWAP)** + +Execute large orders over time using Time-Weighted Average Price: + +* **Split large orders**: Break into smaller executions to reduce market impact +* **Configurable intervals**: Set time between executions +* **Slippage protection**: Built-in slippage limits for each execution +* **Custom amounts**: Specify exact amounts for each execution or split evenly + +## API Structure + +There are two types of actions: + +* `Execute`: Modifies state (place/cancel orders) +* `Query`: Fetches information (list orders, TWAP status) + +**HTTP Endpoints:** + +* `POST [TRIGGER_ENDPOINT]/execute` for order placement and cancellation +* `POST [TRIGGER_ENDPOINT]/query` for querying trigger order status + +`HTTP` requests must set the `Accept-Encoding` to include `gzip`, `br` or `deflate` + +## Rate Limits + +* **Maximum pending orders**: 25 pending trigger orders per product per subaccount +* **TWAP constraints**: Must use IOC execution type, cannot combine with isolated margin + +## Key Requirements + +### **Order Appendix Configuration** + +All trigger orders require proper [Order Appendix](https://docs.nado.xyz/developer-resources/api/order-appendix) configuration: + +* **Trigger type**: Specify price (1), TWAP (2), or TWAP with custom amounts (3) in appendix bits +* **Execution type**: TWAP orders **must** use IOC execution +* **TWAP parameters**: Encode execution count and slippage limits in appendix value field + +## Endpoints + +### Testnet: + +* `https://trigger.test.nado.xyz/v1` + +{% content-ref url="trigger/executes" %} +[executes](https://docs.nado.xyz/developer-resources/api/trigger/executes) +{% endcontent-ref %} + +{% content-ref url="trigger/queries" %} +[queries](https://docs.nado.xyz/developer-resources/api/trigger/queries) +{% endcontent-ref %} diff --git a/docs/nado/developer-resources/api/trigger/executes.md b/docs/nado/developer-resources/api/trigger/executes.md new file mode 100644 index 0000000..7524af1 --- /dev/null +++ b/docs/nado/developer-resources/api/trigger/executes.md @@ -0,0 +1,34 @@ +# Executes + +## Overview + +All executes go through the following endpoint; the exact details of the execution are specified by the JSON payload. + +* **REST**: `POST [TRIGGER_ENDPOINT]/execute` + +## API Response + +All `Execute` messages return the following information: + +```json +{ + "status": "success" | "failure", + "error"?: "{error_msg}", + "error_code"?: {error_code}, + "request_type": "{request_type}", +} +``` + +## Available Executes: + +{% content-ref url="executes/place-order" %} +[place-order](https://docs.nado.xyz/developer-resources/api/trigger/executes/place-order) +{% endcontent-ref %} + +{% content-ref url="executes/cancel-orders" %} +[cancel-orders](https://docs.nado.xyz/developer-resources/api/trigger/executes/cancel-orders) +{% endcontent-ref %} + +{% content-ref url="executes/cancel-product-orders" %} +[cancel-product-orders](https://docs.nado.xyz/developer-resources/api/trigger/executes/cancel-product-orders) +{% endcontent-ref %} diff --git a/docs/nado/developer-resources/api/trigger/executes/cancel-orders.md b/docs/nado/developer-resources/api/trigger/executes/cancel-orders.md new file mode 100644 index 0000000..c562761 --- /dev/null +++ b/docs/nado/developer-resources/api/trigger/executes/cancel-orders.md @@ -0,0 +1,54 @@ +# Cancel Orders + +## Request + +{% tabs %} +{% tab title="REST" %} `POST [TRIGGER_ENDPOINT]/execute` + +**Body** + +```json + +{ + "cancel_orders": { + "tx": { + "sender": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43746573743000000000000000", + "productIds": [0], + "digests": ["0x"], + "nonce": "1" + }, + "signature": "0x" + } +} +``` + +{% endtab %} +{% endtabs %} + +## Request Parameters + +See [Core > Executes > Cancel Orders](https://docs.nado.xyz/developer-resources/gateway/executes/cancel-orders#request-parameters) + +## Response + +#### Success + +```json +{ + "status": "success", + "signature": {signature}, + "request_type": "execute_cancel_orders" +} +``` + +#### Failure + +```json +{ + "status": "failure", + "signature": {signature}, + "error": "{error_msg}", + "error_code": {error_code}, + "request_type": "execute_cancel_orders" +} +``` diff --git a/docs/nado/developer-resources/api/trigger/executes/cancel-product-orders.md b/docs/nado/developer-resources/api/trigger/executes/cancel-product-orders.md new file mode 100644 index 0000000..9b01d3f --- /dev/null +++ b/docs/nado/developer-resources/api/trigger/executes/cancel-product-orders.md @@ -0,0 +1,53 @@ +# Cancel Product Orders + +## Request + +{% tabs %} +{% tab title="REST" %} `POST [TRIGGER_ENDPOINT]/execute` + +**Body** + +```json +{ + "cancel_product_orders": { + "tx": { + "sender": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43746573743000000000000000", + "productIds": [0], + "nonce": "1" + }, + "signature": "0x", + "digest": "0x" + } +} +``` + +{% endtab %} +{% endtabs %} + +## Request Parameters + +See [Core > Executes > Cancel Product Orders](https://docs.nado.xyz/developer-resources/gateway/executes/cancel-product-orders#request-parameters) + +## Response + +#### Success + +```json +{ + "status": "success", + "signature": {signature}, + "request_type": "execute_cancel_product_orders" +} +``` + +#### Failure + +```json +{ + "status": "failure", + "signature": {signature}, + "error": "{error_msg}", + "error_code": {error_code}, + "request_type": "execute_cancel_product_orders" +} +``` diff --git a/docs/nado/developer-resources/api/trigger/executes/place-order.md b/docs/nado/developer-resources/api/trigger/executes/place-order.md new file mode 100644 index 0000000..c210fd3 --- /dev/null +++ b/docs/nado/developer-resources/api/trigger/executes/place-order.md @@ -0,0 +1,240 @@ +# Place Order + +## Rate limits + +* A max of 25 pending trigger orders per product per subaccount + +{% hint style="info" %} +See more details in [Trigger Service Limits](https://docs.nado.xyz/developer-resources/rate-limits#trigger-service-limits). +{% endhint %} + +## Request + +{% tabs %} +{% tab title="Price Trigger" %} `POST [TRIGGER_ENDPOINT]/execute` + +**Body** + +```json + +{ + "place_order": { + "product_id": 1, + "order": { + "sender": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43746573743000000000000000", + "priceX18": "9900000000000000000000", + "amount": "1000000000000000000", + "expiration": "4294967295", + "nonce": "1757062078359666688" + }, + "trigger": { + "price_trigger": { + "price_requirement": { + "oracle_price_below": "9900000000000000000000" + } + } + }, + "signature": "0x", + "id": 100 + } +} +``` + +{% endtab %} + +{% tab title="TWAP Trigger" %} `POST [TRIGGER_ENDPOINT]/execute` + +**Body** + +```json + +{ + "place_order": { + "product_id": 1, + "order": { + "sender": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43746573743000000000000000", + "priceX18": "9900000000000000000000", + "amount": "5000000000000000000", + "expiration": "4294967295", + "nonce": "1757062078359666688", + "appendix": "21474836490" + }, + "trigger": { + "time_trigger": { + "interval": 30, + "amounts": ["1000000000000000000", "1000000000000000000", "1000000000000000000", "1000000000000000000", "1000000000000000000"] + } + }, + "signature": "0x", + "id": 100 + } +} +``` + +{% endtab %} +{% endtabs %} + +## Request Parameters + +
ParameterTypeRequiredDescription
product_idnumberYesId of spot / perp product for which to place order. Use All products query to retrieve all valid product ids.
orderobjectYesOrder object, see Signing section for details on each order field.
signaturestringYesHex string representing hash of the signed order. See Signing section for more details.
triggerobjectYesTrigger criteria can be either:
Price-based: {"price_trigger": {"price_requirement": {"oracle_price_above": "{PRICE}"}}}
Time-based (TWAP): {"time_trigger": {"interval": {SECONDS}, "amounts": ["{AMOUNT1}", "{AMOUNT2}", ...]}}
digeststringNoHex string representing a hash of the order.
spot_leveragebooleanNoIndicates whether leverage should be used; when set to false , placing the order fails if the transaction causes a borrow on the subaccount. Defaults to true.
idnumberNoAn optional id that when provided is returned as part of Fill and OrderUpdate stream events when the order is triggered / executed. See gateway > place order and subscriptions for more details.
+ +{% hint style="info" %} +**Price Trigger Options:** + +* `oracle_price_above`: Order is triggered if the oracle price is above or at the indicated price. +* `oracle_price_below`: Order is triggered if the oracle price is below or at the indicated price. +* `last_price_above`: Order is triggered if the last trade price is above or at the indicated price. +* `last_price_below`: Order is triggered if the last trade price is below or at the indicated price. +* `mid_price_above`: Order is triggered if the mid book price is above or at the indicated price. +* `mid_price_below`: Order is triggered if the mid book price is below or at the indicated price. + +**TWAP (Time-Weighted Average Price) Trigger:** + +* `time_trigger`: Executes orders at regular intervals over time. + * `interval`: Time in seconds between each execution. + * `amounts`: Optional array specifying the exact amount for each execution. If not provided, the total order amount is split evenly across executions. +* **TWAP orders must use IOC (Immediate or Cancel) execution type only** +* **TWAP orders cannot be combined with isolated margin** +* Use the `list_twap_executions` query to track individual execution statuses. + {% endhint %} + +## Trigger Order Dependencies + +**Price triggers** can optionally depend on other orders being filled first. This allows creating complex order chains where one trigger only activates after another order executes. + +**Dependency Configuration:** + +```json +{ + "trigger": { + "price_trigger": { + "price_requirement": { + "oracle_price_above": "50000000000000000000000" + }, + "dependency": { + "digest": "0x1234567890abcdef1234567890abcdef12345678", + "on_partial_fill": false + } + } + } +} +``` + +**Parameters:** + +* `digest`: The order digest (32-byte hex string) that must be filled before this trigger activates +* `on_partial_fill`: + * `true`: Trigger activates when the dependency order is partially filled + * `false`: Trigger only activates when the dependency order is completely filled + +**Important Notes:** + +* Dependencies are **only supported for price triggers**, not TWAP orders +* Dependency orders can be regular orders or other trigger orders +* Circular dependencies are not allowed +* If a dependency order is cancelled, the dependent trigger order is also cancelled + +**Use Cases:** + +* **Take profit after stop loss**: Set a take profit order that only triggers after a stop loss executes +* **Scaling strategies**: Execute multiple orders in sequence based on fills +* **Complex exit strategies**: Chain multiple conditional exits together + +## Constructing Order Appendix + +{% hint style="warning" %} +**CRITICAL**: The order `appendix` field must be correctly configured for trigger orders. The appendix is a 128-bit integer sent as a string. +{% endhint %} + +### Using Python SDK (Recommended) + +```python +from nado_protocol.utils.appendix import build_appendix +from nado_protocol.utils.appendix import OrderAppendixTriggerType + +# Price trigger order +appendix = build_appendix( + order_type=0, # DEFAULT + trigger_type=OrderAppendixTriggerType.PRICE +) +# Result: "4096" + +# TWAP order: 5 executions, 1% slippage (must use IOC) +appendix = build_appendix( + order_type=1, # IOC (required for TWAP) + trigger_type=OrderAppendixTriggerType.TWAP, + twap_times=5, + twap_slippage_frac=0.01 +) +# Result: "21474841600" +``` + +### Manual Bit Manipulation + +```python +def build_trigger_appendix(trigger_type, order_type=0, twap_times=0, twap_slippage_frac=0.0): + appendix = 0 + + # Version (bits 0-7): Always 0 + appendix |= 0 + + # Order type (bits 9-10): 0=DEFAULT, 1=IOC (required for TWAP) + appendix |= (order_type & 0b11) << 9 + + # Trigger type (bits 12-13): 1=PRICE, 2=TWAP, 3=TWAP_CUSTOM_AMOUNTS + appendix |= (trigger_type & 0b11) << 12 + + # TWAP configuration in value field (bits 32-127) + if trigger_type == 2 or trigger_type == 3: # TWAP or TWAP_CUSTOM_AMOUNTS + slippage_x6 = int(twap_slippage_frac * 1_000_000) + value = (twap_times & ((1 << 32) - 1)) | ((slippage_x6 & ((1 << 32) - 1)) << 32) + appendix |= (value & ((1 << 96) - 1)) << 32 + + return str(appendix) + +# Examples: +price_appendix = build_trigger_appendix(1) # Price trigger +# Result: "4096" + +twap_appendix = build_trigger_appendix(2, 1, 5, 0.01) # TWAP: IOC, 5 times, 1% slippage +# Result: "21474841600" + +twap_custom = build_trigger_appendix(3, 1, 10, 0.005) # TWAP_CUSTOM_AMOUNTS: IOC, 10 times, 0.5% slippage +# Result: "42949678080" +``` + +{% hint style="info" %} +**Important constraints:** + +* TWAP orders **must** use IOC (order\_type=1) execution +* TWAP orders **cannot** be combined with isolated margin +* For complete appendix encoding specification, see [Order Appendix](https://docs.nado.xyz/developer-resources/api/order-appendix) documentation + {% endhint %} + +## Response + +#### Success + +```json +{ + "status": "success", + "signature": {signature}, + "data": { + "digest": {order digest} + }, + "request_type": "execute_place_order" + "id": 100 +} +``` + +#### Failure + +```json +{ + "status": "failure", + "signature": {signature}, + "error": "{error_msg}", + "error_code": {error_code}, + "request_type": "execute_place_order" +} +``` diff --git a/docs/nado/developer-resources/api/trigger/executes/place-orders.md b/docs/nado/developer-resources/api/trigger/executes/place-orders.md new file mode 100644 index 0000000..531fac4 --- /dev/null +++ b/docs/nado/developer-resources/api/trigger/executes/place-orders.md @@ -0,0 +1,179 @@ +# Place Orders + +Place multiple trigger orders in a single request. This is more efficient than placing orders individually and allows for better control over batch trigger order placement. + +## Rate limits + +* A max of 25 pending trigger orders per product per subaccount + +{% hint style="info" %} +See more details in [Trigger Service Limits](https://docs.nado.xyz/developer-resources/rate-limits#trigger-service-limits). +{% endhint %} + +{% hint style="warning" %} +**Important**: All orders in a batch must belong to the same subaccount. Orders with different senders will be rejected. +{% endhint %} + +## Request + +`POST [TRIGGER_ENDPOINT]/execute` + +### Body + +```json +{ + "place_orders": { + "orders": [ + { + "product_id": 2, + "order": { + "sender": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43746573743000000000000000", + "priceX18": "100000000000000000000000", + "amount": "1000000000000000000", + "expiration": "4294967295", + "nonce": "1757062078359666688", + "appendix": "4096" + }, + "trigger": { + "price_trigger": { + "price_requirement": { + "oracle_price_below": "100000000000000000000000" + } + } + }, + "signature": "0x...", + "id": 100 + }, + { + "product_id": 3, + "order": { + "sender": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43746573743000000000000000", + "priceX18": "3800000000000000000000", + "amount": "2000000000000000000", + "expiration": "4294967295", + "nonce": "1757062078359666689", + "appendix": "4096" + }, + "trigger": { + "price_trigger": { + "price_requirement": { + "oracle_price_above": "3800000000000000000000" + } + } + }, + "signature": "0x...", + "id": 101 + } + ], + "stop_on_failure": false + } +} +``` + +## Request Parameters + +
ParameterTypeRequiredDescription
ordersarrayYesArray of trigger order objects to place. Each order follows the same structure as Place Order. All orders must have the same sender.
orders[].product_idnumberYesId of spot / perp product for which to place order.
orders[].orderobjectYesOrder object (same structure as single order placement).
orders[].triggerobjectYesTrigger criteria - either price_trigger or time_trigger. See Place Order for details.
orders[].signaturestringYesHex string representing hash of the signed order.
orders[].digeststringNoHex string representing a hash of the order.
orders[].spot_leveragebooleanNoIndicates whether leverage should be used for this order. Defaults to true.
orders[].idnumberNoAn optional id returned in Fill and OrderUpdate events.
stop_on_failurebooleanNoIf true, stops processing remaining orders when the first order fails. Already successfully placed orders are NOT cancelled. Defaults to false.
+ +## Response + +```json +{ + "status": "success", + "data": { + "place_orders": [ + { + "digest": "0x1234...", + "error": null + }, + { + "digest": null, + "error": "Max trigger orders limit reached" + } + ] + } +} +``` + +### Response Fields + +
FieldDescription
digestOrder digest (32-byte hash) if successfully placed, null if failed.
errorError message if order failed, null if successful.
+ +## Behavior + +* **Partial Success**: By default, orders are processed independently. Some orders may succeed while others fail. +* **Stop on Failure**: Set `stop_on_failure: true` to stop processing remaining orders when the first order fails. Already successfully placed orders remain active. +* **Same Sender Required**: All orders in a batch must have the same sender. Mixed sender batches will be rejected with `BatchSenderMismatch` error. +* **Order Signing**: Each order must be individually signed using EIP712 (see [Signing](https://github.com/nadohq/nado-docs/blob/main/docs/developer-resources/api/gateway/executes/signing/README.md) for details). +* **Per-Order Limits**: The 25 pending trigger orders per product per subaccount limit applies to each order individually. + +## Use Cases + +* **Multi-Market Stop Losses**: Set stop loss triggers across multiple products simultaneously +* **Bracket Orders**: Place both take profit and stop loss triggers together +* **Conditional Exits**: Create multiple exit strategies across different products + +## Example + +Placing stop loss triggers for BTC and ETH perps: + +```javascript +const placeTriggerOrdersParams = { + orders: [ + { + product_id: 2, // BTC-PERP + order: { + sender: subaccount, + priceX18: toX18(95000), // Stop at $95k + amount: toX18(-0.1), // Sell 0.1 BTC + expiration: getExpiration(OrderType.DEFAULT), + nonce: genOrderNonce(), + appendix: buildAppendix({ + order_type: 0, + trigger_type: OrderAppendixTriggerType.PRICE + }) + }, + trigger: { + price_trigger: { + price_requirement: { + oracle_price_below: toX18(95000) + } + } + }, + signature: await signOrder(btcOrder), + id: 1 + }, + { + product_id: 3, // ETH-PERP + order: { + sender: subaccount, // Must be same sender + priceX18: toX18(3600), // Stop at $3.6k + amount: toX18(-1), // Sell 1 ETH + expiration: getExpiration(OrderType.DEFAULT), + nonce: genOrderNonce(), + appendix: buildAppendix({ + order_type: 0, + trigger_type: OrderAppendixTriggerType.PRICE + }) + }, + trigger: { + price_trigger: { + price_requirement: { + oracle_price_below: toX18(3600) + } + } + }, + signature: await signOrder(ethOrder), + id: 2 + } + ], + stop_on_failure: false +}; + +const response = await triggerClient.execute({ place_orders: placeTriggerOrdersParams }); +``` + +## See Also + +* [Place Order](https://docs.nado.xyz/developer-resources/api/trigger/executes/place-order) - Single trigger order placement +* [Cancel Orders](https://docs.nado.xyz/developer-resources/api/trigger/executes/cancel-orders) - Cancel multiple trigger orders +* [List Trigger Orders](https://docs.nado.xyz/developer-resources/api/trigger/queries/list-trigger-orders) - Query active trigger orders diff --git a/docs/nado/developer-resources/api/trigger/queries.md b/docs/nado/developer-resources/api/trigger/queries.md new file mode 100644 index 0000000..f3e69b2 --- /dev/null +++ b/docs/nado/developer-resources/api/trigger/queries.md @@ -0,0 +1,29 @@ +# Queries + +All queries go through the following endpoint; the exact details of the query are specified by the JSON payload. + +* **REST**: `POST [TRIGGER_ENDPOINT]/query` + +## API Response + +All `queries` return in the format: + +```json +{ + "status": "success" | "failure", + "data"?: {data}, + "error"?: "{error_msg}", + "error_code"?: {error_code}, + "request_type": "{request_type}" +} +``` + +## Available Queries: + +{% content-ref url="queries/list-trigger-orders" %} +[list-trigger-orders](https://docs.nado.xyz/developer-resources/api/trigger/queries/list-trigger-orders) +{% endcontent-ref %} + +{% content-ref url="queries/list-twap-executions" %} +[list-twap-executions](https://docs.nado.xyz/developer-resources/api/trigger/queries/list-twap-executions) +{% endcontent-ref %} diff --git a/docs/nado/developer-resources/api/trigger/queries/list-trigger-orders.md b/docs/nado/developer-resources/api/trigger/queries/list-trigger-orders.md new file mode 100644 index 0000000..fab1ec1 --- /dev/null +++ b/docs/nado/developer-resources/api/trigger/queries/list-trigger-orders.md @@ -0,0 +1,181 @@ +# List Trigger Orders + +## Request + +{% tabs %} +{% tab title="Basic query" %} `POST [TRIGGER_ENDPOINT]/query` + +**Body** + +```json + +{ + "type": "list_trigger_orders", + "tx": { + "sender": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43746573743000000000000000", + "recvTime": "1688768157050" + }, + "signature": "0x", + "product_ids": [1, 2], + "max_update_time": 1688768157, + "limit": 20 +} +``` + +{% endtab %} + +{% tab title="Fetch by digest" %} `POST [TRIGGER_ENDPOINT]/query` + +**Body** + +```json + +{ + "type": "list_trigger_orders", + "tx": { + "sender": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43746573743000000000000000", + "recvTime": "1688768157050" + }, + "signature": "0x", + "digests": ["0x5886d5eee7dc4879c7f8ed1222fdbbc0e3681a14c1e55d7859515898c7bd2038"], + "limit": 20 +} +``` + +{% endtab %} + +{% tab title="Filter by type and status" %} `POST [TRIGGER_ENDPOINT]/query` + +**Body** + +```json + +{ + "type": "list_trigger_orders", + "tx": { + "sender": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43746573743000000000000000", + "recvTime": "1688768157050" + }, + "signature": "0x", + "trigger_types": ["time_trigger"], + "status_types": ["twap_executing", "waiting_price"], + "product_ids": [1, 2, 3], + "limit": 50 +} +``` + +{% endtab %} + +{% tab title="Filter by reduce-only" %} `POST [TRIGGER_ENDPOINT]/query` + +**Body** + +```json + +{ + "type": "list_trigger_orders", + "tx": { + "sender": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43746573743000000000000000", + "recvTime": "1688768157050" + }, + "signature": "0x", + "reduce_only": true, + "product_ids": [1, 2], + "limit": 20 +} +``` + +{% endtab %} +{% endtabs %} + +## Request Parameters + +{% hint style="info" %} +**Note**: `max_update_time` It's the time that the trigger order last changed state. For example, if a trigger order is placed & pending, the update time = time of placement. If the trigger order is cancelled, then the update time = time of cancellation. +{% endhint %} + +
ParameterTypeRequiredDescription
txobjectYesList trigger orders transaction object. See Signing section for details on the transaction fields.
tx.senderstringYesHex string representing the subaccount's 32 bytes (address + subaccount name) of the tx sender.
tx.recvTimestringYesEncoded time in milliseconds after which the list trigger orders transaction will be ignored. cannot be more than 100 seconds from the time it is received by the server.
signaturestringYesSigned transaction. See Signing section for more details.
product_idsnumber[]NoIf provided, returns trigger orders for the specified products; otherwise, returns trigger orders for all products.
trigger_typesstring[]NoIf provided, filters by trigger type. Values: price_trigger, time_trigger.
status_typesstring[]NoIf provided, filters by order status. Values: cancelled, triggered, internal_error, triggering, waiting_price, waiting_dependency, twap_executing, twap_completed.
max_update_timenumberNoIf provided, returns all trigger orders that were last updated up to max_update_time. must be a unix epoch in seconds.
max_digeststringNoIf provided, returns all trigger orders up to the given order digest (exclusive). This can be used for pagination.
digestsstring[]NoIf provided, only returns the trigger orders for the associated digests. Note: all other filters are ignored when digests is provided.
reduce_onlybooleanNoIf provided, filters trigger orders by reduce-only flag. true returns only orders that can only decrease existing positions. If omitted, returns all orders regardless of reduce-only status.
limitnumberNoIf provided, returns the most recently updated trigger orders up to limit. defaults to 100. max limit is 500.
+ +## Signing + +{% hint style="info" %} +See more details and and examples in our [signing](https://docs.nado.xyz/developer-resources/api/gateway/signing) page. +{% endhint %} + +The solidity typed data struct that needs to be signed is: + +```solidity +struct ListTriggerOrders { + bytes32 sender; + uint64 recvTime; +} +``` + +`sender`: a `bytes32` sent as a hex string; includes the address and the subaccount identifier + +`recvTime`: the time in milliseconds (a `recv_time`) after which the transaction should be ignored by the trigger service. cannot be more than 100 seconds from the time it is received by the server. + +{% hint style="warning" %} +**Note**: for signing you should always use the data type specified in the solidity struct which might be different from the type sent in the request e.g: `recvTime` should be an `uint64` for **Signing** but should be sent as a `string` in the final payload. +{% endhint %} + +## Response + +#### Success + +```json +{ + "status": "success", + "data": { + "orders": [ + { + "order": { + "order": { + "sender": "0x7a5ec2748e9065794491a8d29dcf3f9edb8d7c43000000000000000000000000", + "priceX18": "1000000000000000000", + "amount": "1000000000000000000", + "expiration": "2000000000", + "nonce": "1", + }, + "signature": "0x...", + "product_id": 1, + "spot_leverage": true, + "trigger": { + "price_above": "1000000000000000000" + }, + "digest": "0x..." + }, + "status": "pending", + "placed_at": 1688768157000, + "updated_at": 1688768157050 + } + ] + }, + "request_type": "query_list_trigger_orders" +} +``` + +{% hint style="info" %} +**Note**: trigger orders can have the following statuses: + +* **cancelled**: trigger order was cancelled due to user request, order expiration, or account health issues. +* **triggered**: trigger criteria was met, and order was submitted for execution. +* **internal\_error**: an internal error occurred while processing the trigger order. +* **triggering**: trigger order is currently being processed for execution. +* **waiting\_price**: trigger order is waiting for price criteria to be met. +* **waiting\_dependency**: trigger order is waiting for a dependency order to be filled. +* **twap\_executing**: TWAP order is currently executing individual orders over time. +* **twap\_completed**: TWAP order has completed all scheduled executions. + {% endhint %} + +#### Failure + +```json +{ + "status": "failure", + "signature": {signature} + "error": "{error_msg}" + "error_code": {error_code} + "request_type": "query_list_trigger_orders" +} +``` diff --git a/docs/nado/developer-resources/api/trigger/queries/list-twap-executions.md b/docs/nado/developer-resources/api/trigger/queries/list-twap-executions.md new file mode 100644 index 0000000..df9bba4 --- /dev/null +++ b/docs/nado/developer-resources/api/trigger/queries/list-twap-executions.md @@ -0,0 +1,98 @@ +# List TWAP Executions + +## Request + +{% tabs %} +{% tab title="Get TWAP executions" %} `POST [TRIGGER_ENDPOINT]/query` + +**Body** + +```json + +{ + "type": "list_twap_executions", + "digest": "0x5886d5eee7dc4879c7f8ed1222fdbbc0e3681a14c1e55d7859515898c7bd2038" +} +``` + +{% endtab %} +{% endtabs %} + +## Request Parameters + +
ParameterTypeRequiredDescription
digeststringYesThe digest of the TWAP trigger order to get execution details for.
+ +## Response + +#### Success + +```json +{ + "status": "success", + "data": { + "executions": [ + { + "execution_id": 1, + "scheduled_time": 1688768157, + "status": "pending", + "updated_at": 1688768157050 + }, + { + "execution_id": 2, + "scheduled_time": 1688768187, + "status": { + "executed": { + "executed_time": 1688768187050, + "execute_response": { + "status": "success", + "data": { + "digest": "0x..." + }, + "id": 12345, + "request_type": "place_order" + } + } + }, + "updated_at": 1688768187050 + }, + { + "execution_id": 3, + "scheduled_time": 1688768217, + "status": { + "failed": "Insufficient balance" + }, + "updated_at": 1688768217050 + }, + { + "execution_id": 4, + "scheduled_time": 1688768247, + "status": { + "cancelled": "user_requested" + }, + "updated_at": 1688768247050 + } + ] + }, + "request_type": "query_list_twap_executions" +} +``` + +{% hint style="info" %} +**Note**: TWAP executions can have the following statuses: + +* **pending**: execution is scheduled but has not yet been attempted. +* **executed**: execution was successful, includes execution time and response details from the engine. +* **failed**: execution failed, includes error message. +* **cancelled**: execution was cancelled, includes cancellation reason (e.g., "user\_requested", "linked\_signer\_changed", "expired", "account\_health", "isolated\_subaccount\_closed", "dependent\_order\_cancelled"). + {% endhint %} + +#### Failure + +```json +{ + "status": "failure", + "error": "{error_msg}", + "error_code": {error_code}, + "request_type": "query_list_twap_executions" +} +``` diff --git a/docs/nado/developer-resources/api/v2.md b/docs/nado/developer-resources/api/v2.md new file mode 100644 index 0000000..30ae114 --- /dev/null +++ b/docs/nado/developer-resources/api/v2.md @@ -0,0 +1,41 @@ +# V2 + +Nado V2 API offers REST-based endpoints focused on two key functionalities: + +1. **Gateway Queries**: Access real-time market data, including trading pairs and book liquidity. +2. **Archive (indexer) Queries**: Query historical market data, including 24-hour statistics for all products and recent trades. + +## Endpoints + +### Testnet + +* **Gateway**: `https://gateway.test.nado.xyz/v2` +* **Archive (indexer)**: `https://archive.test.nado.xyz/v2` + +## Gateway + +{% content-ref url="v2/assets" %} +[assets](https://docs.nado.xyz/developer-resources/api/v2/assets) +{% endcontent-ref %} + +{% content-ref url="v2/pairs" %} +[pairs](https://docs.nado.xyz/developer-resources/api/v2/pairs) +{% endcontent-ref %} + +{% content-ref url="v2/orderbook" %} +[orderbook](https://docs.nado.xyz/developer-resources/api/v2/orderbook) +{% endcontent-ref %} + +## Archive (indexer) + +{% content-ref url="v2/tickers" %} +[tickers](https://docs.nado.xyz/developer-resources/api/v2/tickers) +{% endcontent-ref %} + +{% content-ref url="v2/contracts" %} +[contracts](https://docs.nado.xyz/developer-resources/api/v2/contracts) +{% endcontent-ref %} + +{% content-ref url="v2/trades" %} +[trades](https://docs.nado.xyz/developer-resources/api/v2/trades) +{% endcontent-ref %} diff --git a/docs/nado/developer-resources/api/v2/apr.md b/docs/nado/developer-resources/api/v2/apr.md new file mode 100644 index 0000000..c3d8cca --- /dev/null +++ b/docs/nado/developer-resources/api/v2/apr.md @@ -0,0 +1,35 @@ +# APR + +## Request + +{% tabs %} +{% tab title="Get pairs" %} **GET** `[GATEWAY_V2_ENDPOINT]/apr` +{% endtab %} +{% endtabs %} + +## Response + +```json +[ + { + "name": "USDT0", + "symbol": "USDT0", + "product_id": 0, + "deposit_apr": 6.32465621e-10, + "borrow_apr": 0.010050173557473174, + "tvl": 20001010125092.633 + }, + { + "name": "Wrapped BTC", + "symbol": "WBTC", + "product_id": 1, + "deposit_apr": 8.561123e-12, + "borrow_apr": 0.010050166480005895, + "tvl": 1045563178297771.2 + } +] +``` + +## Response Fields + +
Field nameTypeNullableDescription
product_idnumberNoInternal unique ID of spot / perp product
namestringNoAsset name (as represented internally in the exchange).
symbolstringNoAsset symbol (as represented internally in the exchange).
deposit_aprfloatNoThe current estimated APR for depositing or holding this asset. Note: This value should be multiplied by 100 to represent the percentage (%) form.
borrow_aprfloatNoThe current estimated APR for borrowing this asset. Note: This value should be multiplied by 100 to represent the percentage (%) form.
tvltvlNoTotal Value Locked (TVL) represents the current USDT0 value of this asset, calculated as the difference between deposits and borrows.
diff --git a/docs/nado/developer-resources/api/v2/assets.md b/docs/nado/developer-resources/api/v2/assets.md new file mode 100644 index 0000000..a17e34a --- /dev/null +++ b/docs/nado/developer-resources/api/v2/assets.md @@ -0,0 +1,60 @@ +# Assets + +## Rate limits + +* 1200 requests/min or 20 requests/sec per IP address. (**weight = 2**) + +{% hint style="info" %} +See more details in [API Rate limits](https://docs.nado.xyz/developer-resources/api/rate-limits) +{% endhint %} + +## Request + +{% tabs %} +{% tab title="Get assets" %} **GET** `[GATEWAY_V2_ENDPOINT]/assets` +{% endtab %} +{% endtabs %} + +## Response + +```json +[ + { + "product_id": 0, + "ticker_id": null, + "market_type": null, + "name": "USDT0", + "symbol": "USDT0", + "taker_fee": null, + "maker_fee": null, + "can_withdraw": true, + "can_deposit": true + }, + { + "product_id": 2, + "ticker_id": "BTC-PERP_USDT0", + "market_type": "perp", + "name": "Bitcoin Perp", + "symbol": "BTC-PERP", + "maker_fee": 0.0002, + "taker_fee": 0, + "can_withdraw": false, + "can_deposit": false + }, + { + "product_id": 1, + "ticker_id": "BTC_USDT0", + "market_type": "spot", + "name": "Bitcoin", + "symbol": "BTC", + "taker_fee": 0.0003, + "maker_fee": 0, + "can_withdraw": true, + "can_deposit": true + } +] +``` + +## Response Fields + +
Field nameTypeNullableDescription
product_idnumberNoInternal unique ID of spot / perp product
namestringNoAsset name (as represented internally in the exchange).
symbolstringNoAsset symbol (as represented internally in the exchange).
maker_feedecimalNoFees charged for placing a market-making order on the book.
taker_feedecimalNoFees applied when liquidity is removed from the book.
can_withdrawbooleanNoIndicates if asset withdrawal is allowed.
can_depositbooleanNoIndicates if asset deposit is allowed.
ticker_idstringYesIdentifier of a ticker with delimiter to separate base/quote. This is null for assets without market e.g: USDT0
market_typestringYesName of market type (spot or perp) of asset. This is null for assets without a market e.g: USDT0
diff --git a/docs/nado/developer-resources/api/v2/contracts.md b/docs/nado/developer-resources/api/v2/contracts.md new file mode 100644 index 0000000..e9c2121 --- /dev/null +++ b/docs/nado/developer-resources/api/v2/contracts.md @@ -0,0 +1,46 @@ +# Contracts + +## Request + +{% tabs %} +{% tab title="Get pairs" %} **GET** `[ARCHIVE_V2_ENDPOINT]/contracts?edge={true|false}` +{% endtab %} +{% endtabs %} + +## Request Parameters + +
ParameterTypeRequiredDescription
edgeboolNoWether to retrieve volume and OI metrics for all chains. When turned off, it only returns metrics for the current chain. Defaults to true.
+ +## Response + +{% hint style="info" %} +**Note**: the response is a map of `ticker_id` -> contract info object. +{% endhint %} + +```json +{ + "BTC-PERP_USDT0": { + "product_id": 1, + "ticker_id": "BTC-PERP_USDT0", + "base_currency": "BTC-PERP", + "quote_currency": "USDT0", + "last_price": 25744.0, + "base_volume": 794.154, + "quote_volume": 20475749.367766097, + "product_type": "perpetual", + "contract_price": 25830.738843799172, + "contract_price_currency": "USD", + "open_interest": 3059.325, + "open_interest_usd": 79024625.11330591, + "index_price": 25878.913320746455, + "mark_price": 25783.996946729356, + "funding_rate": -0.003664562348812546, + "next_funding_rate_timestamp": 1694379600, + "price_change_percent_24h": -0.6348599635253989 + } +} +``` + +## Response Fields + +
Field NameTypeNullableDescription
product_idu32NoUnique identifier for the product.
ticker_idstringNoIdentifier of a ticker with delimiter to separate base/target.
base_currencystringNoSymbol of the base asset.
quote_currencystringNoSymbol of the target asset.
last_pricedecimalNoLast transacted price of base currency based on given quote currency.
base_volumedecimalNo24-hours trading volume for the pair (unit in base)
quote_volumedecimalNo24-hours trading volume for the pair (unit in quote/target)
product_typestringNoName of product type.
contract_pricestringNoDescribes the price per contract.
contract_price_currencystringNoDescribes the currency which the contract is priced in.
open_interestdecimalNoThe current open interest for the perp contract.
open_interest_usddecimalNoThe value in USD of the current open interest.
index_pricedecimalNoLast calculated index price for underlying of contract
funding_ratedecimalNoCurrent 24hr funding rate. Can compute hourly funding rate dividing by 24.
next_funding_rate_timestampintegerNoTimestamp of the next funding rate change
price_change_percent_24hdecimalNo24-hours % price change of market pair
diff --git a/docs/nado/developer-resources/api/v2/orderbook.md b/docs/nado/developer-resources/api/v2/orderbook.md new file mode 100644 index 0000000..34635e4 --- /dev/null +++ b/docs/nado/developer-resources/api/v2/orderbook.md @@ -0,0 +1,54 @@ +# Orderbook + +## Rate limits + +* 2400 requests/min or 40 requests/sec per IP address. (**weight = 1**) + +{% hint style="info" %} +See more details in [API Rate limits](https://docs.nado.xyz/developer-resources/api/rate-limits) +{% endhint %} + +## Request + +{% tabs %} +{% tab title="Get orderbook" %} **GET** `[GATEWAY_V2_ENDPOINT]/orderbook?ticker_id={ticker_id}&depth={depth}` +{% endtab %} +{% endtabs %} + +## Request Parameters + +
ParameterTypeRequiredDescription
ticker_idstringYesIdentifier of a ticker with delimiter to separate base/target.
depthnumberYesNumber of price levels to retrieve.
+ +## Response + +```json +{ + "product_id": 1, + "ticker_id": "BTC-PERP_USDT0", + "bids": [ + [ + 116215.0, + 0.128 + ], + [ + 116214.0, + 0.172 + ] + ], + "asks": [ + [ + 116225.0, + 0.043 + ], + [ + 116226.0, + 0.172 + ] + ], + "timestamp": 1757913317944 +} +``` + +## Response Fields + +
Field NameTypeNullableDescription
product_idu32NoUnique identifier for the product.
ticker_idstringNoIdentifier of a ticker with delimiter to separate base/target.
bidsdecimal[]NoAn array containing 2 elements. The offer price (first element) and quantity for each bid order (second element).
asksdecimal[]NoAn array containing 2 elements. The ask price (first element) and quantity for each ask order (second element).
timestampintegerNoUnix timestamp in milliseconds for when the last updated time occurred.
diff --git a/docs/nado/developer-resources/api/v2/pairs.md b/docs/nado/developer-resources/api/v2/pairs.md new file mode 100644 index 0000000..f801287 --- /dev/null +++ b/docs/nado/developer-resources/api/v2/pairs.md @@ -0,0 +1,47 @@ +# Pairs + +## Request + +{% tabs %} +{% tab title="Get pairs" %} **GET** `[GATEWAY_V2_ENDPOINT]/pairs?market={spot|perp}` +{% endtab %} +{% endtabs %} + +## Request Parameters + +
ParameterTypeRequiredDescription
marketstringNoIndicates the corresponding market to fetch trading pairs for. Allowed values are: spotand perp. When no market param is provided, it returns all available pairs.
+ +## Response + +```json +[ + { + "product_id": 1, + "ticker_id": "BTC-PERP_USDT0", + "base": "BTC-PERP", + "quote": "USDT0" + }, + { + "product_id": 2, + "ticker_id": "ETH-PERP_USDT0", + "base": "ETH-PERP", + "quote": "USDT0" + }, + { + "product_id": 3, + "ticker_id": "BTC_USDT0", + "base": "BTC", + "quote": "USDT0" + }, + { + "product_id": 4, + "ticker_id": "ETH_USDT0", + "base": "ETH", + "quote": "USDT0" + } +] +``` + +## Response Fields + +
Field nameTypeNullableDescription
product_idu32NoUnique identifier for the product.
ticker_idstringNoIdentifier of a ticker with delimiter to separate base/target.
basestringNoSymbol of the base asset.
quotestringNoSymbol of the target asset.
diff --git a/docs/nado/developer-resources/api/v2/tickers.md b/docs/nado/developer-resources/api/v2/tickers.md new file mode 100644 index 0000000..38c2506 --- /dev/null +++ b/docs/nado/developer-resources/api/v2/tickers.md @@ -0,0 +1,37 @@ +# Tickers + +## Request + +{% tabs %} +{% tab title="Get pairs" %} **GET** `[ARCHIVE_V2_ENDPOINT]/tickers?market={spot|perp}&edge={true|false}` +{% endtab %} +{% endtabs %} + +## Request Parameters + +
ParameterTypeRequiredDescription
marketstringNoIndicates the corresponding market to fetch trading tickers info for. Allowed values are: spotand perp. When no market param is provided, it returns all available tickers.
edgeboolNoWhether to retrieve volume metrics for all chains. When turned off, it only returns metrics for the current chain. Defaults to true.
+ +## Response + +{% hint style="info" %} +**Note**: the response is a map of `ticker_id` -> ticker info object. +{% endhint %} + +```json +{ + "BTC-PERP_USDT0": { + "product_id": 1, + "ticker_id": "BTC-PERP_USDT0", + "base_currency": "BTC", + "quote_currency": "USDT0", + "last_price": 25728.0, + "base_volume": 552.048, + "quote_volume": 14238632.207250029, + "price_change_percent_24h": -0.6348599635253989 + } +} +``` + +## Response Fields + +
Field NameTypeNullableDescription
product_idu32NoUnique identifier for the product.
ticker_idstringNoIdentifier of a ticker with delimiter to separate base/target.
base_currencystringNoSymbol of the base asset.
quote_currencystringNoSymbol of the target asset.
last_pricedecimalNoLast transacted price of base currency based on given quote currency.
base_volumedecimalNo24-hours trading volume for the pair (unit in base)
quote_volumedecimalNo24-hours trading volume for the pair (unit in quote/target)
price_change_percent_24hdecimalNo24-hours % price change of market pair
diff --git a/docs/nado/developer-resources/api/v2/trades.md b/docs/nado/developer-resources/api/v2/trades.md new file mode 100644 index 0000000..cee2e43 --- /dev/null +++ b/docs/nado/developer-resources/api/v2/trades.md @@ -0,0 +1,43 @@ +# Trades + +## Request + +{% tabs %} +{% tab title="Get pairs" %} **GET** `[ARCHIVE_V2_ENDPOINT]/trades?ticker_id=BTC-PERP_USDT0&limit=10&max_trade_id=1000000` +{% endtab %} +{% endtabs %} + +## Request Parameters + +
ParameterTypeRequiredDescription
ticker_idstringYesIdentifier of a ticker with delimiter to separate base/target.
limitintegerNoNumber of historical trades to retrieve. Defaults to 100. Max of 500.
max_trade_idintegerNoMax trade id to include in the result. Use for pagination.
+ +## Response + +```json +[ + { + "product_id": 1, + "ticker_id": "BTC-PERP_USDT0", + "trade_id": 6351, + "price": 112029.5896, + "base_filled": -0.388, + "quote_filled": 43467.4807648, + "timestamp": 1757335618, + "trade_type": "sell" + }, + { + "product_id": 1, + "ticker_id": "BTC-PERP_USDT0", + "trade_id": 6350, + "price": 112032.58899999999, + "base_filled": -0.179, + "quote_filled": 20053.833431, + "timestamp": 1757335618, + "trade_type": "sell" + } +] +``` + +## Response Fields + +
Field NameTypeNullableDescription
product_idu32NoUnique identifier for the product.
ticker_idstringNoIdentifier of a ticker with delimiter to separate base/target.
trade_idintegerNoA unique ID associated with the trade for the currency pair transaction.
pricedecimalNoTrade price of base asset in target currency.
base_filleddecimalNoAmount of base volume filled in trade.
quote_filleddecimalNoAmount of quote/target volume filled in trade.
timestampintegerNoUnix timestamp in seconds for when the transaction occurred.
trade_typestringNoIndicates the type of the transaction that was completed ("buy" or "sell").
diff --git a/docs/nado/developer-resources/api/withdrawing-on-chain.md b/docs/nado/developer-resources/api/withdrawing-on-chain.md new file mode 100644 index 0000000..4b8dcd3 --- /dev/null +++ b/docs/nado/developer-resources/api/withdrawing-on-chain.md @@ -0,0 +1,37 @@ +# Withdrawing (on-chain) + +You can withdraw collateral from Nado directly on-chain, by submitting a slow-mode transaction via the `Endpoint` contract (see [Contracts](https://docs.nado.xyz/developer-resources/api/broken-reference) for addresses). + +{% hint style="info" %} +**Note**: + +* This is an alternative to withdrawing collateral via our off-chain sequencer. See [Withdraw Collateral](https://docs.nado.xyz/developer-resources/api/gateway/executes/withdraw-collateral) for more details. +* Slow mode transactions have a 1 USDT0 fee; as such, an approval of 1 USDT0 is required for the slow mode withdrawal to succeed. + {% endhint %} + +## Steps + +1. Assemble the bytes needed for a withdraw collateral transaction by encoding the following struct alongside the transaction type `2`: + +```solidity +struct WithdrawCollateral { + bytes32 sender; + uint32 productId; + uint128 amount; + uint64 nonce; +} +``` + +2. Submit the transaction via `submitSlowModeTransaction` on our `Endpoint` contract. + +### Example + +```solidity +function withdrawNadoCollateral(address nadoEndpoint, bytes32 sender, uint32 productId, uint128 amount) internal { + WithdrawCollateral memory withdrawal = new WithdrawCollateral(sender, productId, amount, 0); + bytes memory tx = abi.encodePacked(2, abi.encode(withdrawal)); + IEndpoint(nadoEndpoint).submitSlowModeTransaction(tx); +} +``` + +Once the transaction is confirmed, it may take a few seconds for it to make its way into the Nado offchain sequencer and for the withdrawal to be processed. diff --git a/docs/nado/developer-resources/typescript-sdk.md b/docs/nado/developer-resources/typescript-sdk.md new file mode 100644 index 0000000..dca9437 --- /dev/null +++ b/docs/nado/developer-resources/typescript-sdk.md @@ -0,0 +1,9 @@ +# Typescript SDK + +The TypeScript SDK contains the following packages: + +* `@nadohq/client`: top level client package for the majority of use cases. +* `@nadohq/engine-client`: interactions with the offchain sequencer. +* `@nadohq/indexer-client`: query historical data. e.g: candlesticks, events, etc. +* `@nadohq/trigger-client`: interaction with our trigger service (tp/sl/twap orders). +* `@nadohq/shared`: utility library; e.g. numbers, dates. diff --git a/docs/nado/developer-resources/typescript-sdk/getting-started.md b/docs/nado/developer-resources/typescript-sdk/getting-started.md new file mode 100644 index 0000000..fa0c9a5 --- /dev/null +++ b/docs/nado/developer-resources/typescript-sdk/getting-started.md @@ -0,0 +1,38 @@ +# Getting Started + +## Installation + +### Prerequisites + +* [Node](https://nodejs.org/en/download/) +* [Yarn](https://classic.yarnpkg.com/lang/en/docs/install/#mac-stable), if using the `yarn` dependency manager +* [Viem](https://viem.sh/) +* [Bignumber.js](https://github.com/MikeMcl/bignumber.js/) + +{% hint style="info" %} +Version 1.x.x of the SDK now uses `viem` instead of `ethers` to represent wallets and RPC connections. +{% endhint %} + +### Install the packages + +The Nado SDK packages are hosted on NPM. + +#### Run the following: + +{% tabs %} +{% tab title="yarn" %} + +```sh +yarn add @nadohq/client viem bignumber.js +``` + +{% endtab %} + +{% tab title="npm" %} + +```sh +npm install @nadohq/client viem bignumber.js +``` + +{% endtab %} +{% endtabs %} diff --git a/docs/nado/developer-resources/typescript-sdk/how-to.md b/docs/nado/developer-resources/typescript-sdk/how-to.md new file mode 100644 index 0000000..b60128a --- /dev/null +++ b/docs/nado/developer-resources/typescript-sdk/how-to.md @@ -0,0 +1,25 @@ +# How To + +{% content-ref url="how-to/create-a-nado-client" %} +[create-a-nado-client](https://docs.nado.xyz/developer-resources/typescript-sdk/how-to/create-a-nado-client) +{% endcontent-ref %} + +{% content-ref url="how-to/useful-common-functions" %} +[useful-common-functions](https://docs.nado.xyz/developer-resources/typescript-sdk/how-to/useful-common-functions) +{% endcontent-ref %} + +{% content-ref url="how-to/query-markets-and-products" %} +[query-markets-and-products](https://docs.nado.xyz/developer-resources/typescript-sdk/how-to/query-markets-and-products) +{% endcontent-ref %} + +{% content-ref url="how-to/deposit-funds" %} +[deposit-funds](https://docs.nado.xyz/developer-resources/typescript-sdk/how-to/deposit-funds) +{% endcontent-ref %} + +{% content-ref url="how-to/withdraw-funds" %} +[withdraw-funds](https://docs.nado.xyz/developer-resources/typescript-sdk/how-to/withdraw-funds) +{% endcontent-ref %} + +{% content-ref url="how-to/manage-orders" %} +[manage-orders](https://docs.nado.xyz/developer-resources/typescript-sdk/how-to/manage-orders) +{% endcontent-ref %} diff --git a/docs/nado/developer-resources/typescript-sdk/how-to/create-a-nado-client.md b/docs/nado/developer-resources/typescript-sdk/how-to/create-a-nado-client.md new file mode 100644 index 0000000..fd32ef8 --- /dev/null +++ b/docs/nado/developer-resources/typescript-sdk/how-to/create-a-nado-client.md @@ -0,0 +1,84 @@ +# Create a Nado client + +## The `NadoClient` Object + +To start using the SDK, you need an initialized `NadoClient` from the `client` package. The `NadoClient` is the main entrypoint to common APIs. + +## Create a `NadoClient` object + +The `NadoClient` class is rarely instantiated directly. Instead, call the `createNadoClient` function from the `client` package and provide the relevant parameters. + +### Import the dependencies + +```typescript +import { createNadoClient } from '@nado-protocol/client'; +import { createPublicClient, createWalletClient, http } from 'viem'; +import { privateKeyToAccount } from 'viem/accounts'; +import { inkSepolia } from 'viem/chains'; +``` + +### Create a `WalletClient` and `PublicClient` + +The `WalletClient` is optional and required only for write operations + +```typescript +const walletClient = createWalletClient({ + account: privateKeyToAccount('0x...'), + chain: inkSepolia, + transport: http(), +}); + +const publicClient = createPublicClient({ + chain: inkSepolia, + transport: http(), +}); + +``` + +### Call `createNadoClient` + +The first argument is the `ChainEnv`associated with the client. Each client can talk to one chain that Nado is deployed on. For example, use `inkTestnet`to connect to Nado's instance on Ink Sepolia. + +```typescript +const nadoClient = createNadoClient('inkTestnet', { + walletClient, + publicClient, +}); +``` + +## Full example + +```typescript +import { createNadoClient } from '@nado-protocol/client'; +import { createPublicClient, createWalletClient, http } from 'viem'; +import { privateKeyToAccount } from 'viem/accounts'; +import { inkSepolia } from 'viem/chains'; + +function main() { + const walletClient = createWalletClient({ + account: privateKeyToAccount('0x...'), + chain: inkSepolia, + transport: http(), + }); + + const publicClient = createPublicClient({ + chain: inkSepolia, + transport: http(), + }); + + const nadoClient = createNadoClient('inkTestnet', { + walletClient, + publicClient, + }); +} + +main(); +``` + +Run the script, this example uses `ts-node`: + +```sh +ts-node test.ts +``` + +If no errors are thrown, you're good to go! diff --git a/docs/nado/developer-resources/typescript-sdk/how-to/deposit-funds.md b/docs/nado/developer-resources/typescript-sdk/how-to/deposit-funds.md new file mode 100644 index 0000000..d4f5a0a --- /dev/null +++ b/docs/nado/developer-resources/typescript-sdk/how-to/deposit-funds.md @@ -0,0 +1,161 @@ +# Deposit Funds + +## Import the functions + +We'll use a few of the [common functions](https://docs.nado.xyz/developer-resources/typescript-sdk/how-to/useful-common-functions), assuming that they are in a `common.ts` file. The withdraw step requires a nonce as the transaction is executed against the off-chain engine. + +```typescript +import { toFixedPoint } from '@nado-protocol/shared'; +// Change the import source as needed +import { getNadoClient, prettyPrintJson } from './common'; +``` + +## Mint a mock ERC20 token for testing + +Grab a client object and mint mock tokens for the relevant product. This is *only* available on testnets for obvious reasons. + +Minting is on-chain, so we wait for the transaction confirmation for chain state to propagate. + +```typescript +const nadoClient = await getNadoClient(); +const { walletClient, publicClient } = nadoClient.context + +// If you have access to `walletClient`, you can call `walletClient.account.address` +// directly instead of reaching into `nadoClient.context` +const address = walletClient!.account.address; +const subaccountName = 'default'; +// 10 USDT0 (6 decimals) +const depositAmount = toFixedPoint(10, 6); + +const mintTxHash = await nadoClient.spot._mintMockERC20({ + amount: depositAmount, + productId: 0, +}); + +await publicClient.waitForTransactionReceipt({ + hash: mintTxHash, +}); + +``` + +## Make a deposit + +First, call `approveAllowance` to approve the deposit amount. + +This is also an on-chain transaction with a confirmation hash. + +```typescript +const approveTxHash = await nadoClient.spot.approveAllowance({ + amount: depositAmount, + productId: 0, +}); + +await publicClient.waitForTransactionReceipt({ + hash: approveTxHash, +}); +``` + +Now we can deposit the tokens. This transaction is on-chain. + +```typescript +const depositTxHash = await nadoClient.spot.deposit({ + // Your choice of name for the subaccount, this subaccount will be credited with the deposit balance + subaccountName: 'default', + amount: depositAmount, + productId: 0, +}); + +await publicClient.waitForTransactionReceipt({ + hash: depositTxHash, +}); +``` + +{% hint style="info" %} +**Subaccounts** + +* A subaccount is an *independent* trading account within Nado, allowing traders to manage risk across independent subaccounts +* Subaccounts are associated by a string `name` (max 12 char.) and the owner wallet address + {% endhint %} + +After this, we inject a short delay while the offchain sequencer picks up the transaction and credits the account. + +```typescript +await new Promise((resolve) => setTimeout(resolve, 10000)); +``` + +## Query Subaccount balance + +Now, call the `getSubaccountEngineSummary` function to retrieve an overview of your subaccount, including balances. + +```typescript +const subaccountData = + await nadoClient.subaccount.getSubaccountSummary({ + subaccountOwner: address, + subaccountName, + }); +prettyPrintJson('Subaccount Data After Deposit', subaccountData); +``` + +You should see that your balance associated with `productId` of `0` now reflects your deposit amount. + +## Full example + +```typescript +import { toFixedPoint } from '@nado-protocol/shared'; +import { getNadoClient, prettyPrintJson } from './common'; + +async function main() { + const nadoClient = getNadoClient(); + const { walletClient, publicClient } = nadoClient.context; + + // If you have access to `walletClient`, you can call `walletClient.account.address` + // directly instead of reaching into `nadoClient.context` + const address = walletClient!.account.address; + const subaccountName = 'default'; + // 10 USDT0 (6 decimals) + const depositAmount = toFixedPoint(10, 6); + + // TESTNET ONLY - Mint yourself some tokens + const mintTxHash = await nadoClient.spot._mintMockERC20({ + amount: depositAmount, + productId: 0, + }); + // Mint goes on-chain, so wait for confirmation + await publicClient.waitForTransactionReceipt({ + hash: mintTxHash, + }); + + // Deposits require approval on the ERC20 token, this is on-chain as well + const approveTxHash = await nadoClient.spot.approveAllowance({ + amount: depositAmount, + productId: 0, + }); + + await publicClient.waitForTransactionReceipt({ + hash: approveTxHash, + }); + + // Now execute the deposit, which goes on-chain + const depositTxHash = await nadoClient.spot.deposit({ + // Your choice of name for the subaccount, this subaccount will be credited with the deposit balance + subaccountName: 'default', + amount: depositAmount, + productId: 0, + }); + + await publicClient.waitForTransactionReceipt({ + hash: depositTxHash, + }); + + await new Promise((resolve) => setTimeout(resolve, 10000)); + + const subaccountData = + await nadoClient.subaccount.getSubaccountSummary({ + subaccountOwner: address, + subaccountName, + }); + prettyPrintJson('Subaccount Data After Deposit', subaccountData); +} + +main(); +``` diff --git a/docs/nado/developer-resources/typescript-sdk/how-to/manage-orders.md b/docs/nado/developer-resources/typescript-sdk/how-to/manage-orders.md new file mode 100644 index 0000000..8bd1c67 --- /dev/null +++ b/docs/nado/developer-resources/typescript-sdk/how-to/manage-orders.md @@ -0,0 +1,275 @@ +# Manage Orders + +This guide shows you how to: + +* Create an order. +* Place an order. +* Cancel an order. +* Query orders by subaccount. + +If you inspect the underlying types for these transactions, you'll notice that a `nonce` field is required. This is a unique integer in ascending order. Our off-chain engine has a `nonce` query to return the latest nonce for a given subaccount. All this is abstracted away within the SDK, so you do not need to manually use this query. + +## Import the functions + +```typescript +import { getNadoClient, prettyPrintJson } from "./common"; +import { nowInSeconds, toFixedPoint, packOrderAppendix } from "@nado-protocol/shared"; +``` + +## Scaffold Your Subaccount + +To place orders, we need a subaccount with funds. We need to perform the [deposit funds](https://docs.nado.xyz/developer-resources/typescript-sdk/how-to/deposit-funds) step as before, this time with 1000 USDT0. + +```typescript +const nadoClient = getNadoClient(); +const { walletClient, publicClient } = nadoClient.context; + +const address = walletClient!.account.address; +const subaccountName = 'default'; +const depositAmount = toFixedPoint(1000, 6); + +const mintTxHash = await nadoClient.spot._mintMockERC20({ + amount: depositAmount, + productId: 0, +}); +await publicClient.waitForTransactionReceipt({ + hash: mintTxHash, +}); + +const approveTxHash = await nadoClient.spot.approveAllowance({ + amount: depositAmount, + productId: 0, +}); +await publicClient.waitForTransactionReceipt({ + hash: approveTxHash, +}); + +const depositTxHash = await nadoClient.spot.deposit({ + subaccountName: 'default', + amount: depositAmount, + productId: 0, +}); +await publicClient.waitForTransactionReceipt({ + hash: depositTxHash, +}); + +await new Promise((resolve) => setTimeout(resolve, 10000)); +``` + +## Create an order + +Placing an order requires a number of parameters, represented by the `PlaceOrderParams['order']` type. + +In the example below: + +* The order `appendix` indicates order execution type and other flags. Please refer to [Order Appendix](https://docs.nado.xyz/developer-resources/api/order-appendix) for more details. +* The order `expiration` time is given by calling the `nowInSeconds` function from the `utils` package and adding 60 seconds. This means the order will expire 60 seconds from now. +* The `price` field is set at `80000` - a low value (at the time of writing) to prevent execution. This enables us to cancel the order later on without it being instantly filled. Please adjust this price accordingly. +* The `amount` field is set at `10**16` - this is the amount to buy/sell. A positive value is to buy, negative is to sell. + * Amount is normalized to 18 decimal places, which is what `toFixedPoint` does by default. + * **NOTE**: Min limit order size for `BTC` is `10**16` and for `ETH` is `10**17`. Orders below these sizes will fail to be placed. + +```typescript +const orderParams: PlaceOrderParams['order'] = { + // When using the `placeOrder` call, the `subaccountOwner` defaults to the + // address of the `walletClient` + subaccountName: 'default', + expiration: nowInSeconds() + 60, + appendix: packOrderAppendix({ + orderExecutionType: 'ioc', + }), + price: 80000, + // Setting order amount to 10**16 + amount: toFixedPoint(0.01, 18), +}; +``` + +## Place the order + +Use the order parameters to place the order with the `placeOrder` function. + +```typescript +const placeOrderResult = await nadoClient.market.placeOrder({ + order: orderParams, + productId: 2, + // Used for spot orders to enable/disable borrowing + spotLeverage: undefined, +}); + +prettyPrintJson("Place Order Result", placeOrderResult); +``` + +## Alternative order placement + +Alternatively, you can manually use `payloadBuilder` to manually generate the place order payload. This may be useful in cases where you want to build the `tx` separately from sending the execute API call. + +```typescript +// Use one of the following to generate a payload +nadoClient.context.engineClient.payloadBuilder.buildPlaceOrderPayload(...) +nadoClient.context.engineClient.payloadBuilder.buildPlaceOrderPayloadSync(...) + +// Then execute +nadoClient.context.engineClient.execute('place_order', placeOrderPayload.payload); +``` + +## Order digest + +You can optionally generate the order digest, which can then be used to further manage the order e.g: cancelling the order. The order digest is also returned upon executing the `placeOrder` transaction. + +```typescript +import { getOrderDigest } from "@nado-protocol/shared"; + +const productId = 2; +const orderParams: EIP712OrderParams = { + subaccountOwner: address, + subaccountName, + expiration: nowInseconds() + 60, + appendix: packOrderAppendix({ + orderExecutionType: 'post_only', + }), + price: 80000, + // Setting order amount to 10**16 + amount: toFixedPoint(0.01, 18), + nonce: getOrderNonce(), +}; + +// Optional: generate a digest ahead of time so that you can manage the order, alternatively, the digest +// will be returned via placeOrderResult.data.digest +const orderDigest = getOrderDigest({ + chainId: walletClient!.chain.id, + order: orderParams, + productId, +}); +``` + +## Query orders on the subaccount + +Now we can query the subaccount for open orders with the `getOpenSubaccountOrders` function. + +```typescript +const openOrders = await nadoClient.market.getOpenSubaccountOrders({ + subaccountOwner: address, + subaccountName, + productId: 2, + }); + +prettyPrintJson('Subaccount Open Orders', openOrders); +``` + +## Cancel order + +Cancel the order using the digest of the placed order. You can cancel multiple orders at once. + +```typescript +const cancelOrderResult = await nadoClient.market.cancelOrders({ + digests: [placeOrderResult.data.digest], + productIds: [2], + subaccountName: 'default', +}); + +prettyPrintJson('Cancel Order Result', cancelOrderResult); +``` + +### Query Orders to Verify Cancellation + +Run [query orders on the subaccount](#query-orders-on-the-subaccount) again to make sure the cancellation was successful. + +## Clean up + +Finally, clean up by withdrawing the same amount as you have deposited, minus the 1 USDT0 withdrawal fee. + +```typescript +await nadoClient.spot.withdraw({ + productId: 0, + amount: depositAmount - toFixedPoint(1, 6), + subaccountName, +}); +``` + +## Full example + +```typescript +import { PlaceOrderParams } from '@nado-protocol/client'; +import { nowInSeconds, toFixedPoint, packOrderAppendix } from '@nado-protocol/shared'; +import { getNadoClient, prettyPrintJson } from './common'; + +async function main() { + const nadoClient = getNadoClient(); + const { walletClient, publicClient } = nadoClient.context; + + const address = walletClient!.account.address; + const subaccountName = 'default'; + const depositAmount = toFixedPoint(1000, 6); + + const mintTxHash = await nadoClient.spot._mintMockERC20({ + amount: depositAmount, + productId: 0, + }); + await publicClient.waitForTransactionReceipt({ + hash: mintTxHash, + }); + + const approveTxHash = await nadoClient.spot.approveAllowance({ + amount: depositAmount, + productId: 0, + }); + await publicClient.waitForTransactionReceipt({ + hash: approveTxHash, + }); + + const depositTxHash = await nadoClient.spot.deposit({ + subaccountName: 'default', + amount: depositAmount, + productId: 0, + }); + await publicClient.waitForTransactionReceipt({ + hash: depositTxHash, + }); + + await new Promise((resolve) => setTimeout(resolve, 10000)); + + const orderParams: PlaceOrderParams['order'] = { + subaccountName, + expiration: nowInSeconds() + 60, + appendix: packOrderAppendix({ + orderExecutionType: 'post_only', + }), + price: 80000, + // Setting order amount to 10**16 + amount: toFixedPoint(0.01, 18), + }; + + const placeOrderResult = await nadoClient.market.placeOrder({ + order: orderParams, + productId: 2, + // Used for spot orders to enable/disable borrowing + spotLeverage: undefined, + }); + + prettyPrintJson('Place Order Result', placeOrderResult); + + const openOrders = await nadoClient.market.getOpenSubaccountOrders({ + subaccountOwner: address, + subaccountName, + productId: 2, + }); + + prettyPrintJson('Subaccount Open Orders', openOrders); + + const cancelOrderResult = await nadoClient.market.cancelOrders({ + digests: [placeOrderResult.data.digest], + productIds: [2], + subaccountName: 'default', + }); + + prettyPrintJson('Cancel Order Result', cancelOrderResult); + + await nadoClient.spot.withdraw({ + productId: 0, + amount: depositAmount - toFixedPoint(1, 6), + subaccountName, + }); +} + +main(); +``` diff --git a/docs/nado/developer-resources/typescript-sdk/how-to/query-markets-and-products.md b/docs/nado/developer-resources/typescript-sdk/how-to/query-markets-and-products.md new file mode 100644 index 0000000..1e6fe8e --- /dev/null +++ b/docs/nado/developer-resources/typescript-sdk/how-to/query-markets-and-products.md @@ -0,0 +1,66 @@ +# Query Markets & Products + +In this section, we'll be going over fetching: + +* State and config for all markets & products. +* Latest market price for one product. +* Market liquidity for one product (i.e. amount of liquidity at each price tick). + +For all available queries, consult the [API reference](https://nadohq.github.io/nado-typescript-sdk/). + +## All Markets Query + +The `getAllEngineMarkets` function returns the state of all markets from our backend API, which reflects the state of the off-chain matching engine. + +```typescript +// Fetches state from offchain sequencer +const allMarkets = await nadoClient.market.getAllMarkets(); +``` + +## Latest market price + +The `getLatestMarketPrice` function returns the market price data of a single product given by its product id. + +```typescript +const latestMarketPrice = await nadoClient.market.getLatestMarketPrice({ + productId: 1, +}); +``` + +### Market liquidity + +The `getMarketLiquidity` function returns the available liquidity at each price tick. The number of price levels for each side of the book is given by `depth`. For example, a depth of `2` will retrieve 2 levels of bids and 2 levels of asks. Price levels are separated by the `priceIncrement` of the market, given by the `getAllMarkets` query. + +```typescript +const marketLiquidity = await nadoClient.market.getMarketLiquidity({ + productId: 1, + depth: 2, +}); +``` + +## Full example + +```typescript +import {getNadoClient, prettyPrintJson} from './common'; + +async function main() { + const nadoClient = await getNadoClient(); + + const allMarkets = await nadoClient.market.getAllMarkets(); + prettyPrintJson('All Markets', allMarkets); + + const latestMarketPrice = await nadoClient.market.getLatestMarketPrice({ + productId: 1, + }); + prettyPrintJson('Latest Market Price (Product ID 1)', latestMarketPrice); + + const marketLiquidity = await nadoClient.market.getMarketLiquidity({ + productId: 1, + // Per side of the book + depth: 2, + }); + prettyPrintJson('Market Liquidity (Product ID 1)', marketLiquidity); +} + +main(); +``` diff --git a/docs/nado/developer-resources/typescript-sdk/how-to/useful-common-functions.md b/docs/nado/developer-resources/typescript-sdk/how-to/useful-common-functions.md new file mode 100644 index 0000000..19e1208 --- /dev/null +++ b/docs/nado/developer-resources/typescript-sdk/how-to/useful-common-functions.md @@ -0,0 +1,50 @@ +# Useful Common Functions + +These are some utility functions used throughout the guide. You may want to include them in your project. + +* A `getNadoClient` function that returns a ready-made client object connected to Ink Sepolia. +* A `prettyPrintJson` function that logs readable JSON. + +{% hint style="info" %} +**Note** + +* Make sure your account has funds for gas on the relevant network. +* Make sure to replace your private key of choice in the function `getNadoClient` below + {% endhint %} + +```typescript +import { createNadoClient } from '@nado-protocol/client'; +import { toPrintableObject } from '@nado-protocol/shared'; +import { createPublicClient, createWalletClient, http } from 'viem'; +import { privateKeyToAccount } from 'viem/accounts'; +import { inkSepolia } from 'viem/chains'; + +/** + * Creates a Nado client for example scripts + */ +export function getNadoClient() { + const walletClient = createWalletClient({ + account: privateKeyToAccount('0x...'), + chain: inkSepolia, + transport: http(), + }); + + const publicClient = createPublicClient({ + chain: inkSepolia, + transport: http(), + }); + + return createNadoClient('inkTestnet', { + walletClient, + publicClient, + }); +} + +/** + * Util for pretty printing JSON + */ +export function prettyPrintJson(label: string, json: unknown) { + console.log(label); + console.log(JSON.stringify(toPrintableObject(json), null, 2)); +} +``` diff --git a/docs/nado/developer-resources/typescript-sdk/how-to/withdraw-funds.md b/docs/nado/developer-resources/typescript-sdk/how-to/withdraw-funds.md new file mode 100644 index 0000000..26c4d13 --- /dev/null +++ b/docs/nado/developer-resources/typescript-sdk/how-to/withdraw-funds.md @@ -0,0 +1,104 @@ +# Withdraw Funds + +This section assumes you have followed the instructions and run the code in the [deposit funds example](https://docs.nado.xyz/developer-resources/typescript-sdk/how-to/deposit-funds). + +### Call the `withdraw` function + +Note that there is a fee for withdrawals, in our case, it should be 1 USDT0. The `amount` sent to the `withdraw` function is exclusive of fee, hence depositAmount - toFixedPoint(1, 6) + +```typescript +const withdrawTx = await nadoClient.spot.withdraw({ + amount: depositAmount - toFixedPoint(1, 6), + productId: 0, + subaccountName: 'default', +}); + +prettyPrintJson('Withdraw Tx', withdrawTx); +``` + +## Check balances + +Retrieve and log the subaccount balances using `getSubaccountSummary`. You should see that your balance for USDT0 (product ID of 0) is now 0. + +## Full example - deposit and withdraw + +```typescript +import { toFixedPoint } from '@nado-protocol/utils'; +import { getNadoClient, prettyPrintJson } from './common'; + +async function main() { + const nadoClient = getNadoClient(); + const { walletClient, publicClient } = nadoClient.context; + + // If you have access to `walletClient`, you can call `walletClient.account.address` + // directly instead of reaching into `nadoClient.context` + const address = walletClient!.account.address; + const subaccountName = 'default'; + // 10 USDT0 (6 decimals) + const depositAmount = toFixedPoint(10, 6); + + // TESTNET ONLY - Mint yourself some tokens + const mintTxHash = await nadoClient.spot._mintMockERC20({ + amount: depositAmount, + productId: 0, + }); + // Mint goes on-chain, so wait for confirmation + await publicClient.waitForTransactionReceipt({ + hash: mintTxHash, + }); + + // Deposits require approval on the ERC20 token, this is on-chain as well + const approveTxHash = await nadoClient.spot.approveAllowance({ + amount: depositAmount, + productId: 0, + }); + + await publicClient.waitForTransactionReceipt({ + hash: approveTxHash, + }); + + // Now execute the deposit, which goes on-chain + const depositTxHash = await nadoClient.spot.deposit({ + // Your choice of name for the subaccount, this subaccount will be credited with the deposit balance + subaccountName: 'default', + amount: depositAmount, + productId: 0, + }); + + await publicClient.waitForTransactionReceipt({ + hash: depositTxHash, + }); + + await new Promise((resolve) => setTimeout(resolve, 10000)); + + + const subaccountData = + await nadoClient.subaccount.getSubaccountSummary({ + subaccountOwner: address, + subaccountName, + }); + prettyPrintJson('Subaccount Data After Deposit', subaccountData); + + // Now withdraw your funds, this goes to the off-chain engine + // We're withdrawing less than 10 as there are withdrawal fees of 1 USDT0 + const withdrawTx = await nadoClient.spot.withdraw({ + amount: depositAmount - toFixedPoint(1, 6), + productId: 0, + subaccountName, + }); + prettyPrintJson('Withdraw Tx', withdrawTx); + + // Your new subaccount summary should have zero balances + const subaccountDataAfterWithdraw = + await nadoClient.subaccount.getSubaccountSummary({ + subaccountOwner: address, + subaccountName, + }); + prettyPrintJson( + 'Subaccount Data After Withdrawal', + subaccountDataAfterWithdraw, + ); +} + +main(); +``` diff --git a/docs/nado/faqs.md b/docs/nado/faqs.md new file mode 100644 index 0000000..e84ea17 --- /dev/null +++ b/docs/nado/faqs.md @@ -0,0 +1,311 @@ +# FAQs + +Welcome to the Nado Frequently Asked Questions (FAQs). This section provides detailed, practical answers for newcomers to Nado. Whether you're new to perpetual trading or refining your strategy, these responses aim to clarify key concepts and processes. + +If you're depositing into Nado or withdrawing for the first time, we recommend reviewing the relevant responses and associated tutorial sections for a smooth onboarding experience. + +Trade smarter, move faster. + +*** + +### Deposits + +#### Why can't I deposit my ETH? + +Nado exclusively accepts ERC-20 tokens as deposits to ensure seamless integration with our smart contract infrastructure on the INK network. + +Native ETH is not directly supported for deposits into trading positions, as it serves primarily as the gas token for transactions on INK. To use ETH-based value for trading, first bridge or wrap it into a compatible ERC-20 asset (e.g., Wrapped ETH or stablecoins like USDT0) on the Ink network. This process can be initiated through trusted bridges accessible via the Nado interface or external tools, allowing you to convert and deposit in an efficient workflow. + +Users can also send ETH to Nado via the direct-deposit flow available on the app, simplifying the deposit process. + +*** + +#### How do I deposit from other chains onto Ink and deposit on Nado? + +Nado operates on the high-performance Ink L2 network, so deposits from other chains require first bridging assets to Ink before depositing them into Nado. This two-step process ensures compatibility and leverages Ink’s low-cost, EVM-compatible environment. + +1. **Bridge Assets to INK**: Use a reliable cross-chain bridge to transfer supported assets (e.g., ETH, USDT0, USDC) from your source chain (such as Ethereum, Arbitrum, Polygon, etc) to your Ink wallet address. + 1. Recommended Bridges: [USDT0 Native Bridge](https://usdt0.to/transfer), [Superbridge](https://superbridge.app/), [Bungee](https://bungee.exchange/), or[ Relay](https://relay.link/). + 2. Steps: + 1. Connect your wallet and select the source chain. + 2. Choose Ink as the destination network. + 3. Select the asset and amount, then sign the bridging transaction. + 4. Wait for confirmation (typically 5 - 15 minutes); track via the[ Ink Explorer](https://explorer.inkonchain.com). + +{% hint style="info" %} +Ensure your wallet is configured for INK. +{% endhint %} + +2. **Deposit into Nado**: Once assets are on Ink and visible in your wallet: + 1. Visit the[ Nado app](https://app.nado.xyz). + 2. Connect your Ink-configured wallet. + 3. Navigate to the Deposit section on the Portfolio page. + 4. Select the asset, enter the amount, and confirm the on-chain transaction – deposits settle near-instantly on Nado, making collateral available for trading almost immediately. + +{% hint style="info" %} +This method supports assets like ETH (for gas), USDT0, USDC, wETH, and wBTC. Always verify token contract addresses on the Ink Explorer to avoid errors. +{% endhint %} + +*** + +#### How do I deposit funds from a CEX wallet? + +Depositing from a centralized exchange (CEX) to Nado involves withdrawing assets to your Ink-configured wallet first, then depositing into Nado This ensures funds land on the correct network for seamless trading. + +**1. Withdraw from CEX to Ink Wallet**: + +* Log in to your CEX account (e.g., Kraken) and initiate a withdrawal of supported assets (e.g., ETH, USDT0, etc). +* Enter your Ink wallet address as the recipient – double-check it's on the Ink network to prevent loss of funds. You can also use the 'Direct Deposit' feature on Nado to simplify this process. +* Specify the Ink network in the withdrawal options (Kraken supports zero-fee Ink withdrawals for ETH). +* Confirm and complete the withdrawal; funds typically arrive in 10–30 minutes. + +**2. Import and Verify Assets in Wallet:** + +* If the token doesn't auto-appear, manually import it using the INK-specific contract address (e.g., via MetaMask's "Import Token" feature—verify addresses on the Ink Explorer). + +**3. Deposit into Nado:** + +* Connect your wallet to the[ Nado app](https://app.nado.xyz) on the Ink network. +* Go to the Deposit section in the Portfolio. +* Select the asset, input the amount, and approve the transaction – your collateral will be available almost immediately for trading. + +{% hint style="info" %} +**Pro Tip**: Start with a small test withdrawal to confirm the process. For gas fees on Ink, ensure you have at least 0.01 ETH in your wallet. +{% endhint %} + +*** + +### Withdrawals + +Withdrawals from Nado are designed for simplicity and efficiency, operating without the need for any bridging mechanisms. This means your funds move directly from the protocol to your connected wallet in a straightforward, on-chain process, preserving the integrity and speed of your assets' transfer. + +To enhance your withdrawal flexibility, you have the option to enable the borrowing toggle. This feature allows you to borrow additional assets against your existing margin, providing greater liquidity while you initiate the withdrawal. + +> Once your withdrawal request is submitted, it joins a batch of other pending withdrawals for optimized processing. You can easily monitor the real-time status of your withdrawal – including confirmation, pending settlement, and completion – directly in the **Withdrawals History** tab, accessible via the Portfolio page in the Nado interface. + +Nado employs a gas-optimization strategy to minimize user fees, batching and submitting withdrawal transactions to the Ink L2 network only when gas prices are at their lowest. + +While all user actions within Nado execute instantaneously, the actual on-chain settlement of withdrawals may take up to 30 minutes during periods of elevated network congestion. This 30-minute window serves as the targeted maximum pending time: Nado's automated system will proactively submit the transaction to Nado after this interval, even in high-gas environments, to ensure timely resolution. + +In rare cases where processing exceeds this timeframe, it is typically attributable to sustained spikes in network gas costs beyond Nado's control. Rest assured, if your withdrawal appears as "pending" in the Nado app, it has been successfully queued and validated internally. Settlement will occur automatically once gas fees decrease or fall below the predefined optimization threshold at the time of submission. + +{% hint style="info" %} +Please be aware that exact withdrawal times can vary due to real-time fluctuations in network conditions and gas pricing. For comprehensive tracking, we also recommend reviewing the Account History section within the Portfolio page, which provides a detailed log of all deposit and withdrawal activities. +{% endhint %} + +*** + +### General FAQs + +#### Why do I need to deposit? + +Depositing collateral into Nado's smart contracts is essential for enabling leveraged trading on the exchange. These contracts operate in a non-custodial manner, meaning your funds remain under your control and are stored on-chain. You can withdraw your available balance whenever you choose, providing full flexibility while powering your positions. + +*** + +#### Do I control my assets? + +Yes, you maintain complete control over your assets in Nado. Only you can initiate trades, access funds, or execute withdrawals. The protocol's non-custodial smart contracts ensure that no third party holds custody, giving you sovereign authority over your portfolio. + +*** + +#### What is unified margin trading and how is it unique? + +Unified margin consolidates all your account balances and open positions into one margin pool, enabling real-time margin offsets that reduce overall margin requirements and enhance capital efficiency. + +**How it Works** + +* **Shared Collateral Pool**: Every asset in your Nado account (e.g., USDT0 deposits, kBTC holdings, etc) and all open positions contribute to a unified health score, calculated by Nado's on-chain risk engine. This score reflects your total available collateral and maintenance margin levels before hitting margin limits and liquidation thresholds. +* **Automatic Netting & Rebalancing**: Positive PnL from one position can offset losses in another, dynamically adjusting margin needs. Maintenance margin (the buffer against liquidation) and initial margin (required to open positions) are computed holistically, often resulting in lower margin requirements than isolated margin trades. +* **Risk Tiers for Visibility**: Monitor your portfolio through intuitive risk levels, updated in real-time for proactive management. + +> The core advantage of unified margin is maximizing capital efficiency. + +Your portfolio operates as an interconnected system, where balanced exposures minimize tied-up funds and amplify buying power. It's suited for strategies like basis trades or multi-asset hedges, eliminating the need for constant rebalancing. + +*** + +#### How do I earn interest on deposits? + +Interest is earned automatically on all asset deposits into Nado, as they are integrated into the protocol's underlying money markets. These markets facilitate leveraged spot trading and borrowing opportunities, with your deposits participating passively to generate yield. + +The on-chain smart contracts ensure that borrowers always meet margin requirements, maintaining system stability and security for all participants. + +*** + +#### What are the fees? + +The fee model follows a classic maker-taker structure across spot and perpetuals markets: + +* **Makers** (limit orders adding liquidity): Earn rebates at higher tiers. +* **Takers** (market orders removing liquidity): Pay a modest fee. + +All fees are calculated in basis points (bps, or 0.01%) of the trade's notional value and settled instantly in USDT0 from your collateral. + +Setting Nado apart from fixed-rate models, the volume-based scaling tiers update monthly to encourage deeper orderbooks and sustained activity. As your 30-day trading volume (maker + taker) climbs, taker fees decrease and maker rebates increase, creating a virtuous cycle of growing efficiency and participation. + +> For complete fee details including minimum fees for small taker orders, see the [Fees & Rebates](https://docs.nado.xyz/fees-and-rebates) page. + +*** + +#### Are there take-profit and stop-loss orders? + +Yes, Nado supports take-profit (TP) and stop-loss (SL) orders for open perpetual positions. These conditional order types allow you to automatically exit positions at predefined price levels, helping manage risk and secure profits in volatile markets. + +Nado also offers traders more advanced order types, including TWAP orders and scale orders (coming soon). + +*** + +#### Why do I have less funds available than what I deposited? (“I deposited $50, why does it say I only have $40 to trade with?”) + +In Nado's unified margin engine, the Available Margin metric represents the value of your deposited collateral, adjusted by the initial margin weights of each asset. This weighting accounts for varying levels of volatility across collateral types, applying a discount to more volatile assets to ensure prudent risk management. As a result, not all collateral contributes at full face value to your trading capacity. + +> For example, stable assets like USDT0 are weighted 1:1 with their nominal value, providing direct usability. This system allows you to leverage diverse collateral types while maintaining overall account stability. + +*** + +#### Is there a minimum amount to trade? + +Nado imposes a $5 equivalent initial deposit minimum for a user's trading account. After the initial deposit, there is no universal minimum trade amount. + +> However, certain order types enforce a minimum order size based on each product's parameters. Additionally, all taker orders are subject to a minimum fee calculated based on the product's minimum size, even for order types that allow smaller sizes. See the [Fees & Rebates](https://docs.nado.xyz/fees-and-rebates) page for complete details on minimum fees and order requirements. + +*** + +#### Why is there a negative sign in front of my asset balance? + +A negative balance indicator for an asset signifies that you are currently borrowing that asset within the protocol. This can occur as part of position management, such as when leveraging borrows to enter or maintain trades. + +*** + +#### I didn’t borrow USDT0. Why is my balance negative? + +A negative USDT0 balance can arise automatically in scenarios involving perpetual positions with unrealized negative PnL, especially when using collateral assets that are not USDT0. + +Throughout the duration of holding such positions, the protocol settles PnL in USDT0 between winning and losing trades. If sufficient USDT0 is unavailable in your account, Nado will borrow it on your behalf to cover the settlement, resulting in a temporary negative balance. + +*** + +#### How do I repay borrows? + +To repay any outstanding borrows in Nado, locate and select the Repay button through one of the following access points: + +* **Balances Table**: Click the drop-down menu on the right-most side of the relevant row. + +You have two primary options for repayment: + +1. **Direct Deposit**: Deposit the exact amount of the borrowed asset to settle the balance in full. +2. **Asset Conversion**: Sell or convert another held asset (e.g., swap wETH for USDT0) to generate the necessary funds for repayment. + +This process restores your balance to positive and frees up additional margin for trading. + +*** + +#### Why was my position liquidated if the chart shows that the price didn’t hit my Liq. price? + +The Liq. Price displayed in the Perp Positions table is an estimated value calculated based on your current account state and the specific position's health. In a multi-position portfolio, fluctuations in other open positions can indirectly affect this estimate, causing it to shift without direct price movement in the charted asset. + +> **Note**: The trading terminal chart on the app uses the traded price on Nado, but liquidations use the oracle price. + +Liquidations are triggered solely by the market's Oracle Price, sourced from the Chaos Labs Oracle and submitted to the on-chain smart contracts at regular time intervals or in response to significant price changes. + +{% hint style="info" %} +If the Oracle Price causes your account to fall below maintenance margin requirements, liquidation occurs regardless of the displayed estimate. This ensures objective, real-time risk management aligned with on-chain data. +{% endhint %} + +*** + +#### How do liquidations work? + +When a subaccount's maintenance health falls below zero, Nado initiates liquidation to restore solvency, closing elements in a structured sequence that minimizes market impact. Any user can act as a liquidator by submitting a transaction to purchase discounted assets or cover marked-up liabilities, earning a profit while aiding recovery. + +> The process pauses if initial health rises above zero at any step, giving positions a chance to rebound. + +Liquidators specify a product and amount to target, with the system rounding down to the optimal size that brings maintenance health back to non-negative, whilst making sure the initial health is non-positive. – balancing efficiency with user protection. + +The sequence of liquidation operations is as follows: + +1. **Cancel Open Orders**: All pending orders in the subaccount are voided to free up resources. +2. **Liquidate Assets**: Spot balances, long spreads, and positive-PnL perpetuals are sold at a discount. +3. **Liquidate Liabilities**: Borrows and short spreads are repaid at a markup. + +*** + +#### What is Maintenance Margin Usage? + +Maintenance Margin Usage is an indicator of when liquidation begins if you hit 100%. It indicates the percentage of your maintenance margin that is consumed by open positions. It provides a real-time gauge of how close your account is to liquidation thresholds. + +* **Low Risk**: 0 – 40% +* **Medium Risk**: 40 – 70% +* **High Risk**: 70 – 90% +* **Extreme Risk**: 90 – 100% + +> If Maintenance Margin Usage reaches 100%, your account is immediately eligible for liquidation. At this point, you will be unable to open new positions until some margin is freed up, either through position closures, additional deposits, or positive PnL realizations. + +{% hint style="info" %} +Monitoring this metric helps prevent overexposure and ensures you retain capacity for opportunistic trades. +{% endhint %} + +*** + +#### What is Available Margin? + +Available Margin quantifies the amount of tradable funds or collateral in your account, calculated as the initial weighted margin that remains unused. This represents your immediate buying power for initiating new positions. + +{% hint style="info" %} +Should Available Margin reach $0, new position openings will be restricted. It is also commonly referred to as Free Collateral, serving as a key indicator of your account's liquidity for trading. +{% endhint %} + +*** + +#### What is Maintenance Margin? + +Maintenance Margin represents the buffer of funds or collateral in your account before it reaches liquidation eligibility. If this value drops to $0, your account enters a high-risk state and may be subject to automated liquidation. + +Maintaining a positive Maintenance Margin is crucial for position sustainability — regularly review this alongside market conditions to adjust leverage proactively. + +{% hint style="info" %} +You must maintain a Maintenance Margin value above $0 to avoid liquidation. +{% endhint %} + +*** + +#### What are Initial and Maintenance weights? + +In exchanges limited to dollar-pegged collateral (e.g., stablecoins), assets are typically weighted at full face value for simplicity. However, Nado's cross-margin system accepts multiple collateral types and applies dual weights to account for volatility, ensuring robust risk controls: + +1. **Initial Weight**: Determines the collateral value available for opening new positions (i.e., trading capacity). +2. **Maintenance Weight**: Sets the threshold for sustaining positions without triggering liquidation. + +These weights provide traders with clear visibility into both offensive (trading) and defensive (risk) aspects of their portfolio. + +*** + +#### Initial vs. Maintenance Margin + +Initial and maintenance weighted margins offer traders dual insights into account health: your capacity to enter trades and proximity to liquidation risks. + +* **Initial Margin**: The total funds available for trading, computed as initial weighted collateral minus initial weighted margin requirements. +* **Maintenance Margin**: The minimum funds required to avoid liquidation, calculated as maintenance weighted collateral minus maintenance weighted margin requirements. + +*** + +#### Are there deposit caps on the NLP during the Private Alpha? + +Yes, during Private Alpha, **the NLP *****deposit amount***** is capped at 20,000 USDT0 per Nado trading account**. This means users cannot *add* more than 20,000 USDT0 into the vault. + +However, **the position value itself can grow beyond 20,000 USDT0 through yield**. For example, if a user deposits 19,000 USDT0 and their position appreciates to 20,000 USDT0, that’s allowed, they simply can’t deposit additional funds once they’ve hit the deposit limit. + +Any updates to the deposit cap will be communicated in advance and reflected in the Nado app during Private Alpha or later. + +*** + +#### What is the deposit APY for the NLP? + +Yields for LPs in the NLP vault are variable and subject to change based on the prevailing market conditions and other factors including but not limited to the vault strategy’s PnL and the total LP capital (USDT0) deposited into the vault. + +*** + +{% hint style="info" %} +For any other questions or feedback, please refer to the Nado community and support channels for assistance. +{% endhint %} diff --git a/docs/nado/fees-and-rebates.md b/docs/nado/fees-and-rebates.md new file mode 100644 index 0000000..fc105e8 --- /dev/null +++ b/docs/nado/fees-and-rebates.md @@ -0,0 +1,116 @@ +# Fees & Rebates + +Nado's fee structure is designed for precision and fairness, rewarding liquidity providers while keeping costs lean for active traders. + +The fee model follows a classic maker-taker structure across spot and perpetuals markets: + +* **Makers** (limit orders adding liquidity): Earn rebates at higher tiers. +* **Takers** (market orders removing liquidity): Pay a modest fee. + +All fees are calculated in basis points (bps, or 0.01%) of the trade's notional value and settled instantly in USDT0 from your collateral. + +Setting Nado apart from fixed-rate models, the volume-based scaling tiers update monthly to encourage deeper orderbooks and sustained activity. As your 30-day trading volume (maker + taker) climbs, taker fees decrease and maker rebates increase, creating a virtuous cycle of growing efficiency and participation. + +Traders can monitor their current tier and monthly volume accrual directly in the account overview, with epochs resetting on the first of each UTC month. + +Fee tiers compete with DeFi's most cost-effective venues, letting high-volume users pay as little as 1.5 bps as a taker – while elite makers are compensated to bolster the book. + +*** + +### Trading Fees & Scaling Tiers + +Nado's fee tiers scale based on your total trading volume (maker + taker) over the prior 30 days, aggregated across spot and perpetuals. Starting at accessible entry levels, they progress to elite rebates, encouraging consistent engagement without arbitrary barriers. Benefits include: + +* **Capital Efficiency**: Lower fees free up more funds for positioning, amplifying returns on scale. +* **Liquidity Incentives**: Maker rebates (expressed as negative fees) directly reward orderbook depth, tightening spreads for all traders. +* **Transparency**: Real-time tier visibility lets you strategize volume to unlock better rates, with no hidden multipliers. + +
+ +Fees apply per execution: + +* For a partial fill, only the matched portion incurs fee charges. +* No fees are applied to deposits or non-trading actions like borrowing. + +#### Minimum Fee for Takers + +Taker orders are subject to a minimum fee based on each product's minimum size: + +* **Orders ≥ minimum size**: Standard fee applies to the full notional value: `orderSize × feeRate` +* **Orders < minimum size**: Minimum fee applies: `minSize × feeRate` (effectively treating small orders as if they were minSize) + +{% hint style="info" %} +The minimum size is measured in **notional dollar value** (USDT0), not the asset quantity itself. For example, if minSize is $100, you could trade 0.001 BTC at $100,000/BTC (= $100 notional) or 0.033 ETH at $3,000/ETH (= $100 notional). +{% endhint %} + +{% hint style="info" %} +While certain order types enforce a minimum order size, the minimum fee applies to ALL taker orders regardless of size. This ensures consistent fee collection even for order types that allow smaller sizes. +{% endhint %} + +**Example - Large order**: Assume minSize = $100. A $50,000 taker order at 3.5 bps: + +* Fee: $50,000 × 0.035% = $17.50 (standard calculation) + +**Example - Small order**: Assume minSize = $100. A $75 taker order at 3.5 bps: + +* Fee: $100 × 0.035% = $0.035 (minimum fee applies, calculated as if the order was minSize) + +Maker orders do not have minimum fee requirements and are charged or receive rebates on the full notional value based on your fee tier (see fee table above). + +#### Example: Impact of Fee Tiers on a Trade + +Consider a $50,000 ETH perpetuals market order (taker) or limit order (maker) at ETH $3,000. + +> **Entry Tier** ($0 volume): Taker pays 3.5 bps = $17.50 (0.035% × $50,000). Maker pays 1 bp = $5.00. + +> **Mid-Tier** ($25M volume): Taker pays 3 bps = $15.00. Maker pays 0.5 bps = $2.50. + +> **Elite Tier** ($5B+ volume): Taker pays 1.5 bps = $7.50. Maker receives rebate of 0.8 bps = -$4.00 (added to collateral). + +The scaling dynamic rewards whales without alienating newcomers, as even base rates (3.5 bps taker) beat many other exchange venues fee models. + +{% hint style="info" %} +Maker rebates accrue instantly to your subaccount, boosting health and compounding on unified cross-margin. +{% endhint %} + +*** + +### Sequencer & Network Fees + +Nado's off-chain sequencer handles order matching for sub-15 ms speed, with on-chain settlement via Ink L2. + +To cover onchain interactions without gas volatility, Nado charges flat, predictable fees in USDT0 (or equivalent asset for withdrawals) – cheaper than typical L2 costs. + +* **Deposits**: 0 USDT0 (instant bridging from Ink). +* **Order Placement** (Maker): 0 USDT0. +* **Order Placement** (Taker): 0 USDT0 (bundled with trading fee). +* **Subaccount Transfers**: + * Standard: 1 USDT0 + * Isolated subaccounts: 0.1 USDT0 (when either sender or recipient is isolated) +* **Liquidations**: 1 USDT0 (to the liquidator). +* **Withdrawals**: + * USDT0: 1 USDT0 + * wETH: 0.0006 wETH + * kBTC: 0.00004 kBTC + +Sequencer fees are exclusively applied upon successful execution of the corresponding action. Failed actions, such as an attempt at an under-collateralized withdrawal, incur no sequencer fee charge. + +Nado submits orders on-chain to the Ink L2 in batches to optimize for gas efficiency. During instances of excessive network congestion and high gas costs on-chain, Nado withdrawals may queue longer than usual, typically settling within \~30 minutes maximum. + +### Fast Withdrawals + +Users can also select the option for "Fast Withdrawals" anytime for withdrawal priority, which incurs an extra USDT0 charge, calculated as follows: + +$$ +\text{Fast Withdrawal Fee} = 1 , \text{USDT0} + \max\left(5 , \text{USDT0}, 10 , \text{bps} \times \text{amount}\right) +$$ + +For BTC, the fee calculation is: + +$$ +\text{Fast Withdrawal Fee (BTC)} = 1 , \text{USDT0} + \max\left(0.0002 , \text{BTC}, 10 , \text{bps} \times \text{amount}\right), \text{ when the withdrawal fee for BTC is } 0.00004 , \text{BTC} +$$ + +With tiered scaling and minimal overhead, Nado's fee model turns every fill into forward momentum – precise, scalable, and built for the long-haul. + +*** diff --git a/docs/nado/funding-rates.md b/docs/nado/funding-rates.md new file mode 100644 index 0000000..706503c --- /dev/null +++ b/docs/nado/funding-rates.md @@ -0,0 +1,85 @@ +# Funding Rates + +Funding rates are a core feature of perpetual contracts on Nado, designed to keep the price of the contracts closely aligned with the underlying spot price of the asset, even though perpetuals have no expiration date. + +Unlike traditional futures, which naturally converge to spot prices as expiry approaches, perpetuals rely on funding rates to achieve this balance. These rates represent small, periodic payments exchanged between long and short positions – longs pay shorts when the perpetual price exceeds the spot price (positive rate), and shorts pay longs in the opposite case (negative rate). + +> The funding rate mechanism encourages trading activity that pulls prices back toward equilibrium, ensuring fair and efficient markets. + +On Nado, funding rates settle every hour and are proportional to your position's notional value – meaning the full size of the trade, not just your margin. Rates are displayed in real-time on the trading terminal and are capped at 2% per day to avoid excessive swings. + +By incorporating funding rates into your strategy, you can anticipate holding costs or potential rebates on open positions, turning what might seem like a minor detail into a key factor for long-term trades. + +*** + +### How Funding Rates Work + +Nado calculates funding rates using a transparent formula that compares the perpetual contract's mark price to the spot index price, sourced from third-party oracles. This difference, known as the funding index, determines the rate paid or received at each funding interval. + +The goal is to mimic the natural price convergence of expiring contracts, but continuously, fostering balanced liquidity without fixed deadlines. + +#### Key Components + +* **Spot Index Price**: An aggregated benchmark from major exchanges (Binance, Coinbase Pro, Kraken) via the Stork oracle network. It uses a median of real-time prices to resist manipulation, providing a stable reference for the asset's true market value. +* **Perpetual Mark Price**: A time-weighted average price (TWAP) calculated from Nado's orderbook over the funding interval. This reflects on-chain trading activity without relying solely on external oracles, ensuring accuracy tied to actual liquidity. +* **Funding Interval**: Payments occur hourly, on the hour, between all open positions in the market. This frequent adjustment keeps deviations minimal. +* **Calculation Formulas**: + +$$ +\text{Funding Index} = \frac{\text{TWAP(mark price)} - \text{TWAP(spot\_index)}}{\text{TWAP(spot\_index)}} +$$ + +$$ +\text{Hourly Funding Rate} = \frac{\text{funding\_index}}{24} +$$ + +$$ +\text{Annualized Funding} = \text{funding\_index} \times 365 +$$ + +> **Payment Mechanics**: The rate applies to the notional value of your position. For example, a 0.01% hourly rate on a $10,000 notional long means paying (or receiving) $1 that hour, impacting a user’s unsettled USDT0. No platform fees are involved – it's a direct transfer between longs and shorts. + +These components work together like a thermostat in a room: the index detects temperature (price) deviations from the set point (spot), and the rate adjusts the "heat" (incentives) to restore balance, operating smoothly in the background. + +*** + +### Positive Funding Rates + +A positive funding rate occurs when the perpetual mark price trades above the spot index price, indicating stronger demand for longs. In this scenario, holders of long positions pay shorts, which discourages excessive bullishness and encourages shorts to enter, gradually pushing the perpetual price down toward spot levels. + +> This dynamic is common in bullish markets, where optimism drives perps higher. The funding rate acts as a counterbalance, rendering extended long positions more expensive while rewarding shorts for providing liquidity. + +**Example**: Suppose ETH's spot index price is $3,000, but Nado's ETH perpetual mark price is $3,030 (a 1% premium). This yields a positive funding rate of +0.01% per hour (or about 3.65% annualized). + +* Alice holds a long position of 10 ETH perpetuals, with a notional value of $30,300 (10 × $3,030). +* Alice’s initial margin is $3,030 (10x leverage). +* At settlement, Alice pays shorts: 0.01% of $30,300 = $3.03 for the hour. + +Over 24 hours, this totals about $72.72, subtracted from Alice’s collateral. + +If ETH rises 2% during the day (to $3,060 spot), Alice’s position gains $606 in PnL. Subtracting the $72.72 funding cost, and Alice’s net gain is $533.28 – a meaningful reduction in PnL for holding the position over extended periods. + +In contrast, a short position of the same size would receive that $72.72 funding rate payment, boosting their returns as the premium unwinds. Historically, such rates can persist for weeks during a market uptrend, so monitoring the funding rates helps decide whether to close, hedge, or flip to short for the rebate. + +*** + +### Negative Funding Rates + +When the perpetual mark price falls below the spot index price, the funding rate turns negative, meaning shorts pay longs. This setup counters bearish pressure by rendering short positions more costly to keep open, attracting longs to buy the "discount" and lift prices back up. + +> Negative rates often emerge in market downtrends or when spot sentiment outpaces perpetuals, providing a subtle boost to longs and a reminder for shorts to reassess. + +**Example**: Now, ETH's spot index is $3,000, but the perpetual mark is $2,970 (a 1% discount), resulting in a -0.01% hourly rate. + +* Alice’s Long Position: 10 ETH perpetuals, notional $29,700 (10 × $2,970). +* Initial Margin: $2,970 (10x leverage). +* Shorts Pay Alice: 0.01% of $29,700 = $2.97 for the hour. +* Daily Total: about $71.28 added to Alice’s collateral. + +If ETH drops 2% (to $2,940 spot), Alice’s position loses $594 in PnL. The $71.28 funding receipt softens this to a net loss of $522.72. For a short of the same size, they'd pay out $71.28, compounding their costs in a falling market. + +In extended bear phases – where negatives have lasted months – this can turn defensive longs into earners, offsetting volatility and rewarding patience. + +Funding rates evolve with open interest and market sentiment, so make sure to review them in Nado's trading terminal before entering trades. By factoring in funding rates, traders can align their strategies with these built-in incentives, enhancing precision across spot, perps, and beyond. + +*** diff --git a/docs/nado/legal.md b/docs/nado/legal.md new file mode 100644 index 0000000..2322935 --- /dev/null +++ b/docs/nado/legal.md @@ -0,0 +1,5 @@ +# Legal + +{% content-ref url="legal/restricted-territories" %} +[restricted-territories](https://docs.nado.xyz/legal/restricted-territories) +{% endcontent-ref %} diff --git a/docs/nado/legal/restricted-territories.md b/docs/nado/legal/restricted-territories.md new file mode 100644 index 0000000..5e2f436 --- /dev/null +++ b/docs/nado/legal/restricted-territories.md @@ -0,0 +1,23 @@ +# Restricted Territories + +Nado is not available in certain jurisdictions. Access to the platform is determined by the location from which a person connects. Depending on the region, persons may be fully blocked from accessing Nado or may be able to view the interface but not interact with it. If you attempt to access or interact with Nado from a restricted region, the interface will limit or block functionality accordingly. + +#### Fully Restricted Territories + +Persons located in the following jurisdictions cannot access or use Nado: + +* Belarus +* Cuba +* Iran +* North Korea +* Russia +* Ukraine + +#### View-Only Territories + +Persons located in the following jurisdictions may access and view the interface only. Trading and all interactive features are disabled: + +* Canada +* United States of America + +Please refer to the Terms of Use for further details. diff --git a/docs/nado/liquidations.md b/docs/nado/liquidations.md new file mode 100644 index 0000000..46fde4d --- /dev/null +++ b/docs/nado/liquidations.md @@ -0,0 +1,212 @@ +# Liquidations + +Liquidations on Nado serve as a critical safety valve in leveraged trading, automatically closing positions when your subaccount's maintenance health dips below zero – ensuring losses never exceed your deposited collateral and preserving the platform's stability for all users. + +> The liquidation process, triggered by on-chain calculations, uses the mark oracle price from the Chaos Oracle network for fairness, drawing from a time-weighted average of third-party exchange data. + +Nado's design emphasizes precision where liquidations prioritize minimal disruption, starting with the most liquid assets and halting if health recovers mid-process. Complementing the liquidation mechanism is the insurance fund, a dedicated USDT0 reserve that absorbs rare shortfalls, acting as the platform's first line of defense. + +{% hint style="info" %} +The insurance fund is sustained by 50% of all liquidation profits. It helps to prevent widespread loss socialization, maintaining platform solvency even during extreme volatility. +{% endhint %} + +*** + +### How Liquidations Work + +When a subaccount's maintenance health falls below zero, Nado initiates liquidation to restore solvency, closing elements in a structured sequence that minimizes market impact. Any user can act as a liquidator by submitting a transaction to purchase discounted assets or cover marked-up liabilities, earning a profit while aiding recovery. + +> The process pauses if initial health rises above zero at any step, giving positions a chance to rebound. + +#### NLP Liquidation Priority + +The [Nado Liquidity Provider (NLP)](https://docs.nado.xyz/nlp) vault actively participates in liquidations to maintain platform stability and generate yield for liquidity providers. When a liquidatable position is detected, the NLP may front-run external liquidators under certain conditions: + +* **Perpetual Positions Only**: The NLP will attempt to front-run liquidations for perpetual futures positions. The NLP does not liquidate spot positions or spread positions. +* **Risk-Based Throttling**: When the NLP vault's risk exposure exceeds a predefined threshold, it will stop front-running liquidations, allowing external liquidators to process them instead. This ensures the NLP maintains healthy risk parameters. + +Maintenance Margin Usage is an indicator of when liquidation begins. It indicates the percentage of your maintenance margin that is consumed by open positions. It provides a real-time gauge of how close your account is to liquidation thresholds. + +* **Low Risk**: 0 – 40% +* **Medium Risk**: 40 – 70% +* **High Risk**: 70 – 90% +* **Extreme Risk**: 90 – 100% + +> If Maintenance Margin Usage reaches 100%, your account is immediately eligible for liquidation. At this point, you will be unable to open new positions until some margin is freed up, either through position closures, additional deposits, or positive PnL realizations. + +
+ +#### Liquidation Sequence + +Liquidators specify a product and amount to target, with the system rounding down to the optimal size that brings maintenance health back to non-negative, whilst making sure the initial health is non-positive. – balancing efficiency with user protection. + +The sequence of liquidation operations is as follows: + +1. **Cancel Open Orders**: All pending orders in the subaccount are voided to free up resources. +2. **Liquidate Assets**: Spot balances, long spreads, and positive-PnL perpetuals are sold at a discount. +3. **Liquidate Liabilities**: Borrows and short spreads are repaid at a markup. + +#### Liquidation Price + +The execution price is the oracle price adjusted by the maintenance weight divisor with a 0.5%/0.25% floor, incentivizing quick intervention. + +**For Longs**: + +$$ +\text{Liquidation Price} = \text{oracle\_price} \times \left(1 - \max\left(\frac{1 - \text{maint\_asset\_weight}}{5}, 0.005\right)\right) +$$ + +**For Shorts**: + +$$ +\text{Liquidation Price} = \text{oracle\_price} \times \left(1 + \max\left(\frac{\text{maint\_liability\_weight} - 1}{5}, 0.005\right)\right) +$$ + +{% hint style="info" %} +**Minimum Penalty: 0.5%** - At very high leverage, the liquidation penalty is capped at a minimum of 0.5% to ensure liquidators are always incentivized. +{% endhint %} + +*** + +**For Spread Liquidations**: + +When liquidating spread positions (offsetting spot and perp positions), the penalty calculation differs based on which side you're closing: + +**Selling Spread (amount > 0)**: Selling spot, closing perp short + +1. Calculate penalty: `penalty = (1 - perp_maint_asset_weight) / 10` +2. Apply minimum floor: `penalty = max(penalty, 0.0025)` (0.25%) +3. Liquidation price (discount): `spot_price × (1 - penalty)` + +$$ +\text{Liquidation Price} = \text{spot\_price} \times \left(1 - \max\left(\frac{1 - \text{perp\_maint\_asset\_weight}}{10}, 0.0025\right)\right) +$$ + +**Buying Spread (amount < 0)**: Buying spot to cover short, closing perp long + +1. Calculate penalty: `penalty = (spot_maint_liability_weight - 1) / 10` +2. Apply minimum floor: `penalty = max(penalty, 0.0025)` (0.25%) +3. Liquidation price (markup): `spot_price × (1 + penalty)` + +$$ +\text{Liquidation Price} = \text{spot\_price} \times \left(1 + \max\left(\frac{\text{spot\_maint\_liability\_weight} - 1}{10}, 0.0025\right)\right) +$$ + +{% hint style="info" %} +**Minimum Penalty: 0.25%** - Spread liquidations have a lower minimum of 0.25% (half of non-spread) since they're less risky. +{% endhint %} + +*** + +**For non-spread liquidations, for example**, this creates a gross profit for liquidators: + +$$ +\text{Long} = \text{oracle\_price} \times \max\left(\frac{1 - \text{maint\_asset\_weight}}{5}, 0.005\right) +$$ + +$$ +\text{Short} = \text{oracle\_price} \times \max\left(\frac{\text{maint\_liability\_weight} - 1}{5}, 0.005\right) +$$ + +But Nado allocates 50% of this to the insurance fund, so net profit is half: + +$$ +\text{Net Long} = \text{oracle\_price} \times \max\left(\frac{1 - \text{maint\_asset\_weight}}{10}, 0.0025\right) +$$ + +$$ +\text{Net Short} = \text{oracle\_price} \times \max\left(\frac{\text{maint\_liability\_weight} - 1}{10}, 0.0025\right) +$$ + +*** + +#### Outcomes & Insolvency + +Liquidation yields two possibilities per product:
+ +1. **Positive Outcome**: The subaccount receives USDT0 (e.g., from selling positive spot balances or closing profitable perps). +2. **Negative Outcome**: The subaccount spends USDT0 (e.g., repaying borrows or covering losing perps). + +If all viable liquidations are negative and would deplete USDT0 below zero, the account becomes insolvent – positions are closed at a net loss, generating bad debt. At this point, further safeguards are activated. + +**Example** + +Suppose you have 31,000 USDT0 and short 10 ETH spot ($30,000 at $3000 / ETH) now. ETH spot maintenance\_liability\_weight is 1.05, then: + +$$ +\text{Maintenance Health}: 31{,}000 - 30{,}000 \times 1.05 = -500 , \text{USDT0} +$$ + +A liquidator targets 5 ETH short: + +* **Oracle Price** = $3,000 / ETH +* **Liquidation Price** = $3,000 \* (1 + (1.05 - 1) / 5) = $3,030 / ETH +* **Profit** = $30 / ETH +* **Liquidation Fee** (sent to insurance fund) = $15 / ETH + +Then: + +* You lose 5 ETH short and the loss equals 3,030 \* 5 = 15,150 USDT0. +* You now have a 5 ETH short and 15,850 USDT0 and the maintenance health increases to 100 USDT0. You cannot be liquidated for now. +* The liquidator gets 5 ETH short with (3030 - 15) \* 5 = 15,075 USDT0 (in fact, it will be 15,074 considering $1 fee by Nado, not insurance). +* Liquidation fee (15 \* 5 = 75 USDT0) is distributed to the insurance fund. + +**Note**: The liquidator cannot liquidate too much (e.g., 10 ETH short) if it will let the initial health > 0. + +#### High-Leverage Example (Minimum Penalty Floor) + +At very high leverage, the minimum penalty floor becomes active. Suppose you have a 50x leveraged long SOL position: + +* **Position**: Long 1,000 SOL-PERP at $100/SOL (entered when SOL was $100) +* **Current Price**: $100/SOL (oracle price) +* **Maintenance Asset Weight**: 0.99 (for 50x leverage) +* **Your USDT0**: $500 (maintenance health is now negative) + +The natural penalty would be: + +$$ +\text{Natural Penalty} = \frac{1 - 0.99}{5} = \frac{0.01}{5} = 0.002 = 0.2% +$$ + +But since 0.2% < 0.5% minimum, the **0.5% floor applies**: + +* **Liquidation Price** = $100 × (1 - 0.005) = $99.50/SOL +* **Gross Profit** = $100 × 0.005 = $0.50/SOL +* **Liquidation Fee** (to insurance) = $0.25/SOL +* **Liquidator Net Profit** = $0.25/SOL + +If a liquidator takes 500 SOL: + +* You lose 500 SOL-PERP and pay 500 × $99.50 = $49,750 USDT0 +* You now have 500 SOL-PERP long and $50,250 USDT0 (maintenance health restored) +* Liquidator gets 500 SOL-PERP for 500 × $99.50 = $49,750 USDT0 (market value: $50,000) +* Insurance fund receives $125 USDT0 + +Without the 0.5% floor, liquidators would only earn 0.2% ($100 total), making them unwilling to act. The floor ensures liquidations remain profitable even at extreme leverage. + +*** + +### Insurance Fund & Socialization + +The insurance fund is Nado's buffer against insolvency – a segregated USDT0 pool that covers bad debt by paying liquidators to absorb underwater positions. + +Seeded by the Nado team, it's replenished with 50% of liquidation profits, growing with platform activity. + +If depleted, losses socialize in tiers to spread impact equitably: + +1. **Perpetual Socialization**: Pro-rata deductions from other positions in the same market (e.g., all ETH-PERP holders share a 0.1% collateral trim). +2. **USDT0 Depositor Socialization**: Remaining shortfalls hit USDT0 balances across the platform. + +This multi-layer approach – insurance first, then targeted, then broad – ensures platform resilience and solvency amid extreme volatility. + +#### Example — Insolvency Resolution + +Post-liquidation, your subaccount owes $1,000 bad debt on a liquidated SOL-PERP. + +* The insurance fund ($1M total) injects $1,000, paying a liquidator $1,050 to take the position (5% incentive). +* The insurance fund’s capital is reduced accordingly to cover the liquidator payment. +* If the insurance fund is depleted, SOL-PERP holders each lose 0.01% of margin (pro-rata). + +Users are encouraged to monitor health proactively via the Nado app. With these liquidation mechanisms, Nado turns potential tempests into manageable swells – protecting capital while enabling confident trades. + +*** diff --git a/docs/nado/maintenance-windows.md b/docs/nado/maintenance-windows.md new file mode 100644 index 0000000..8ca4c59 --- /dev/null +++ b/docs/nado/maintenance-windows.md @@ -0,0 +1,25 @@ +# Maintenance Windows + +Nado features two fixed maintenance windows each week to deploy technical updates, new features, product enhancements, UX/UI improvements, and optimizations. + +These windows are intentionally scheduled during periods of lowest market activity to minimize disruption. Users should expect limited app functionality for up to 10 minutes once a window begins – comparable to downtime on major CEXs and DEXs. In rare cases requiring more extensive updates, downtime may exceed 10 minutes; advance notice will be posted approximately one hour prior in the Official Nado Comms Channels. + +> User assets remain fully secured on-chain and are never at risk during a maintenance window. + +Not every scheduled window is utilized. A Status Bar in the bottom-left corner of the Nado app interface clearly indicates system state: + +* **Operational** = All systems are operational. +* **Maintenance** = An update is ongoing (degraded app performance is possible). + +For detailed real-time monitoring, visit the dedicated [Nado Status Page](https://nado-xyz.betteruptime.com/), which displays current and historical uptime for both the application and API, upcoming scheduled maintenance details, and monthly-categorized incident reports since mainnet launch. + +The fixed maintenance schedule (in UTC) is as follows: + +* **Window #1**: Mondays at 14:00 UTC +* **Window #2**: Thursdays at 14:00 UTC + +{% hint style="info" %} +Automated traders using the Nado API / SDK should account for these windows, as connections may experience brief downtime. For questions or coordination, reach out via support ticket or directly to admins in the Official Nado API Client Channel. +{% endhint %} + +*** diff --git a/docs/nado/margin-types.md b/docs/nado/margin-types.md new file mode 100644 index 0000000..6b144ab --- /dev/null +++ b/docs/nado/margin-types.md @@ -0,0 +1,106 @@ +# Margin Types + +Nado offers two margin modes to suit your trading style: + +1. **Unified Margin** +2. **Isolated Margin** + +Unified margin is a form of cross-margin that treats your entire account – deposits, positions, and unrealized profits / losses – as a single, interconnected pool of collateral across spot, perpetuals, and money markets (e.g., spot borrowing). A single collateral pool allows assets to automatically offset risks via a single health score that dynamically adjusts to your portfolio’s collateral – computed via the on-chain risk engine. + +> Unified margin is ideal for diversified portfolios where you want to maximize capital efficiency without intensive manual adjustments. + +Isolated margin, on the other hand, assigns a fixed amount of collateral to one specific perpetual position, keeping it separate from the rest of your account – like placing a single bet in a sealed compartment to protect the rest of your funds. Isolated margin is widely popular for confining collateral risks to a single perpetual position, where margin maintenance and liquidation risks only impact that specific open position. + +> Isolated margin is useful for high-volatility trades, such as using higher leverage or opening positions in more price-volatile altcoin perpetuals. If you prefer strict risk limits per position, then isolated margin enables you to ensure that one trade’s outcome doesn’t affect others. + +Both modes are available on Nado using the same account – with unified cross-margin as the default margin type. Users can switch seamlessly between unified cross-margin and isolated margin via the Nado order panel to adapt their strategy as markets shift. + +*** + +### Unified Margin + +Unified margin is a form of cross-margin that consolidates all your account balances and open positions into one margin pool, enabling real-time margin offsets that reduce overall margin requirements and enhance capital efficiency. + +
+ +#### How it Works + +* **Shared Collateral Pool**: Every asset in your Nado account (e.g., USDT0 deposits, kBTC holdings, etc) and all open positions contribute to a unified health score, calculated by Nado's on-chain risk engine. This score reflects your total available collateral and maintenance margin levels before hitting margin limits and liquidation thresholds. +* **Automatic Netting & Rebalancing**: Positive PnL from one position can offset losses in another, dynamically adjusting margin needs. Maintenance margin (the buffer against liquidation) and initial margin (required to open positions) are computed holistically, often resulting in lower margin requirements than isolated margin trades. +* **Risk Tiers for Visibility**: Monitor your portfolio through intuitive risk levels, updated in real-time for proactive management. + +#### Benefits & Examples + +The core advantage of unified cross-margin is maximizing capital efficiency. + +Your portfolio operates as an interconnected system, where balanced exposures minimize tied-up funds and amplify buying power. It's suited for strategies like basis trades or multi-asset hedges, eliminating the need for constant rebalancing. + +**Example**: Consider the following basis trade scenario: + +* You deposit 10,000 USDT0 and open a long wETH spot position worth $20,000 (2x leverage). +* Simultaneously, you short an ETH perpetuals contract for $20,000 notional (also 2x leverage). +* With unified margin and custom logic for handling spread trades, offsetting exposure nets out – meaning your effective margin requirement may drop notably compared to an isolated margin ETH perpetual of the same size. The risk engine recognizes the hedge and adjusts the margin levels accordingly. +* More specifically, if the ETH price dips 5%, the spot loss is balanced by the perpetual’s profits, maintaining health above 80% without requiring additional collateral deposits. + +As a result, unified margin not only saves capital but also automates risk adjustments during market swings, letting you scale your trades without constantly making manual changes. + +#### Leverage & Margin Requirements Under Unified Margin + +When trading with cross-margin, the leverage selector in the order panel does not determine how much margin is reserved for your position. Instead, it acts purely as a front-end sizing control that limits your maximum notional size per order. + +Under the hood, margin requirements are calculated using a market’s risk parameters. Because unified margin exposes your entire account as a shared collateral pool, Nado does not lock a specific amount of margin based on the leverage selected. + +{% hint style="info" %} +The margin shown is a visual metric to gauge the amount of margin utilized by a position. +{% endhint %} + +What this means in practice: + +* Selecting 5x leverage does not assign 20% of notional as margin for an order. +* Initial margin is calculated using the market’s risk weights. +* Your entire account equity is available to support the new position, rather than a fixed, isolated amount. + +As a result, traders may see cases such as: + +* A $4,000 notional position +* Max leverage: 20x +* Selected leverage: 5x +* Actual margin used: \~$200, because the risk engine is applying the market’s leverage parameters, not the front-end slider. + +Unified margin is designed to maximize capital efficiency, allowing Nado’s risk engine to evaluate your account holistically rather than locking collateral per position. The leverage slider remains a tool for controlling order size, not a determinant of allocated margin. + +*** + +### Isolated Margin + +Isolated margin dedicates a precise amount of collateral to a single perpetual position, ensuring its risks stay contained. + +The isolated margin type is available exclusively for perpetuals, with a cap of one isolated position per perpetual market. It complements unified margin by allowing users to express their trading strategies with a sharper focus, removing the position’s margin impact from their Nado account’s broader portfolio of assets. + +
+ +#### How it Works + +* **Dedicated Allocation**: When opening a position, select isolated mode and specify the desired leverage amount. This amount alone determines the position's health and leverage (up to 20x), independent of your main Nado account. +* **Independent Risk Checks**: Each isolated position has its own liquidation threshold – no shared PnL or offsets. If your position breaches maintenance margin (typically 50% of initial), it's liquidated solely on its collateral, leaving the rest of your account intact and unaffected. +* **Integration with Cross-Margin**: Funds on Nado transfer smoothly from your unified pool to fund a new isolated margin position. Notably, traders can still utilize both unified and isolated margin in the same market (e.g., cross-margin spot wETH alongside isolated ETH perps) per their trading strategy or risk preferences. + +#### Benefits & Examples + +Isolated margin excels in targeted risk control, limiting downside to predefined amounts while enabling bolder leverage on specific markets. It promotes precise position sizing and eases oversight of outliers, functioning as a safeguard for exploratory trades amid broader stability. + +Unlike unified margin’s shared collateral pool, isolated acts as a firewall. It’s ideal for testing new strategies or riding short-term swings without portfolio-wide risk impacts. + +**Example**: Consider the following SOL perpetual scenario. + +* Bob starts with 10,000 USDT0 in spot assets in his account. +* Bob allocates 2,000 USDT0 to a 10x leveraged SOL-PERP long amid a volatile market – a $20,000 notional position. +* Bob’s main Nado account now holds 8,000 USDT0 in spot assets, while the 2,000 USDT0 functions as the collateral for his SOL-PERP position. +* If the SOL price drops by 10% and triggers liquidation of Bob’s SOL-PERP, Bob only loses the 2,000 USDT0 collateral. +* The remaining 8,000 USDT0 spot holdings in Bob’s main account remain untouched, preserving 80% of his capital and limiting the risk exclusively to his SOL-PERP. + +Nado users can also adjust their open isolated margin positions by simply adding / removing margin via the trading terminal (with optional borrowing from money markets). This recalculates health instantly without affecting other positions. + +Switch between modes anytime in the order panel, or manage isolated trades directly from the positions table. With clear health indicators and automated calculations, Nado equips you to trade with confidence, turning market flux into focused opportunity. + +*** diff --git a/docs/nado/market-parameters.md b/docs/nado/market-parameters.md new file mode 100644 index 0000000..0f65f8d --- /dev/null +++ b/docs/nado/market-parameters.md @@ -0,0 +1,3 @@ +# Market Parameters + +
diff --git a/docs/nado/mission.md b/docs/nado/mission.md new file mode 100644 index 0000000..b369298 --- /dev/null +++ b/docs/nado/mission.md @@ -0,0 +1,11 @@ +# Mission + +We saw a market fractured by fragmentation and compromise – slow executions, thin liquidity, and platforms built for survival, not supremacy. Born from the battle-tested fires of Kraken and the Ink Foundation, Nado rejects the chaos. We engineer the world's most performant DEX: a powerful orderbook fusing blistering speeds with perpetual and spot markets – anchored by cross-margin efficiency. + +Our mission cuts deeper. We hand traders the reins to tame DeFi's turbulence – funneling raw volatility into your clear signal, your edge. + +Trust your instincts. Execute with unmatched speed, depth, and precision. Don't just endure volatility’s chaos, make it yours to command. + +Nado. Welcome to the perfect storm. + +*** diff --git a/docs/nado/nlp.md b/docs/nado/nlp.md new file mode 100644 index 0000000..077ebf1 --- /dev/null +++ b/docs/nado/nlp.md @@ -0,0 +1,101 @@ +# NLP + +In decentralized finance, perpetual futures markets blend high potential with intense volatility. + +Liquidity in altcoin pairs often lags behind major markets, fostering gaps in depth and efficiency that complicate trading. The Nado Liquidity Provider (NLP) aims to counter this within Nado's CLOB DEX, via a vault architecture that bolsters infrastructure and delivers yields to liquidity providers (LPs). + +> NLP transforms USDT0 deposits from idle capital into active liquidity across the exchange. + +Seamlessly embedded in Nado's orderbook, it directs LP capital toward bespoke vault strategies deployed across Nado’s perpetual markets. LPs accrue yield while narrowing spreads, enhancing order fill execution, and minimizing slippage. + +For retail LPs or traders, the NLP offers a path to competitive APYs and better liquidity, turning altcoin volatility into shared platform strength. + +*** + +### How NLP Works: From Deposit to Dynamic Yield + +Under the hood, NLP operates as a federation of sub-vaults, each tied to a dedicated strategy and funding is allocated across these pools by predefined weights — overseen by weighted subaccounts + +LP capital is distributed proportionally, with automated rebalancing – calculated off-contract to minimize proportional errors – occurring seamlessly during deposits and withdrawals to preserve equilibrium. + +Yields are sourced from unrealized PnL from the vault strategy deployed in perpetual markets, harnessed through liquidations and market-making while prioritizing capital safeguards and swift redemptions over leveraged exposures. + +> Users share equally in all PnLs, creating a diversified buffer that tempers volatility into more predictable passive income. As outcomes compound, user LP shares generate yield on USDT0 deposits proportional to the total share of LP capital deposited into the vault. + +
+ +To make the LP token pricing mechanism more accessible, consider it like determining the value of your portion of a shared pie where the size of each slice adjusts based on the pie’s total worth. Here's a step-by-step explanation: + +* **Calculate Total Assets**: Begin with the total USDT0 deposited into the vault – this reflects the collective USDT0 contributions from all LPs. +* **Incorporate Unrealized PnLs**: Next, factor in the current estimated profits or losses from open perpetual positions. These "unrealized" values are calculated using up-to-date prices from oracle feeds, providing a realistic snapshot of ongoing trades without waiting for them to close. +* **Divide by Outstanding LP Tokens**: Finally, divide the combined total (assets plus unrealized PnLs) by the number of LP tokens in circulation. Each LP token reflects a proportional claim on this full value, much like dividing a pie amongst your friends. + +This approach ensures transparency and equity. When you withdraw, you receive an amount based on the vault's value at that exact moment. It's a real-time reflection of performance.Withdrawals redeem at the current price, though a 4-day lock post-mint encourages long-term alignment, with burns available only after this period. + +{% hint style="info" %} +A modest withdrawal fee guards against oracle latency: 1 USDT0 for sequencer costs, plus the greater of 1 USDT0 or 10 bps of the amount – redistributed to remaining providers as a shared incentive for long-term alignment. +{% endhint %} + +*** + +### Key Features: What Powers the NLP + +The NLP fuses institutional-grade strategies with seamless retail access, unlocking yields tailored to Nado's altcoin prowess. These innovations help amplify DEX liquidity, sparking a self-reinforcing loop where every deposit fuels better trades across the platform. + +#### Accessible, Diversified Strategies + +Sub-vaults are capable of deploying multiple approaches in parallel. Currently, only one vault is live for the Private Alpha — primarily utilizing a liquidations and maker spread strategy. All profit and loss (PnL) outcomes are aggregated and distributed proportionally across the vault, thereby promoting effective risk diversification and contributing to the overall stabilization of yields. + +At the core of the system, strategies are calibrated to target perpetual futures contracts. By harnessing oracles for price feeds, the strategies can mitigate custody-related vulnerabilities – such as those arising from asset transfers or off-chain exposures. + +Under the hood, a robust architecture of dedicated subaccounts function as specialized pools governed by weighted ownership structures. + +Consider, for instance, allocations to institutional-grade traders specializing in market-making: these entities are empowered to execute orders on behalf of the pool, leveraging their expertise to generate alpha in real-time. + +All ensuing profits – net of operational nuances – are then channeled back to LPs, accruing proportionally to each liquidity provider's share and fostering a collaborative ecosystem where individual contributions amplify collective gains. + +> It is important to note that NLP vault yields are inherently variable, influenced by prevailing market conditions and the performance dynamics of the underlying strategies. + +Moving forward, additional vaults with differing strategies can be added to the NLP to boost yield and provide more depth for altcoin perpetuals on the Nado orderbook. + +#### USDT0-Only Simplicity + +Deposits and redemptions are streamlined exclusively through USDT0, eliminating the complexities and distractions inherent in managing multiple assets. This singular focus creates a clean, predictable entry point for liquidity providers, allowing them to engage without the encumbrance of fragmented holdings or conversion overheads. + +> **Important**: The value of the NLP position is capped at a maximum of 20,000 USDT0 per Nado trading account during the Private Alpha. +> +> For instance, if a user deposits 19,000 USDT0 and the position grows to 20,000 USDT0, they would reach the cap. Any changes to these caps will be communicated ahead of time and updated on the Nado app interface either during the Private Alpha or later. + +#### Calibrated Risk Controls + +Real-time oracle updates keep share prices sharp, with fees used as a latency buffer. Health gates enforce safe leverage, pausing withdrawals if needed for deleveraging. The vault's design ensures it cannot be liquidated – even in unhealthy states – allowing compounding to endure volatility without exposure to forced closures. + +{% hint style="info" %} +NLP never liquidates spot assets, but NLP pool owners can trade spot assets, minimizing risks associated with custody and asset transfers. The NLP’s design, where the NLP cannot be liquidated even in an unhealthy state, allows compounding to better endure volatility. +{% endhint %} + +The 4-day lock post-mint fosters better longer-term commitment, balancing liquidity with stability. + +#### Altcoin Liquidity Amplifier + +The NLP strategically targets long-tail perpetual futures contracts – less active trading pairs characterized by wider bid-ask spreads and shallower order book depths, where traditional liquidity provision often falls short. + +The protocol's core advantage emerges by allocating capital to these altcoin pairs, where vault strategies actively contribute to improved liquidity and execution in those markets – helping to narrow spreads under more sustained book depth. + +Additionally, the NLP helps accelerate new listings, bridging the gap between emerging assets and eager traders. Each USDT0 deposit into the vault acts as a catalyst, transforming idle capital into a dynamic way to enhance trading liquidity while returning yield. + +*** + +### Navigating Volatility with Precision + +In the perpetual landscape, where volatility unfolds with unyielding intensity, liquidity emerges as the defining differentiator – facilitating seamless order matching, reducing slippage, and enabling the precision required for sustained edge extraction. + +> NLP democratizes access to these vault-caliber tactics for retail liquidity providers, delivering real yield for passive capital allocation. + +A core tenet of NLP is its insulation from the perils of isolated leverage exposure, such as unanticipated liquidations that can erode individual positions. The vault's fortified architecture – bolstered by diversified sub-strategies – serves as a resilient intermediary. + +Potential disruptions, whether from oracle discrepancies or transient strategy under-performance, are attenuated through layered fee structures and gating mechanisms that distribute impacts across the LP base, ensuring no single event overwhelms the system. + +In summary, the NLP turns idle USDT0 deposits into more active capital on the platform, compounding yields and improving platform liquidity via market depth on less liquid altcoin pairs. + +*** diff --git a/docs/nado/onboarding-tutorial.md b/docs/nado/onboarding-tutorial.md new file mode 100644 index 0000000..641d270 --- /dev/null +++ b/docs/nado/onboarding-tutorial.md @@ -0,0 +1,150 @@ +# Onboarding Tutorial + +This comprehensive guide walks you through the essential steps to set up your wallet, bridge assets to the Ink L2 network, and begin trading on Nado. Whether you're new to decentralized trading or transitioning from another platform, follow these instructions sequentially for a seamless experience. + +> Nado operates on the Ink L2 network, a high-performance EVM-compatible layer two network optimized for DeFi applications and powered by the Optimism Superchain. + +By the end of this tutorial, you'll have a funded wallet connected to Ink and ready to execute your first trade on Nado. Let's dive in. + +*** + +### Step 1. Choose Your Wallet + +Nado and the Ink network support a variety of popular wallets, ensuring compatibility with both desktop and mobile users. Select the one that best aligns with your needs for security, usability, and features: + +* **MetaMask** (Browser + Mobile): The most widely used wallet for EVM-compatible chains, ideal for beginners with its intuitive interface. +* **Rabby Wallet** (Browser): Tailored for active traders, offering automatic network switching, enhanced transaction simulations, and improved security previews. +* **WalletConnect** (All Compatible Wallets): Connect mobile-first options such as: + * Trust Wallet + * OKX Wallet + * Rainbow Wallet + * Crypto.com DeFi Wallet + * Ledger Live (via WalletConnect for hardware security) + +{% hint style="info" %} +If you're handling significant balances, prioritize hardware-integrated options like Ledger for added protection. +{% endhint %} + +*** + +### Step 2. Add the INK Network to Your Wallet + +To interact with Nado on the Ink network, configure your wallet to recognize Ink as a custom chain. Use one of the following methods: + +#### **Method 1**: One-Click Addition + +1. Visit [ChainList](https://chainlist.org/). +2. Search for "Ink." +3. Select the Ink network and click “Add to MetaMask” (or your chosen wallet). This automatically populates the required details. + +#### **Method 2**: Manual Configuration + +If the one-click method isn't available, add the network manually in your wallet settings: + +* **Network Name**: INK +* **RPC URL**:[ https://rpc-gel.inkonchain.com](https://rpc-gel.inkonchain.com) +* **Chain ID**: 57073 +* **Currency Symbol**: ETH +* **Block Explorer URL**:[ https://explorer.inkonchain.com](https://explorer.inkonchain.com) + +{% hint style="info" %} +Once added, switch your wallet to the Ink network via the network drop-down menu. Confirm the connection by checking your wallet balance (it should display ETH as the native token). +{% endhint %} + +*** + +### Step 3. Acquire ETH on INK for Gas Fees + +All transactions on the Ink network require a small amount of ETH to cover gas fees. Even minimal trades or deposits will incur these costs, so start with at least 0.01 – 0.05 ETH to cover initial activities both on the Ink L2 network and when interacting with Nado. + +#### Option A: Receive ETH Directly on Ink + +* Withdraw ETH from a centralized exchange (CEX), bridge protocol, different wallet you control, or a transfer from a fiat → crypto onboarding platform you prefer. +* Use your Ink wallet address as the recipient. Ensure the sending address specifies the Ink network during withdrawal to avoid sending to the wrong chain. + +{% hint style="info" %} +**Pro Tip**: Kraken offers zero-fee withdrawals to the Ink network, making it a cost-effective choice. +{% endhint %} + +#### Option B: Bridge ETH from Another Chain + +If you don't have ETH on Ink, bridge it from a supported source chain (e.g., Ethereum, Arbitrum, Polygon, etc.) using a supported bridge: + +1. Visit an official bridge supported by Nado such as the [Superbridge](https://superbridge.app/), [Bungee](https://bungee.exchange/), or [Relay](https://relay.link/). +2. Select your source chain (e.g., Base) and set Ink as the destination. +3. Choose ETH as the asset to bridge (or swap other assets like USDT0 into ETH during the process). +4. Enter the amount, review fees, and sign the transaction in your wallet. +5. Wait for confirmation – bridging typically takes 5 – 15 minutes on average with some variance depending on network congestion. Track progress on the bridge's dashboard or the Ink block explorer. + +{% hint style="info" %} +Once bridged, your ETH will appear in your Ink wallet balance. +{% endhint %} + +*** + +### Step 4. Add Additional Assets to Your Ink Wallet + +With ETH secured for gas, fund your wallet with trading collateral such as stablecoins (e.g., USDT0 or other supported tokens. This prepares you for deposits into Nado. + +* Follow the same receipt or bridging process as in Step 3: Withdraw from a wallet or CEX into your Ink address or bridge from another chain. +* Supported assets for deposits on Nado include wETH, USDT0, kBTC, wBTC, and USDC. + +{% hint style="info" %} +**Important**: Wallets do not always auto-detect custom tokens. +{% endhint %} + +If an asset doesn't appear in your wallet: + +1. Open your wallet and navigate to Import Token or Add Custom Token. +2. Paste the token's contract address (ensure it's the Ink-specific address, not from Ethereum or another chain – verify via the[ Ink Explorer](https://explorer.inkonchain.com)). +3. Confirm the import. The token should now display with its balance. + +*** + +### Step 5. Connect Your Wallet & Start Trading on Nado + +With your Ink wallet funded, you're ready to engage with the Nado app: + +1. Visit the[ Nado app](https://app.nado.xyz) (bookmark this URL for security). +2. Click Connect Wallet in the top-right corner. +3. Select your wallet provider (e.g., MetaMask) and ensure it's switched to the Ink network. +4. Approve the connection request in your wallet – this grants Nado read-only access to your balances without custody. +5. Navigate to the Deposit section on the Portfolio page. +6. Select an asset (e.g., USDT0), enter the amount, and confirm the transaction. Deposits are near-instant on Nado, making your collateral available for trading immediately. + +To open your first position: + +* Browse the available markets in the trading terminal (e.g., ETH-PERP, BTC-PERP). +* Use the order form to place a limit or market order, leveraging Nado's unified margin system. +* Monitor your positions via the Portfolio tab. + +*** + +### Troubleshooting Common Issues + +Encountering hurdles? Use this section to resolve them quickly. If issues persist, submit a support ticket for team assistance. + +* **Wallet Not Switching to INK Network?** In MetaMask or Rabby, open the network dropdown menu and manually select Ink. If unavailable, re-add the network per Step 2 above. +* **"Insufficient Gas Fee" Error?** Your wallet lacks ETH for transaction costs. To resolve the issue, bridge a small amount of ETH (as low as 0.001) from another chain or wallet to your Ink address. +* **Tokens Not Showing in Wallet?** Manually import the token using the associated contract address (Step 4). Double-check it's the Ink version via the block explorer. +* **Bridge Transaction Stuck?** Monitor the status both on the[ Ink Explorer](https://explorer.inkonchain.com) and the bridge you're using using your transaction hash. If pending beyond 30 minutes, it may be due to network congestion – retry with a smaller amount or contact bridge support. + +{% hint style="info" %} +For Nado-specific issues, submit a support ticket for team assistance. +{% endhint %} + +*** + +### Security Best Practices + +Security is paramount in DeFi. Adhere to these tips to help safeguard your assets: + +* **Verify URLs**: Always access Nado via bookmarked links or official sources. Beware of phishing sites mimicking the Nado domain. +* **Protect Your Seed Phrase**: Never share it with anyone, including support teams. Store it offline in a secure manner. +* **Use Hardware Wallets**: For larger wallet balances, integrate Ledger or Trezor via WalletConnect to keep private keys offline. +* **Enable Protections**: Activate MetaMask's phishing detection and transaction simulation features. Rabby Wallet excels here by previewing full transaction impacts. +* **Additional Habits**: Avoid clicking unsolicited links in emails / DMs, use two-factor authentication on linked exchanges, and regularly review connected dApps in your wallet settings. + +By following this tutorial, you're equipped to trade confidently on Nado. + +*** diff --git a/docs/nado/onboarding-tutorial/bridging-usdt0-to-ink.md b/docs/nado/onboarding-tutorial/bridging-usdt0-to-ink.md new file mode 100644 index 0000000..ee7dc29 --- /dev/null +++ b/docs/nado/onboarding-tutorial/bridging-usdt0-to-ink.md @@ -0,0 +1,45 @@ +# Bridging USDT0 to Ink + +USDT0 is the quote asset on Nado. The native USDT0 bridge delivers fast, low-cost transfers directly to Ink — typically completing in 2-10 minutes. + +In this tutorial, learn how to transfer USDT0 from another chain to Ink and deposit directly into Nado to begin trading. + +*** + +### Step-by-Step: Bridge USDT → USDT0 on Ink + +1. Go to the official USDT0 bridge: +2. Click the prominent ‘Transfer’ button on the homepage. +3. Connect your wallet (MetaMask, Rabby, or WalletConnect) — ensure it’s set to the chain where your USDT currently resides (e.g., Ethereum, Arbitrum, Base, etc.). +4. Select USDT as the source token and your current chain as the source network. +5. Choose Ink (USDT0) as the destination chain. +6. Enter the amount of USDT you wish to bridge. +7. Review the summary carefully: + * “You will receive” (exact USDT0 amount on Ink, usually 1:1 minus minimal fees). + * Estimated bridge fee and gas on destination. + * Expected arrival time (typically 2–10 minutes). +8. Approve the USDT spend (if first time on this chain) and confirm the transaction. +9. Wait for completion — track progress directly on the bridge interface or via the [Ink Explorer](https://explorer.inkonchain.com/) using your transaction hash. + +{% hint style="info" %} +**Note:** The bridge is non-custodial and funds arrival is automatic — no claiming step needed. +{% endhint %} + +*** + +### After Bridging: Deposit USDT0 into Nado + +1. Switch your wallet to the Ink network (if not already). +2. Visit[ https://app.nado.xyz](https://app.nado.xyz) and connect your wallet. +3. Navigate to Portfolio → Deposit (or click the Deposit button in the top-right). +4. Select USDT0 from the asset list. +5. Enter the amount (or click Max) and confirm the transaction — deposits are near-instant on Nado. +6. Once confirmed, your USDT0 balance appears in the Balances table and is immediately available for spot trading, perpetuals, lending, borrowing, or depositing into the NLP vault. + +You’re now fully funded with USDT0 on Ink and ready to trade on Nado. + +*For more details on the USDT0 bridge, please refer to the* [*official website*](https://usdt0.to/) *and* [*documentation*](https://docs.usdt0.to/)*.* + +*** + +*Not financial advice. Trading digital assets involves market risk, including the risk of loss. Trading with leverage magnifies gains and losses and may lead to rapid liquidation, up to and including the loss of all collateral.* diff --git a/docs/nado/oracles.md b/docs/nado/oracles.md new file mode 100644 index 0000000..8b2e5b0 --- /dev/null +++ b/docs/nado/oracles.md @@ -0,0 +1,77 @@ +# Oracles + +Oracles are a cornerstone of Nado's market pricing, bridging off-chain market data to on-chain executions with speed, security, and risk intelligence – ensuring every trade, from spot fills to perpetual funding, reflects precise value without delay, distortion, or exploitation. + +Oracle feeds update continuously via Nado's sequencer, bundling prices with actions for seamless on-chain settlement on the Ink L2 – minimizing gas and maximizing precision across Nado markets. + +*** + +### How Nado's Oracle Model Works + +Nado's oracles ingest prices from diverse, high-fidelity sources, aggregating them into tamper-resistant indexes that power critical functions like collateral valuation, liquidations, and funding rates. + +> [Chaos Labs](https://chaoslabs.xyz/oracles) is the oracle provider powering Nado, delivering high-throughput and risk-aware price feeds for the Nado orderbook. + +Chaos Protocol is a decentralized network of nodes that performs off-chain computations with advanced anomaly detection and outlier filtering, then pushes secure, real-time updates on-chain via EVM-compatible feeds tailored to Nado's products on Ink L2. This architecture treats data as an active risk variable, enabling circuit breakers to halt anomalous feeds and protect against manipulation – all while maintaining millisecond-level latency for seamless DEX operations. + +#### Key Benefits + +* **Low-Latency**: Chaos’ decentralized node network delivers WebSocket-driven updates rivaling TradFi speeds, minimizing stale prices and enabling real-time mark pricing without bottlenecks. +* **Robustness & Resilience**: Built-in anomaly detection and circuit breakers proactively identify and mitigate exploits, ensuring feeds remain resilient even in volatile or adversarial markets. +* **Cost-Efficiency**: Optimized aggregation reduces update overhead, supporting broad asset coverage without inflating gas costs on Ink L2. +* **Security & Decentralization**: A growing network of validators eliminates single points of failure, with zero-knowledge proofs for reserve validations and real-time risk parameter adjustments. +* **Risk Intelligence**: Beyond pricing, feeds incorporate dynamic risk signals for automated protocol tuning, enhancing solvency and capital efficiency. + +*** + +### Price Feeds + +Nado leverages Chaos Labs' Edge Price Oracles for three core feed types, each engineered for precision, performance, and market integrity. These feeds aggregate real-time price data from multiple sources including centralized and decentralized exchanges (CEXs / DEXs), with each source assigned a reliability-based weight. + +> The system calculates a weighted median price (the 50th percentile by cumulative weight) rather than a simple average, providing superior resistance to outliers and manipulation. + +Chaos Labs aggregates data from a minimum of 3 sources per feed (generally 5-7 for blue-chip assets like BTC and ETH, with exceptions per feed for coverage optimization). Each feed requires a minimum quorum of 3 valid sources to compute and publish. Prices are recomputed every 500ms, ensuring low-latency freshness. Validation layers – including staleness filtering, deviation detection, and the quorum requirement – are applied to ensure data integrity. + +#### Source Selection & Weightings + +Chaos Labs selects high-fidelity sources from top centralized exchanges (CEXs) and decentralized exchanges (DEXs), assigning reliability-based relative weights normalized to sum to 1.0. Weights are generally as follows, with exceptions applied per feed for asset-specific liquidity or coverage: + +**CEXs (Primary Spot / Perp Venues):** + +* **Binance**: 3/11 ≈ 0.27 +* **Coinbase**: 3/11 ≈ 0.27 +* **Bybit**: 2/11 ≈ 0.18 +* **OKX**: 2/11 ≈ 0.18 +* **Others** (Kraken, Gate, Bitget combined): 1/11 ≈ 0.09 (divided equally among them, ≈ 0.03 each) + +**DEXs (Equal Weighting for DeFi Depth)**: + +* **Uniswap** (and forks) +* **Curve** (and forks) +* **Hyperliquid** + +{% hint style="info" %} +DEX weights are equally distributed (e.g., 1/3 ≈ 0.33 each if all three are active for a feed), blended with CEXs in the overall aggregation. This hybrid approach ensures broad market representation while prioritizing liquid venues. +{% endhint %} + +#### Spot Oracle Prices + +These benchmark underlying asset values from leading spot venues, using a weighted median of the last trade across USD / BUSD-paired exchanges like Binance, Coinbase, and Uniswap. Chaos Labs' anomaly algorithms filter noise in real-time, smoothing volatility for accurate collateral health scores and borrow limits. + +> **Example**: For ETH / USD (a blue-chip feed using 6 sources), if Binance (weight: 0.27) quotes $3,000, Coinbase (0.27) $2,995, Bybit (0.18) $3,002, Uniswap (DEX, 0.17), and two others (combined 0.11) at $3,005 and $2,990, the weighted median at the 500ms recompute interval yields $3,000 – rejecting a spiked $3,100 outlier from a low-weight source to prevent inflated valuations, after passing staleness and deviation checks with a minimum quorum of 3 sources. + +#### Perpetual Prices + +Tailored for Nado's perpetual markets, these feeds aggregate open interest-weighted prices from major perp venues (e.g., Binance Futures, Bybit), incorporating funding rate signals and liquidity depth. Chaos’ risk component auto-adjusts for imbalances, ensuring fair settlement without premium decay distortions. + +> **Example**: In a BTC-PERP with $2B open interest skewed long (using 7 sources for this blue-chip feed), the feed blends prices from high-reliability venues like Binance ($60,000, weight: 0.27), Coinbase ($59,990, 0.27), Bybit ($60,010, 0.18), Hyperliquid (DEX, equal weight ≈ 0.08), and others (combined 0.20), yielding a weighted median index of $59,980 every 500ms – triggering a +0.02% funding rate to balance positions, validated by deviation detection and a minimum quorum of 3 sources. + +#### Mark Price + +Nado's fair-value anchor for risk management, this weighted median-derived mid-price from orderbook depth (top 2% bids / asks) across spot and perp sources provides a manipulation-resistant reference for liquidations and PnL. Chaos Labs' circuit breakers pause updates if deviations exceed 1%, safeguarding against flash crashes. + +> **Example**: In a thin ETH book with weighted median mid-price at $3,000 (from spot sources like Coinbase $2,990 weight: 0.27 and perp like Bybit $3,010 weight: 0.18, plus DEXs and others totaling 0.55; using 5 sources), versus spot $2,990, a -0.01% funding applies to your 10x long on $30,000 notional – crediting $3 hourly to offset a 1% dip's $300 loss, netting -$297 and preserving your edge in flux, after staleness filtering confirms all sources are within the 500ms recompute interval and quorum of at least 3 is met. + +Nado’s oracle model turns market signals into actionable clarity – robust, rapid, and resilient. + +*** diff --git a/docs/nado/order-types.md b/docs/nado/order-types.md new file mode 100644 index 0000000..7ffee6c --- /dev/null +++ b/docs/nado/order-types.md @@ -0,0 +1,59 @@ +# Order Types + +Nado equips traders with a streamlined set of order types to navigate volatility with precision – executing at market speed or guarding edges with calculated triggers. Available across spot and perpetuals, these integrate seamlessly with the orderbook, ensuring fills that align with your strategy without excess complexity. + +*** + +### Market Order + +Executes immediately at the best available price in the orderbook. Ideal for quick entries or exits in liquid markets, where speed of execution beats exact pricing. Slippage is minimized by deep liquidity, but monitor thin books to avoid wider spreads. + +> **Example**: You're bullish on ETH during a sudden rally and want to buy 1 ETH right now. Place a market buy order – it fills instantly at the current ask price of $2,500, even if it slips slightly to $2,510 in a fast-moving market. + +### Limit Order + +Place a buy or sell at a specific price or better, resting in the orderbook until matched. Provides control over entry / exit points – buy below current price, sell above – for strategies like scaling in or taking profits. Makers are charged lower fees, rewarding patience. + +> **Example**: ETH is trading at $2,500, but you believe it'll dip before rising. Set a limit buy order for 1 ETH at $2,450 or lower. If the price drops and hits your level, it executes automatically; otherwise, it sits until conditions align, earning you maker rebates if filled. + +### Stop Market Order + +Triggers a market order once the asset hits a predefined stop price, converting to immediate execution. Use to limit losses (stop-loss) or chase breakouts (stop-buy). In turbulent swings, it activates swiftly via oracle feeds, protecting capital without manual intervention. + +> **Example**: You hold a long position in BTC at $60,000 and want to cap losses at 5%. Set a stop market sell order at $57,000. If BTC crashes to that level, it triggers a market sell, closing your position at the next available price (say, $56,900) to stem further downside. + +### Stop Limit Order + +Triggers a limit order at a stop price, then executes only at your specified limit or better, combining protection with price discipline. For example, sell if price drops to $50 (stop), but only at $49.50 or higher (limit). Stop-limit orders shield against price gaps while avoiding poor fills in fast markets. + +> **Example**: Short on SOL at $150, you set a stop limit buy at $160 (stop) with a limit of $162. If SOL surges to $160, it places a limit buy order at $162 or less. This locks in profits if it retraces slightly, but skips execution if it gaps wildly above $162. + +### TWAP (Time-Weighted Average Price) + +TWAP orders slice a large order into smaller chunks executed evenly over a set duration, averaging the price to reduce market impact. Perfect for institutional volumes or gradual accumulations – specify time window and total size, and Nado handles the cadence. + +TWAP orders lower slippage in volatile sessions, preserving your edge. + +> **Example**: You want to accumulate 100 ETH over the next hour without spiking the price from $2,500. Set a TWAP buy for 100 ETH over 60 minutes – it breaks into \~1.67 ETH chunks every minute, averaging your entry at $2,505 despite intra-hour swings between $2,490 and $2,520. + +### Scaled Orders (Coming Soon) + +Deploys a ladder of limit orders across a predefined price range, with customizable price and size distributions for staggered, gradual execution. Ideal for scaling into or out of trades amid volatility, reducing slippage and averaging fills over multiple levels. Select flat, increasing, or decreasing price distribution, paired with even split, increasing, or decreasing size distribution; anchor to current market price. + +Configure with Fill-or-Kill (FoK) or Immediate-or-Cancel (IOC) behavior, or allow partial fills. + +> **Example**: ETH trades at $3,000; you aim to buy 10 ETH on a potential dip without rattling the book. Set a scaled buy for 10 ETH over $2,950–$2,900 (5 even orders of 2 ETH). As price falls to $2,940, the lowest fills first at $2,900; further drops trigger the rest, netting an average entry of \~$2,920 versus a single large limit's impact. + +*** + +{% hint style="info" %} +To promote fair and orderly markets while protecting liquidity providers from ultra-low-latency predatory strategies, Nado applies a fixed 30 ms speed bump to all non-post-only orders (i.e., any aggressive order that would immediately cross and take liquidity from the book). + +* Post-only orders (limit orders explicitly flagged as post-only) are exempt and execute with zero added latency. +* The 30 ms delay is currently uniform across all accounts and trading tiers. +* Future updates may introduce tiered speed-bump adjustments based on maker volume, NLP participation, or other contribution metrics — details will be announced in advance. + +This mechanism significantly reduces adverse selection risk for passive liquidity providers without materially impacting retail or legitimate high-frequency trading strategies. +{% endhint %} + +*** diff --git a/docs/nado/orderbook-architecture.md b/docs/nado/orderbook-architecture.md new file mode 100644 index 0000000..c786ab9 --- /dev/null +++ b/docs/nado/orderbook-architecture.md @@ -0,0 +1,67 @@ +# Orderbook Architecture + +Nado harnesses DeFi's turbulent energy into a unified edge, fusing CEX speed with on-chain sovereignty on Ink L2. Its architecture integrates spot, perpetuals, and money markets into one risk-calibrated engine. The orderbook delivers 5–15 ms latency, MEV-proof executions, and superior capital efficiency – fueling Ink L2's premier liquidity hub. + +At its core, Nado's design rests on two interlocking pillars: + +1. **A protocol-level clearinghouse and risk engine housed on-chain.** +2. **A powerful off-chain sequencer – the central-limit orderbook (CLOB).** + +
+ +Threaded through Ink's EVM-compatible rails, the two pillars form a low-latency CLOB DEX where every order converges with exacting precision. + +*** + +### The Nado Orderbook + +In decentralized exchanges (DEXs) like Nado, an orderbook is the core engine powering trades. It acts like a real-time marketplace ledger, listing all open buy orders (bids) from users willing to purchase assets at specific prices and sell orders (asks) from those ready to sell. + +These orders are stacked by prices: + +* **Bids**: Increase as you go lower (deeper discounts for buyers) +* **Asks**: Rise higher (premiums for sellers). + +This creates a transparent view of supply and demand, enabling efficient matching of trades without intermediaries. + +
+ +The depth chart visual above illustrates this dynamically:
+ +* **Green Side** (left): Represents bids – buy orders below the current market price. Denser green bands indicate stronger buying interest and liquidity at those levels, meaning large trades can execute with minimal price slippage. +* **Red Side** (right): Shows asks – sell orders above the market price. Thicker red areas signal robust selling pressure and depth. +* **Center** (mid-price line): The equilibrium point, calculated as the average of the highest bid and lowest ask. It's where the market "balances" and most trades occur. + +Mid-book depth zooms in on liquidity around this mid-price – the heart of the orderbook. It measures how much volume (in assets or value) sits just on either side, showing market resilience. + +Deeper mid-book (denser dots / bands near the center) means the DEX can handle bigger trades smoothly, reducing volatility. In Nado, this depth ensures fair, slippage-resistant trades, even in volatile crypto conditions. + +*** + +### On-Chain Clearinghouse – Calibrated Control + +Every position breathes through Nado's on-chain clearinghouse – a vigilant hub that computes exposures, collaterals, and offsets in real-time. Unified cross-margin spans your portfolio where liabilities net out intuitively across spot, leverage, and borrows. + +Oracle feeds pulse sub-second, triggering liquidations only when thresholds breach. Orders submitted to the sequencer are batched and submitted on-chain, starving MEV opportunities and letting your trading signal cut clean through the noise. + +*** + +### Off-Chain Sequencer – Blistering Performance + +Speed defines the strike. Nado's off-chain sequencer matches orders in 5–15 ms, then batches them for seamless on-chain settlement via Ink L2. As a central-limit orderbook (CLOB), it accepts limit orders through the intuitive front-end app or an HFT-optimized API, empowering automated, high-performance strategies. + +The sequencer holds no custody: assets stay locked in on-chain smart contracts, under your sole control. It cannot censor trades or forge signatures – all settlements, from withdrawals to liquidations, demand on-chain verification. + +Millisecond latencies and periodic batching render MEV extraction uneconomical, shielding executions from front-running. Like an accelerator atop on-chain rails, the sequencer delivers CEX-grade matching without custody shadows. + +Nado's design channels market flux into high-velocity execution – a decisive edge for traders. + +*** + +### Nado’s Horizon: The Perfect Convergence + +Nado's technical stack unifies liquidity across products, its capital efficiency turns portfolios into weapons, and the Ink L2 offers sub-second finality. + +Battle-tested on chains past amidst cycles of DeFi’s volatility, and now re-engineered for Ink's frontier, Nado delivers the tools for traders to outperform the market. + +*** diff --git a/docs/nado/pnl-settlements.md b/docs/nado/pnl-settlements.md new file mode 100644 index 0000000..cae1158 --- /dev/null +++ b/docs/nado/pnl-settlements.md @@ -0,0 +1,102 @@ +# PnL Settlements + +PnL settlements on Nado represent the ongoing realization of profits and losses from your open positions, seamlessly converting unrealized gains or losses into actual USDT0 balances without interrupting your trading flow. At its core, PnL – profit and loss – measures the difference between a position's current market value and its opening cost, providing a real-time snapshot of performance. + +On Nado, the settlement process runs continuously in the background. + +As perpetuals fluctuate, positive PnL (winners) draws USDT0 from negative PnL (losers) across the platform, much like a self-balancing scale that redistributes weight to keep the entire system level. This automatic mechanism ensures your account value accurately reflects economic reality, with unsettled PnL acting as a bridge between open trades and settled cash. + +
+ +For settlements on Nado, you'll see two key metrics:
+ +1. **Perp PnL** (aggregate across all perpetual positions) +2. **Position PnL** (for a specific trade) + +These are updated in real-time, influencing your subaccount health and available margin. No manual intervention is needed – settlements happen transparently and automatically. + +*** + +### How PnL Settlements Work + +Nado breaks lifetime PnL into two components: + +* **Unsettled USDT0** (pending transfers) +* **Settled USDT0** (already realized in your balance) + +Settlements occur automatically whenever losers' negative PnL can fund winners' positives, altering USDT0 balances accordingly. + +
+ +This continuous flow prevents distortions, maintaining equilibrium across dynamic trading activity on the platform. More specifically:
+ +* **Unsettled USDT0**: The portion of PnL yet to transfer between accounts. Positive values signal incoming deposits to your USDT0 balance; negative ones indicate upcoming withdrawals. It fluctuates with position volatility and hold duration – not always mirroring total PnL. +* **Settled USDT0**: PnL already incorporated into your USDT0 holdings, visible on the Portfolio Overview. Over time, as settlements accumulate, your assets and borrows adjust to reflect these changes. + +$$ +\text{Total PnL} = \text{Unsettled USDT0} + \text{Settled USDT0} +$$ + +Users can view unsettled USDT0 by checking the Unsettled column in the Balances table of the Margin Manager page on the Nado app. + +> For a full audit of your settlements, navigate to **Portfolio → History → Settlements Table**, which logs every transfer with timestamps and amounts. + +*** + +### View PnL Metrics + +* **Perp PnL**: Track total unrealized across perpetuals on the Portfolio Overview or Perpetual Positions pages – essential for gauging subaccount health. +* **Position PnL**: Drill into specifics via the Perp Positions table, showing individual trade performance. + +$$ +\text{Account Value} \neq \text{Assets} - \text{Borrows} \pm \text{Perp PnL} +$$ + +**Instead**: + +$$ +\text{Account Value} = \text{Assets} - \text{Borrows} \pm \text{Unsettled USDT0} +$$ + +This formula captures the dynamic nature of settlements, ensuring your displayed equity aligns with on-chain reality. + +*** + +### Examples of Settlement in Action + +Settlements aren't tied directly to a position's overall direction. Instead, they depend on market timing and peer activity. A winning trade might yield negative unsettled USDT0 if it funded others mid-hold, while a loser could show positive if it received from bigger shorts. + +You open a 5 ETH long perpetual at $3,000 (notional $15,000, 5x leverage on $3,000 margin) and a 3 ETH short at $3,000. + +* ETH rises to $3,100 +* Your long gains $500 PnL (+3.33%) +* Your short loses $300 (-3.33%) +* Unsettled USDT0: +$200 net (winners pull from platform losers) + +Over multiple hours, $150 settles into your USDT0 (from external shorts funding your long), leaving $50 unsettled. Your account value rises to $3,200 (original $3,000 + $200 net PnL), but assets now include the settled $150. + +#### Volatility Swing Examples + +Consider the scenario if ETH dips to $2,900 mid-day. + +* Your long now is - $500 PnL, short +$300. +* Unsettled flips to -$200 (your short funds platform longs). +* By evening's recovery to $3,050, it rebounds to +$150 unsettled. + +> **Total Lifetime PnL**: +$250, with $100 settled – demonstrating how swings create interim negatives even on net winners. + +*** + +### Closing Positions & Balance Adjustments + +When you close a position, any remaining unsettled USDT0 settles automatically into your USDT0 balance, typically within minutes. This finalizes the trade's economics where gains boost collateral for new plays, losses deduct without surprise. + +Open positions cause gradual USDT0 balance shifts as back-end settlements process – normal behavior that doesn't restrict trading. Your buying power remains tied to health, not just balances, so monitor both to better gauge your risk profile. + +#### Handling Negative PnL Without USDT0 + +If negative PnL accrues without sufficient USDT0, settlements continue but only as long as your subaccount health stays positive, Nado auto-borrows the shortfall from its embedded money markets at the prevailing rates. This keeps positions viable during drawdowns, but it adds interest. + +With PnL settlements woven into Nado's fabric, your trades evolve from static bets to living strategies – precise, adaptive, and always aligned with the market's pulse. + +*** diff --git a/docs/nado/products.md b/docs/nado/products.md new file mode 100644 index 0000000..b8fe10c --- /dev/null +++ b/docs/nado/products.md @@ -0,0 +1,104 @@ +# Products + +### Spot Trading + +To get started, deposit supported collaterals such as USDT0 or wETH into your unified account. These assets serve as margin, enabling up to 5x leverage on spot positions for amplified exposure without needing separate approvals. + +For example, you can swap wETH for USDT0 seamlessly or open a leveraged long on wETH using your USDT0 balance. + +> Capital efficiency is core to Nado's design. All deposits earn automatic native yields, compounded from platform fees and lending activity. + +Unlike traditional DEXs with isolated pools, Nado unifies liquidity across products, so your spot holdings can directly offset risks in perpetuals or fund borrows from money markets. This turns spot trading into a strategic anchor – borrow to scale positions or hedge against perpetuals for balanced plays – all within one account. + +*** + +### Perpetual Trading + +Perpetual futures on Nado let you go long or short on assets like BTC, ETH, SOL, BNB, and XRP with up to 20x leverage and USDT0 settlement. + +Positions are funded through your unified margin system, where the entire portfolio, including spot holdings, borrows, and other open perp positions, nets against exposures in real time. A long wETH spot position, for instance, can collateralize a short SOL perp, reducing overall risk and freeing up capital. + +> Basis trading is streamlined where users can open a spot-perp pair directly to capture convergence opportunities, executing delta-neutral strategies without bridging assets or managing multiple interfaces. This simplifies complex setups, like funding rate arbitrage, into single, atomic trades. + +All perps benefit from MEV protections and oracle-based pricing to prevent manipulation, ensuring fair execution amid market swings. + +*** + +### Money Markets + +Nado's money markets enable lending, borrowing, and yield accrual, intertwined with your trades for frictionless efficiency. + +Lenders supply USDT0 or wETH to the pool, earning a proportional share of borrower interest as passive yield on idle assets. Borrowers access spot collateral-backed loans at dynamic rates shaped by supply and demand – rates rise with borrowing pressure to curb excess, and they fall amid liquidity abundance to spur activity. + +> All loans remain overcollateralized via smart contracts: post crypto assets as collateral, held securely on-chain until repayment. + +Unified margin nets exposures, so healthy spot or perp positions cover borrows without forced liquidations. + +*** + +#### Dynamic Interest Rate Model + +Nado's money markets use a dynamic interest rate model to adapt rates in real time to supply and demand, promoting balanced utilization and efficient capital flow. + +Every 15 minutes, the system recalculates rates based on the utilization ratio (R), defined as the proportion of borrowed assets to the total available supply in the pool: + +$$ +R = \frac{\text{borrowed}}{\text{borrowed} + \text{available}} +$$ + +This frequent adjustment ensures rates respond swiftly to market shifts. As borrowing demand rises and R approaches 1, borrow rates climb to discourage over-leveraging, while deposit rates may dip to attract more liquidity. Conversely, when R falls (abundant supply), borrow rates decrease to incentivize uptake, and deposit rates rise to reward lenders. + +For each spot product (e.g., wETH or USDT0), the model employs four tunable parameters to shape the rate curve: + +* **small\_cap**: The utilization threshold below which rates remain flat at a minimal "floor" level, encouraging baseline borrowing without over-penalizing low demand. +* **large\_cap**: The high-utilization ceiling where rates plateau or escalate sharply to cap extreme borrowing. +* **floor**: The minimum borrow rate (as a percentage), setting a baseline cost even in low-demand scenarios. +* **inflection**: The pivot point where the curve bends from gradual to steeper increases, fine-tuning sensitivity around moderate utilization (typically 50 - 80%). + +These parameters create a "kinked linear" curve for the optimal rate. It stays constant (at the floor) for R below small\_cap, then slopes linearly upward to the inflection point, accelerates more aggressively toward large\_cap, and caps thereafter. + +This design prevents rates from stagnating at zero (discouraging idle capital) or spiking uncontrollably (avoiding liquidity crunches). + +The final rates incorporate a protocol fee (interest\_fee = 0.2, or 20%) to sustain the platform: + +$$ +\text{Deposit Rate} = \text{Optimal Rate} \times (1 - \text{interest\_fee}) \left( \text{Lenders receive } 80% \text{ of the optimal rate as yield.} \right) +$$ + +$$ +\text{Borrow Rate} = \frac{\text{Optimal Rate}}{1 - \text{interest\_fee}} \left( \text{Borrowers pay the full optimal rate} \right) +$$ + +Nado’s model fosters resilience by auto-balancing the pool, rewarding patient lenders with competitive yields, and keeping borrowing accessible without fixed hurdles. All while netting exposures across your unified account for seamless integration with spot and perpetual trades. + +*** + +### Nado Liquidity Provider (NLP) + +Nado's Liquidity Provider (NLP) transforms idle USDT0 into dynamic liquidity for perpetual markets, fueling tighter spreads and smoother executions across altcoin pairs. As a vault-based protocol embedded in the CLOB DEX, NLP channels deposits into diversified strategies that capture yields from liquidations and market-making — while netting risks in real-time for resilient capital efficiency. This complements money markets by extending passive income opportunities, turning volatility into shared platform strength without isolated leverage exposures. + +At its core, NLP federates sub-vaults weighted by strategy, with automated rebalancing ensuring proportional allocations during deposits and withdrawals. Yields accrue from unrealized PnL in perpetual positions, aggregated equitably across LPs and reflected in real-time token pricing: + +$$ +\frac{\text{total assets} + \text{oracle-fed unrealized gains}}{\text{outstanding lp shares}} +$$ + +Withdrawals redeem at current value after a 4-day lock to align long-term commitment, buffered by a modest fee (1 USDT0 fixed plus the greater of 1 USDT0 or 10 bps) that redistributes to holders and guards against oracle latency. + +> Capped at 20,000 USDT0 per account during Private Alpha, this USDT0-only design strips away multi-asset friction, delivering clean, predictable access. + +Key safeguards include health gates that pause outflows for deleveraging, non-liquidatable vaults to weather storms, and oracle-driven price fidelity — ensuring compounding endures without forced closures. Strategies target long-tail altcoins, liquidations, narrowing bid-ask gaps and accelerating new listings, while diversified subaccounts (like maker spreads) amplify depth without custody risks. + +Variable yields, shaped by market dynamics, reward retail LPs with institutional-grade alpha, creating a virtuous cycle: every deposit enhances liquidity, compounding returns and precision for all traders on Nado. + +*** + +### The Edge Converges + +Nado's products form a cohesive ecosystem – fully integrated at the protocol level for seamless capital efficiency, not bolted-on features. Unified liquidity aggregates depth across spot, perpetuals, and money markets, reducing slippage and enabling reliable fills even in thin conditions. + +Cross-margining treats your entire portfolio as collateral, netting exposures to free up funds and minimize over-collateralization. + +On Nado, portfolios become precision tools. + +*** diff --git a/docs/nado/subaccounts-and-health.md b/docs/nado/subaccounts-and-health.md new file mode 100644 index 0000000..43b7a5f --- /dev/null +++ b/docs/nado/subaccounts-and-health.md @@ -0,0 +1,341 @@ +# Subaccounts & Health + +Subaccounts and health form the backbone of risk management on Nado, allowing traders to segment trading activities while maintaining a clear view of their overall exposure per each subaccount. + +> Health is Nado's weighted measure of account stability, factoring in asset quality, volatility, and liquidity to determine your buffer against liquidations and capacity for new trades. + +By default, subaccounts use unified margin for efficiency, but you can toggle isolated margin per position. Health calculations update in real-time via the on-chain risk engine, displayed as intuitive gauges in the Nado app. Green for safe sailing, red for imminent gales. + +*** + +### Subaccounts + +A subaccount is essentially a dedicated trading compartment tied to your main wallet address, enabling up to four subaccounts per address on Nado. Notably, the 4 subaccounts is the cap for users on the front-end, while API users have no limit on the number of subaccounts per wallet address. + +> Subaccounts let you divide your wallet into up to four independent silos – each with its own balances, positions, and margin. Each subaccount operates autonomously. + +#### Subaccount Naming + +When working with Nado, subaccounts follow a specific naming convention: + +* **`default`** - This is your main/primary subaccount. Every wallet address automatically has a "default" subaccount upon first interaction. +* **`default_1`** - Your first additional subaccount (automatically assigned when created) +* **`default_2`** - Your second additional subaccount +* **`default_3`** - Your third additional subaccount + +{% hint style="info" %} +When you create additional subaccounts on the frontend, they are automatically assigned these sequential names (`default_1`, `default_2`, `default_3`). You can connect up to **4 subaccounts** simultaneously: `default` (main), `default_1`, `default_2`, and `default_3`. +{% endhint %} + +Deposits, withdrawals, positions, and PnL remain fully isolated within it, ensuring a liquidation in one never affects the others. This structure protects diversified strategies – for instance, dedicating one subaccount to conservative spot holds and another to aggressive perpetual positions – while tying all to the same wallet address for independent oversight of each subaccount. + +#### Key Benefits + +* **Risk Isolation**: Limit fallout from volatile or riskier trades – only the subaccount's assets are at stake during liquidation. +* **Capital Efficiency**: Within each subaccount, unified margin pools resources across spot, perps, and money markets by default – maximizing capital efficiency. +* **Compounding Ease**: Unrealized PnL from winning trades automatically bolster the subaccount's margin, fueling further opportunities without manual transfers between accounts. +* **Flexibility**: Switch between cross- and isolated-margin modes per subaccount, adapting to strategy needs without disrupting the whole. Unified cross-margin is the default account type on Nado. + +To create a subaccount, navigate to the Nado app’s account menu and assign a unique label or name for that specific subaccount. Of note, users are required to make an initial transfer for each subaccount in order for it to be created. Either deposits from the user’s wallet or transfers on Nado from an existing subaccount will work. + +{% hint style="info" %} +Transfers between subaccounts are instant and incur a network fee: $1 USDT0 for standard transfers, or $0.10 USDT0 when either the sender or recipient is an isolated subaccount. For teams or advanced users, this mirrors a multi-account structure on centralized exchanges, but fully on-chain. +{% endhint %} + +*** + +### Health + +Health quantifies your subaccount's resilience, using weighted calculations to blend all assets and positions into a unified risk score. It accounts for variances like an asset's volatility (e.g., BTC vs. a stablecoin) or liquidity. + +Low health signals more risk, a negative value on maintenance health triggers liquidation, and initial health gates new entries, preventing overextension. + +There are two health thresholds: + +1. **Maintenance Health**: Your liquidation buffer; if < 0, the subaccount risks partial or full close-out to protect the protocol. +2. **Initial Health**: Your available collateral; if < 0, you can't open new positions until collateral deposits restore it to a positive value. + +In traditional terms, maintenance health approximates "USDT0 to liquidation," and initial health mirrors "free collateral." Weights adjust for these nuances. For example, stable assets get higher weights for more robust risk profiles, and volatile ones receive lower weights to reflect increased risk profiles. + +#### Weight Parameters + +Every product (spot tokens, perps) has four weights: + +* **maintenance\_asset\_weight**: Discounts / rewards assets for maintenance health. +* **maintenance\_liability\_weight**: Penalizes liabilities (e.g., shorts) for maintenance. +* **initial\_asset\_weight**: Similar for initial health, often stricter. +* **initial\_liability\_weight**: Heightens scrutiny on borrowings. + +These ensure balanced contributions. For example, a high-volatility token might have a 0.8 asset weight, meaning it counts as 80% of its value toward health. + +#### Spot Health + +Spot assets serve as Nado's primary collateral, powering trades across products. Their health is straightforward, reflecting value adjusted for stability. + +$$ +\text{Spot Health} = weight \times amount \times price +$$ + +**Example** + +You hold 5 BTC in a subaccount, with BTC at $10,000. + +**Weights**: + +* maintenance\_asset\_weight = 0.9 +* initial\_asset\_weight = 0.8 + +> **Initial Health** = 5 × $10,000 × 0.8 = $40,000 + +> **Maintenance Health** = 5 × $10,000 × 0.9 = $45,000 + +{% hint style="info" %} +With no positions, your $40,000 initial health means that much buying power for new trades. If BTC dips to $9,000, health recalculates to $36,000 initial – prompting a deposit if nearing zero. +{% endhint %} + +#### Perpetual Health + +Perpetuals introduce leverage, so health nets current value against entry costs, capturing unrealized PnL. + +**Example** (**Short Position**) + +You short 5 BTC-perps at entry / entry price $10,000, current $10,000. + +**Weights**: + +* maintenance\_asset\_weight = 0.95 +* maintenance\_liability\_weight = 1.05 +* initial\_asset\_weight = 0.9 +* initial\_liability\_weight = 1.1 + +> **Initial Health** = -5 × $10,000 × 1.1 − (-5 × $10,000) = -$5,000 + +> **Maintenance Health** = -5 × $10,000 × 1.05 − (-5 × $10,000) = -$2,500 + +{% hint style="info" %} +This equates to 10x leverage (1 / (1 - 0.9) = 10). On other platforms, you'd see $5,000 initial margin and $2,500 maintenance – same thresholds, weighted for Nado's unified cross-margin. If BTC rises to $11,000, your short loses $5,000 PnL, dropping maintenance health further; a collateral deposit restores it. +{% endhint %} + +#### Leverage Calculation + +Nado's weights imply maximum leverage: + +$$ +\text{Leverage} = \frac{1}{1 - \text{initial\_asset\_weight}} +$$ + +{% hint style="info" %} +For BTC's 0.9 weight, that would be 10x max – scalable per asset, ensuring volatility-matched limits. +{% endhint %} + +*** + +### Special Cases + +#### Spreads + +Spreads are offsetting positions on the same asset (e.g., long spot BTC, short BTC-PERP). The system recognizes spread trades as hedges, boosting your health beyond what the individual positions would provide. + +> **Intuition**: If you're long spot BTC and short BTC-PERP, both positions move together. When BTC goes up, your spot gains but your perp loses. When BTC goes down, your spot loses but your perp gains. This hedging reduces risk, so the system gives you better health. + +The spread health calculation follows this logic: + +*** + +**Step 1: Calculate Basis Amount** + +The basis amount is how much of your position is actually hedged (the overlapping part): + +$$ +\text{basis\_amount} = \begin{cases} +\min(\text{spot\_amount}, -\text{perp\_amount}) & \text{if spot\_amount} > 0 \\ +-\max(\text{spot\_amount}, -\text{perp\_amount}) & \text{if spot\_amount} \leq 0 +\end{cases} +$$ + +**Example 1 - Long Spot, Short Perp**: + +``` +Position: +2 wBTC spot, -3 BTC-PERP + +Since spot_amount > 0: + basis_amount = min(2, -(-3)) + = min(2, 3) + = 2 + +Result: You have a spread of size 2 (the hedged portion) + The remaining -1 BTC-PERP is a naked short +``` + +**Example 2 - Short Spot, Long Perp**: + +``` +Position: -5 wBTC spot, +3 BTC-PERP + +Since spot_amount < 0: + basis_amount = -max(-5, -3) + = -(-3) + = 3 + +Result: You have a spread of size 3 (the hedged portion) + The remaining -2 wBTC spot is a naked short +``` + +*** + +**Step 2: Calculate Existing Weight** + +This is the average penalty already applied to your positions (before considering the spread benefit): + +$$ +\text{existing\_weight} = \frac{\text{spot\_long\_weight} + \text{perp\_long\_weight}}{2} +$$ + +**Example** (using 20x leverage BTC): + +``` +spot_long_weight = 0.95 (for 20x leverage) +perp_long_weight = 0.95 (for 20x leverage) + +existing_weight = (0.95 + 0.95) / 2 = 0.95 +``` + +*** + +**Step 3: Calculate Spread Weight** + +The spread weight gives you better treatment than individual positions. It's calculated from the underlying product weights: + +**Base Spread Weight**: + +$$ +\text{spread\_weight} = 1 - \frac{1 - \text{product\_weight}}{5} +$$ + +Where product\_weight is: + +* **If spot\_amount > 0** (long spot, short perp): Use perp\_long\_weight +* **If spot\_amount ≤ 0** (short spot, long perp): Use spot\_long\_weight + +**Spread Weight Caps** (to manage extreme leverage): + +* **Initial health**: + +$$ +\text{spread\_weight} = \min(\text{spread\_weight}, 0.99) +$$ + +* **Maintenance health**: + +$$ +\text{spread\_weight} = \min(\text{spread\_weight}, 0.994) +$$ + +**Example** (using 20x leverage): + +``` +perp_long_weight = 0.95 + +Base spread_weight = 1 - (1 - 0.95) / 5 + = 1 - 0.05 / 5 + = 1 - 0.01 + = 0.99 + +Since 0.99 equals the cap, no adjustment needed. +Final spread_weight = 0.99 +``` + +*** + +**Step 4: Calculate Spread Health Contribution** + +$$ +\text{spread\_health\_increase} = \text{basis\_amount} \times (\text{spot\_price} + \text{perp\_price}) \times (\text{spread\_weight} - \text{existing\_weight}) +$$ + +**Complete Example**: + +``` +Position: +2 wBTC spot, -3 BTC-PERP +BTC spot price: $90,000 +BTC perp price: $90,000 +Both at 20x leverage (long_weight = 0.95) + +Step 1 - Basis Amount: + basis_amount = min(2, 3) = 2 + +Step 2 - Existing Weight: + existing_weight = (0.95 + 0.95) / 2 = 0.95 + +Step 3 - Spread Weight: + spread_weight = 1 - (1 - 0.95) / 5 = 0.99 + +Step 4 - Health Increase: + spread_health_increase = 2 × ($90,000 + $90,000) × (0.99 - 0.95) + = 2 × $180,000 × 0.04 + = $14,400 extra health! + +What this means: + - Without spread benefit: Each position uses weight 0.95 + - With spread benefit: The overlapping 2 BTC uses effective weight 0.99 + - You gain $14,400 in health, allowing more leverage on the spread +``` + +**Key Takeaway**: Spreads give you better health (higher effective weight) because both legs move together, reducing risk. The system recognizes this and gives you \~5x more leverage on the hedged portion! + +*** + +**High-Leverage Example (Spread Weight Cap)** + +At very high leverage, the spread weight caps become active. Suppose you have 50x leverage positions: + +``` +Position: +1 wBTC spot, -1 BTC-PERP +BTC spot price: $90,000 +BTC perp price: $90,000 +Both at 50x leverage (long_weight = 0.98) + +Step 1 - Basis Amount: + basis_amount = min(1, 1) = 1 + +Step 2 - Existing Weight: + existing_weight = (0.98 + 0.98) / 2 = 0.98 + +Step 3 - Spread Weight (INITIAL HEALTH): + Natural calculation: spread_weight = 1 - (1 - 0.98) / 5 + = 1 - 0.02 / 5 + = 1 - 0.004 + = 0.996 + + But 0.996 > 0.99 (initial health cap) + Final spread_weight = 0.99 (CAPPED!) + +Step 3 - Spread Weight (MAINTENANCE HEALTH): + Same natural calculation: 0.996 + + But 0.996 > 0.994 (maintenance health cap) + Final spread_weight = 0.994 (CAPPED!) + +Step 4 - Health Increase: + Initial health boost = 1 × ($90,000 + $90,000) × (0.99 - 0.98) + = $180,000 × 0.01 + = $1,800 + + Maintenance health boost = 1 × ($90,000 + $90,000) × (0.994 - 0.98) + = $180,000 × 0.014 + = $2,520 + +What this means: + - At 20x leverage (weight 0.95): Natural spread_weight = 0.99, no capping needed + - At 50x leverage (weight 0.98): Natural spread_weight = 0.996, gets capped to 0.99/0.994 + - The caps prevent excessive leverage on spreads while still providing significant benefit + - You still get improved health, but the benefit is limited at extreme leverage levels +``` + +{% hint style="info" %} +Notably, health is calculated by applying the penalty first as if there is no spread, then increasing health for all the spread pairs. The existing penalty means the already applied health deduction when no spread is taken into account. +{% endhint %} + +With these tools, Nado turns complexity into clarity, letting you navigate markets with measured confidence. + +*** diff --git a/src/cli/args.ts b/src/cli/args.ts index 99f2d6b..e35a867 100644 --- a/src/cli/args.ts +++ b/src/cli/args.ts @@ -4,7 +4,7 @@ export interface CliOptions { strategy?: StrategyId; silent: boolean; help: boolean; - exchange?: "aster" | "grvt" | "lighter" | "backpack"; + exchange?: "aster" | "grvt" | "lighter" | "backpack" | "paradex" | "nado"; } const STRATEGY_VALUES = new Set([ @@ -75,7 +75,14 @@ function assignStrategy(options: CliOptions, raw: string): void { function assignExchange(options: CliOptions, raw: string): void { const normalized = raw.trim().toLowerCase(); if (!normalized) return; - if (normalized === "aster" || normalized === "grvt" || normalized === "lighter" || normalized === "backpack") { + if ( + normalized === "aster" || + normalized === "grvt" || + normalized === "lighter" || + normalized === "backpack" || + normalized === "paradex" || + normalized === "nado" + ) { options.exchange = normalized as CliOptions["exchange"]; } else if (normalized === "gravity" || normalized === "grav" || normalized === "grv") { options.exchange = "grvt"; @@ -84,7 +91,7 @@ function assignExchange(options: CliOptions, raw: string): void { export function printCliHelp(): void { // eslint-disable-next-line no-console - console.log(`Usage: bun run index.ts [--strategy ] [--exchange ] [--silent]\n\n` + + console.log(`Usage: bun run index.ts [--strategy ] [--exchange ] [--silent]\n\n` + `Options:\n` + ` --strategy, -s Automatically start the specified strategy without the interactive menu.\n` + ` Aliases: offset, offset-maker for the offset maker engine.\n` + diff --git a/src/cli/strategy-runner.ts b/src/cli/strategy-runner.ts index 2e2fae5..d459b19 100644 --- a/src/cli/strategy-runner.ts +++ b/src/cli/strategy-runner.ts @@ -92,8 +92,8 @@ const STRATEGY_FACTORIES: Record = { throw new Error("Basis arbitrage strategy is disabled. Set ENABLE_BASIS_STRATEGY=true to enable it."); } const exchangeId = resolveExchangeId(); - if (exchangeId !== "aster") { - throw new Error("Basis arbitrage strategy currently only supports the Aster exchange"); + if (exchangeId !== "aster" && exchangeId !== "nado") { + throw new Error("Basis arbitrage strategy currently only supports the Aster and Nado exchanges"); } const adapter = createAdapterOrThrow(basisConfig.futuresSymbol); const engine = new BasisArbEngine(basisConfig, adapter); diff --git a/src/config.ts b/src/config.ts index 655cfa8..e4a18dc 100644 --- a/src/config.ts +++ b/src/config.ts @@ -31,6 +31,7 @@ const SYMBOL_PRIORITY_BY_EXCHANGE: Record { }; export const basisConfig: BasisArbConfig = { + // Default symbols depend on venue: Nado uses product symbols (e.g. BTC-PERP / KBTC), while Aster uses pair symbols. + // Users can always override via BASIS_* env vars. futuresSymbol: resolveBasisSymbol( ["BASIS_FUTURES_SYMBOL", "ASTER_FUTURES_SYMBOL", "ASTER_SYMBOL", "TRADE_SYMBOL"], - "ASTERUSDT" + (process.env.EXCHANGE ?? "").trim().toLowerCase() === "nado" ? "BTC-PERP" : "ASTERUSDT" ), spotSymbol: resolveBasisSymbol( ["BASIS_SPOT_SYMBOL", "ASTER_SPOT_SYMBOL", "ASTER_SYMBOL", "TRADE_SYMBOL"], - "ASTERUSDT" + (process.env.EXCHANGE ?? "").trim().toLowerCase() === "nado" ? "KBTC" : "ASTERUSDT" ), refreshIntervalMs: parseNumber(process.env.BASIS_REFRESH_INTERVAL_MS, 1000), maxLogEntries: parseNumber(process.env.BASIS_MAX_LOG_ENTRIES, 200), diff --git a/src/exchanges/adapter.ts b/src/exchanges/adapter.ts index 12474be..a102f59 100644 --- a/src/exchanges/adapter.ts +++ b/src/exchanges/adapter.ts @@ -27,6 +27,16 @@ export interface KlineListener { (klines: AsterKline[]): void; } +export interface FundingRateSnapshot { + symbol: string; + fundingRate: number; + updateTime: number; +} + +export interface FundingRateListener { + (snapshot: FundingRateSnapshot): void; +} + export interface ExchangePrecision { priceTick: number; qtyStep: number; @@ -45,6 +55,7 @@ export interface ExchangeAdapter { watchDepth(symbol: string, cb: DepthListener): void; watchTicker(symbol: string, cb: TickerListener): void; watchKlines(symbol: string, interval: string, cb: KlineListener): void; + watchFundingRate?(symbol: string, cb: FundingRateListener): void; createOrder(params: CreateOrderParams): Promise; cancelOrder(params: { symbol: string; orderId: number | string }): Promise; cancelOrders(params: { symbol: string; orderIdList: Array }): Promise; diff --git a/src/exchanges/create-adapter.ts b/src/exchanges/create-adapter.ts index d191951..23fde0f 100644 --- a/src/exchanges/create-adapter.ts +++ b/src/exchanges/create-adapter.ts @@ -4,6 +4,7 @@ import { GrvtExchangeAdapter, type GrvtCredentials } from "./grvt/adapter"; import { LighterExchangeAdapter, type LighterCredentials } from "./lighter/adapter"; import { BackpackExchangeAdapter, type BackpackCredentials } from "./backpack/adapter"; import { ParadexExchangeAdapter, type ParadexCredentials } from "./paradex/adapter"; +import { NadoExchangeAdapter, type NadoCredentials } from "./nado/adapter"; export interface ExchangeFactoryOptions { symbol: string; @@ -13,9 +14,10 @@ export interface ExchangeFactoryOptions { lighter?: LighterCredentials; backpack?: BackpackCredentials; paradex?: ParadexCredentials; + nado?: NadoCredentials; } -export type SupportedExchangeId = "aster" | "grvt" | "lighter" | "backpack" | "paradex"; +export type SupportedExchangeId = "aster" | "grvt" | "lighter" | "backpack" | "paradex" | "nado"; export function resolveExchangeId(value?: string | null): SupportedExchangeId { const fallback = (value ?? process.env.EXCHANGE ?? process.env.TRADE_EXCHANGE ?? "aster") @@ -26,6 +28,7 @@ export function resolveExchangeId(value?: string | null): SupportedExchangeId { if (fallback === "lighter") return "lighter"; if (fallback === "backpack") return "backpack"; if (fallback === "paradex") return "paradex"; + if (fallback === "nado") return "nado"; return "aster"; } @@ -34,6 +37,7 @@ export function getExchangeDisplayName(id: SupportedExchangeId): string { if (id === "lighter") return "Lighter"; if (id === "backpack") return "Backpack"; if (id === "paradex") return "Paradex"; + if (id === "nado") return "Nado"; return "AsterDex"; } @@ -51,5 +55,8 @@ export function createExchangeAdapter(options: ExchangeFactoryOptions): Exchange if (id === "paradex") { return new ParadexExchangeAdapter({ ...options.paradex, symbol: options.symbol }); } + if (id === "nado") { + return new NadoExchangeAdapter({ ...options.nado, symbol: options.symbol }); + } return new AsterExchangeAdapter({ ...options.aster, symbol: options.symbol }); } diff --git a/src/exchanges/nado/adapter.ts b/src/exchanges/nado/adapter.ts new file mode 100644 index 0000000..2a966eb --- /dev/null +++ b/src/exchanges/nado/adapter.ts @@ -0,0 +1,219 @@ +import { setTimeout, clearTimeout } from "timers"; +import type { + AccountListener, + DepthListener, + ExchangeAdapter, + ExchangePrecision, + FundingRateListener, + KlineListener, + OrderListener, + TickerListener, +} from "../adapter"; +import type { AsterOrder, CreateOrderParams } from "../types"; +import { extractMessage } from "../../utils/errors"; +import { NadoGateway, type NadoGatewayOptions } from "./gateway"; +import type { ChainEnv } from "@nadohq/shared"; +import type { Address } from "viem"; + +export interface NadoCredentials { + env?: ChainEnv; + symbol?: string; + subaccountOwner?: Address; + subaccountName?: string; + signerPrivateKey?: string; + gatewayWsUrl?: string; + subscriptionsWsUrl?: string; + archiveUrl?: string; + triggerUrl?: string; + pollIntervals?: NadoGatewayOptions["pollIntervals"]; + marketSlippagePct?: number; + stopTriggerSource?: NadoGatewayOptions["stopTriggerSource"]; +} + +export class NadoExchangeAdapter implements ExchangeAdapter { + readonly id = "nado"; + + private readonly gateway: NadoGateway; + private readonly symbol: string; + private initPromise: Promise | null = null; + private readonly initContexts = new Set(); + private retryTimer: ReturnType | null = null; + private retryDelayMs = 3000; + private lastInitErrorAt = 0; + + constructor(credentials: NadoCredentials = {}) { + const signerPrivateKey = credentials.signerPrivateKey ?? process.env.NADO_SIGNER_PRIVATE_KEY; + const subaccountOwner = (credentials.subaccountOwner ?? + (process.env.NADO_SUBACCOUNT_OWNER as Address | undefined) ?? + (process.env.NADO_EVM_ADDRESS as Address | undefined)) as Address | undefined; + const symbol = + credentials.symbol ?? + process.env.NADO_SYMBOL ?? + process.env.TRADE_SYMBOL ?? + "BTC-PERP"; + + if (!signerPrivateKey) { + throw new Error("Missing NADO_SIGNER_PRIVATE_KEY environment variable"); + } + if (!subaccountOwner) { + throw new Error("Missing NADO_SUBACCOUNT_OWNER (or NADO_EVM_ADDRESS) environment variable"); + } + + this.symbol = symbol; + this.gateway = new NadoGateway({ + env: credentials.env, + symbol, + subaccountOwner, + subaccountName: credentials.subaccountName ?? process.env.NADO_SUBACCOUNT_NAME ?? "default", + signerPrivateKey, + gatewayWsUrl: credentials.gatewayWsUrl ?? process.env.NADO_GATEWAY_WS_URL, + subscriptionsWsUrl: credentials.subscriptionsWsUrl ?? process.env.NADO_SUBSCRIPTIONS_WS_URL, + archiveUrl: credentials.archiveUrl ?? process.env.NADO_ARCHIVE_URL, + triggerUrl: credentials.triggerUrl ?? process.env.NADO_TRIGGER_URL, + pollIntervals: credentials.pollIntervals, + marketSlippagePct: + credentials.marketSlippagePct ?? + (process.env.NADO_MARKET_SLIPPAGE_PCT ? Number(process.env.NADO_MARKET_SLIPPAGE_PCT) : undefined), + stopTriggerSource: + credentials.stopTriggerSource ?? + (process.env.NADO_STOP_TRIGGER_SOURCE as NadoGatewayOptions["stopTriggerSource"] | undefined), + logger: (context, error) => this.logError(context, error), + }); + } + + supportsTrailingStops(): boolean { + return false; + } + + watchAccount(cb: AccountListener): void { + void this.ensureInitialized("watchAccount"); + this.gateway.onAccount(this.safeInvoke("watchAccount", cb)); + } + + watchOrders(cb: OrderListener): void { + void this.ensureInitialized("watchOrders"); + this.gateway.onOrders(this.safeInvoke("watchOrders", cb)); + } + + watchDepth(symbol: string, cb: DepthListener): void { + void this.ensureInitialized(`watchDepth:${symbol}`); + this.gateway.onDepth(symbol, this.safeInvoke("watchDepth", cb)); + } + + watchTicker(symbol: string, cb: TickerListener): void { + void this.ensureInitialized(`watchTicker:${symbol}`); + this.gateway.onTicker(symbol, this.safeInvoke("watchTicker", cb)); + } + + watchKlines(symbol: string, interval: string, cb: KlineListener): void { + void this.ensureInitialized(`watchKlines:${symbol}:${interval}`); + this.gateway.onKlines(symbol, interval, this.safeInvoke("watchKlines", cb)); + } + + watchFundingRate(symbol: string, cb: FundingRateListener): void { + void this.ensureInitialized(`watchFundingRate:${symbol}`); + this.gateway.onFundingRate(symbol, this.safeInvoke("watchFundingRate", cb)); + } + + async createOrder(params: CreateOrderParams): Promise { + await this.ensureInitialized("createOrder"); + return this.gateway.createOrder(params); + } + + async cancelOrder(params: { symbol: string; orderId: number | string }): Promise { + await this.ensureInitialized("cancelOrder"); + await this.gateway.cancelOrder(params); + } + + async cancelOrders(params: { symbol: string; orderIdList: Array }): Promise { + await this.ensureInitialized("cancelOrders"); + await this.gateway.cancelOrders(params); + } + + async cancelAllOrders(params: { symbol: string }): Promise { + await this.ensureInitialized("cancelAllOrders"); + await this.gateway.cancelAllOrders(params); + } + + async getPrecision(): Promise { + try { + return await this.gateway.getPrecision(this.symbol); + } catch (error) { + this.logError("getPrecision", error); + return null; + } + } + + private safeInvoke void>(context: string, cb: T): T { + const wrapped = ((...args: any[]) => { + try { + cb(...args); + } catch (error) { + console.error(`[NadoExchangeAdapter] ${context} handler failed: ${extractMessage(error)}`); + } + }) as T; + return wrapped; + } + + private ensureInitialized(context?: string): Promise { + if (!this.initPromise) { + this.initContexts.clear(); + this.initPromise = this.gateway + .ensureInitialized(this.symbol) + .then((value) => { + this.clearRetry(); + return value; + }) + .catch((error) => { + this.handleInitError("initialize", error); + this.initPromise = null; + this.scheduleRetry(); + throw error; + }); + } + + if (context && !this.initContexts.has(context)) { + this.initContexts.add(context); + this.initPromise.catch((error) => { + this.handleInitError(context, error); + this.scheduleRetry(); + }); + } + + return this.initPromise; + } + + private scheduleRetry(): void { + if (this.retryTimer) return; + this.retryTimer = setTimeout(() => { + this.retryTimer = null; + if (this.initPromise) return; + this.retryDelayMs = Math.min(this.retryDelayMs * 2, 60_000); + void this.ensureInitialized("retry"); + }, this.retryDelayMs); + } + + private clearRetry(): void { + if (this.retryTimer) { + clearTimeout(this.retryTimer); + this.retryTimer = null; + } + this.retryDelayMs = 3000; + } + + private handleInitError(context: string, error: unknown): void { + const now = Date.now(); + if (now - this.lastInitErrorAt < 5000) return; + this.lastInitErrorAt = now; + console.error(`[NadoExchangeAdapter] ${context} failed`, error); + } + + private logError(context: string, error: unknown): void { + const detail = extractMessage(error); + const message = `[NadoExchangeAdapter] ${context} failed: ${detail}`; + const criticalContexts = ["initialize", "accountPoll", "ordersPoll", "triggerOrdersPoll"]; + if (criticalContexts.some((prefix) => context.startsWith(prefix)) || process.env.NADO_DEBUG === "1") { + console.error(message); + } + } +} diff --git a/src/exchanges/nado/gateway.ts b/src/exchanges/nado/gateway.ts new file mode 100644 index 0000000..fad40c1 --- /dev/null +++ b/src/exchanges/nado/gateway.ts @@ -0,0 +1,2110 @@ +import NodeWebSocket from "ws"; +import BigNumber from "bignumber.js"; +import { IndexerClient } from "@nadohq/indexer-client"; +import { TriggerClient } from "@nadohq/trigger-client"; +import { ENGINE_WS_CLIENT_ENDPOINTS, ENGINE_WS_SUBSCRIPTION_CLIENT_ENDPOINTS } from "@nadohq/engine-client"; +import type { ChainEnv, WalletClientWithAccount } from "@nadohq/shared"; +import { + getNadoEIP712Values, + getNadoEIP712Domain, + getSignedTransactionRequest, + getOrderNonce, + getOrderVerifyingAddress, + packOrderAppendix, +} from "@nadohq/shared"; +import { createWalletClient, custom, privateKeyToAccount, type Address } from "viem"; +import type { + AccountListener, + DepthListener, + FundingRateListener, + KlineListener, + OrderListener, + TickerListener, +} from "../adapter"; +import type { + AsterAccountAsset, + AsterAccountPosition, + AsterAccountSnapshot, + AsterDepth, + AsterKline, + AsterOrder, + AsterTicker, + CreateOrderParams, + TimeInForce, +} from "../types"; +import type { PriceTriggerRequirementType } from "@nadohq/trigger-client"; +import type { + NadoBestBidOfferEvent, + NadoContractsResponse, + NadoFundingRateEvent, + NadoLatestCandlestickEvent, + NadoOrderUpdateEvent, + NadoPositionChangeEvent, + NadoSubaccountInfoResponse, + NadoSubaccountOrdersResponse, + NadoSubscriptionAck, + NadoSymbolsResponse, + NadoTradeEvent, +} from "./types"; + +const WebSocketCtor: typeof NodeWebSocket = NodeWebSocket as unknown as typeof NodeWebSocket; + +const X18 = new BigNumber("1e18"); + +type NadoOrderExecution = "default" | "ioc" | "fok" | "post_only"; +type NadoTriggerPriceSource = "oracle" | "last" | "mid"; + +const DEFAULT_ACCOUNT_POLL_MS = 5_000; +const DEFAULT_ORDERS_POLL_MS = 2_000; +const DEFAULT_TRIGGER_ORDERS_POLL_MS = 5_000; +const DEFAULT_KLINES_LIMIT = 200; +const WS_PING_INTERVAL_MS = 30_000; +const WS_STALE_TIMEOUT_MS = 75_000; + +const DEFAULT_MARKET_SLIPPAGE_PCT = 0.01; + +export interface NadoGatewayOptions { + env?: ChainEnv; + symbol: string; + subaccountOwner: Address; + subaccountName?: string; + signerPrivateKey: string; + gatewayWsUrl?: string; + subscriptionsWsUrl?: string; + archiveUrl?: string; + triggerUrl?: string; + pollIntervals?: { + account?: number; + orders?: number; + triggerOrders?: number; + }; + marketSlippagePct?: number; + stopTriggerSource?: NadoTriggerPriceSource; + logger?: (context: string, error: unknown) => void; +} + +type SymbolMeta = { + productId: number; + symbol: string; + type: "spot" | "perp"; + priceIncrementX18: string; + sizeIncrementX18: string; + minSizeX18: string; +}; + +type BestBidOffer = { + bidX18: string; + askX18: string; + bidQtyX18: string; + askQtyX18: string; + timestampNs: string; +}; + +type LocalOrder = { + digest: string; + productId: number; + priceX18: string; + amountX18: string; + unfilledAmountX18: string; + appendix?: string; + orderType?: string; + placedAtSec?: number; + clientId?: number | null; +}; + +type TriggerOrder = { + digest: string; + productId: number; + priceX18: string; + amountX18: string; + appendix?: string; + trigger: { stopPriceX18: string }; + status: string; + updatedAtMs: number; +}; + +type PollIntervals = { + account: number; + orders: number; + triggerOrders: number; +}; + +function parseEnvChainEnv(value?: string | null): ChainEnv | undefined { + const normalized = (value ?? "").trim(); + if (!normalized) return undefined; + if (normalized === "inkMainnet" || normalized === "inkTestnet" || normalized === "local") { + return normalized; + } + const lowered = normalized.toLowerCase(); + if (["mainnet", "prod", "production", "ink", "ink-mainnet"].includes(lowered)) return "inkMainnet"; + if (["testnet", "test", "sepolia", "ink-sepolia", "ink-testnet"].includes(lowered)) return "inkTestnet"; + return undefined; +} + +function normalizeSymbolInput(value: string): string { + const raw = (value ?? "").trim(); + const upper = raw.toUpperCase(); + if (!upper) return upper; + if (/^[A-Z0-9]+USDT0$/.test(upper) && !upper.includes("-") && !upper.includes("/")) { + const base = upper.replace(/USDT0$/, ""); + if (base) return `${base}-PERP`; + } + if (/^[A-Z0-9]+PERP$/.test(upper) && !upper.includes("-")) { + const base = upper.replace(/PERP$/, ""); + if (base) return `${base}-PERP`; + } + return upper.replace("/", "-"); +} + +function toX18BigInt(value: BigNumber.Value): bigint { + const scaled = new BigNumber(value).multipliedBy(X18).integerValue(BigNumber.ROUND_DOWN); + return BigInt(scaled.toFixed(0)); +} + +function fromX18(valueX18: BigNumber.Value): BigNumber { + return new BigNumber(valueX18).div(X18); +} + +function toBigNumberFromDecimalish(value: unknown): BigNumber | null { + if (value == null) return null; + if (BigNumber.isBigNumber(value)) return value as BigNumber; + if (typeof value === "string" || typeof value === "number") { + const parsed = new BigNumber(value); + return parsed.isFinite() ? parsed : null; + } + if (typeof (value as any).toFixed === "function") { + try { + const text = (value as any).toFixed(); + const parsed = new BigNumber(text); + return parsed.isFinite() ? parsed : null; + } catch { + return null; + } + } + if (typeof (value as any).toString === "function") { + try { + const parsed = new BigNumber(String(value)); + return parsed.isFinite() ? parsed : null; + } catch { + return null; + } + } + return null; +} + +function safeToNumber(value: string): number { + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : 0; +} + +function nowMs(): number { + return Date.now(); +} + +function nsToMs(ns: string): number { + try { + const nanos = BigInt(ns); + return Number(nanos / 1_000_000n); + } catch { + return nowMs(); + } +} + +function mapTimeInForceToExecution(timeInForce: TimeInForce | undefined): NadoOrderExecution { + switch (timeInForce) { + case "IOC": + return "ioc"; + case "FOK": + return "fok"; + case "GTX": + return "post_only"; + default: + return "default"; + } +} + +function mapExecutionToOrderType(type: NadoOrderExecution | undefined): "LIMIT" | "MARKET" { + if (type === "ioc" || type === "fok") return "MARKET"; + return "LIMIT"; +} + +function parseNadoErrorCode(value: unknown): number | null { + const code = Number(value); + return Number.isFinite(code) ? code : null; +} + +function createWalletClientFromPrivateKey(privateKey: string): WalletClientWithAccount { + const trimmed = privateKey.trim(); + if (!/^0x[0-9a-fA-F]{64}$/.test(trimmed)) { + throw new Error("NADO_SIGNER_PRIVATE_KEY must be a 0x-prefixed 32-byte hex string"); + } + const account = privateKeyToAccount(trimmed as `0x${string}`); + return createWalletClient({ + account, + transport: custom({ + request: async () => { + throw new Error("RPC transport is not configured for Nado signing"); + }, + }), + }); +} + +export class NadoGateway { + private readonly env: ChainEnv; + private readonly gatewayWsUrl: string; + private readonly subscriptionsWsUrl: string; + private readonly archiveUrl: string; + private readonly triggerUrl: string; + private readonly logger: (context: string, error: unknown) => void; + + private readonly walletClient: WalletClientWithAccount; + private readonly indexer: IndexerClient; + private readonly trigger: TriggerClient; + + private readonly subaccountOwner: Address; + private readonly subaccountName: string; + + private chainId: number | null = null; + private endpointAddr: Address | null = null; + + private initialized = false; + private initPromise: Promise | null = null; + + private readonly pollIntervals: PollIntervals; + private readonly marketSlippagePct: number; + private readonly stopTriggerSource: NadoTriggerPriceSource; + + private readonly symbolMetaBySymbol = new Map(); + private readonly symbolMetaByProductId = new Map(); + private readonly displaySymbolByCanonical = new Map(); + + private readonly bestBidOfferByProductId = new Map(); + private readonly lastTradePriceByProductId = new Map(); + private readonly fundingRateByProductId = new Map(); + + private readonly openOrdersByDigest = new Map(); + private readonly triggerOrdersByDigest = new Map(); + + private accountSnapshot: AsterAccountSnapshot | null = null; + private lastAccountSyncAt = 0; + + private gatewayWs: NodeWebSocket | null = null; + private subscriptionWs: NodeWebSocket | null = null; + private gatewayPingTimer: ReturnType | null = null; + private subscriptionPingTimer: ReturnType | null = null; + private gatewayLastMessageAt = 0; + private subscriptionLastMessageAt = 0; + private subscriptionConnectPromise: Promise | null = null; + private subscriptionReconnectTimer: ReturnType | null = null; + private subscriptionReconnectDelayMs = 1_000; + + private gatewayQueue: Promise = Promise.resolve(); + + private readonly accountListeners = new Set(); + private readonly orderListeners = new Set(); + private readonly depthListeners = new Map>(); + private readonly tickerListeners = new Map>(); + private readonly fundingRateListeners = new Map>(); + private readonly klineListeners = new Map>(); + + private accountPollTimer: ReturnType | null = null; + private ordersPollTimer: ReturnType | null = null; + private triggerOrdersPollTimer: ReturnType | null = null; + private klinesState = new Map(); + + private subscriptionRequestId = 1; + private subscriptionAuthComplete = false; + private readonly subscriptionStreams = new Set(); + + private primarySymbol: string; + + constructor(options: NadoGatewayOptions) { + this.env = options.env ?? parseEnvChainEnv(process.env.NADO_ENV) ?? "inkMainnet"; + this.gatewayWsUrl = options.gatewayWsUrl ?? ENGINE_WS_CLIENT_ENDPOINTS[this.env]; + this.subscriptionsWsUrl = + options.subscriptionsWsUrl ?? ENGINE_WS_SUBSCRIPTION_CLIENT_ENDPOINTS[this.env]; + this.archiveUrl = + options.archiveUrl ?? + (process.env.NADO_ARCHIVE_URL ?? (this.env === "inkMainnet" + ? "https://archive.prod.nado.xyz/v1" + : "https://archive.test.nado.xyz/v1")); + this.triggerUrl = + options.triggerUrl ?? + (process.env.NADO_TRIGGER_URL ?? (this.env === "inkMainnet" + ? "https://trigger.prod.nado.xyz/v1" + : "https://trigger.test.nado.xyz/v1")); + + this.logger = options.logger ?? ((context, error) => console.error(`[NadoGateway] ${context}:`, error)); + this.walletClient = createWalletClientFromPrivateKey(options.signerPrivateKey); + this.indexer = new IndexerClient({ url: this.archiveUrl, walletClient: this.walletClient }); + this.trigger = new TriggerClient({ url: this.triggerUrl, walletClient: this.walletClient }); + + this.subaccountOwner = options.subaccountOwner; + this.subaccountName = options.subaccountName ?? "default"; + + this.pollIntervals = { + account: options.pollIntervals?.account ?? DEFAULT_ACCOUNT_POLL_MS, + orders: options.pollIntervals?.orders ?? DEFAULT_ORDERS_POLL_MS, + triggerOrders: options.pollIntervals?.triggerOrders ?? DEFAULT_TRIGGER_ORDERS_POLL_MS, + }; + + const slippage = options.marketSlippagePct ?? safeToNumber(process.env.NADO_MARKET_SLIPPAGE_PCT ?? ""); + this.marketSlippagePct = Number.isFinite(slippage) && slippage > 0 ? slippage : DEFAULT_MARKET_SLIPPAGE_PCT; + this.stopTriggerSource = options.stopTriggerSource ?? (process.env.NADO_STOP_TRIGGER_SOURCE as NadoTriggerPriceSource) ?? "oracle"; + + this.primarySymbol = options.symbol; + this.rememberDisplaySymbol(this.primarySymbol); + } + + async ensureInitialized(symbol?: string): Promise { + if (symbol) { + this.primarySymbol = symbol; + this.rememberDisplaySymbol(symbol); + } + if (this.initialized) return; + if (this.initPromise) return this.initPromise; + this.initPromise = this.doInitialize().then( + () => { + this.initialized = true; + }, + (error) => { + this.initPromise = null; + throw error; + } + ); + return this.initPromise; + } + + onAccount(listener: AccountListener): () => void { + this.accountListeners.add(listener); + if (this.accountSnapshot) { + listener(this.accountSnapshot); + } + + void this.ensureInitialized() + .then(() => this.ensureSubscriptionConnected()) + .then(() => this.ensureAccountStreamsForListeners()) + .catch((error) => this.logger("account:subscribe", error)); + + return () => { + this.accountListeners.delete(listener); + }; + } + + onOrders(listener: OrderListener): () => void { + this.orderListeners.add(listener); + // Emit current view if we have one. + if (this.openOrdersByDigest.size > 0 || this.triggerOrdersByDigest.size > 0) { + listener(this.buildAsterOrdersSnapshot()); + } + + void this.ensureInitialized() + .then(() => this.ensureSubscriptionConnected()) + .then(() => this.ensureAccountStreamsForListeners()) + .catch((error) => this.logger("orders:subscribe", error)); + + return () => { + this.orderListeners.delete(listener); + }; + } + + onDepth(symbol: string, listener: DepthListener): () => void { + const key = this.rememberDisplaySymbol(symbol); + const set = this.depthListeners.get(key) ?? new Set(); + set.add(listener); + this.depthListeners.set(key, set); + const meta = this.symbolMetaBySymbol.get(key); + if (meta) { + const snapshot = this.buildDepthSnapshot(meta.productId, this.resolveDisplaySymbol(key, meta.symbol)); + if (snapshot) listener(snapshot); + } + + void this.ensureInitialized() + .then(() => this.ensureSubscriptionConnected()) + .then(() => this.ensureMarketStreamsForListeners()) + .catch((error) => this.logger("depth:subscribe", error)); + + return () => { + const listeners = this.depthListeners.get(key); + if (listeners) { + listeners.delete(listener); + if (listeners.size === 0) this.depthListeners.delete(key); + } + }; + } + + onTicker(symbol: string, listener: TickerListener): () => void { + const key = this.rememberDisplaySymbol(symbol); + const set = this.tickerListeners.get(key) ?? new Set(); + set.add(listener); + this.tickerListeners.set(key, set); + const meta = this.symbolMetaBySymbol.get(key); + if (meta) { + const snapshot = this.buildTickerSnapshot(meta.productId, this.resolveDisplaySymbol(key, meta.symbol)); + if (snapshot) listener(snapshot); + } + + void this.ensureInitialized() + .then(() => this.ensureSubscriptionConnected()) + .then(() => this.ensureMarketStreamsForListeners()) + .catch((error) => this.logger("ticker:subscribe", error)); + + return () => { + const listeners = this.tickerListeners.get(key); + if (listeners) { + listeners.delete(listener); + if (listeners.size === 0) this.tickerListeners.delete(key); + } + }; + } + + onFundingRate(symbol: string, listener: FundingRateListener): () => void { + const key = this.rememberDisplaySymbol(symbol); + const set = this.fundingRateListeners.get(key) ?? new Set(); + set.add(listener); + this.fundingRateListeners.set(key, set); + + const meta = this.symbolMetaBySymbol.get(key); + if (meta) { + const cached = this.fundingRateByProductId.get(meta.productId); + if (cached) { + const updateSec = Number(cached.updateTimeSec); + const updateTime = Number.isFinite(updateSec) ? updateSec * 1000 : nsToMs(cached.timestampNs); + const fundingRate = fromX18(cached.rateX18).toNumber(); + if (Number.isFinite(fundingRate)) { + listener({ + symbol: this.resolveDisplaySymbol(key, meta.symbol), + fundingRate, + updateTime, + }); + } + } + } + + void this.ensureInitialized() + .then(() => this.ensureSubscriptionConnected()) + .then(() => this.ensureFundingStreamsForListeners()) + .catch((error) => this.logger("funding_rate:subscribe", error)); + + return () => { + const listeners = this.fundingRateListeners.get(key); + if (listeners) { + listeners.delete(listener); + if (listeners.size === 0) this.fundingRateListeners.delete(key); + } + }; + } + + onKlines(symbol: string, interval: string, listener: KlineListener): () => void { + const keySymbol = this.rememberDisplaySymbol(symbol); + const key = `${keySymbol}:${interval}`; + const set = this.klineListeners.get(key) ?? new Set(); + set.add(listener); + this.klineListeners.set(key, set); + const state = this.klinesState.get(key); + if (state?.klines?.length) { + listener(state.klines); + } + + void this.ensureInitialized() + .then(() => this.ensureSubscriptionConnected()) + .then(() => this.ensureKlinesStream(key)) + .catch((error) => this.logger("klines:subscribe", error)); + + return () => { + const listeners = this.klineListeners.get(key); + if (listeners) { + listeners.delete(listener); + if (listeners.size === 0) this.klineListeners.delete(key); + } + }; + } + + async createOrder(params: CreateOrderParams): Promise { + await this.ensureInitialized(params.symbol); + const meta = this.getSymbolMetaOrThrow(params.symbol); + if (params.type === "STOP_MARKET") { + return this.createStopOrder(meta, params); + } + if (params.type === "TRAILING_STOP_MARKET") { + throw new Error("Nado does not support trailing stop orders"); + } + if (params.type === "MARKET") { + return this.createMarketOrder(meta, params); + } + return this.createLimitOrder(meta, params); + } + + async cancelOrder(params: { symbol: string; orderId: number | string }): Promise { + await this.cancelOrders({ symbol: params.symbol, orderIdList: [params.orderId] }); + } + + async cancelOrders(params: { symbol: string; orderIdList: Array }): Promise { + await this.ensureInitialized(params.symbol); + const meta = this.getSymbolMetaOrThrow(params.symbol); + const digests = params.orderIdList.map((value) => String(value)); + const triggerDigests = digests.filter((digest) => this.triggerOrdersByDigest.has(digest)); + const engineDigests = digests.filter((digest) => !this.triggerOrdersByDigest.has(digest)); + + if (engineDigests.length > 0) { + try { + await this.sendCancelOrders(meta.productId, engineDigests); + for (const digest of engineDigests) { + this.openOrdersByDigest.delete(digest); + } + } catch (error) { + // If engine cancellation fails due to unknown order, we still try trigger cancellation. + if (!triggerDigests.length) { + throw error; + } + } + } + + if (triggerDigests.length > 0) { + await this.cancelTriggerOrders(meta.productId, triggerDigests); + for (const digest of triggerDigests) { + this.triggerOrdersByDigest.delete(digest); + } + } + + this.emitOrders(); + } + + async cancelAllOrders(params: { symbol: string }): Promise { + await this.ensureInitialized(params.symbol); + const meta = this.getSymbolMetaOrThrow(params.symbol); + await Promise.all([ + this.sendCancelProductOrders([meta.productId]).catch((error) => { + // Best-effort; trigger cancellations still apply. + this.logger("cancelAllOrders:engine", error); + }), + this.cancelTriggerProductOrders([meta.productId]).catch((error) => { + this.logger("cancelAllOrders:trigger", error); + }), + ]); + // Reset local caches for the product. + for (const [digest, order] of Array.from(this.openOrdersByDigest.entries())) { + if (order.productId === meta.productId) this.openOrdersByDigest.delete(digest); + } + for (const [digest, order] of Array.from(this.triggerOrdersByDigest.entries())) { + if (order.productId === meta.productId) this.triggerOrdersByDigest.delete(digest); + } + this.emitOrders(); + } + + async getPrecision(symbol: string): Promise<{ + priceTick: number; + qtyStep: number; + priceDecimals?: number; + sizeDecimals?: number; + marketId?: number; + minBaseAmount?: number; + minQuoteAmount?: number; + } | null> { + await this.ensureInitialized(symbol); + const symbolKey = this.rememberDisplaySymbol(symbol); + const meta = this.symbolMetaBySymbol.get(symbolKey); + if (!meta) return null; + const priceTick = fromX18(meta.priceIncrementX18).toNumber(); + const qtyStep = fromX18(meta.sizeIncrementX18).toNumber(); + const minBaseAmount = fromX18(meta.minSizeX18).toNumber(); + return { + priceTick, + qtyStep, + marketId: meta.productId, + minBaseAmount, + }; + } + + private async doInitialize(): Promise { + await this.openGatewayWebsocket(); + const contracts = await this.queryContracts(); + this.chainId = Number(contracts.chain_id); + this.endpointAddr = contracts.endpoint_addr as Address; + await this.queryAndCacheSymbols(); + + await this.ensureSubscriptionConnected(); + + await Promise.all([ + this.refreshAccountSnapshot(), + this.refreshOrdersSnapshot(), + this.refreshTriggerOrdersSnapshot(), + this.bootstrapKlinesForActiveListeners(), + ]); + + this.startPolling(); + this.initialized = true; + } + + private getSymbolMetaOrThrow(symbol: string): SymbolMeta { + const normalized = this.rememberDisplaySymbol(symbol); + const meta = this.symbolMetaBySymbol.get(normalized); + if (meta) return meta; + const available = Array.from(this.symbolMetaBySymbol.keys()) + .slice(0, 15) + .join(", "); + throw new Error(`Unknown Nado symbol: ${symbol}. Available examples: ${available}${this.symbolMetaBySymbol.size > 15 ? ", ..." : ""}`); + } + + private async openGatewayWebsocket(): Promise { + if (this.gatewayWs && this.gatewayWs.readyState === WebSocketCtor.OPEN) return; + await new Promise((resolve, reject) => { + const ws = new WebSocketCtor(this.gatewayWsUrl); + this.gatewayWs = ws; + this.gatewayLastMessageAt = nowMs(); + + const cleanup = () => { + ws.removeAllListeners(); + }; + + ws.on("open", () => { + this.gatewayLastMessageAt = nowMs(); + this.startGatewayPing(); + resolve(); + }); + ws.on("message", () => { + this.gatewayLastMessageAt = nowMs(); + }); + ws.on("error", (err) => { + cleanup(); + reject(err); + }); + ws.on("close", () => { + cleanup(); + this.stopGatewayPing(); + }); + }); + } + + private hasActiveSubscriptionListeners(): boolean { + if (this.accountListeners.size > 0) return true; + if (this.orderListeners.size > 0) return true; + if (this.depthListeners.size > 0) return true; + if (this.tickerListeners.size > 0) return true; + if (this.fundingRateListeners.size > 0) return true; + if (this.klineListeners.size > 0) return true; + return false; + } + + private resetSubscriptionReconnect(): void { + if (this.subscriptionReconnectTimer) { + clearTimeout(this.subscriptionReconnectTimer); + this.subscriptionReconnectTimer = null; + } + this.subscriptionReconnectDelayMs = 1_000; + } + + private scheduleSubscriptionReconnect(reason: string): void { + if (this.subscriptionReconnectTimer) return; + if (!this.hasActiveSubscriptionListeners()) return; + const delay = this.subscriptionReconnectDelayMs; + this.subscriptionReconnectTimer = setTimeout(() => { + this.subscriptionReconnectTimer = null; + void this.ensureSubscriptionConnected().catch((error) => { + this.logger(`subscriptionReconnect:${reason}`, error); + this.scheduleSubscriptionReconnect("retry"); + }); + }, delay); + this.subscriptionReconnectDelayMs = Math.min(this.subscriptionReconnectDelayMs * 2, 60_000); + } + + private async ensureSubscriptionConnected(): Promise { + if (this.subscriptionWs && this.subscriptionWs.readyState === WebSocketCtor.OPEN && this.subscriptionAuthComplete) { + return; + } + if (this.subscriptionConnectPromise) { + return this.subscriptionConnectPromise; + } + this.subscriptionConnectPromise = this.openSubscriptionWebsocket() + .then(() => { + this.resetSubscriptionReconnect(); + }) + .finally(() => { + this.subscriptionConnectPromise = null; + }); + return this.subscriptionConnectPromise; + } + + private async openSubscriptionWebsocket(): Promise { + const existing = this.subscriptionWs; + if (existing && existing.readyState === WebSocketCtor.OPEN) { + if (!this.subscriptionAuthComplete) { + await this.authenticateSubscription(); + await this.resubscribeAllStreams(); + } + return; + } + + this.subscriptionAuthComplete = false; + this.subscriptionStreams.clear(); + + const ws = new WebSocketCtor(this.subscriptionsWsUrl, { perMessageDeflate: true }); + this.subscriptionWs = ws; + this.subscriptionLastMessageAt = nowMs(); + + await new Promise((resolve, reject) => { + const cleanup = () => { + ws.off("open", handleOpen); + ws.off("error", handleError); + ws.off("close", handleClose); + }; + const handleOpen = () => { + cleanup(); + this.subscriptionLastMessageAt = nowMs(); + this.startSubscriptionPing(); + resolve(); + }; + const handleError = (error: unknown) => { + cleanup(); + reject(error); + }; + const handleClose = () => { + cleanup(); + reject(new Error("Subscription websocket closed before open")); + }; + + ws.once("open", handleOpen); + ws.once("error", handleError); + ws.once("close", handleClose); + }).catch((error) => { + try { + ws.removeAllListeners(); + } catch { + // ignore + } + if (this.subscriptionWs === ws) { + this.subscriptionWs = null; + } + throw error; + }); + + ws.on("message", (data) => { + this.subscriptionLastMessageAt = nowMs(); + this.handleSubscriptionMessage(data); + }); + + ws.on("close", () => { + ws.removeAllListeners(); + this.stopSubscriptionPing(); + this.subscriptionAuthComplete = false; + this.subscriptionStreams.clear(); + if (this.subscriptionWs === ws) { + this.subscriptionWs = null; + } + this.scheduleSubscriptionReconnect("close"); + }); + + ws.on("error", (error) => { + this.logger("subscriptionWs:error", error); + }); + + await this.authenticateSubscription(); + await this.resubscribeAllStreams(); + } + + private startGatewayPing(): void { + if (this.gatewayPingTimer) return; + this.gatewayPingTimer = setInterval(() => { + const ws = this.gatewayWs; + if (!ws || ws.readyState !== WebSocketCtor.OPEN) return; + const now = nowMs(); + if (now - this.gatewayLastMessageAt > WS_STALE_TIMEOUT_MS) { + try { + ws.terminate(); + } catch (error) { + this.logger("gatewayWs:terminate", error); + } + return; + } + try { + ws.ping(); + } catch (error) { + this.logger("gatewayWs:ping", error); + } + }, WS_PING_INTERVAL_MS); + } + + private stopGatewayPing(): void { + if (this.gatewayPingTimer) { + clearInterval(this.gatewayPingTimer); + this.gatewayPingTimer = null; + } + } + + private startSubscriptionPing(): void { + if (this.subscriptionPingTimer) return; + this.subscriptionPingTimer = setInterval(() => { + const ws = this.subscriptionWs; + if (!ws || ws.readyState !== WebSocketCtor.OPEN) return; + const now = nowMs(); + if (now - this.subscriptionLastMessageAt > WS_STALE_TIMEOUT_MS) { + try { + ws.terminate(); + } catch (error) { + this.logger("subscriptionWs:terminate", error); + } + return; + } + try { + ws.ping(); + } catch (error) { + this.logger("subscriptionWs:ping", error); + } + }, WS_PING_INTERVAL_MS); + } + + private stopSubscriptionPing(): void { + if (this.subscriptionPingTimer) { + clearInterval(this.subscriptionPingTimer); + this.subscriptionPingTimer = null; + } + } + + private enqueueGateway(task: () => Promise): Promise { + const queued = this.gatewayQueue.then(task, task) as Promise; + this.gatewayQueue = queued.then( + () => undefined, + () => undefined + ); + return queued; + } + + private async sendGatewayRequest(payload: unknown): Promise { + return this.enqueueGateway(async () => { + await this.openGatewayWebsocket(); + const ws = this.gatewayWs; + if (!ws || ws.readyState !== WebSocketCtor.OPEN) { + throw new Error("Nado gateway websocket not connected"); + } + return await new Promise((resolve, reject) => { + const handle = (data: NodeWebSocket.RawData) => { + try { + const text = typeof data === "string" ? data : data.toString("utf8"); + const parsed = JSON.parse(text) as T; + ws.off("message", handle); + resolve(parsed); + } catch (error) { + ws.off("message", handle); + reject(error); + } + }; + ws.on("message", handle); + try { + ws.send(JSON.stringify(payload)); + } catch (error) { + ws.off("message", handle); + reject(error); + } + }); + }); + } + + private async queryContracts(): Promise<{ chain_id: string; endpoint_addr: string }> { + const response = (await this.sendGatewayRequest({ type: "contracts" })) as NadoContractsResponse; + if (response.status !== "success" || !response.data) { + const code = parseNadoErrorCode(response.error_code); + const msg = response.error ?? "contracts query failed"; + throw this.decorateNadoError(msg, code); + } + return response.data; + } + + private async queryAndCacheSymbols(): Promise { + const response = (await this.sendGatewayRequest({ type: "symbols" })) as NadoSymbolsResponse; + if (response.status !== "success" || !response.data?.symbols) { + const code = parseNadoErrorCode(response.error_code); + const msg = response.error ?? "symbols query failed"; + throw this.decorateNadoError(msg, code); + } + this.symbolMetaBySymbol.clear(); + this.symbolMetaByProductId.clear(); + for (const entry of Object.values(response.data.symbols)) { + const meta: SymbolMeta = { + productId: entry.product_id, + symbol: entry.symbol, + type: entry.type, + priceIncrementX18: entry.price_increment_x18, + sizeIncrementX18: entry.size_increment, + minSizeX18: entry.min_size, + }; + const key = normalizeSymbolInput(entry.symbol); + this.symbolMetaBySymbol.set(key, meta); + this.symbolMetaByProductId.set(entry.product_id, meta); + } + } + + private async querySubaccountInfo(): Promise> { + const subaccountHex = this.getSubaccountHex(); + const response = (await this.sendGatewayRequest({ + type: "subaccount_info", + subaccount: subaccountHex, + })) as NadoSubaccountInfoResponse; + if (response.status !== "success" || !response.data) { + const code = parseNadoErrorCode(response.error_code); + const msg = response.error ?? "subaccount_info query failed"; + throw this.decorateNadoError(msg, code); + } + return response.data; + } + + private async querySubaccountOrders(productId: number): Promise> { + const sender = this.getSubaccountHex() as `0x${string}`; + const response = (await this.sendGatewayRequest({ + type: "subaccount_orders", + sender, + product_id: productId, + })) as NadoSubaccountOrdersResponse; + if (response.status !== "success" || !response.data) { + const code = parseNadoErrorCode(response.error_code); + const msg = response.error ?? "subaccount_orders query failed"; + throw this.decorateNadoError(msg, code); + } + return response.data; + } + + private startPolling(): void { + if (!this.accountPollTimer) { + this.accountPollTimer = setInterval(() => { + void this.refreshAccountSnapshot(); + }, this.pollIntervals.account); + } + if (!this.ordersPollTimer) { + this.ordersPollTimer = setInterval(() => { + void this.refreshOrdersSnapshot(); + }, this.pollIntervals.orders); + } + if (!this.triggerOrdersPollTimer) { + this.triggerOrdersPollTimer = setInterval(() => { + void this.refreshTriggerOrdersSnapshot(); + }, this.pollIntervals.triggerOrders); + } + } + + private async refreshAccountSnapshot(): Promise { + try { + const data = await this.querySubaccountInfo(); + this.lastAccountSyncAt = nowMs(); + this.accountSnapshot = this.mapSubaccountInfoToAsterSnapshot(data); + this.emitAccount(); + } catch (error) { + this.logger("accountPoll", error); + } + } + + private async refreshOrdersSnapshot(): Promise { + try { + const primaryMeta = this.getSymbolMetaOrThrow(this.primarySymbol); + const data = await this.querySubaccountOrders(primaryMeta.productId); + this.openOrdersByDigest.clear(); + for (const order of data.orders ?? []) { + this.openOrdersByDigest.set(order.digest, { + digest: order.digest, + productId: order.product_id, + priceX18: order.price_x18, + amountX18: order.amount, + unfilledAmountX18: order.unfilled_amount, + appendix: order.appendix, + orderType: order.order_type, + placedAtSec: order.placed_at, + }); + } + this.emitOrders(); + } catch (error) { + this.logger("ordersPoll", error); + } + } + + private async refreshTriggerOrdersSnapshot(): Promise { + if (!this.chainId || !this.endpointAddr) return; + try { + const primaryMeta = this.getSymbolMetaOrThrow(this.primarySymbol); + const response = await this.trigger.listOrders({ + chainId: this.chainId, + verifyingAddr: this.endpointAddr, + subaccountOwner: this.subaccountOwner, + subaccountName: this.subaccountName, + productIds: [primaryMeta.productId], + limit: 200, + }); + this.triggerOrdersByDigest.clear(); + for (const orderInfo of response.orders) { + const order = orderInfo.order; + if (!order?.digest || !order?.productId) continue; + if (order.triggerCriteria?.type !== "price") continue; + const stopDecimal = order.triggerCriteria.criteria?.triggerPrice; + const stopPrice = toBigNumberFromDecimalish(stopDecimal); + const price = toBigNumberFromDecimalish(order.price); + const amount = toBigNumberFromDecimalish(order.amount); + if (!stopPrice || !price || !amount) continue; + const stopPriceX18 = toX18BigInt(stopPrice); + const priceX18 = toX18BigInt(price); + const amountX18 = amount.toFixed(0); + this.triggerOrdersByDigest.set(order.digest, { + digest: order.digest, + productId: order.productId, + priceX18: String(priceX18), + amountX18, + appendix: undefined, + trigger: { stopPriceX18: String(stopPriceX18) }, + status: orderInfo.status.type, + updatedAtMs: orderInfo.updatedAt, + }); + } + this.emitOrders(); + } catch (error) { + this.logger("triggerOrdersPoll", error); + } + } + + private async bootstrapKlinesForActiveListeners(): Promise { + for (const key of Array.from(this.klineListeners.keys())) { + await this.ensureKlinesStream(key); + } + } + + private async ensureKlinesStream(key: string): Promise { + const [symbol, interval] = key.split(":", 2); + if (!symbol || !interval) return; + const meta = this.getSymbolMetaOrThrow(symbol); + const existing = this.klinesState.get(key); + const periodSec = existing?.periodSec ?? this.mapIntervalToSeconds(interval); + + if (!existing) { + const initial = await this.fetchCandlesticks(meta.productId, periodSec, DEFAULT_KLINES_LIMIT); + this.klinesState.set(key, { productId: meta.productId, periodSec, klines: initial }); + this.emitKlines(key); + } + + await this.subscribeLatestCandlestick(meta.productId, periodSec); + } + + private mapIntervalToSeconds(interval: string): number { + const normalized = (interval ?? "").trim().toLowerCase(); + if (normalized === "1m") return 60; + if (normalized === "5m") return 300; + if (normalized === "15m") return 900; + if (normalized === "1h") return 3600; + if (normalized === "2h") return 7200; + if (normalized === "4h") return 14400; + if (normalized === "1d") return 86400; + if (normalized === "1w") return 604800; + const asNumber = Number(normalized); + return Number.isFinite(asNumber) && asNumber > 0 ? Math.floor(asNumber) : 60; + } + + private async fetchCandlesticks(productId: number, periodSec: number, limit: number): Promise { + try { + const result = await this.indexer.getCandlesticks({ productId, period: periodSec, limit }); + const reversed = Array.from(result).reverse(); + return reversed.map((candle) => { + const timestampSec = Number(candle.time.toFixed(0)); + const openTime = Number.isFinite(timestampSec) ? timestampSec * 1000 : nowMs(); + const closeTime = openTime + periodSec * 1000 - 1; + return { + openTime, + closeTime, + open: candle.open.toFixed(), + high: candle.high.toFixed(), + low: candle.low.toFixed(), + close: candle.close.toFixed(), + volume: candle.volume.toFixed(), + numberOfTrades: 0, + }; + }); + } catch (error) { + this.logger("klines:fetch", error); + return []; + } + } + + private emitAccount(): void { + if (!this.accountSnapshot) return; + for (const listener of Array.from(this.accountListeners)) { + try { + listener(this.accountSnapshot); + } catch (error) { + this.logger("accountListener", error); + } + } + } + + private emitOrders(): void { + const snapshot = this.buildAsterOrdersSnapshot(); + for (const listener of Array.from(this.orderListeners)) { + try { + listener(snapshot); + } catch (error) { + this.logger("orderListener", error); + } + } + } + + private emitDepth(productId: number): void { + const meta = this.symbolMetaByProductId.get(productId); + if (!meta) return; + const key = normalizeSymbolInput(meta.symbol); + const listeners = this.depthListeners.get(key); + if (!listeners?.size) return; + const snapshot = this.buildDepthSnapshot(productId, this.resolveDisplaySymbol(key, meta.symbol)); + if (!snapshot) return; + for (const listener of Array.from(listeners)) { + try { + listener(snapshot); + } catch (error) { + this.logger("depthListener", error); + } + } + } + + private emitTicker(productId: number): void { + const meta = this.symbolMetaByProductId.get(productId); + if (!meta) return; + const key = normalizeSymbolInput(meta.symbol); + const listeners = this.tickerListeners.get(key); + if (!listeners?.size) return; + const snapshot = this.buildTickerSnapshot(productId, this.resolveDisplaySymbol(key, meta.symbol)); + if (!snapshot) return; + for (const listener of Array.from(listeners)) { + try { + listener(snapshot); + } catch (error) { + this.logger("tickerListener", error); + } + } + } + + private emitFundingRate(productId: number): void { + const meta = this.symbolMetaByProductId.get(productId); + if (!meta) return; + const key = normalizeSymbolInput(meta.symbol); + const listeners = this.fundingRateListeners.get(key); + if (!listeners?.size) return; + const cached = this.fundingRateByProductId.get(productId); + if (!cached) return; + const updateSec = Number(cached.updateTimeSec); + const updateTime = Number.isFinite(updateSec) ? updateSec * 1000 : nsToMs(cached.timestampNs); + const fundingRate = fromX18(cached.rateX18).toNumber(); + if (!Number.isFinite(fundingRate)) return; + + const snapshot = { + symbol: this.resolveDisplaySymbol(key, meta.symbol), + fundingRate, + updateTime, + }; + + for (const listener of Array.from(listeners)) { + try { + listener(snapshot); + } catch (error) { + this.logger("fundingRateListener", error); + } + } + } + + private emitKlines(key: string): void { + const state = this.klinesState.get(key); + if (!state) return; + const listeners = this.klineListeners.get(key); + if (!listeners?.size) return; + for (const listener of Array.from(listeners)) { + try { + listener(state.klines); + } catch (error) { + this.logger("klineListener", error); + } + } + } + + private buildAsterOrdersSnapshot(): AsterOrder[] { + const orders: AsterOrder[] = []; + for (const order of this.openOrdersByDigest.values()) { + const meta = this.symbolMetaByProductId.get(order.productId); + if (!meta) continue; + const canonical = normalizeSymbolInput(meta.symbol); + const displaySymbol = this.resolveDisplaySymbol(canonical, meta.symbol); + const side = new BigNumber(order.amountX18).isNegative() ? "SELL" : "BUY"; + const execType = (order.orderType ?? "").toLowerCase() as NadoOrderExecution | ""; + const type = mapExecutionToOrderType(execType === "" ? undefined : execType); + const origQty = fromX18(new BigNumber(order.amountX18).abs()).toFixed(); + const remaining = fromX18(new BigNumber(order.unfilledAmountX18).abs()).toFixed(); + const executed = new BigNumber(origQty).minus(remaining); + orders.push({ + orderId: order.digest, + clientOrderId: order.clientId != null ? String(order.clientId) : order.digest, + symbol: displaySymbol, + side, + type, + status: "NEW", + price: fromX18(order.priceX18).toFixed(), + origQty, + executedQty: executed.isFinite() ? executed.toFixed() : "0", + stopPrice: "0", + time: order.placedAtSec ? order.placedAtSec * 1000 : this.lastAccountSyncAt || nowMs(), + updateTime: nowMs(), + reduceOnly: false, + closePosition: false, + }); + } + + for (const trigger of this.triggerOrdersByDigest.values()) { + const meta = this.symbolMetaByProductId.get(trigger.productId); + if (!meta) continue; + const canonical = normalizeSymbolInput(meta.symbol); + const displaySymbol = this.resolveDisplaySymbol(canonical, meta.symbol); + const side = new BigNumber(trigger.amountX18).isNegative() ? "SELL" : "BUY"; + const origQty = fromX18(new BigNumber(trigger.amountX18).abs()).toFixed(); + orders.push({ + orderId: trigger.digest, + clientOrderId: trigger.digest, + symbol: displaySymbol, + side, + type: "STOP_MARKET", + status: trigger.status?.toUpperCase() || "NEW", + price: fromX18(trigger.priceX18).toFixed(), + origQty, + executedQty: "0", + stopPrice: fromX18(trigger.trigger.stopPriceX18).toFixed(), + time: trigger.updatedAtMs, + updateTime: trigger.updatedAtMs, + reduceOnly: true, + closePosition: true, + }); + } + + return orders; + } + + private buildDepthSnapshot(productId: number, symbol: string): AsterDepth | null { + const bbo = this.bestBidOfferByProductId.get(productId); + if (!bbo) return null; + const bids: [string, string][] = [[fromX18(bbo.bidX18).toFixed(), fromX18(bbo.bidQtyX18).toFixed()]]; + const asks: [string, string][] = [[fromX18(bbo.askX18).toFixed(), fromX18(bbo.askQtyX18).toFixed()]]; + return { + lastUpdateId: productId, + bids, + asks, + eventTime: nsToMs(bbo.timestampNs), + symbol, + }; + } + + private buildTickerSnapshot(productId: number, symbol: string): AsterTicker | null { + const bbo = this.bestBidOfferByProductId.get(productId); + if (!bbo) return null; + const trade = this.lastTradePriceByProductId.get(productId); + const bid = fromX18(bbo.bidX18); + const ask = fromX18(bbo.askX18); + const mid = bid.plus(ask).div(2); + const last = trade ? fromX18(trade.priceX18) : mid; + const lastPrice = last.toFixed(); + return { + symbol, + lastPrice, + openPrice: lastPrice, + highPrice: lastPrice, + lowPrice: lastPrice, + volume: "0", + quoteVolume: "0", + eventTime: trade ? nsToMs(trade.timestampNs) : nsToMs(bbo.timestampNs), + bidPrice: bid.toFixed(), + askPrice: ask.toFixed(), + markPrice: mid.toFixed(), + }; + } + + private mapSubaccountInfoToAsterSnapshot(data: NonNullable): AsterAccountSnapshot { + const now = nowMs(); + const assets: AsterAccountAsset[] = []; + const positions: AsterAccountPosition[] = []; + + const spotBalanceByProductId = new Map(); + for (const entry of data.spot_balances ?? []) { + spotBalanceByProductId.set(entry.product_id, entry.balance.amount); + } + for (const [productId, amountX18] of Array.from(spotBalanceByProductId.entries())) { + const meta = this.symbolMetaByProductId.get(productId); + const assetName = meta?.symbol ?? `PRODUCT_${productId}`; + const amount = fromX18(amountX18).toFixed(); + assets.push({ + asset: assetName, + walletBalance: amount, + availableBalance: amount, + updateTime: now, + assetId: productId, + }); + } + + const perpOracleByProductId = new Map(); + for (const product of data.perp_products ?? []) { + perpOracleByProductId.set(product.product_id, product.oracle_price_x18); + } + + for (const entry of data.perp_balances ?? []) { + const meta = this.symbolMetaByProductId.get(entry.product_id); + if (!meta) continue; + const amountX18 = entry.balance.amount; + if (!amountX18 || amountX18 === "0") continue; + const canonical = normalizeSymbolInput(meta.symbol); + const displaySymbol = this.resolveDisplaySymbol(canonical, meta.symbol); + const vQuoteX18 = entry.balance.v_quote_balance ?? "0"; + const oracleX18 = perpOracleByProductId.get(entry.product_id) ?? "0"; + + const amountAbs = new BigNumber(amountX18).abs(); + const positionAmt = fromX18(amountX18).toFixed(); + const markPrice = fromX18(oracleX18).toFixed(); + + const entryPrice = (() => { + if (amountAbs.isZero()) return "0"; + const value = new BigNumber(vQuoteX18).negated().div(amountX18); + return value.isFinite() ? value.abs().toFixed() : "0"; + })(); + + const unrealizedProfit = (() => { + if (amountAbs.isZero()) return "0"; + const pnlX18 = new BigNumber(amountX18) + .multipliedBy(oracleX18) + .div(X18) + .plus(vQuoteX18); + return fromX18(pnlX18).toFixed(); + })(); + + positions.push({ + symbol: displaySymbol, + positionAmt, + entryPrice, + unrealizedProfit, + positionSide: "BOTH", + updateTime: now, + markPrice, + }); + } + + const totalUnrealized = positions.reduce((sum, p) => sum.plus(p.unrealizedProfit ?? "0"), new BigNumber(0)); + + return { + canTrade: true, + canDeposit: true, + canWithdraw: true, + updateTime: now, + totalWalletBalance: "0", + totalUnrealizedProfit: totalUnrealized.toFixed(), + positions, + assets, + marketType: this.getPrimaryMarketType(), + }; + } + + private getPrimaryMarketType(): "perp" | "spot" { + const primary = this.symbolMetaBySymbol.get(normalizeSymbolInput(this.primarySymbol)); + return primary?.type === "spot" ? "spot" : "perp"; + } + + private getSubaccountHex(): string { + // SDK helper used inside @nadohq/shared when signing; for API we just need the 32-byte hex + // Sender = bytes20(owner) + bytes12(subaccountName padded) + const owner = this.subaccountOwner.replace(/^0x/, "").toLowerCase(); + const name = Buffer.from(this.subaccountName, "utf8").toString("hex"); + const padded = (name + "0".repeat(24)).slice(0, 24); + return `0x${owner}${padded}`; + } + + private decorateNadoError(message: string, errorCode: number | null): Error & { code?: number; status?: number } { + const error = new Error(message) as Error & { code?: number; status?: number }; + if (errorCode != null) { + (error as any).code = errorCode; + if (errorCode === 1000 || errorCode === 1015) { + (error as any).status = 429; + } + } + return error; + } + + private async authenticateSubscription(): Promise { + if (!this.subscriptionWs || this.subscriptionWs.readyState !== WebSocketCtor.OPEN) { + throw new Error("Subscription websocket not connected"); + } + if (!this.chainId || !this.endpointAddr) { + throw new Error("Nado contracts not loaded"); + } + const sender = this.getSubaccountHex(); + const expirationMs = nowMs() + 90_000; + + const signature = await this.walletClient.signTypedData({ + domain: getNadoEIP712Domain(this.endpointAddr, this.chainId), + types: { + StreamAuthentication: [ + { name: "sender", type: "bytes32" }, + { name: "expiration", type: "uint64" }, + ], + }, + primaryType: "StreamAuthentication", + message: { + sender, + expiration: BigInt(expirationMs), + }, + }); + + const id = this.nextSubscriptionRequestId(); + const response = await this.sendSubscriptionRequest({ + method: "authenticate", + id, + tx: { + sender, + expiration: String(expirationMs), + }, + signature, + }); + if (response?.id !== id) { + throw new Error("Unexpected subscription authenticate response"); + } + this.subscriptionAuthComplete = true; + } + + private async resubscribeAllStreams(): Promise { + // Ensure basic public streams always follow active listeners. + await this.ensureMarketStreamsForListeners(); + await this.ensureFundingStreamsForListeners(); + await this.ensureAccountStreamsForListeners(); + await this.ensureKlineStreamsForListeners(); + } + + private async ensureMarketStreamsForListeners(): Promise { + const wantedProductIds = new Set(); + for (const symbol of Array.from(this.depthListeners.keys())) { + const meta = this.symbolMetaBySymbol.get(symbol); + if (meta) wantedProductIds.add(meta.productId); + } + for (const symbol of Array.from(this.tickerListeners.keys())) { + const meta = this.symbolMetaBySymbol.get(symbol); + if (meta) wantedProductIds.add(meta.productId); + } + + if (wantedProductIds.size === 0) { + const primaryMeta = this.symbolMetaBySymbol.get(normalizeSymbolInput(this.primarySymbol)); + if (primaryMeta) wantedProductIds.add(primaryMeta.productId); + } + + for (const productId of Array.from(wantedProductIds)) { + await this.subscribeBestBidOffer(productId); + await this.subscribeTrades(productId); + } + } + + private async ensureFundingStreamsForListeners(): Promise { + const wantedProductIds = new Set(); + for (const symbol of Array.from(this.fundingRateListeners.keys())) { + const meta = this.symbolMetaBySymbol.get(symbol); + if (!meta) continue; + if (meta.type !== "perp") continue; + wantedProductIds.add(meta.productId); + } + + for (const productId of Array.from(wantedProductIds)) { + await this.subscribeFundingRate(productId); + } + } + + private async ensureAccountStreamsForListeners(): Promise { + if (!this.subscriptionAuthComplete) return; + if (this.accountListeners.size === 0 && this.orderListeners.size === 0) return; + const subaccount = this.getSubaccountHex(); + // Subscribe across all products for this subaccount. + await this.subscribeOrderUpdates(subaccount, null); + await this.subscribePositionChanges(subaccount, null); + } + + private async ensureKlineStreamsForListeners(): Promise { + for (const key of Array.from(this.klineListeners.keys())) { + await this.ensureKlinesStream(key); + } + } + + private async subscribeBestBidOffer(productId: number): Promise { + const streamKey = `best_bid_offer:${productId}`; + if (this.subscriptionStreams.has(streamKey)) return; + await this.sendSubscriptionRequest({ + method: "subscribe", + id: this.nextSubscriptionRequestId(), + stream: { type: "best_bid_offer", product_id: productId }, + }); + this.subscriptionStreams.add(streamKey); + } + + private async subscribeTrades(productId: number): Promise { + const streamKey = `trade:${productId}`; + if (this.subscriptionStreams.has(streamKey)) return; + await this.sendSubscriptionRequest({ + method: "subscribe", + id: this.nextSubscriptionRequestId(), + stream: { type: "trade", product_id: productId }, + }); + this.subscriptionStreams.add(streamKey); + } + + private async subscribeOrderUpdates(subaccount: string, productId: number | null): Promise { + const streamKey = `order_update:${subaccount}:${productId ?? "all"}`; + if (this.subscriptionStreams.has(streamKey)) return; + await this.sendSubscriptionRequest({ + method: "subscribe", + id: this.nextSubscriptionRequestId(), + stream: { type: "order_update", subaccount, product_id: productId }, + }); + this.subscriptionStreams.add(streamKey); + } + + private async subscribePositionChanges(subaccount: string, productId: number | null): Promise { + const streamKey = `position_change:${subaccount}:${productId ?? "all"}`; + if (this.subscriptionStreams.has(streamKey)) return; + await this.sendSubscriptionRequest({ + method: "subscribe", + id: this.nextSubscriptionRequestId(), + stream: { type: "position_change", subaccount, product_id: productId }, + }); + this.subscriptionStreams.add(streamKey); + } + + private async subscribeLatestCandlestick(productId: number, periodSec: number): Promise { + const streamKey = `latest_candlestick:${productId}:${periodSec}`; + if (this.subscriptionStreams.has(streamKey)) return; + await this.sendSubscriptionRequest({ + method: "subscribe", + id: this.nextSubscriptionRequestId(), + stream: { type: "latest_candlestick", product_id: productId, granularity: periodSec }, + }); + this.subscriptionStreams.add(streamKey); + } + + private async subscribeFundingRate(productId: number): Promise { + const streamKey = `funding_rate:${productId}`; + if (this.subscriptionStreams.has(streamKey)) return; + await this.sendSubscriptionRequest({ + method: "subscribe", + id: this.nextSubscriptionRequestId(), + stream: { type: "funding_rate", product_id: productId }, + }); + this.subscriptionStreams.add(streamKey); + } + + private nextSubscriptionRequestId(): number { + this.subscriptionRequestId += 1; + return this.subscriptionRequestId; + } + + private async sendSubscriptionRequest(payload: any): Promise { + const ws = this.subscriptionWs; + if (!ws || ws.readyState !== WebSocketCtor.OPEN) { + throw new Error("Subscription websocket not connected"); + } + return await new Promise((resolve, reject) => { + const id = payload?.id; + let timeout: ReturnType | null = null; + const handle = (data: NodeWebSocket.RawData) => { + try { + const text = typeof data === "string" ? data : data.toString("utf8"); + const parsed = JSON.parse(text); + // Only resolve for an ack. + if (id != null && parsed && typeof parsed === "object" && parsed.id === id) { + ws.off("message", handle); + if (timeout) clearTimeout(timeout); + resolve(parsed as T); + } + } catch (error) { + ws.off("message", handle); + if (timeout) clearTimeout(timeout); + reject(error); + } + }; + ws.on("message", handle); + try { + ws.send(JSON.stringify(payload)); + } catch (error) { + ws.off("message", handle); + if (timeout) clearTimeout(timeout); + reject(error); + } + // Safety timeout in case we never get an ack. + timeout = setTimeout(() => { + timeout = null; + ws.off("message", handle); + reject(new Error("Subscription request timed out")); + }, 10_000); + }); + } + + private handleSubscriptionMessage(data: NodeWebSocket.RawData): void { + let message: any; + try { + const text = typeof data === "string" ? data : data.toString("utf8"); + message = JSON.parse(text); + } catch (_error) { + return; + } + if (!message || typeof message !== "object") return; + // Acks are handled by request promises. + if ("id" in message && "result" in message) return; + + const type = message.type; + if (type === "best_bid_offer") { + this.handleBestBidOffer(message as NadoBestBidOfferEvent); + return; + } + if (type === "trade") { + this.handleTrade(message as NadoTradeEvent); + return; + } + if (type === "order_update") { + this.handleOrderUpdate(message as NadoOrderUpdateEvent); + return; + } + if (type === "position_change") { + this.handlePositionChange(message as NadoPositionChangeEvent); + return; + } + if (type === "funding_rate") { + this.handleFundingRate(message as NadoFundingRateEvent); + return; + } + if (type === "latest_candlestick") { + this.handleLatestCandlestick(message as NadoLatestCandlestickEvent); + return; + } + } + + private handleBestBidOffer(event: NadoBestBidOfferEvent): void { + this.bestBidOfferByProductId.set(event.product_id, { + bidX18: event.bid_price, + askX18: event.ask_price, + bidQtyX18: event.bid_qty, + askQtyX18: event.ask_qty, + timestampNs: event.timestamp, + }); + this.emitDepth(event.product_id); + this.emitTicker(event.product_id); + } + + private handleTrade(event: NadoTradeEvent): void { + this.lastTradePriceByProductId.set(event.product_id, { priceX18: event.price, timestampNs: event.timestamp }); + this.emitTicker(event.product_id); + } + + private handleOrderUpdate(event: NadoOrderUpdateEvent): void { + const existing = this.openOrdersByDigest.get(event.digest); + if (!existing) { + // We'll refresh via poll loop; avoid spamming queries here. + return; + } + existing.unfilledAmountX18 = event.amount; + if (event.reason === "cancelled" || (event.reason === "filled" && event.amount === "0")) { + this.openOrdersByDigest.delete(event.digest); + } + this.emitOrders(); + } + + private handlePositionChange(_event: NadoPositionChangeEvent): void { + // Position change events do not contain enough context to rebuild a full snapshot reliably (oracle prices, etc). + // We rely on subaccount_info polling for correctness. + if (nowMs() - this.lastAccountSyncAt > Math.min(this.pollIntervals.account, 5_000)) { + void this.refreshAccountSnapshot(); + } + } + + private handleFundingRate(event: NadoFundingRateEvent): void { + this.fundingRateByProductId.set(event.product_id, { + rateX18: event.funding_rate_x18, + updateTimeSec: event.update_time, + timestampNs: event.timestamp, + }); + this.emitFundingRate(event.product_id); + } + + private handleLatestCandlestick(event: NadoLatestCandlestickEvent): void { + for (const [key, state] of Array.from(this.klinesState.entries())) { + if (state.productId !== event.product_id) continue; + if (state.periodSec !== event.granularity) continue; + const openTime = event.timestamp * 1000; + const closeTime = openTime + state.periodSec * 1000 - 1; + const next: AsterKline = { + openTime, + closeTime, + open: fromX18(event.open_x18).toFixed(), + high: fromX18(event.high_x18).toFixed(), + low: fromX18(event.low_x18).toFixed(), + close: fromX18(event.close_x18).toFixed(), + volume: fromX18(event.volume).toFixed(), + numberOfTrades: 0, + }; + const existingIdx = state.klines.findIndex((k) => k.openTime === openTime); + if (existingIdx >= 0) { + state.klines[existingIdx] = next; + } else { + state.klines.push(next); + if (state.klines.length > 500) { + state.klines = state.klines.slice(-500); + } + } + this.klinesState.set(key, state); + this.emitKlines(key); + } + } + + private async createLimitOrder(meta: SymbolMeta, params: CreateOrderParams): Promise { + if (!this.chainId) throw new Error("Nado not initialized (chainId missing)"); + const side = params.side; + const qty = params.quantity ?? 0; + if (!Number.isFinite(qty) || qty <= 0) { + throw new Error("Invalid order quantity"); + } + const price = params.price; + if (price == null || !Number.isFinite(price) || price <= 0) { + throw new Error("Invalid order price"); + } + + const amountX18 = toX18BigInt(qty); + const signedAmount = side === "SELL" ? -amountX18 : amountX18; + + const appendix = packOrderAppendix({ + orderExecutionType: mapTimeInForceToExecution(params.timeInForce), + reduceOnly: params.reduceOnly === "true", + }); + + const orderParams = { + subaccountOwner: this.subaccountOwner, + subaccountName: this.subaccountName, + price: new BigNumber(price).toFixed(), + amount: signedAmount, + expiration: 4294967295, + nonce: getOrderNonce(), + appendix, + } as const; + + const signature = await getSignedTransactionRequest({ + requestType: "place_order", + requestParams: orderParams, + chainId: this.chainId, + verifyingContract: getOrderVerifyingAddress(meta.productId), + walletClient: this.walletClient, + }); + + const order = getNadoEIP712Values("place_order", orderParams) as any; + + const payload = { + place_order: { + product_id: meta.productId, + order, + signature, + id: null, + }, + }; + + const response = await this.sendGatewayRequest(payload); + if (response?.status !== "success") { + const code = parseNadoErrorCode(response?.error_code); + throw this.decorateNadoError(response?.error ?? "place_order failed", code); + } + + const digest = response?.data?.digest; + const orderId = typeof digest === "string" && digest.startsWith("0x") ? digest : String(digest ?? "unknown"); + + this.openOrdersByDigest.set(orderId, { + digest: orderId, + productId: meta.productId, + priceX18: order.priceX18, + amountX18: order.amount, + unfilledAmountX18: order.amount, + appendix: order.appendix, + orderType: mapTimeInForceToExecution(params.timeInForce), + placedAtSec: Math.floor(nowMs() / 1000), + }); + this.emitOrders(); + + const canonical = normalizeSymbolInput(meta.symbol); + const displaySymbol = this.resolveDisplaySymbol(canonical, meta.symbol); + return { + orderId, + clientOrderId: orderId, + symbol: displaySymbol, + side, + type: "LIMIT", + status: "NEW", + price: String(price), + origQty: String(qty), + executedQty: "0", + stopPrice: "0", + time: nowMs(), + updateTime: nowMs(), + reduceOnly: params.reduceOnly === "true", + closePosition: params.closePosition === "true", + }; + } + + private async createMarketOrder(meta: SymbolMeta, params: CreateOrderParams): Promise { + if (!this.chainId) throw new Error("Nado not initialized (chainId missing)"); + const side = params.side; + const qty = params.quantity ?? 0; + if (!Number.isFinite(qty) || qty <= 0) { + throw new Error("Invalid order quantity"); + } + const bbo = this.bestBidOfferByProductId.get(meta.productId); + const reference = (() => { + if (bbo) { + const best = side === "BUY" ? bbo.askX18 : bbo.bidX18; + return fromX18(best); + } + const trade = this.lastTradePriceByProductId.get(meta.productId); + if (trade) return fromX18(trade.priceX18); + return null; + })(); + if (!reference) { + throw new Error("Market order rejected: missing best bid/offer"); + } + const limitPrice = side === "BUY" + ? reference.multipliedBy(1 + this.marketSlippagePct) + : reference.multipliedBy(1 - this.marketSlippagePct); + + const amountX18 = toX18BigInt(qty); + const signedAmount = side === "SELL" ? -amountX18 : amountX18; + + const appendix = packOrderAppendix({ + orderExecutionType: "ioc", + reduceOnly: params.reduceOnly === "true", + }); + + const orderParams = { + subaccountOwner: this.subaccountOwner, + subaccountName: this.subaccountName, + price: limitPrice.toFixed(), + amount: signedAmount, + expiration: 4294967295, + nonce: getOrderNonce(), + appendix, + } as const; + + const signature = await getSignedTransactionRequest({ + requestType: "place_order", + requestParams: orderParams, + chainId: this.chainId, + verifyingContract: getOrderVerifyingAddress(meta.productId), + walletClient: this.walletClient, + }); + + const order = getNadoEIP712Values("place_order", orderParams) as any; + + const payload = { + place_order: { + product_id: meta.productId, + order, + signature, + id: null, + }, + }; + + const response = await this.sendGatewayRequest(payload); + if (response?.status !== "success") { + const code = parseNadoErrorCode(response?.error_code); + throw this.decorateNadoError(response?.error ?? "place_order failed", code); + } + const digest = response?.data?.digest; + const orderId = typeof digest === "string" && digest.startsWith("0x") ? digest : String(digest ?? "unknown"); + + const canonical = normalizeSymbolInput(meta.symbol); + const displaySymbol = this.resolveDisplaySymbol(canonical, meta.symbol); + return { + orderId, + clientOrderId: orderId, + symbol: displaySymbol, + side, + type: "MARKET", + status: "NEW", + price: limitPrice.toFixed(), + origQty: String(qty), + executedQty: "0", + stopPrice: "0", + time: nowMs(), + updateTime: nowMs(), + reduceOnly: params.reduceOnly === "true", + closePosition: params.closePosition === "true", + }; + } + + private async createStopOrder(meta: SymbolMeta, params: CreateOrderParams): Promise { + if (!this.chainId || !this.endpointAddr) throw new Error("Nado not initialized (contracts missing)"); + const side = params.side; + const qty = params.quantity ?? 0; + if (!Number.isFinite(qty) || qty <= 0) { + throw new Error("Invalid order quantity"); + } + const stopPrice = params.stopPrice ?? 0; + if (!Number.isFinite(stopPrice) || stopPrice <= 0) { + throw new Error("Invalid stop price"); + } + + const amountX18 = toX18BigInt(qty); + const signedAmount = side === "SELL" ? -amountX18 : amountX18; + + const stopPriceX18 = toX18BigInt(stopPrice); + const limitFromStop = side === "BUY" + ? fromX18(stopPriceX18).multipliedBy(1 + this.marketSlippagePct) + : fromX18(stopPriceX18).multipliedBy(1 - this.marketSlippagePct); + + const appendix = packOrderAppendix({ + orderExecutionType: "ioc", + reduceOnly: true, + triggerType: "price", + }); + + const requirementType: PriceTriggerRequirementType = (() => { + const source = this.stopTriggerSource; + if (source === "last") { + return side === "BUY" ? "last_price_above" : "last_price_below"; + } + if (source === "mid") { + return side === "BUY" ? "mid_price_above" : "mid_price_below"; + } + return side === "BUY" ? "oracle_price_above" : "oracle_price_below"; + })(); + + const nonce = getOrderNonce(); + const executeResponse = await this.trigger.placeTriggerOrder({ + chainId: this.chainId, + verifyingAddr: getOrderVerifyingAddress(meta.productId), + productId: meta.productId, + nonce, + order: { + subaccountOwner: this.subaccountOwner, + subaccountName: this.subaccountName, + price: limitFromStop.toFixed(), + amount: signedAmount, + expiration: 4294967295, + appendix, + }, + triggerCriteria: { + type: "price", + criteria: { + type: requirementType, + triggerPrice: new BigNumber(stopPrice).toFixed(), + }, + } as any, + } as any); + + const digest = executeResponse?.data?.digest ?? null; + const orderId = typeof digest === "string" && digest.startsWith("0x") ? digest : String(digest ?? "unknown"); + + this.triggerOrdersByDigest.set(orderId, { + digest: orderId, + productId: meta.productId, + priceX18: String(toX18BigInt(limitFromStop.toFixed())), + amountX18: String(signedAmount), + appendix: String(appendix), + trigger: { stopPriceX18: String(stopPriceX18) }, + status: "waiting_price", + updatedAtMs: nowMs(), + }); + this.emitOrders(); + + const canonical = normalizeSymbolInput(meta.symbol); + const displaySymbol = this.resolveDisplaySymbol(canonical, meta.symbol); + return { + orderId, + clientOrderId: orderId, + symbol: displaySymbol, + side, + type: "STOP_MARKET", + status: "NEW", + price: limitFromStop.toFixed(), + origQty: String(qty), + executedQty: "0", + stopPrice: String(stopPrice), + time: nowMs(), + updateTime: nowMs(), + reduceOnly: true, + closePosition: true, + }; + } + + private async sendCancelOrders(productId: number, digests: string[]): Promise { + if (!this.chainId || !this.endpointAddr) throw new Error("Nado not initialized (contracts missing)"); + const nonce = getOrderNonce(); + const cancelParams = { + subaccountOwner: this.subaccountOwner, + subaccountName: this.subaccountName, + productIds: digests.map(() => productId), + digests, + nonce, + } as const; + const signature = await getSignedTransactionRequest({ + requestType: "cancel_orders", + requestParams: cancelParams, + chainId: this.chainId, + verifyingContract: this.endpointAddr, + walletClient: this.walletClient, + }); + const tx = getNadoEIP712Values("cancel_orders", cancelParams) as any; + const payload = { cancel_orders: { tx, signature } }; + const response = await this.sendGatewayRequest(payload); + if (response?.status !== "success") { + const code = parseNadoErrorCode(response?.error_code); + throw this.decorateNadoError(response?.error ?? "cancel_orders failed", code); + } + } + + private async sendCancelProductOrders(productIds: number[]): Promise { + if (!this.chainId || !this.endpointAddr) throw new Error("Nado not initialized (contracts missing)"); + const nonce = getOrderNonce(); + const cancelParams = { + subaccountOwner: this.subaccountOwner, + subaccountName: this.subaccountName, + productIds, + nonce, + } as const; + const signature = await getSignedTransactionRequest({ + requestType: "cancel_product_orders", + requestParams: cancelParams, + chainId: this.chainId, + verifyingContract: this.endpointAddr, + walletClient: this.walletClient, + }); + const tx = getNadoEIP712Values("cancel_product_orders", cancelParams) as any; + const payload = { cancel_product_orders: { tx, signature, digest: null } }; + const response = await this.sendGatewayRequest(payload); + if (response?.status !== "success") { + const code = parseNadoErrorCode(response?.error_code); + throw this.decorateNadoError(response?.error ?? "cancel_product_orders failed", code); + } + } + + private async cancelTriggerOrders(productId: number, digests: string[]): Promise { + if (!this.chainId || !this.endpointAddr) throw new Error("Nado not initialized (contracts missing)"); + await this.trigger.cancelTriggerOrders({ + chainId: this.chainId, + verifyingAddr: this.endpointAddr, + subaccountOwner: this.subaccountOwner, + subaccountName: this.subaccountName, + productIds: digests.map(() => productId), + digests, + }); + } + + private async cancelTriggerProductOrders(productIds: number[]): Promise { + if (!this.chainId || !this.endpointAddr) throw new Error("Nado not initialized (contracts missing)"); + await this.trigger.cancelProductOrders({ + chainId: this.chainId, + verifyingAddr: this.endpointAddr, + subaccountOwner: this.subaccountOwner, + subaccountName: this.subaccountName, + productIds, + }); + } + + private rememberDisplaySymbol(rawSymbol: string): string { + const trimmed = (rawSymbol ?? "").trim(); + const canonical = normalizeSymbolInput(trimmed); + if (trimmed && !this.displaySymbolByCanonical.has(canonical)) { + this.displaySymbolByCanonical.set(canonical, trimmed); + } + return canonical; + } + + private resolveDisplaySymbol(canonical: string, fallback: string): string { + return this.displaySymbolByCanonical.get(canonical) ?? fallback; + } +} diff --git a/src/exchanges/nado/order.ts b/src/exchanges/nado/order.ts new file mode 100644 index 0000000..c3b815d --- /dev/null +++ b/src/exchanges/nado/order.ts @@ -0,0 +1,93 @@ +import type { AsterOrder, CreateOrderParams } from "../types"; +import type { + BaseOrderIntent, + ClosePositionIntent, + LimitOrderIntent, + MarketOrderIntent, + StopOrderIntent, + TrailingStopOrderIntent, +} from "../order-schema"; +import { toStringBoolean } from "../order-schema"; + +function applyCommonFields(params: CreateOrderParams, intent: BaseOrderIntent): CreateOrderParams { + if (params.quantity === undefined) { + params.quantity = intent.quantity; + } + if (params.timeInForce === undefined && intent.timeInForce) { + params.timeInForce = intent.timeInForce; + } + if (intent.reduceOnly !== undefined) { + params.reduceOnly = toStringBoolean(intent.reduceOnly); + } + if (intent.closePosition !== undefined) { + params.closePosition = toStringBoolean(intent.closePosition); + } + return params; +} + +export async function createLimitOrder(intent: LimitOrderIntent): Promise { + const params: CreateOrderParams = applyCommonFields( + { + symbol: intent.symbol, + side: intent.side, + type: "LIMIT", + quantity: intent.quantity, + price: intent.price, + timeInForce: intent.timeInForce ?? "GTC", + }, + intent + ); + return intent.adapter.createOrder(params); +} + +export async function createMarketOrder(intent: MarketOrderIntent): Promise { + const params: CreateOrderParams = applyCommonFields( + { + symbol: intent.symbol, + side: intent.side, + type: "MARKET", + quantity: intent.quantity, + timeInForce: intent.timeInForce ?? "IOC", + }, + intent + ); + return intent.adapter.createOrder(params); +} + +export async function createStopOrder(intent: StopOrderIntent): Promise { + const params: CreateOrderParams = applyCommonFields( + { + symbol: intent.symbol, + side: intent.side, + type: "STOP_MARKET", + quantity: intent.quantity, + stopPrice: intent.stopPrice, + timeInForce: intent.timeInForce ?? "GTC", + reduceOnly: toStringBoolean(intent.reduceOnly ?? true), + closePosition: toStringBoolean(intent.closePosition ?? true), + }, + intent + ); + return intent.adapter.createOrder(params); +} + +export async function createTrailingStopOrder(_intent: TrailingStopOrderIntent): Promise { + throw new Error("Nado exchange does not support trailing stop orders"); +} + +export async function createClosePositionOrder(intent: ClosePositionIntent): Promise { + const params: CreateOrderParams = applyCommonFields( + { + symbol: intent.symbol, + side: intent.side, + type: "MARKET", + quantity: intent.quantity, + reduceOnly: "true", + closePosition: toStringBoolean(intent.closePosition ?? true), + timeInForce: intent.timeInForce ?? "IOC", + }, + intent + ); + return intent.adapter.createOrder(params); +} + diff --git a/src/exchanges/nado/types.ts b/src/exchanges/nado/types.ts new file mode 100644 index 0000000..e7aaa6f --- /dev/null +++ b/src/exchanges/nado/types.ts @@ -0,0 +1,166 @@ +export type NadoProductType = "spot" | "perp"; + +export interface NadoContractsResponse { + status: "success" | "failure"; + data?: { + chain_id: string; + endpoint_addr: string; + }; + error?: string; + error_code?: number; + request_type?: string; +} + +export interface NadoSymbolsResponse { + status: "success" | "failure"; + data?: { + symbols: Record< + string, + { + type: NadoProductType; + product_id: number; + symbol: string; + price_increment_x18: string; + size_increment: string; + min_size: string; + maker_fee_rate_x18?: string; + taker_fee_rate_x18?: string; + } + >; + }; + error?: string; + error_code?: number; + request_type?: string; +} + +export interface NadoSubaccountOrdersResponse { + status: "success" | "failure"; + data?: { + sender: string; + product_id: number; + orders: Array<{ + product_id: number; + sender: string; + price_x18: string; + amount: string; + expiration: string; + nonce: string; + unfilled_amount: string; + digest: string; + placed_at: number; + appendix?: string; + order_type?: string; + }>; + }; + error?: string; + error_code?: number; + request_type?: string; +} + +export interface NadoSubaccountInfoResponse { + status: "success" | "failure"; + data?: { + subaccount: string; + exists: boolean; + healths: Array<{ assets: string; liabilities: string; health: string }>; + spot_balances: Array<{ product_id: number; balance: { amount: string } }>; + perp_balances: Array<{ + product_id: number; + balance: { + amount: string; + v_quote_balance: string; + last_cumulative_funding_x18?: string; + }; + }>; + spot_products: Array<{ + product_id: number; + oracle_price_x18: string; + book_info?: { + size_increment: string; + price_increment_x18: string; + min_size: string; + }; + }>; + perp_products: Array<{ + product_id: number; + oracle_price_x18: string; + book_info?: { + size_increment: string; + price_increment_x18: string; + min_size: string; + }; + }>; + }; + error?: string; + error_code?: number; + request_type?: string; +} + +export interface NadoSubscriptionAck { + result: unknown; + id: number; +} + +export interface NadoOrderUpdateEvent { + type: "order_update"; + timestamp: string; + product_id: number; + digest: string; + amount: string; + reason: "filled" | "cancelled" | "placed"; + id?: number; +} + +export interface NadoPositionChangeEvent { + type: "position_change"; + timestamp: string; + product_id: number; + subaccount: string; + isolated: boolean; + amount: string; + v_quote_amount: string; + reason: string; +} + +export interface NadoBestBidOfferEvent { + type: "best_bid_offer"; + timestamp: string; + product_id: number; + bid_price: string; + bid_qty: string; + ask_price: string; + ask_qty: string; +} + +export interface NadoTradeEvent { + type: "trade"; + timestamp: string; + product_id: number; + price: string; + taker_qty: string; + maker_qty: string; + is_taker_buyer: boolean; +} + +export interface NadoLatestCandlestickEvent { + type: "latest_candlestick"; + timestamp: number; + product_id: number; + granularity: number; + open_x18: string; + high_x18: string; + low_x18: string; + close_x18: string; + volume: string; +} + +export interface NadoFundingRateEvent { + type: "funding_rate"; + // timestamp when the event was generated, in nanoseconds + timestamp: string; + product_id: number; + // latest 24hr funding rate, multiplied by 1e18 + funding_rate_x18: string; + // epoch time in seconds when the funding rate was updated + update_time: string; +} diff --git a/src/exchanges/order-router.ts b/src/exchanges/order-router.ts index ef3395d..4e890f3 100644 --- a/src/exchanges/order-router.ts +++ b/src/exchanges/order-router.ts @@ -13,8 +13,9 @@ import * as backpackOrders from "./backpack/order"; import * as grvtOrders from "./grvt/order"; import * as lighterOrders from "./lighter/order"; import * as paradexOrders from "./paradex/order"; +import * as nadoOrders from "./nado/order"; -type ExchangeKey = "aster" | "backpack" | "grvt" | "lighter" | "paradex"; +type ExchangeKey = "aster" | "backpack" | "grvt" | "lighter" | "paradex" | "nado"; interface ExchangeOrderHandlers { limit(intent: LimitOrderIntent): Promise; @@ -60,9 +61,16 @@ const handlerMap: Record = { trailingStop: paradexOrders.createTrailingStopOrder, close: paradexOrders.createClosePositionOrder, }, + nado: { + limit: nadoOrders.createLimitOrder, + market: nadoOrders.createMarketOrder, + stop: nadoOrders.createStopOrder, + trailingStop: nadoOrders.createTrailingStopOrder, + close: nadoOrders.createClosePositionOrder, + }, }; -const knownExchanges: ExchangeKey[] = ["aster", "backpack", "grvt", "lighter", "paradex"]; +const knownExchanges: ExchangeKey[] = ["aster", "backpack", "grvt", "lighter", "paradex", "nado"]; function normalizeExchangeId(value: string | undefined | null): string | undefined { if (!value) return undefined; @@ -115,4 +123,3 @@ export function routeTrailingStopOrder(intent: TrailingStopOrderIntent): Promise export function routeCloseOrder(intent: ClosePositionIntent): Promise { return getHandlers(intent).close(intent); } - diff --git a/src/exchanges/resolve-from-env.ts b/src/exchanges/resolve-from-env.ts index 13a9a44..6dc7890 100644 --- a/src/exchanges/resolve-from-env.ts +++ b/src/exchanges/resolve-from-env.ts @@ -4,7 +4,9 @@ import type { AsterCredentials } from "./aster-adapter"; import type { LighterCredentials } from "./lighter/adapter"; import type { BackpackCredentials } from "./backpack/adapter"; import type { ParadexCredentials } from "./paradex/adapter"; +import type { NadoCredentials } from "./nado/adapter"; import { t } from "../i18n"; +import type { Address } from "viem"; interface BuildAdapterOptions { symbol: string; @@ -35,6 +37,11 @@ export function buildAdapterFromEnv(options: BuildAdapterOptions): ExchangeAdapt return createExchangeAdapter({ exchange: id, symbol, paradex: credentials }); } + if (id === "nado") { + const credentials = resolveNadoCredentials(symbol); + return createExchangeAdapter({ exchange: id, symbol, nado: credentials }); + } + return createExchangeAdapter({ exchange: id, symbol, grvt: { symbol } }); } @@ -115,6 +122,37 @@ function resolveParadexCredentials(): ParadexCredentials { return credentials; } +function resolveNadoCredentials(symbol: string): NadoCredentials { + const signerPrivateKey = process.env.NADO_SIGNER_PRIVATE_KEY; + const subaccountOwner = process.env.NADO_SUBACCOUNT_OWNER ?? process.env.NADO_EVM_ADDRESS; + + if (!signerPrivateKey || !subaccountOwner) { + throw new Error(t("env.missingNado")); + } + if (!isHex32(signerPrivateKey)) { + throw new Error(t("env.invalidNadoPrivateKey")); + } + if (!isHexAddress(subaccountOwner)) { + throw new Error(t("env.invalidNadoAddress")); + } + + const credentials: NadoCredentials = { + symbol: process.env.NADO_SYMBOL ?? symbol, + signerPrivateKey, + subaccountOwner: subaccountOwner as Address, + subaccountName: process.env.NADO_SUBACCOUNT_NAME ?? undefined, + env: process.env.NADO_ENV as any, + gatewayWsUrl: process.env.NADO_GATEWAY_WS_URL ?? undefined, + subscriptionsWsUrl: process.env.NADO_SUBSCRIPTIONS_WS_URL ?? undefined, + archiveUrl: process.env.NADO_ARCHIVE_URL ?? undefined, + triggerUrl: process.env.NADO_TRIGGER_URL ?? undefined, + marketSlippagePct: parseOptionalNumber(process.env.NADO_MARKET_SLIPPAGE_PCT), + stopTriggerSource: process.env.NADO_STOP_TRIGGER_SOURCE as any, + }; + + return credentials; +} + function isHex32(value: string): boolean { return /^0x[0-9a-fA-F]{64}$/.test(value.trim()); } diff --git a/src/i18n/index.ts b/src/i18n/index.ts index e54442f..66db965 100644 --- a/src/i18n/index.ts +++ b/src/i18n/index.ts @@ -229,8 +229,8 @@ const translations: Record = { "grid.direction.long": { zh: "多", en: "Long" }, "grid.direction.short": { zh: "空", en: "Short" }, "basis.onlyAster": { - zh: "期现套利策略目前仅支持 Aster 交易所。请设置 EXCHANGE=aster 后重试。", - en: "Basis arbitrage currently supports only Aster. Set EXCHANGE=aster and retry.", + zh: "期现套利策略目前仅支持 Aster / Nado 交易所。请设置 EXCHANGE=aster 或 EXCHANGE=nado 后重试。", + en: "Basis arbitrage currently supports only Aster and Nado. Set EXCHANGE=aster or EXCHANGE=nado and retry.", }, "basis.startFailed": { zh: "无法启动期现套利策略: {message}", @@ -326,6 +326,18 @@ const translations: Record = { zh: "PARADEX_WALLET_ADDRESS 必须是有效的 0x 开头 40 字节十六进制地址", en: "PARADEX_WALLET_ADDRESS must be a valid 0x-prefixed 40-byte hex address", }, + "env.missingNado": { + zh: "Nado 需要配置 NADO_SIGNER_PRIVATE_KEY 与 NADO_SUBACCOUNT_OWNER (或 NADO_EVM_ADDRESS)", + en: "Nado requires NADO_SIGNER_PRIVATE_KEY and NADO_SUBACCOUNT_OWNER (or NADO_EVM_ADDRESS)", + }, + "env.invalidNadoPrivateKey": { + zh: "NADO_SIGNER_PRIVATE_KEY 必须是 0x 开头的 32 字节十六进制字符串", + en: "NADO_SIGNER_PRIVATE_KEY must be a 0x-prefixed 32-byte hex string", + }, + "env.invalidNadoAddress": { + zh: "NADO_SUBACCOUNT_OWNER / NADO_EVM_ADDRESS 必须是有效的 0x 开头 40 字节十六进制地址", + en: "NADO_SUBACCOUNT_OWNER / NADO_EVM_ADDRESS must be a valid 0x-prefixed 40-byte hex address", + }, "log.subscribe.accountFail": { zh: "订阅账户失败: {error}", en: "Failed to subscribe account: {error}", @@ -430,6 +442,14 @@ const translations: Record = { zh: "处理期货深度异常: {error}", en: "Error processing futures depth: {error}", }, + "log.basis.subscribeSpotDepthFail": { + zh: "订阅现货深度失败: {error}", + en: "Failed to subscribe spot depth: {error}", + }, + "log.basis.processSpotDepthError": { + zh: "处理现货深度异常: {error}", + en: "Error processing spot depth: {error}", + }, "log.basis.futuresReady": { zh: "期货深度已就绪 ({symbol})", en: "Futures depth ready ({symbol})", @@ -438,6 +458,14 @@ const translations: Record = { zh: "获取现货盘口失败: {error}", en: "Failed to fetch spot orderbook: {error}", }, + "log.basis.subscribeFundingRateFail": { + zh: "订阅资金费率失败: {error}", + en: "Failed to subscribe funding rate: {error}", + }, + "log.basis.processFundingRateError": { + zh: "处理资金费率异常: {error}", + en: "Error processing funding rate: {error}", + }, "log.basis.fundingReady": { zh: "资金费率已就绪 ({symbol})", en: "Funding rate ready ({symbol})", @@ -446,6 +474,14 @@ const translations: Record = { zh: "获取资金费率失败: {error}", en: "Failed to fetch funding rate: {error}", }, + "log.basis.subscribeAccountFail": { + zh: "订阅账户快照失败: {error}", + en: "Failed to subscribe account snapshot: {error}", + }, + "log.basis.processAccountError": { + zh: "处理账户快照异常: {error}", + en: "Error processing account snapshot: {error}", + }, "log.basis.spotBalanceError": { zh: "获取现货余额失败: {error}", en: "Failed to fetch spot balance: {error}", diff --git a/src/strategy/basis-arb-engine.ts b/src/strategy/basis-arb-engine.ts index ae6072f..6d9a1e6 100644 --- a/src/strategy/basis-arb-engine.ts +++ b/src/strategy/basis-arb-engine.ts @@ -1,6 +1,6 @@ import type { BasisArbConfig } from "../config"; -import type { ExchangeAdapter } from "../exchanges/adapter"; -import type { AsterDepth, AsterSpotBookTicker } from "../exchanges/types"; +import type { ExchangeAdapter, FundingRateSnapshot } from "../exchanges/adapter"; +import type { AsterAccountSnapshot, AsterDepth, AsterSpotBookTicker } from "../exchanges/types"; import { AsterSpotRestClient, AsterRestClient } from "../exchanges/aster/client"; import { createTradeLog, type TradeLogEntry } from "../logging/trade-log"; import { StrategyEventEmitter } from "./common/event-emitter"; @@ -82,8 +82,8 @@ interface FuturesBalanceStateEntry { export class BasisArbEngine { private readonly events = new StrategyEventEmitter(); private readonly tradeLog: ReturnType; - private readonly spotClient: Pick; - private readonly futuresClient: Pick; + private readonly spotClient: Pick | null; + private readonly futuresClient: Pick | null; private readonly now: () => number; private readonly config: BasisArbConfig; private readonly exchange: ExchangeAdapter; @@ -109,8 +109,9 @@ export class BasisArbEngine { constructor(config: BasisArbConfig, exchange: ExchangeAdapter, deps: BasisArbDependencies = {}) { this.config = config; this.exchange = exchange; - this.spotClient = deps.spotClient ?? new AsterSpotRestClient(); - this.futuresClient = deps.futuresClient ?? new AsterRestClient(); + const isAster = exchange.id === "aster"; + this.spotClient = deps.spotClient ?? (isAster ? new AsterSpotRestClient() : null); + this.futuresClient = deps.futuresClient ?? (isAster ? new AsterRestClient() : null); this.now = deps.now ?? (() => Date.now()); this.tradeLog = createTradeLog(this.config.maxLogEntries); this.bootstrap(); @@ -118,6 +119,7 @@ export class BasisArbEngine { start(): void { if (this.timer) return; + if (this.exchange.id !== "aster") return; this.timer = setInterval(() => { void this.pollSpot(); void this.pollFunding(); @@ -164,6 +166,48 @@ export class BasisArbEngine { processFail: (error) => t("log.basis.processFuturesDepthError", { error: String(error) }), } ); + + if (this.exchange.id === "nado") { + safeSubscribe( + this.exchange.watchDepth.bind(this.exchange, this.config.spotSymbol), + (depth) => { + this.applySpotDepth(depth); + }, + log, + { + subscribeFail: (error) => t("log.basis.subscribeSpotDepthFail", { error: String(error) }), + processFail: (error) => t("log.basis.processSpotDepthError", { error: String(error) }), + } + ); + + if (typeof this.exchange.watchFundingRate === "function") { + safeSubscribe( + (cb) => { + this.exchange.watchFundingRate?.(this.config.futuresSymbol, cb); + }, + (snapshot) => { + this.applyFundingRateSnapshot(snapshot); + }, + log, + { + subscribeFail: (error) => t("log.basis.subscribeFundingRateFail", { error: String(error) }), + processFail: (error) => t("log.basis.processFundingRateError", { error: String(error) }), + } + ); + } + + safeSubscribe( + this.exchange.watchAccount.bind(this.exchange), + (snapshot) => { + this.applyAccountSnapshot(snapshot); + }, + log, + { + subscribeFail: (error) => t("log.basis.subscribeAccountFail", { error: String(error) }), + processFail: (error) => t("log.basis.processAccountError", { error: String(error) }), + } + ); + } } private applyFuturesDepth(depth: AsterDepth): void { @@ -189,7 +233,7 @@ export class BasisArbEngine { } private async pollSpot(): Promise { - if (this.spotInFlight || this.stopped) return; + if (!this.spotClient || this.spotInFlight || this.stopped) return; this.spotInFlight = true; try { const result = await this.spotClient.getBookTicker(this.config.spotSymbol); @@ -208,7 +252,7 @@ export class BasisArbEngine { } private async pollFunding(): Promise { - if (this.fundingInFlight || this.stopped) return; + if (!this.futuresClient || this.fundingInFlight || this.stopped) return; this.fundingInFlight = true; try { const data = await this.futuresClient.getPremiumIndex(this.config.futuresSymbol); @@ -237,7 +281,7 @@ export class BasisArbEngine { } private async pollSpotAccount(): Promise { - if (this.spotAccountInFlight || this.stopped) return; + if (!this.spotClient || this.spotAccountInFlight || this.stopped) return; this.spotAccountInFlight = true; try { // Spot balances via spot REST @@ -267,7 +311,7 @@ export class BasisArbEngine { } private async pollFuturesAccount(): Promise { - if (this.futuresAccountInFlight || this.stopped) return; + if (this.exchange.id !== "aster" || this.futuresAccountInFlight || this.stopped) return; this.futuresAccountInFlight = true; try { // Futures balances via futures REST @@ -316,6 +360,70 @@ export class BasisArbEngine { this.emitUpdate(); } + private applySpotDepth(depth: AsterDepth): void { + if (!depth?.bids?.length || !depth?.asks?.length) { + return; + } + const topBid = Number(depth.bids[0]?.[0]); + const topAsk = Number(depth.asks[0]?.[0]); + if (!Number.isFinite(topBid) || !Number.isFinite(topAsk)) { + return; + } + this.spot.bid = topBid; + this.spot.ask = topAsk; + this.spot.updatedAt = depth.eventTime ?? depth.tradeTime ?? this.now(); + if (!this.feedReady.spot) { + this.feedReady.spot = true; + this.tradeLog.push("info", t("log.basis.spotReady", { symbol: this.config.spotSymbol })); + } + if (this.feedReady.futures && this.feedReady.spot && this.marketReadyAt == null) { + this.marketReadyAt = this.now(); + } + this.emitUpdate(); + } + + private applyFundingRateSnapshot(snapshot: FundingRateSnapshot): void { + const rate = snapshot.fundingRate; + if (!Number.isFinite(rate)) return; + this.funding.rate = rate; + this.funding.nextFundingTime = null; + this.funding.updatedAt = Number.isFinite(snapshot.updateTime) ? snapshot.updateTime : this.now(); + if (!this.feedReady.funding) { + this.feedReady.funding = true; + this.tradeLog.push("info", t("log.basis.fundingReady", { symbol: this.config.futuresSymbol })); + } + this.emitUpdate(); + } + + private applyAccountSnapshot(snapshot: AsterAccountSnapshot): void { + const assets = Array.isArray(snapshot.assets) ? snapshot.assets : []; + + const spotBalances: SpotBalanceStateEntry[] = []; + const futuresBalances: FuturesBalanceStateEntry[] = []; + + for (const asset of assets) { + const name = String(asset.asset ?? ""); + const wallet = Number(asset.walletBalance ?? 0); + const available = Number(asset.availableBalance ?? 0); + if (!name) continue; + if (!Number.isFinite(wallet) || !Number.isFinite(available)) continue; + if (Math.abs(wallet) === 0 && Math.abs(available) === 0) continue; + + if (name === "USDT0") { + futuresBalances.push({ asset: name, wallet, available }); + continue; + } + const locked = Math.max(wallet - available, 0); + spotBalances.push({ asset: name, free: available, locked }); + } + + spotBalances.sort((a, b) => a.asset.localeCompare(b.asset)); + futuresBalances.sort((a, b) => a.asset.localeCompare(b.asset)); + this.spotBalances = spotBalances; + this.futuresBalances = futuresBalances; + this.emitUpdate(); + } + private emitUpdate(): void { // Build a single snapshot, evaluate signals against EXACTLY the same data, then emit that snapshot const snapshot = this.buildSnapshot(); diff --git a/src/ui/BasisApp.tsx b/src/ui/BasisApp.tsx index 16b6308..a7f2ea5 100644 --- a/src/ui/BasisApp.tsx +++ b/src/ui/BasisApp.tsx @@ -31,7 +31,7 @@ export function BasisApp({ onExit }: BasisAppProps) { ); useEffect(() => { - if (exchangeId !== "aster") { + if (exchangeId !== "aster" && exchangeId !== "nado") { setError(new Error(t("basis.onlyAster"))); return; }