# Dango SDK > Reference and concept docs for the TypeScript, Python, and Rust Dango SDKs. ## TypeScript SDK The TypeScript SDK is a viem-style client for the Dango execution environment. It speaks GraphQL over HTTP and WebSocket to a Dango indexer, signs transactions with EIP-712 typed data, and ships as six packages: `@left-curve/sdk`, `@left-curve/crypto`, `@left-curve/encoding`, `@left-curve/types`, `@left-curve/utils`, and `@left-curve/config`. ### Start here * [Installation](./getting-started/installation) — install the SDK and its peer requirements * [First Call](./getting-started/first-call) — five-minute hello world against testnet * [Project Setup](./getting-started/project-setup) — networks, signers, env vars ### Concepts * [Packages](./concepts/packages) — what lives in which package, and when to reach for a sub-package * [Clients](./concepts/clients) — `createPublicClient` vs `createSignerClient` * [Signers & Authentication](./concepts/signers-and-authentication) — `PrivateKeySigner`, session signers, passkeys * [Transactions](./concepts/transactions) — sign, broadcast, poll * [Subscriptions](./concepts/subscriptions) — the WebSocket model * [Encoding & Types](./concepts/encoding-and-types) — base units, `Decimal`, `bigint` * [Error Handling](./concepts/error-handling) — `BaseError` hierarchy * [Rate Limits & Quotas](./concepts/rate-limits) — 167 reqs / 10s, 30 subs / WS ### API Reference * Clients: [createPublicClient](./clients/createPublicClient), [createSignerClient](./clients/createSignerClient), [createBaseClient](./clients/createBaseClient) * Actions, grouped by domain: [App](./actions/app/transfer), [DEX](./actions/dex/swapExactAmountIn), [Perps](./actions/perps/submitPerpsOrder), [Account Factory](./actions/account-factory/registerUser), [Gateway](./actions/gateway/transferRemote), [Indexer](./actions/indexer/queryBlock), [Oracle](./actions/oracle/getPrices), [Hyperlane](./actions/hyperlane/Addr32) * [Types](./types/Address) — major user-facing types * [Errors](./errors/BaseError) — `BaseError` and friends ## Account A single on-chain account belonging to a user. ### Definition ```ts type Account = { readonly address: Address readonly index: AccountIndex readonly owner: number } ``` ### Fields **`address`** — `Address`. The account's on-chain address. **`index`** — `AccountIndex` (`number`). The account's index within its user. **`owner`** — `number`. The owning user's index. ### Construction Use the [`toAccount`](../concepts/encoding-and-types) helper to assemble one from a `User`, `AccountIndex`, and `Address`. ```ts import { toAccount } from "@left-curve/sdk" import type { Account, User } from "@left-curve/sdk" const account: Account = toAccount({ user, accountIndex: 0, address }) ``` ### See also * [`AccountInfo`](./AccountInfo) — `{ index, owner }` (no address) * [`AccountDetails`](./AccountDetails) — adds `username` * [`User`](./User) ## AccountDetails `Account` plus the owner's username. Returned by [`getAccountInfo`](../actions/account-factory/getAccountInfo) after resolving the user. ### Definition ```ts type AccountDetails = Account & { readonly username: Username } ``` ### Fields **`address`** — `Address`. Account address. **`index`** — `AccountIndex` (`number`). **`owner`** — `number`. User index. **`username`** — `Username` (`string`). The user's username. ### See also * [`Account`](./Account) * [`User`](./User) * [`getAccountInfo`](../actions/account-factory/getAccountInfo) ## AccountInfo Account record stored by the account factory (no address attached). ### Definition ```ts type AccountInfo = { readonly index: AccountIndex readonly owner: number } ``` ### Fields **`index`** — `AccountIndex` (`number`). **`owner`** — `number`. The owning user's index. ### See also * [`Account`](./Account) — includes the address * [`AccountDetails`](./AccountDetails) — also includes the username * [`getAccountInfo`](../actions/account-factory/getAccountInfo) * [`getAllAccountInfo`](../actions/account-factory/getAllAccountInfo) ## Address A 0x-prefixed hex string identifying an account or contract on Dango. ### Definition ```ts type Address = `0x${string}` ``` ### Construction ```ts import type { Address } from "@left-curve/sdk" import { isValidAddress } from "@left-curve/sdk" const addr: Address = "0x1234567890abcdef1234567890abcdef12345678" if (!isValidAddress(candidate)) { throw new Error(`invalid address: ${candidate}`) } ``` ### Notes * A valid address is exactly 42 characters: `"0x"` plus 40 hex digits (20 bytes). * The branded `0x${string}` template literal type prevents arbitrary strings from being passed where addresses are expected. * The SDK exports two helpers: [`computeAddress`](../concepts/encoding-and-types) derives an address from `(deployer, codeHash, salt)`, and `isValidAddress` validates a string at runtime. ### See also * [`Coin`](./Coin) * [`Account`](./Account) * [Concepts: Encoding & Types](../concepts/encoding-and-types) ## AppConfig The Dango app-level configuration: system contract addresses, fee rates, liquidation parameters, and minimum deposits. ### Definition ```ts type AppConfig = { addresses: { accountFactory: Address gateway: Address lending: Address oracle: Address dex: Address perps: Address warp: Address taxman: Address hyperlane: { ism: Address mailbox: Address va: Address } } makerFeeRate: string takerFeeRate: string maxLiquidationBonus: string minLiquidationBonus: string targetUtilizationRate: string minimumDeposit: Record } ``` ### Fields **`addresses`** — every well-known system contract. **`makerFeeRate`**, **`takerFeeRate`** — DEX fee rates (as decimal strings). **`maxLiquidationBonus`**, **`minLiquidationBonus`** — bounds on liquidation incentives. **`targetUtilizationRate`** — lending pool target. **`minimumDeposit`** — `Record`. Minimum amount per denom for new accounts. ### See also * [`ChainConfig`](./ChainConfig) * [`getAppConfig`](../actions/app/getAppConfig) ## Base64 A base64-encoded string. ### Definition ```ts type Base64 = string ``` ### Notes * Used for Wasm code (in `storeCode`), raw storage keys/values, signatures, and serialized data. * Convert with `encodeBase64` / `decodeBase64` from `@left-curve/encoding`. URL-safe variants exist as `encodeBase64Url` / `decodeBase64Url`. ### See also * [`Hex`](./Hex) * [Concepts: Encoding & Types](../concepts/encoding-and-types) ## BlockInfo A block header. Used by `queryStatus` to return the latest block. ### Definition ```ts type BlockInfo = { height: string timestamp: string hash: string } ``` ### Fields **`height`** — `string`. Block height (string-encoded to avoid 2^53 overflow concerns). **`timestamp`** — `string`. Block timestamp. **`hash`** — `string`. Block hash. ### See also * [`queryStatus`](../actions/app/queryStatus) * [`IndexedBlock`](./IndexedBlock) — block with transaction list ## Candle An OHLC candle for a spot pair at a given interval. ### Definition ```ts type Candle = { quoteDenom: Denom baseDenom: Denom interval: CandleIntervals blockHeight: number open: string high: string low: string close: string volumeBase: string volumeQuote: string timeStart: string timeStartUnix: number timeEnd: string timeEndUnix: number } ``` ### Fields OHLC values (`open`, `high`, `low`, `close`) are decimal strings. Volume is split between base (`volumeBase`) and quote (`volumeQuote`). Time is returned both as ISO 8601 (`timeStart`, `timeEnd`) and Unix seconds (`timeStartUnix`, `timeEndUnix`). `interval` is one of `ONE_SECOND`, `ONE_MINUTE`, `FIVE_MINUTES`, `FIFTEEN_MINUTES`, `ONE_HOUR`, `FOUR_HOURS`, `ONE_DAY`, `ONE_WEEK`. ### See also * [`queryCandles`](../actions/dex/queryCandles) * [`candlesSubscription`](../actions/indexer/candlesSubscription) * [`PerpsCandle`](./PerpsCandle) ## Chain The chain configuration consumed by `createTransport` and the client factories. ### Definition ```ts type Chain = { id: ChainId // string name: string url: string nativeCoin: Denom blockExplorer: { name: string txPage: string contractPage: string accountPage: string } } ``` ### Fields **`id`** — `string`. Chain id (e.g. `"dango-1"`, `"dango-testnet-1"`). **`name`** — `string`. Human-readable name. **`url`** — `string`. GraphQL endpoint URL. **`nativeCoin`** — `Denom`. The chain's native gas token. **`blockExplorer`** — explorer URL templates with `${txHash}` / `${address}` placeholders. ### Construction The SDK ships four ready-made chain configs: ```ts import { local, devnet, testnet, mainnet } from "@left-curve/sdk" ``` To define a custom chain, use the internal `defineChain` helper or build the object literally. ### See also * [Getting Started: Project Setup](../getting-started/project-setup) * [`createPublicClient`](../clients/createPublicClient) ## ChainConfig The chain-level configuration: owner, system contracts, permissions, cronjobs. ### Definition ```ts type ChainConfig = { owner: Address bank: Address taxman: Address cronjobs: Record permissions: { upload: Permission instantiate: Permission } maxOrphanAge: Duration } type Permission = | "everybody" | "nobody" | { somebodies: Address[] } ``` ### Fields **`owner`** — `Address`. Account that can update this config. **`bank`** — `Address`. Bank contract (token transfers). **`taxman`** — `Address`. Fee handler contract. **`cronjobs`** — map of contract address to interval (in seconds). **`permissions.upload`** — who can `storeCode`. **`permissions.instantiate`** — who can `instantiate`. **`maxOrphanAge`** — `Duration` (seconds). Orphaned codes are deleted after this age. ### See also * [`AppConfig`](./AppConfig) * [`configure`](../actions/app/configure) ## Coin A `{ denom, amount }` pair representing some quantity of one token. ### Definition ```ts type Coin = { readonly denom: Denom readonly amount: string } ``` ### Fields **`denom`** — `Denom`. Token denomination (e.g. `"dango"`, `"bridge/usdc"`). **`amount`** — `string`. Amount in base units. String form preserves arbitrary precision. ### Construction ```ts import type { Coin } from "@left-curve/sdk" const oneDango: Coin = { denom: "dango", amount: "1000000" } ``` ### Notes * Always base units (atomic, indivisible). Use [`formatUnits`](../concepts/encoding-and-types) to display. ### See also * [`Coins`](./Coins) — record of denom → amount * [`Denom`](./Denom) * [`Funds`](./Funds) ## Coins A record mapping denoms to base-unit amount strings. The canonical multi-coin payload across the SDK. ### Definition ```ts type Coins = Record ``` ### Construction ```ts import type { Coins } from "@left-curve/sdk" const wallet: Coins = { dango: "1500000000", "bridge/usdc": "1000000", } ``` ### Notes * Order-independent; keys are denoms, values are base-unit amounts. * Identical shape to [`Funds`](./Funds) — the SDK distinguishes them by intent only (`Coins` is a balance/snapshot, `Funds` is "coins attached to a call"). ### See also * [`Coin`](./Coin) — single denom variant * [`Funds`](./Funds) * [`getBalances`](../actions/app/getBalances) ## ContractInfo Contract metadata returned by [`getContractInfo`](../actions/app/getContractInfo). ### Definition ```ts type ContractInfo = { codeHash: Hex label?: string admin?: Address } ``` ### Fields **`codeHash`** — `Hex`. SHA-256 of the contract's current Wasm. **`label`** — `string`, optional. Human-readable label set at instantiation. **`admin`** — `Address`, optional. Account allowed to call [`migrate`](../actions/app/migrate). ### See also * [`getContractInfo`](../actions/app/getContractInfo) * [`getContractsInfo`](../actions/app/getContractsInfo) ## Credential The authentication bundle attached to a transaction. Either a standard credential from the primary key or a session credential. ### Definition ```ts type Credential = | { standard: StandardCredential } | { session: SessionCredential } type StandardCredential = { keyHash: KeyHash signature: Signature } type SessionCredential = { sessionInfo: SigningSessionInfo sessionSignature: Base64 authorization: StandardCredential } ``` ### Variants **`standard`** — the user's primary key signed the transaction directly. `signature` is the raw signature; `keyHash` identifies which key was used. **`session`** — a session key signed the transaction. `authorization` is the primary key's signature over `sessionInfo`, proving the session is authorized. `sessionSignature` is the session key's signature over the tx. ### See also * [`Signature`](./Signature) * [`SigningSession`](./SigningSession) * [Concepts: Signers & Authentication](../concepts/signers-and-authentication) ## Denom A token denomination — a chain-unique string identifier. ### Definition ```ts type Denom = string ``` ### Construction ```ts import type { Denom } from "@left-curve/sdk" const dango: Denom = "dango" const usdc: Denom = "bridge/usdc" const lp: Denom = "lp/dango/bridge_usdc" ``` ### Notes * Denoms are free-form strings. By convention, system tokens are short (`"dango"`), bridged tokens use the `bridge/` prefix, and LP tokens use `lp/{base}/{quote}` with the slash in the quote denom replaced by an underscore. * Use the branded type to keep denoms and addresses distinct at compile time. ### See also * [`Coin`](./Coin) * [`Coins`](./Coins) ## Funds Coins attached to a contract call (e.g. inside an `execute` or `transferRemote` message). ### Definition ```ts type Funds = Record ``` ### Construction ```ts import type { Funds } from "@left-curve/sdk" const funds: Funds = { "bridge/usdc": "1000000", } ``` ### Notes * Same shape as [`Coins`](./Coins); the distinct alias signals intent in action signatures. * The SDK builds the EIP-712 typed data for `funds` automatically. See [`execute`](../actions/app/execute). ### See also * [`Coins`](./Coins) * [`execute`](../actions/app/execute) ## Hex A hex-encoded string. ### Definition ```ts type Hex = string ``` ### Notes * Used for code hashes, key bytes, and key hashes. Some hex strings are conventionally lower-case (code hashes), others upper-case (key hashes). * For strict template-literal typing (`0x${string}`), use [`Address`](./Address) instead. * Validate with `isHex` from `@left-curve/encoding`; convert with `encodeHex` / `decodeHex`. ### See also * [`Base64`](./Base64) * [Concepts: Encoding & Types](../concepts/encoding-and-types) ## IndexedBlock A block as returned by the indexer — header plus transactions and cron outcomes. ### Definition ```ts type IndexedBlock = { blockHeight: number createdAt: string hash: string appHash: string cronsOutcomes: string // JSON-encoded outcomes transactions: IndexedTransaction[] } ``` ### See also * [`queryBlock`](../actions/indexer/queryBlock) * [`blockSubscription`](../actions/indexer/blockSubscription) — omits `transactions` * [`BlockInfo`](./BlockInfo) ## Key A public key that can be associated with a Dango account. Discriminated by key family. ### Definition ```ts type Key = | { secp256k1: Base64 } // compressed pubkey | { ethereum: Address } // EOA address | { secp256r1: Base64 } // compressed pubkey const KeyTag = { secp256r1: 0, secp256k1: 1, ethereum: 2, } as const ``` ### Construction ```ts import type { Key } from "@left-curve/sdk" const k1: Key = { secp256k1: "Ahw5..." } const eth: Key = { ethereum: "0x1234567890abcdef1234567890abcdef12345678" } const r1: Key = { secp256r1: "BJw5..." } ``` ### Notes * Use the `in` operator or check the present field to narrow: ```ts if ("secp256k1" in key) { /* ... */ } ``` * `KeyTag` is the discriminant value the chain uses when packing a key into a salt — see [`createAccountSalt`](../concepts/encoding-and-types). ### See also * [`KeyHash`](./KeyHash) * [`PublicKey`](./PublicKey) * [`updateKey`](../actions/account-factory/updateKey) ## KeyHash Uppercase hex SHA-256 of a public key or credential id. Used to identify keys on chain. ### Definition ```ts type KeyHash = Hex ``` ### Construction ```ts import { createKeyHash } from "@left-curve/sdk" const hash = createKeyHash(publicKeyBytes) // "ABCDEF0123456789..." ``` The helper sha256-hashes the input and upper-cases the hex output. ### See also * [`Key`](./Key) * [`createKeyHash`](../concepts/encoding-and-types) ## Message A discriminated union of the seven transaction-level messages Dango accepts. ### Definition ```ts type Message = | { configure: MsgConfigure } | { upgrade: MsgUpgrade } | { transfer: MsgTransfer } | { upload: MsgStoreCode } | { instantiate: MsgInstantiate } | { execute: MsgExecute } | { migrate: MsgMigrate } ``` ### Variants **`configure`** — update chain or app config. **`upgrade`** — schedule a chain upgrade. **`transfer`** — `Record`. Send funds to one or more recipients. **`upload`** — store a Wasm code blob (`{ code: Base64 }`). **`instantiate`** — create a new contract (`{ codeHash, msg, salt, funds?, admin? }`). **`execute`** — call a deployed contract (`{ contract, msg, funds? }`). **`migrate`** — change a contract's code hash (`{ contract, newCodeHash, msg }`). ### Notes * Use the `in` operator to narrow: `if ("transfer" in msg) { ... }`. * For one-off construction, the typed wrappers ([`transfer`](../actions/app/transfer), [`execute`](../actions/app/execute), etc.) are simpler. Build `Message` values directly only when batching multiple kinds in one tx via [`signAndBroadcastTx`](../actions/app/signAndBroadcastTx). ### See also * [`Tx`](./Tx) * [`signAndBroadcastTx`](../actions/app/signAndBroadcastTx) ## OrderId A spot order id. ### Definition ```ts type OrderId = string ``` ### Notes * Returned by the chain after a new order lands. Use [`ordersByUser`](../actions/dex/ordersByUser) to enumerate active ids for an account. ### See also * [`getOrder`](../actions/dex/getOrder) * [`batchUpdateOrders`](../actions/dex/batchUpdateOrders) ## PairId The identifier of a spot trading pair — base and quote denom. ### Definition ```ts type PairId = { baseDenom: string quoteDenom: string } ``` ### Construction ```ts import type { PairId } from "@left-curve/sdk" const dangoUsdc: PairId = { baseDenom: "dango", quoteDenom: "bridge/usdc" } ``` ### Notes * A `SwapRoute` is `PairId[]` — an ordered list of hops. ### See also * [`PairParams`](./PairParams) * [`getPair`](../actions/dex/getPair) ## PairParams Parameters of a single spot trading pair. ### Definition ```ts type PairParams = { lpDenom: Denom curveInvariant: "xyk" swapFeeRate: string bucketSizes: string[] minOrderSizeBase: string minOrderSizeQuote: string } ``` ### Fields **`lpDenom`** — `Denom`. Denomination of the pair's LP token. **`curveInvariant`** — `"xyk"`. The passive pool curve (only `xyk` currently). **`swapFeeRate`** — `string`. Fee on instant swaps. **`bucketSizes`** — `string[]`. Price bucket widths for the depth chart. **`minOrderSizeBase`**, **`minOrderSizeQuote`** — minimum order sizes denominated in base and quote. ### See also * [`PairId`](./PairId) * [`PairUpdate`](./PairUpdate) * [`getPair`](../actions/dex/getPair) ## PairStats 24h indexer-computed stats for a spot pair. ### Definition ```ts type PairStats = { quoteDenom: Denom baseDenom: Denom currentPrice: string | undefined price24HAgo: string | undefined volume24H: string priceChange24H: string | undefined } ``` ### Fields **`currentPrice`** — current price (latest fill). **`price24HAgo`** — price 24 hours ago. **`volume24H`** — total volume over the last 24 hours. **`priceChange24H`** — relative change vs 24 hours ago. Any field may be `undefined` if the indexer lacks data. ### See also * [`getPairStats`](../actions/dex/getPairStats) * [`getAllPairStats`](../actions/dex/getAllPairStats) * [`PerpsPairStats`](./PerpsPairStats) ## PairUpdate A pair listing returned by `getPairs` — id plus parameters. ### Definition ```ts type PairUpdate = { baseDenom: Denom quoteDenom: Denom params: PairParams } ``` ### See also * [`PairId`](./PairId) * [`PairParams`](./PairParams) * [`getPairs`](../actions/dex/getPairs) ## PerpsCandle An OHLC candle for a perps pair. ### Definition ```ts type PerpsCandle = { pairId: string interval: CandleIntervals minBlockHeight: number maxBlockHeight: number open: string high: string low: string close: string volume: string volumeUsd: string timeStart: string timeStartUnix: number timeEnd: string timeEndUnix: number } ``` ### Fields OHLC values are decimal strings. `volume` is in base asset; `volumeUsd` is the USD equivalent. `minBlockHeight` and `maxBlockHeight` bracket the candle's block range. ### See also * [`queryPerpsCandles`](../actions/perps/queryPerpsCandles) * [`perpsCandlesSubscription`](../actions/indexer/perpsCandlesSubscription) * [`Candle`](./Candle) ## PerpsEvent A perps event — fill, liquidation, or deleverage. The `data` field is a discriminated payload. ### Definition ```ts type PerpsEvent = { idx: number blockHeight: number txHash: string eventType: "order_filled" | "liquidated" | "deleveraged" userAddr: string pairId: string data: OrderFilledData | LiquidatedData | DeleveragedData createdAt: string } ``` ### Notes * Narrow `data` by `eventType`: ```ts if (event.eventType === "order_filled") { const filled = event.data as OrderFilledData } ``` ### See also * [`queryPerpsEvents`](../actions/perps/queryPerpsEvents) * [`PerpsTrade`](./PerpsTrade) ## PerpsOrderKind A perps order's kind — market or limit. ### Definition ```ts type PerpsOrderKind = | { market: { maxSlippage: string } } | { limit: { limitPrice: string timeInForce: "GTC" | "IOC" | "POST" clientOrderId?: string | null } } ``` ### Construction ```ts import type { PerpsOrderKind } from "@left-curve/sdk" const market: PerpsOrderKind = { market: { maxSlippage: "0.005" } } const limit: PerpsOrderKind = { limit: { limitPrice: "65000", timeInForce: "GTC" }, } ``` ### Notes * `clientOrderId` is not allowed with `timeInForce: "IOC"`. ### See also * [`submitPerpsOrder`](../actions/perps/submitPerpsOrder) * [`cancelPerpsOrder`](../actions/perps/cancelPerpsOrder) ## PerpsPairParam Parameters of a single perps pair. ### Definition ```ts type PerpsPairParam = { tickSize: string minOrderSize: string maxAbsOi: string maxAbsFundingRate: string initialMarginRatio: string maintenanceMarginRatio: string impactSize: string vaultLiquidityWeight: string vaultHalfSpread: string vaultMaxQuoteSize: string bucketSizes: string[] } ``` ### Fields **`tickSize`** — minimum price increment. **`minOrderSize`** — minimum position size. **`maxAbsOi`** — open-interest cap per side. **`maxAbsFundingRate`** — cap on absolute funding rate. **`initialMarginRatio`**, **`maintenanceMarginRatio`** — margin requirements. **`impactSize`** — size used for impact-price computation. **`vaultLiquidityWeight`**, **`vaultHalfSpread`**, **`vaultMaxQuoteSize`** — vault market-making params. **`bucketSizes`** — depth-chart bucket widths. ### See also * [`getPerpsPairParam`](../actions/perps/getPerpsPairParam) * [`getPerpsPairParams`](../actions/perps/getPerpsPairParams) * [`PerpsParam`](./PerpsParam) ## PerpsPairStats 24h indexer-computed stats for a perps pair. ### Definition ```ts type PerpsPairStats = { pairId: string currentPrice: string | undefined price24HAgo: string | undefined volume24H: string priceChange24H: string | undefined } ``` ### See also * [`getPerpsPairStats`](../actions/perps/getPerpsPairStats) * [`getAllPerpsPairStats`](../actions/perps/getAllPerpsPairStats) * [`PairStats`](./PairStats) ## PerpsParam Global perps parameters. ### Definition ```ts type PerpsParam = { maxUnlocks: number maxOpenOrders: number maxActionBatchSize: number liquidationBufferRatio: string makerFeeRates: RateSchedule takerFeeRates: RateSchedule protocolFeeRate: string liquidationFeeRate: string fundingPeriod: number vaultTotalWeight: string vaultCooldownPeriod: number referralActive: boolean minReferrerVolume: string referrerCommissionRates: RateSchedule vaultDepositCap: string | null } type RateSchedule = { base: string tiers: Record } ``` ### Notes * `RateSchedule` has a base rate plus volume-tier overrides keyed by min-volume thresholds. * `vaultCooldownPeriod` is in seconds. ### See also * [`getPerpsParam`](../actions/perps/getPerpsParam) * [`PerpsPairParam`](./PerpsPairParam) ## PerpsTrade A perps trade fill. ### Definition ```ts type PerpsTrade = { orderId: string pairId: string user: string fillPrice: string fillSize: string closingSize: string openingSize: string realizedPnl: string fee: string createdAt: string blockHeight: number tradeIdx: number fillId?: string | null isMaker?: boolean | null } ``` ### Fields **`closingSize`**, **`openingSize`** — portion of the fill that closed an existing position vs opened new exposure. **`realizedPnl`** — closing PnL. Prior to v0.17.0 also bundled funding; later versions report it separately. **`fillId`** — shared between the two `OrderFilled` events of a single match. `null` for trades before v0.15.0. **`isMaker`** — `true` for maker, `false` for taker, `null` for trades before v0.16.0. ### See also * [`perpsTradesSubscription`](../actions/indexer/perpsTradesSubscription) * [`PerpsEvent`](./PerpsEvent) ## PerpsUserState A user's perps state — margin, positions, vault shares. ### Definition ```ts type PerpsUserState = { margin: string vaultShares: string positions: Record unlocks: PerpsUnlock[] reservedMargin: string openOrderCount: number } type PerpsPosition = { size: string entryPrice: string entryFundingPerUnit: string conditionalOrderAbove?: ConditionalOrder conditionalOrderBelow?: ConditionalOrder } type PerpsUnlock = { endTime: string amountToRelease: string } ``` ### Fields **`margin`** — total collateral. **`vaultShares`** — vault shares held. **`positions`** — `Record`. **`unlocks`** — pending vault withdrawals waiting on cooldown. **`reservedMargin`** — margin reserved for open orders. **`openOrderCount`** — active perps orders. ### See also * [`PerpsUserStateExtended`](./PerpsUserStateExtended) — adds PnL, equity, liquidation prices * [`getPerpsUserState`](../actions/perps/getPerpsUserState) ## PerpsUserStateExtended Extended perps user state with optional PnL, equity, margin, and liquidation fields. ### Definition ```ts type PerpsUserStateExtended = { margin: string vaultShares: string unlocks: PerpsUnlock[] reservedMargin: string openOrderCount: number equity: string | null availableMargin: string | null maintenanceMargin: string | null positions: Record } type PerpsPositionExtended = PerpsPosition & { unrealizedPnl: string | null unrealizedFunding: string | null liquidationPrice: string | null } ``` ### Notes * Each `* | null` field is populated only when the matching `include*` flag was set in the query. See [`getPerpsUserStateExtended`](../actions/perps/getPerpsUserStateExtended). ### See also * [`PerpsUserState`](./PerpsUserState) — non-extended * [`getPerpsUserStateExtended`](../actions/perps/getPerpsUserStateExtended) ## Price An oracle price entry — current price, EMA, precision, and timestamp. ### Definition ```ts type Price = { humanizedPrice: string humanizedEma: string precision: number timestamp: number } ``` ### Fields **`humanizedPrice`** — `string`. Price of one human-unit of the token (e.g. price of 1 ATOM, not 1 uatom). **`humanizedEma`** — `string`. Exponential moving average of the humanized price. **`precision`** — `number`. Decimal places of the token (e.g. 6 for `bridge/usdc`). **`timestamp`** — `number`. Unix timestamp (seconds) of the price. ### See also * [`getPrices`](../actions/oracle/getPrices) ## PublicKey A public key as returned by the indexer — includes metadata like creation time and key type. ### Definition ```ts type PublicKey = { keyHash: KeyHash publicKey: Hex keyType: "SECP256R1" | "SECP256K1" | "ETHEREUM" createdBlockHeight: number createdAt: DateTime } ``` ### Fields **`keyHash`** — `KeyHash`. The hash of the public key. **`publicKey`** — `Hex`. The public key bytes, hex-encoded. **`keyType`** — uppercase of one of `KeyTag`'s keys. **`createdBlockHeight`** — `number`. Height at which the key was added. **`createdAt`** — `DateTime`. ISO 8601 timestamp. ### See also * [`Key`](./Key) — the on-chain representation * [`getUserKeys`](../actions/account-factory/getUserKeys) ## Signature A discriminated signature carried by a credential. One of three families: secp256k1, WebAuthn passkey, or EIP-712. ### Definition ```ts type Signature = | { secp256k1: Base64 } | { passkey: PasskeySignature } | { eip712: Eip712Signature } type Secp256k1Signature = Base64 type PasskeySignature = { sig: Base64 client_data: Base64 authenticator_data: Base64 } type Eip712Signature = { sig: Base64 typed_data: Base64 } ``` ### Notes * Narrow with `in`: ```ts if ("secp256k1" in sig) { /* ... */ } ``` ### See also * [`Credential`](./Credential) * [`Signer`](./Signer) ## Signer The interface all signing implementations satisfy. `signTx` signs a tx; `signArbitrary` signs an off-chain payload. ### Definition ```ts type Signer = { getKeyHash(): Promise signTx(signDoc: SignDoc): Promise signArbitrary( payload: ArbitraryDoc, ): Promise } ``` ### Methods **`getKeyHash()`** — return the `KeyHash` of the key this signer uses. **`signTx(signDoc)`** — sign an EIP-712-typed transaction document. Returns the credential plus the (possibly re-shaped) signed payload. **`signArbitrary(payload)`** — sign an arbitrary typed data payload. Used for session creation and off-chain proofs. ### Built-in implementations * [`PrivateKeySigner`](../concepts/signers-and-authentication) — local secp256k1 key. * [`createSessionSigner`](../concepts/signers-and-authentication) — session key + stored authorization. Note: `signArbitrary` signs `payload.message` rather than the full payload. ### See also * [`Credential`](./Credential) * [`SigningSession`](./SigningSession) * [Concepts: Signers & Authentication](../concepts/signers-and-authentication) ## SigningSession A session-key bundle. Pair the ephemeral session key with the primary-key authorization that legitimizes it. ### Definition ```ts type SigningSession = { publicKey: Uint8Array privateKey: Uint8Array keyHash: string sessionInfo: SigningSessionInfo authorization: StandardCredential } type SigningSessionInfo = { chainId: string sessionKey: Base64 expireAt: string // seconds, as string } ``` ### Fields **`publicKey`** — `Uint8Array`. Session key's public bytes. **`privateKey`** — `Uint8Array`. Session key's private bytes. **`keyHash`** — `string`. The primary key's hash (used to look up the authorization on chain). **`sessionInfo`** — `SigningSessionInfo`. Chain id, session pubkey, expiry. **`authorization`** — `StandardCredential`. The primary key's signature over `sessionInfo`. ### Construction Use [`createSession`](../actions/account-factory/createSession) on a signer client to mint a bundle, then persist it. ### See also * [`createSession`](../actions/account-factory/createSession) * [`createSessionSigner`](../concepts/signers-and-authentication) * [`Credential`](./Credential) ## Trade A spot trade fill. ### Definition ```ts type Trade = { addr: Address quoteDenom: Denom baseDenom: Denom timeInForce: TimeInForceOptions direction: Directions blockHeight: number createdAt: string filledBase: string filledQuote: string refundBase: string refundQuote: string feeBase: string feeQuote: string clearingPrice: string } ``` ### Fields **`addr`** — `Address`. The trader. **`direction`** — `"bid"` or `"ask"` (`Directions`). **`filledBase`**, **`filledQuote`** — amounts filled in base/quote. **`refundBase`**, **`refundQuote`** — amounts refunded if the order over-funded. **`feeBase`**, **`feeQuote`** — fees paid. **`clearingPrice`** — the price the trade cleared at. ### See also * [`queryTrades`](../actions/dex/queryTrades) * [`tradesSubscription`](../actions/indexer/tradesSubscription) * [`PerpsTrade`](./PerpsTrade) ## Tx A signed transaction. Holds messages, sender, gas, credential, and metadata. ### Definition ```ts type Tx = { sender: Address msgs: Message[] gasLimit: number credential: Credential data: Metadata } type UnsignedTx = Pick ``` ### Fields **`sender`** — `Address`. The signing/paying account. **`msgs`** — `Message[]`. The list of messages to execute. **`gasLimit`** — `number`. Maximum gas the tx may consume. **`credential`** — `Json` (typically `Credential`). The signature bundle. **`data`** — `Json` (typically `{ chainId, userIndex, nonce }`). EIP-712 metadata signed with the tx. ### See also * [`Message`](./Message) * [`Credential`](./Credential) * [`signAndBroadcastTx`](../actions/app/signAndBroadcastTx) * [Concepts: Transactions](../concepts/transactions) ## User A user record — index, name, registered keys, and owned accounts. ### Definition ```ts type User = { index: number name: Username keys: Record accounts: Record } ``` ### Fields **`index`** — `number`. Chain-unique user index (monotonically assigned). **`name`** — `Username` (`string`). Human-readable username. **`keys`** — `Record`. All authorized keys, keyed by their hash. **`accounts`** — `Record`. All owned accounts. ### See also * [`Account`](./Account) * [`Key`](./Key) * [`PublicKey`](./PublicKey) * [`getUser`](../actions/account-factory/getUser) ## First Call Hit the public testnet with a read-only client. No signer needed. ### Hello, chain ```ts import { createPublicClient, createTransport, testnet } from "@left-curve/sdk" const client = createPublicClient({ chain: testnet, transport: createTransport(), }) const status = await client.queryStatus() console.log(status.chainId, status.block.height) ``` `createTransport()` reads the URL from the chain config (`testnet.url`). To override, pass a URL: `createTransport("https://api-testnet.dango.zone")`. ### Query a balance ```ts import { createPublicClient, createTransport, testnet } from "@left-curve/sdk" import type { Address } from "@left-curve/sdk" const client = createPublicClient({ chain: testnet, transport: createTransport(), }) const address: Address = "0x1234567890abcdef1234567890abcdef12345678" const amount = await client.getBalance({ address, denom: "dango" }) console.log(amount) ``` `getBalance` returns a base-unit `string`. Parse with `BigInt` or `Decimal` before doing arithmetic — see [Encoding & Types](../concepts/encoding-and-types). ### Next * [Project Setup](./project-setup) — add a signer and broadcast transactions * [Concepts: Clients](../concepts/clients) — when to reach for `createSignerClient` import { HomePage } from 'vocs/components' ## Installation Install `@left-curve/sdk` from your package manager. Node 20 or later is required. ### Install ### Sub-packages `@left-curve/sdk` re-exports the most-used pieces of its sibling packages. Install a sub-package directly only when you want a smaller bundle or you need a symbol that the main entry does not re-export. ```bash pnpm add @left-curve/crypto pnpm add @left-curve/encoding pnpm add @left-curve/types pnpm add @left-curve/utils ``` See [Concepts: Packages](../concepts/packages) for the full split. ### TypeScript Strict mode must be on: ```json { "compilerOptions": { "strict": true, "target": "ES2022", "module": "ESNext", "moduleResolution": "Bundler" } } ``` ### Next * [First Call](./first-call) — call `queryStatus` against testnet * [Project Setup](./project-setup) — wire up signers and chain configs ## Project Setup Wire up signers, chains, and environment variables for a real project. ### Pick a chain The SDK ships four chain configs: ```ts import { local, devnet, testnet, mainnet } from "@left-curve/sdk" ``` | Chain | `id` | URL | | ------- | ----------------- | -------------------------------- | | local | `localdango-1` | `http://localhost:8080` | | devnet | `dev-9` | `https://api-devnet.dango.zone` | | testnet | `dango-testnet-1` | `https://api-testnet.dango.zone` | | mainnet | `dango-1` | `https://api-mainnet.dango.zone` | Override the URL by passing it to `createTransport(url)`. ### Add a signer `createSignerClient` requires a `Signer`. The SDK ships `PrivateKeySigner` for server-side and CLI usage. ```ts import { createSignerClient, createTransport, testnet, PrivateKeySigner } from "@left-curve/sdk" const signer = PrivateKeySigner.fromMnemonic(process.env.DANGO_MNEMONIC!) const client = createSignerClient({ chain: testnet, transport: createTransport(), signer, }) ``` Other signer paths: * `PrivateKeySigner.fromPrivateKey(bytes)` — bring your own bytes * `PrivateKeySigner.fromRandomKey()` — generate a fresh ephemeral key * `createSessionSigner(session)` — sign with a session credential See [Concepts: Signers & Authentication](../concepts/signers-and-authentication) for the full breakdown. ### Environment variables Treat the mnemonic like a private key. Use `.env` and never commit it. ```bash DANGO_MNEMONIC="your twelve or twenty four word seed phrase here" DANGO_RPC_URL="https://api-testnet.dango.zone" ``` ```ts import "dotenv/config" import { createSignerClient, createTransport, testnet, PrivateKeySigner } from "@left-curve/sdk" const signer = PrivateKeySigner.fromMnemonic(process.env.DANGO_MNEMONIC!) const client = createSignerClient({ chain: testnet, transport: createTransport(process.env.DANGO_RPC_URL), signer, }) ``` ### Browser caveats `PrivateKeySigner` works in the browser but exposes the mnemonic in memory. For wallets, use passkeys ([`createWebAuthnCredential`](../concepts/signers-and-authentication)) or session keys backed by a stored authorization. ### Next * [Concepts: Clients](../concepts/clients) — the public/signer split * [Concepts: Transactions](../concepts/transactions) — sign and broadcast ## BaseError Base class for every SDK error. Carries structured metadata for triage. ### Definition ```ts class BaseError extends Error { override name: string // "BaseError" shortMessage: string details: string metaMessages?: string[] constructor(shortMessage: string, args?: { cause?: BaseError | Error details?: string metaMessages?: string[] name?: string }) } ``` ### Fields **`name`** — `string`. Discriminator. Overridden by subclasses (`"HttpRequestError"`, `"TimeoutError"`, `"UrlRequiredError"`). **`shortMessage`** — `string`. The one-line summary passed at construction. **`details`** — `string`. Detailed explanation. Derived from `cause.details` (when cause is a `BaseError`) or `cause.message`, else from the explicit `details` arg. **`metaMessages`** — `string[] | undefined`. Contextual lines composed into the final `message`. ### Narrowing `BaseError` is **not** currently exported from `@left-curve/sdk`. There is no `@left-curve/sdk/errors` subpath. Narrow by `name` instead of `instanceof`: ```ts try { await client.getBalance({ address, denom: "dango" }) } catch (err) { if (err instanceof Error && err.name === "HttpRequestError") { // handle network failure } } ``` The final `message` is composed as: ``` {shortMessage} {metaMessages joined by newline} Details: {details} ``` ### Notes * For richer narrowing today, check `err.name` against the known discriminator strings (`"BaseError"`, `"HttpRequestError"`, `"TimeoutError"`, `"UrlRequiredError"`). * See [Concepts: Error Handling](../concepts/error-handling) for the recommended pattern. ### See also * [`HttpRequestError`](./HttpRequestError) * [`TimeoutError`](./TimeoutError) * [`UrlRequiredError`](./UrlRequiredError) * [Concepts: Error Handling](../concepts/error-handling) ## HttpRequestError Thrown when a GraphQL HTTP request fails — non-2xx response, network failure, or GraphQL errors. ### Definition ```ts class HttpRequestError extends BaseError { override name: string // "HttpRequestError" body?: object | object[] headers?: Headers status?: number url: string constructor(args: { body?: object | object[] cause?: Error details?: string headers?: Headers status?: number url: string }) } ``` ### Fields **`url`** — `string`. The URL that failed. **`status`** — `number | undefined`. HTTP status code, when available. **`headers`** — `Headers | undefined`. Response headers, when available. **`body`** — `object | object[] | undefined`. The GraphQL operation body sent. Inherits `shortMessage`, `details`, `metaMessages`, `name` from [`BaseError`](./BaseError). ### Construction The transport throws this internally — typically you only catch it: ```ts try { await client.queryStatus() } catch (err) { if (err instanceof Error && err.name === "HttpRequestError") { const e = err as Error & { url: string; status?: number } console.error(`HTTP ${e.status ?? "?"} at ${e.url}`) } else { throw err } } ``` ### Notes * Thrown by the `createTransport` request handler when `errors[]` is non-empty in the GraphQL response, or when the underlying HTTP request fails outright. ### See also * [`TimeoutError`](./TimeoutError) * [`BaseError`](./BaseError) * [Concepts: Error Handling](../concepts/error-handling) ## TimeoutError Thrown when a request exceeds its configured timeout. ### Definition ```ts class TimeoutError extends BaseError { override name: string // "TimeoutError" constructor(args: { body: object | object[] url: string }) } ``` ### Fields Inherits `shortMessage` (`"The request took too long to respond."`), `details` (`"The request timed out."`), `metaMessages` (`["URL: ...", "Request body: ..."]`), `name` from [`BaseError`](./BaseError). ### Construction The transport throws this internally. Typical handling: ```ts try { await client.queryStatus() } catch (err) { if (err instanceof Error && err.name === "TimeoutError") { // back off and retry } else { throw err } } ``` The default timeout is 10 seconds. Override via `createTransport(url, { timeout: 5000 })`. ### See also * [`HttpRequestError`](./HttpRequestError) * [`BaseError`](./BaseError) * [Concepts: Rate Limits & Quotas](../concepts/rate-limits) — when to back off ## UrlRequiredError Thrown by `createTransport` when no URL is supplied and the chain config also lacks one. ### Definition ```ts class UrlRequiredError extends BaseError { constructor() } ``` ### Fields Inherits `shortMessage` (`"No URL was provided to the Transport. Please provide a valid RPC URL to the Transport."`), `name` from [`BaseError`](./BaseError). ### Construction The transport throws this during client creation: ```ts import { createBaseClient, createTransport } from "@left-curve/sdk" // No URL on transport, no URL on chain → UrlRequiredError const transport = createTransport() createBaseClient({ chain: { id: "x", name: "x", url: "", nativeCoin: "x", blockExplorer: {} as any }, transport, }) ``` ### Notes * Pass a URL to `createTransport(url)` or set `chain.url` to fix. ### See also * [`BaseError`](./BaseError) * [Getting Started: Project Setup](../getting-started/project-setup) ## Clients **What this teaches:** when to use `createPublicClient` vs `createSignerClient`, and how both relate to `createBaseClient`. ### Mental model A client bundles three things: a transport (the GraphQL endpoint), a chain config, and a signer (optional). On top of that, actions extend the client object so you can call `client.getBalance({...})` instead of `getBalance(client, {...})`. The split: * `createBaseClient` — bare scaffolding with `transport`, `chain`, optional `signer`, and `extend()`. You only call this directly when you need to assemble actions yourself. * `createPublicClient` — base + `publicActions`. Read-only. No signer. * `createSignerClient` — base + `publicActions` + `signerActions`. Requires a `Signer`. Can broadcast transactions. ### When to use which Use a public client for indexers, dashboards, anything that does not mutate state: ```ts import { createPublicClient, createTransport, testnet } from "@left-curve/sdk" const client = createPublicClient({ chain: testnet, transport: createTransport(), }) const balances = await client.getBalances({ address: "0x1234567890abcdef1234567890abcdef12345678", }) ``` Use a signer client when you need to broadcast: ```ts import { createSignerClient, createTransport, testnet, PrivateKeySigner } from "@left-curve/sdk" const signer = PrivateKeySigner.fromMnemonic(process.env.DANGO_MNEMONIC!) const client = createSignerClient({ chain: testnet, transport: createTransport(), signer, }) await client.transfer({ sender: "0x1234567890abcdef1234567890abcdef12345678", transfer: { "0xabcdef1234567890abcdef1234567890abcdef12": { dango: "1000000" }, }, }) ``` A signer client has every method a public client has plus the mutation surface. It is a strict superset. ### Gateway namespacing The `gateway` domain is the only one with namespaced actions. You call them as `client.gateway.transferRemote(...)`, not `client.transferRemote(...)`. Every other domain is flat on the client. ```ts await client.gateway.transferRemote({ remote, recipient, sender, funds }) await client.gateway.getWithdrawalFee({ denom, remote }) ``` ### Tree-shakable style Every method on a client is also exported as a standalone function. Use that form when bundle size matters: ```ts import { createPublicClient, createTransport, getBalance, testnet } from "@left-curve/sdk" const client = createPublicClient({ chain: testnet, transport: createTransport() }) const amount = await getBalance(client, { address, denom: "dango" }) ``` ### Next * [Signers & Authentication](./signers-and-authentication) — what to plug into `signer` ## Encoding & Types **What this teaches:** base-unit vs human-readable amounts, when to use `Decimal` vs `bigint` vs `number`, and how the SDK's JSON serialization handles snake\_case. ### Base units, always Every amount the chain returns or accepts is in base units, as a `string`. `dango` has 6 decimals — 1 DANGO is `"1000000"`. `bridge/usdc` has 6 decimals as well. ```ts const oneDango = "1000000" // base units const twoBridge = "2000000" // base units ``` Never parse amounts into `number` for math: ```ts // BAD — silent precision loss above 2^53 const total = Number("9007199254740993") + Number("1") // GOOD — keep as string, do math with bigint or Decimal import { Decimal } from "@left-curve/utils" const total = Decimal("9007199254740993").add("1") ``` All on-chain amount fields are exchanged as base-unit `string`. Convert to `BigInt` (integer) or `Decimal` (fractional) at the boundary — never to `number`. ### When to use which | Type | Use for | | --------- | ---------------------------------------------------- | | `bigint` | On-chain integer math, atomic amounts, nonces | | `Decimal` | Prices, rates, anything with a non-integer dimension | | `number` | Block heights, indices, viewport math, timers | | `string` | The wire format for all on-chain amounts | ```ts import { Decimal, formatUnits, parseUnits } from "@left-curve/utils" // Display: base units → human-readable string const display = formatUnits("1234567", 6) // "1.234567" // Submit: human-readable string → base units const atomic = parseUnits("1.5", 6) // "1500000" // Math: keep as Decimal until you serialize const price = Decimal("100.25") const qty = Decimal("3") const notional = price.mul(qty).toFixed(6) // "300.750000" ``` ### JSON shape on the wire Contract messages are snake\_case on the wire and camelCase in TypeScript. The SDK converts automatically via `snakeCaseJsonSerialization` and `camelCaseJsonDeserialization` from `@left-curve/encoding`. Write camelCase in your code: ```ts await client.execute({ sender, execute: { contract, msg: { batchUpdateOrders: { creates: [...] } }, // camelCase }, }) ``` ### Branded types `Address`, `Denom`, `Hex`, `Base64`, and others are branded types from `@left-curve/types`. They are structurally strings but the brand prevents passing a denom where an address is expected: ```ts import type { Address, Denom } from "@left-curve/sdk" const addr = "0x1234567890abcdef1234567890abcdef12345678" as Address const denom = "dango" as Denom // type error — Address is not assignable to Denom // transfer({ denom: addr, ... }) ``` ### Next * [Error Handling](./error-handling) — what throws and how to narrow it ## Error Handling **What this teaches:** the `BaseError` hierarchy, what each error means, and the current limitation around importing them. ### Hierarchy Every SDK error extends `BaseError`: ``` BaseError ├── HttpRequestError // GraphQL HTTP failure (status, url, body) ├── TimeoutError // request exceeded its timeout └── UrlRequiredError // transport invoked without a URL ``` `BaseError` carries structured metadata: `shortMessage`, `details`, `metaMessages`, `name`. The full error message is composed from those parts at construction. ### Current limitation `BaseError`, `HttpRequestError`, `TimeoutError`, and `UrlRequiredError` are not re-exported from the `@left-curve/sdk` entry point today. To narrow with `instanceof`, import them from the type package's internal paths or fall back to `name` checks: ```ts // Today: import directly from the sub-paths or check by name try { await client.queryStatus() } catch (err) { if (err instanceof Error && err.name === "HttpRequestError") { // handle HTTP failures } else if (err instanceof Error && err.name === "TimeoutError") { // handle timeouts } else { throw err } } ``` If you need the actual classes for `instanceof`, you must import them from the type package directly — they are not exposed on the main entry barrel. This will be revisited in a future release. ### Triaging a thrown error `BaseError` instances always have: * `name` — discriminator, safe to switch on * `shortMessage` — single-line summary * `details` — derived from `cause.details` or `cause.message` * `metaMessages` — array of contextual lines (status code, URL, request body) ```ts function logFailure(err: unknown) { if (err instanceof Error && "shortMessage" in err) { const e = err as Error & { shortMessage: string; metaMessages?: string[] } console.error(e.name, e.shortMessage) e.metaMessages?.forEach((m) => console.error(" ", m)) return } console.error(err) } ``` ### Errors that are not `BaseError` Several actions still throw plain `Error` for assertion-style failures. Examples: * [`broadcastTxSync`](../actions/app/broadcastTxSync) throws `Error("failed to broadcast tx! code: 1, log: ...")` on `checkTx` failure. * [`signAndBroadcastTx`](../actions/app/signAndBroadcastTx) throws `Error("account not found")` when the sender has no on-chain account. * [`queryApp`](../actions/app/queryApp) and friends throw `Error("expecting ... response, got ...")` when the response shape does not match the requested variant. * [`submitConditionalOrders`](../actions/perps/submitConditionalOrders) throws `Error("submitConditionalOrders requires at least one order")` on empty input. For these, narrow by message string or by the surrounding action you called. ### Pages * [`BaseError`](../errors/BaseError) * [`HttpRequestError`](../errors/HttpRequestError) * [`TimeoutError`](../errors/TimeoutError) * [`UrlRequiredError`](../errors/UrlRequiredError) ### Next * [Rate Limits & Quotas](./rate-limits) — what the chain enforces server-side ## Packages **What this teaches:** which of the six SDK packages to import from, and when reaching for a sub-package is worth it. ### The split | Package | Contents | | ---------------------- | ---------------------------------------------------------- | | `@left-curve/sdk` | Clients, actions, transports, chain configs, signers | | `@left-curve/crypto` | Hashes (`sha256`, `keccak256`), key pairs, WebAuthn | | `@left-curve/encoding` | Hex, base64, UTF-8, JSON, binary codecs | | `@left-curve/types` | TypeScript types and `as const` enum maps (no runtime API) | | `@left-curve/utils` | `Decimal` math, formatters, retry, subscription helper | | `@left-curve/config` | Shared `tsconfig`/`biome`/`tsup` presets (config-only) | `@left-curve/sdk` re-exports the most commonly used pieces of every sibling package, so most consumers import only from `@left-curve/sdk`. ### What `@left-curve/sdk` re-exports Types: `Address`, `Coin`, `Coins`, `Chain`, `Denom`, `KeyHash`, `Account`, `PublicClientConfig`, `SignerClientConfig`, and the perps type family (`PerpsUserState`, `PerpsPairParam`, etc.). Runtime: `Direction`, `OrderType`, `TimeInForceOption` const maps. `formatUnits` and `parseUnits` from `@left-curve/utils`. `Secp256k1` from `@left-curve/crypto`. ### When to reach for a sub-package Import from a sub-package directly when: * **Bundle size matters.** Pulling `@left-curve/crypto` alone is smaller than the full `@left-curve/sdk`. * **The symbol is not re-exported.** Examples: `Decimal`, `sha256`, `keccak256`, `encodeHex`, `withRetry`, error classes (`BaseError`, `HttpRequestError`, `TimeoutError`, `UrlRequiredError`). * **You write a pure utility library** that does not need clients or chains. ```ts // Re-exported, so this works import { Address, Coin, formatUnits, Secp256k1 } from "@left-curve/sdk" // Not re-exported — import from the sub-package import { Decimal, withRetry } from "@left-curve/utils" import { sha256 } from "@left-curve/crypto" import { encodeHex } from "@left-curve/encoding" // Error classes are not currently exported from any package entry. See Error Handling. ``` ### The hyperlane subpath Hyperlane encoders live behind a subpath, not the main entry: ```ts import { Addr32, Message, TokenMessage } from "@left-curve/sdk/hyperlane" ``` ### Next * [Clients](./clients) — the two client factories that anchor every project ## Rate Limits & Quotas **What this teaches:** the two server-side limits the indexer enforces, and how to stay under them without auto-throttling. ### The limits * **HTTP:** 167 requests per 10 seconds per IP. * **WebSocket:** 30 concurrent subscriptions per connection. The SDK does not auto-handle either limit. Hitting them surfaces as failed requests (HTTP 429 for HTTP, subscription rejection for WS). Plan capacity in your application layer. ### Back off on HTTP failures Use `withRetry` from `@left-curve/utils` to retry transient HTTP failures with exponential backoff: ```ts import { withRetry } from "@left-curve/utils" import { createPublicClient, createTransport, testnet } from "@left-curve/sdk" const client = createPublicClient({ chain: testnet, transport: createTransport() }) const balance = await withRetry( async () => client.getBalance({ address, denom: "dango" }), { retryCount: 5, delay: 500 }, ) ``` `withRetry` doubles the delay on each attempt. Combine with a budget — bail out if total wait exceeds your latency budget. ### Shard subscriptions across clients The 30-sub cap is per WebSocket connection, not per process. To run more than 30, instantiate multiple clients: ```ts import { createPublicClient, createTransport, testnet } from "@left-curve/sdk" function newClient() { return createPublicClient({ chain: testnet, transport: createTransport() }) } const clients = [newClient(), newClient(), newClient()] // Distribute subscriptions round-robin const pairs = [/* 75 pairs */] const unsubs = pairs.map((pair, i) => clients[i % clients.length].candlesSubscription({ baseDenom: pair.base, quoteDenom: pair.quote, interval: "ONE_MINUTE", next: (data) => handle(data), }), ) ``` Each client opens its own WS. Three clients gives you 90 subscriptions. ### Batching HTTP Pass `batch: true` to `createTransport` to coalesce concurrent HTTP requests into a single GraphQL batch (hardcoded to 20 ops per batch, 20 ms window). This trades latency for throughput when issuing many parallel queries: ```ts import { createTransport } from "@left-curve/sdk" const transport = createTransport(undefined, { batch: true }) ``` ### What the SDK does NOT do * No automatic rate-limit backoff. Retries are opt-in via `withRetry`. * No subscription sharding across connections. You orchestrate that. * No per-method throttling. Concurrent calls hit the server immediately. Plan accordingly for production workloads. ### Next * Back to [Concepts: Packages](./packages) for the package overview, or browse the [API Reference](../clients/createPublicClient). ## Signers & Authentication **What this teaches:** the `Signer` interface, the two built-in implementations, and how session keys delegate authority. ### The interface A signer implements three methods: ```ts type Signer = { getKeyHash(): Promise signTx(signDoc: SignDoc): Promise signArbitrary(payload: ArbitraryDoc): Promise } ``` `signTx` signs an EIP-712-typed transaction document. `signArbitrary` signs an arbitrary message, used for session creation and off-chain proofs. `getKeyHash` identifies which on-chain key the signer represents. ### PrivateKeySigner Backed by a local secp256k1 key. Three construction paths: ```ts import { PrivateKeySigner } from "@left-curve/sdk" const a = PrivateKeySigner.fromMnemonic("your twelve word mnemonic here ...") const b = PrivateKeySigner.fromPrivateKey(new Uint8Array(32)) const c = PrivateKeySigner.fromRandomKey() ``` `fromMnemonic` uses BIP-39 + BIP-32 to derive the key. `fromRandomKey` generates an ephemeral keypair — useful for one-shot scripts. For `signArbitrary`, this signer signs `payload.message` (SHA-256 over the canonical-serialized message). The returned `signed:` field is the raw signature. ### Session signer A session signer signs with an ephemeral session key plus a stored authorization from the user's primary key. The user authorizes the session once; the session signs many transactions until it expires. ```ts import { createSessionSigner } from "@left-curve/sdk" import type { SigningSession } from "@left-curve/sdk" const session: SigningSession = await loadSessionFromStorage() const signer = createSessionSigner(session) ``` Both signers hash `payload.message` the same way. The divergence is in what gets returned: `createSessionSigner` returns a `SessionCredential` (the session + the signed-message envelope) as `signed:`, so the verifier can re-derive the message from chain state. Use [`createSession`](../actions/account-factory/createSession) on a signer client to mint a `SigningSession`. Persist it to storage and rehydrate on next load. ### Passkeys (WebAuthn) For browser wallets, the SDK exposes WebAuthn helpers via `@left-curve/crypto`: ```ts import { createWebAuthnCredential, requestWebAuthnSignature, verifyWebAuthnSignature, } from "@left-curve/crypto" ``` These return raw P-256 signatures. Wire them into a custom `Signer` implementation that wraps the user's passkey for `signTx`. The SDK does not ship a built-in passkey signer — the wallet layer owns that. ### Next * [Transactions](./transactions) — sign, broadcast, and poll a transfer ## Subscriptions **What this teaches:** the WebSocket subscription model, the WS-only vs HTTP-fallback split, and the connection lifecycle. ### Mental model Every `*Subscription` action on the client opens a long-lived stream over WebSocket and invokes a `next` callback for each new payload. The unsubscribe function the call returns is the only way to close the stream from the client side. ```ts import { createPublicClient, createTransport, testnet } from "@left-curve/sdk" const client = createPublicClient({ chain: testnet, transport: createTransport(), }) const unsubscribe = client.blockSubscription({ next: ({ block }) => console.log("new block", block.blockHeight), error: (err) => console.error(err), }) // later unsubscribe() ``` ### WS-only vs HTTP-fallback Most subscriptions are WS-only — they throw if the transport has `disableWs: true` or the WS client failed to connect: * `blockSubscription` * `accountSubscription` * `candlesSubscription` * `eventsSubscription` * `eventsByAddressesSubscription` * `perpsCandlesSubscription` * `transferSubscription` Four subscriptions fall back to HTTP polling when WS is unavailable: * `tradesSubscription` * `perpsTradesSubscription` * `allPairStatsSubscription` * `allPerpsPairStatsSubscription` * `queryAppSubscription` For these, pass `httpInterval` (default 3-5 seconds depending on the subscription) to control the poll rate. ### Connection lifecycle The transport opens one WebSocket per client, shared by every subscription. It auto-reconnects with exponential backoff (1s → 30s, 10 retries by default; tune via `wsRetry` on `createTransport`). On `document.visibilitychange` to visible or `window.online`, the transport eagerly reconnects. Inspect connection state from the client's transport: ```ts const status = client.subscribe.getClientStatus?.() console.log(status?.isConnected) ``` ### Subscription limits A single WS connection accepts at most 30 concurrent subscriptions. Beyond that the server rejects new subscribe calls. See [Rate Limits & Quotas](./rate-limits) for the workaround. ### Cleanup Always call the returned unsubscribe function. Without it, the WS subscription stays open until the connection closes: ```ts const unsubscribe = client.candlesSubscription({ baseDenom: "dango", quoteDenom: "bridge/usdc", interval: "ONE_MINUTE", next: (data) => updateChart(data.candles), }) // in your component teardown unsubscribe() ``` ### Next * [Rate Limits & Quotas](./rate-limits) — the 30-sub cap and how to shard across clients import { Step, Steps } from 'vocs/components' ## Transactions **What this teaches:** the sign → broadcast → poll loop, and where `signAndBroadcastTx` fits. :::warning[DEX currently disabled] The Dango DEX is currently disabled. Calls described on this page will not execute on the live network until the DEX is enabled. ::: ### The lifecycle A transaction is a list of `Message` variants (`transfer`, `execute`, `instantiate`, etc.). The signer produces an EIP-712 typed-data signature over the messages plus metadata (chain id, nonce, sender). The signed transaction is sent via the indexer's `broadcastTxSync` mutation. The SDK polls `queryTx` until the indexer reports the transaction landed (or fails). [`signAndBroadcastTx`](../actions/app/signAndBroadcastTx) wraps all four steps. Higher-level actions like [`transfer`](../actions/app/transfer), [`execute`](../actions/app/execute), and [`swapExactAmountIn`](../actions/dex/swapExactAmountIn) build their messages then call `signAndBroadcastTx`. ### Worked example ```ts import { createSignerClient, createTransport, testnet, PrivateKeySigner } from "@left-curve/sdk" const client = createSignerClient({ chain: testnet, transport: createTransport(), signer: PrivateKeySigner.fromMnemonic(process.env.DANGO_MNEMONIC!), }) const result = await client.signAndBroadcastTx({ sender: "0x1234567890abcdef1234567890abcdef12345678", messages: [ { transfer: { "0xabcdef1234567890abcdef1234567890abcdef12": { dango: "1000000" }, }, }, ], }) console.log(result.hash) ``` Behind the scenes: * The client queries `queryStatus` for the chain id (if not on the chain config), `getAccountSeenNonces` for the next nonce, and `getAccountInfo` for the user index. * It calls [`simulate`](../actions/app/simulate) to estimate gas (multiplier `1.3`) unless `gasLimit` is set. * The signer signs the EIP-712 typed data, the SDK calls `broadcastTxSync`, then polls `queryTx` up to 30 times at 500ms intervals. ### Gas `simulate` returns `gasUsed * 1.3`. To override, pass `gasLimit` explicitly: ```ts await client.signAndBroadcastTx({ sender, messages: [...], gasLimit: 500_000, }) ``` ### Failure modes * The simulated transaction fails — `simulate` throws with the contract error. * The transaction enters the mempool but fails on commit — `queryTx` returns a non-zero code; the SDK throws. * The transaction never lands — the poll loop exhausts retries; the SDK throws "Transaction not found". See [Error Handling](./error-handling) for narrowing these. ### Next * [Subscriptions](./subscriptions) — listen for events instead of polling ## createBaseClient Factory that builds the bare client object — transport, chain, optional signer, and `extend()`. Used internally by `createPublicClient` and `createSignerClient`; reach for it directly only to compose custom action sets. ### Setup ```ts import { createBaseClient, createTransport, testnet } from "@left-curve/sdk" const base = createBaseClient({ chain: testnet, transport: createTransport(), }) ``` Without extension, `base` exposes only the wiring (`request`, `subscribe`, `chain`, `transport`, `uid`, `name`, `type`, and `extend`). ### Configuration **`chain`** — `Chain`. Chain config. **`transport`** — `Transport`. GraphQL transport from `createTransport(url?, config?)`. **`signer`** — `Signer | undefined`, optional. When set, the client type includes the signer slot. Required to attach mutation actions. **`name`** — `string`, optional, default: `"Base Client"`. Free-form label. **`type`** — `string`, optional, default: `"base"`. Type marker. ### Extending `extend()` is the primitive that builds richer clients. Pass a function that takes the current client and returns new properties: ```ts import { createBaseClient, createTransport, testnet } from "@left-curve/sdk" import { publicActions } from "@left-curve/sdk" const base = createBaseClient({ chain: testnet, transport: createTransport(), }) const client = base.extend(publicActions) const status = await client.queryStatus() ``` `createPublicClient` is exactly this: a base client extended with `publicActions`. `createSignerClient` chains two extends — `publicActions` then `signerActions`. ### Custom extensions Add your own actions in the same shape: ```ts import { createBaseClient, createTransport, testnet } from "@left-curve/sdk" import { publicActions } from "@left-curve/sdk" import type { Client } from "@left-curve/sdk" function customActions(client: Client) { return { getDangoBalance: (address: `0x${string}`) => client.getBalance({ address, denom: "dango" }), } } const client = createBaseClient({ chain: testnet, transport: createTransport() }) .extend(publicActions) .extend(customActions) const balance = await client.getDangoBalance("0x1234567890abcdef1234567890abcdef12345678") ``` ### End-to-end example ```ts import { createBaseClient, createTransport, testnet, publicActions, signerActions, PrivateKeySigner } from "@left-curve/sdk" const signer = PrivateKeySigner.fromMnemonic(process.env.DANGO_MNEMONIC!) const client = createBaseClient({ chain: testnet, transport: createTransport(), signer, }) .extend(publicActions) .extend(signerActions) // Equivalent to createSignerClient({ chain, transport, signer }). ``` ### Notes * `extend` returns a new client; it does not mutate the original. * Reserved keys (`signer`, `chain`, `name`, `type`, `request`, `subscribe`, `uid`, `extend`) are filtered out of extension return values. ### See also * [createPublicClient](./createPublicClient) — the standard read-only assembly * [createSignerClient](./createSignerClient) — the standard signing assembly ## createPublicClient Factory that builds a read-only client extended with every query action. ### Setup ```ts import { createPublicClient, createTransport, testnet } from "@left-curve/sdk" const client = createPublicClient({ chain: testnet, transport: createTransport(), }) ``` `createPublicClient` returns a `PublicClient = Client`. The `signer` slot is `undefined`, so the type system forbids calling any signing action. ### Configuration **`chain`** — `Chain`. Chain config (id, name, URL, native coin). Use `local`, `devnet`, `testnet`, or `mainnet` from `@left-curve/sdk`. **`transport`** — `Transport`. GraphQL transport instance from `createTransport(url?, config?)`. **`name`** — `string`, optional, default: `"Public Client"`. Free-form label for debugging. ### Methods #### App | Method | Description | | ----------------------------------------------------- | ------------------------------------ | | [`getAppConfig`](../actions/app/getAppConfig) | App-level config (cached) | | [`getBalance`](../actions/app/getBalance) | Balance of one denom for an address | | [`getBalances`](../actions/app/getBalances) | Paginated `Coins` map for an address | | [`getSupply`](../actions/app/getSupply) | Total supply of a single token | | [`getSupplies`](../actions/app/getSupplies) | Paginated map of token supplies | | [`getCode`](../actions/app/getCode) | Fetch a stored Wasm code by hash | | [`getCodes`](../actions/app/getCodes) | Paginated list of stored codes | | [`getContractInfo`](../actions/app/getContractInfo) | Contract metadata by address | | [`getContractsInfo`](../actions/app/getContractsInfo) | Paginated contract metadata | | [`queryWasmRaw`](../actions/app/queryWasmRaw) | Raw base64 storage value | | [`queryWasmSmart`](../actions/app/queryWasmSmart) | Typed JSON query against a contract | | [`queryApp`](../actions/app/queryApp) | Generic typed query against the app | | [`queryStatus`](../actions/app/queryStatus) | Chain id and latest block info | | [`queryTx`](../actions/app/queryTx) | Indexed transaction by hash | | [`simulate`](../actions/app/simulate) | Gas-simulate an unsigned tx | #### Account Factory | Method | Description | | ------------------------------------------------------------------------- | -------------------------------------- | | [`forgotUsername`](../actions/account-factory/forgotUsername) | Users associated with a key hash | | [`getAccountInfo`](../actions/account-factory/getAccountInfo) | Account details for an address | | [`getAccountSeenNonces`](../actions/account-factory/getAccountSeenNonces) | Most-recent nonces consumed by account | | [`getAccountStatus`](../actions/account-factory/getAccountStatus) | Current `UserStatus` for an address | | [`getAllAccountInfo`](../actions/account-factory/getAllAccountInfo) | Paginated map of all accounts | | [`getCodeHash`](../actions/account-factory/getCodeHash) | Current account contract code hash | | [`getNextAccountIndex`](../actions/account-factory/getNextAccountIndex) | Next account index for a username | | [`getUser`](../actions/account-factory/getUser) | `User` by index or name | | [`getUserKeys`](../actions/account-factory/getUserKeys) | Indexer-backed list of `PublicKey` | #### DEX | Method | Description | | ------------------------------------------------------------------------- | -------------------------------------- | | [`dexStatus`](../actions/dex/dexStatus) | Whether the DEX is paused | | [`getOrder`](../actions/dex/getOrder) | Details of a single active order | | [`getPair`](../actions/dex/getPair) | Parameters of a single base/quote pair | | [`getPairs`](../actions/dex/getPairs) | Paginated list of trading pairs | | [`getPairStats`](../actions/dex/getPairStats) | 24h stats for one pair | | [`getAllPairStats`](../actions/dex/getAllPairStats) | 24h stats for every pair | | [`ordersByUser`](../actions/dex/ordersByUser) | Active orders for a user | | [`queryCandles`](../actions/dex/queryCandles) | Paginated candles | | [`queryTrades`](../actions/dex/queryTrades) | Paginated trades | | [`simulateSwapExactAmountIn`](../actions/dex/simulateSwapExactAmountIn) | Quote output for an exact-input swap | | [`simulateSwapExactAmountOut`](../actions/dex/simulateSwapExactAmountOut) | Quote input for an exact-output swap | | [`simulateWithdrawLiquidity`](../actions/dex/simulateWithdrawLiquidity) | Coins returned for burning LP | #### Perps | Method | Description | | ------------------------------------------------------------------------- | ----------------------------------------- | | [`getPerpsUserState`](../actions/perps/getPerpsUserState) | `PerpsUserState` for a user | | [`getPerpsUserStateExtended`](../actions/perps/getPerpsUserStateExtended) | Extended state with PnL/equity/liq flags | | [`getPerpsOrdersByUser`](../actions/perps/getPerpsOrdersByUser) | Active perps orders for a user | | [`getPerpsLiquidityDepth`](../actions/perps/getPerpsLiquidityDepth) | Bid/ask depth buckets for a pair | | [`getPerpsPairParam`](../actions/perps/getPerpsPairParam) | `PerpsPairParam` for one pair | | [`getPerpsPairParams`](../actions/perps/getPerpsPairParams) | Map of params across pairs | | [`getPerpsParam`](../actions/perps/getPerpsParam) | Global perps parameters | | [`getPerpsPairStats`](../actions/perps/getPerpsPairStats) | Indexer 24h stats for a perps pair | | [`getAllPerpsPairStats`](../actions/perps/getAllPerpsPairStats) | Indexer 24h stats for every perps pair | | [`getPerpsPairState`](../actions/perps/getPerpsPairState) | `PerpsPairState` (OI, funding) for a pair | | [`getPerpsState`](../actions/perps/getPerpsState) | Global runtime state | | [`getPerpsVaultState`](../actions/perps/getPerpsVaultState) | Perps vault state | | [`getVaultSnapshots`](../actions/perps/getVaultSnapshots) | Historical vault snapshots | | [`getFeeRateOverride`](../actions/perps/getFeeRateOverride) | Per-user fee rate override | | [`queryPerpsCandles`](../actions/perps/queryPerpsCandles) | Paginated perps candles | | [`queryPerpsEvents`](../actions/perps/queryPerpsEvents) | Paginated perps events | #### Oracle | Method | Description | | ------------------------------------------ | -------------------------------------------- | | [`getPrices`](../actions/oracle/getPrices) | Paginated `Record` from oracle | #### Indexer | Method | Description | | ----------------------------------------------------------------------------------- | ----------------------------------- | | [`queryBlock`](../actions/indexer/queryBlock) | Block (by height or latest) | | [`searchTxs`](../actions/indexer/searchTxs) | Paginated transaction search | | [`accountSubscription`](../actions/indexer/accountSubscription) | Account creation events | | [`blockSubscription`](../actions/indexer/blockSubscription) | New finalized blocks | | [`candlesSubscription`](../actions/indexer/candlesSubscription) | Live candles for a pair | | [`eventsSubscription`](../actions/indexer/eventsSubscription) | Filtered live events | | [`eventsByAddressesSubscription`](../actions/indexer/eventsByAddressesSubscription) | Live events for a set of addresses | | [`transferSubscription`](../actions/indexer/transferSubscription) | Live transfer events for a username | | [`tradesSubscription`](../actions/indexer/tradesSubscription) | Live spot trades for a pair | | [`perpsCandlesSubscription`](../actions/indexer/perpsCandlesSubscription) | Live perps candles | | [`perpsTradesSubscription`](../actions/indexer/perpsTradesSubscription) | Live perps trades | | [`queryAppSubscription`](../actions/indexer/queryAppSubscription) | Live result of a `queryApp` request | | [`allPairStatsSubscription`](../actions/indexer/allPairStatsSubscription) | Live 24h stats for all spot pairs | | [`allPerpsPairStatsSubscription`](../actions/indexer/allPerpsPairStatsSubscription) | Live 24h stats for all perps pairs | ### End-to-end example ```ts import { createPublicClient, createTransport, testnet } from "@left-curve/sdk" import type { Address } from "@left-curve/sdk" const client = createPublicClient({ chain: testnet, transport: createTransport(), }) const status = await client.queryStatus() const address: Address = "0x1234567890abcdef1234567890abcdef12345678" const balances = await client.getBalances({ address }) const pairs = await client.getPairs({ limit: 10 }) const unsubscribe = client.blockSubscription({ next: ({ block }) => console.log("block", block.blockHeight), }) // later unsubscribe() ``` ### Notes * The client is a plain object, not a class. Method calls are just property reads followed by function calls. * For tree-shakable usage, import each action and pass the client positionally: `getBalance(client, {...})`. ### See also * [createSignerClient](./createSignerClient) — when you also need to broadcast * [createBaseClient](./createBaseClient) — for custom action extension * [Concepts: Clients](../concepts/clients) ## createSignerClient Factory that builds a client with every query action and every signing action. ### Setup ```ts import { createSignerClient, createTransport, testnet, PrivateKeySigner } from "@left-curve/sdk" const signer = PrivateKeySigner.fromMnemonic(process.env.DANGO_MNEMONIC!) const client = createSignerClient({ chain: testnet, transport: createTransport(), signer, }) ``` `createSignerClient` returns `SignerClient = Client`. It is a strict superset of [`createPublicClient`](./createPublicClient): every method on the public client is also on the signer client. ### Configuration **`chain`** — `Chain`. Chain config. Use `local`, `devnet`, `testnet`, or `mainnet` from `@left-curve/sdk`. **`transport`** — `Transport`. GraphQL transport from `createTransport(url?, config?)`. **`signer`** — `Signer`. Implementation of the `Signer` interface. Use `PrivateKeySigner` for server-side keys or `createSessionSigner` for session-key flows. **`name`** — `string`, optional, default: `"Signer Client"`. Free-form label. **`type`** — `string`, optional, default: `"dango"`. Type marker. ### Methods A signer client exposes every method on [`createPublicClient`](./createPublicClient) plus the following mutations. #### App mutations | Method | Description | | ------------------------------------------------------------------- | -------------------------------------------------- | | [`broadcastTxSync`](../actions/app/broadcastTxSync) | Broadcast a signed tx via the indexer | | [`signAndBroadcastTx`](../actions/app/signAndBroadcastTx) | Sign messages with the client signer and broadcast | | [`transfer`](../actions/app/transfer) | Send a record of `Address -> Coins` | | [`execute`](../actions/app/execute) | Execute one or more contract messages | | [`instantiate`](../actions/app/instantiate) | Instantiate a contract from a code hash | | [`migrate`](../actions/app/migrate) | Migrate a contract to a new code hash | | [`storeCode`](../actions/app/storeCode) | Upload a Wasm code blob | | [`storeCodeAndInstantiate`](../actions/app/storeCodeAndInstantiate) | Upload and instantiate in one tx | #### Account Factory mutations | Method | Description | | --------------------------------------------------------------- | -------------------------------------------- | | [`registerUser`](../actions/account-factory/registerUser) | Create a new user and first account | | [`registerAccount`](../actions/account-factory/registerAccount) | Register an additional account | | [`updateKey`](../actions/account-factory/updateKey) | Insert or delete a key on the account | | [`updateUsername`](../actions/account-factory/updateUsername) | Change the username | | [`createSession`](../actions/account-factory/createSession) | Sign `SigningSessionInfo` and return session | #### DEX mutations | Method | Description | | --------------------------------------------------------- | ------------------------------------------ | | [`batchUpdateOrders`](../actions/dex/batchUpdateOrders) | Create and/or cancel multiple limit orders | | [`swapExactAmountIn`](../actions/dex/swapExactAmountIn) | Instant swap with exact input | | [`swapExactAmountOut`](../actions/dex/swapExactAmountOut) | Instant swap with exact output | | [`provideLiquidity`](../actions/dex/provideLiquidity) | Add liquidity to a pair | | [`withdrawLiquidity`](../actions/dex/withdrawLiquidity) | Withdraw liquidity from a pair | #### Perps mutations | Method | Description | | --------------------------------------------------------------------- | ------------------------------------------ | | [`depositMargin`](../actions/perps/depositMargin) | Deposit collateral into the perps account | | [`withdrawMargin`](../actions/perps/withdrawMargin) | Withdraw collateral | | [`submitPerpsOrder`](../actions/perps/submitPerpsOrder) | Submit a market or limit perps order | | [`cancelPerpsOrder`](../actions/perps/cancelPerpsOrder) | Cancel one or all perps orders | | [`submitConditionalOrder`](../actions/perps/submitConditionalOrder) | Submit one trigger-based conditional order | | [`submitConditionalOrders`](../actions/perps/submitConditionalOrders) | Submit a batch of conditional orders | | [`cancelConditionalOrder`](../actions/perps/cancelConditionalOrder) | Cancel conditional order(s) | | [`setReferral`](../actions/perps/setReferral) | Set a referrer/referee mapping | | [`setFeeShareRatio`](../actions/perps/setFeeShareRatio) | Set the referrer fee share ratio | | [`vaultAddLiquidity`](../actions/perps/vaultAddLiquidity) | Deposit into the perps vault | | [`vaultRemoveLiquidity`](../actions/perps/vaultRemoveLiquidity) | Burn vault shares for a withdrawal | ### End-to-end example ```ts import { createSignerClient, createTransport, testnet, PrivateKeySigner } from "@left-curve/sdk" import type { Address } from "@left-curve/sdk" const signer = PrivateKeySigner.fromMnemonic(process.env.DANGO_MNEMONIC!) const client = createSignerClient({ chain: testnet, transport: createTransport(), signer, }) const sender: Address = "0x1234567890abcdef1234567890abcdef12345678" // Move funds const receipt = await client.transfer({ sender, transfer: { "0xabcdef1234567890abcdef1234567890abcdef12": { dango: "1000000" }, }, }) // Place a limit order await client.batchUpdateOrders({ sender, creates: [ { baseDenom: "dango", quoteDenom: "bridge/usdc", amount: { bid: { quote: "10000000" } }, price: { limit: "0.5" }, timeInForce: "GTC", }, ], }) ``` ### Notes * The signer is held by reference. Mutating its key after client creation is undefined behavior — build a new client instead. * Mutations call `simulate` internally to estimate gas. To skip simulation, pass `gasLimit` explicitly. ### See also * [createPublicClient](./createPublicClient) — when you only need queries * [Concepts: Transactions](../concepts/transactions) — the sign/broadcast loop * [Concepts: Signers & Authentication](../concepts/signers-and-authentication) ## cancelConditionalOrder Cancel one conditional order, all for a pair, or every conditional order. ### Signature ```ts function cancelConditionalOrder( client: Client, parameters: { sender: Address request: PerpsCancelConditionalOrderRequest }, ): Promise<{ hash: Uint8Array } & TxData> type PerpsCancelConditionalOrderRequest = | { one: { pairId: string; triggerDirection: "above" | "below" } } | { allForPair: { pairId: string } } | "all" ``` ### Example ```ts import { createSignerClient, createTransport, testnet, PrivateKeySigner } from "@left-curve/sdk" import type { Address } from "@left-curve/sdk" const client = createSignerClient({ chain: testnet, transport: createTransport(), signer: PrivateKeySigner.fromMnemonic(process.env.DANGO_MNEMONIC!), }) const sender: Address = "0x1234567890abcdef1234567890abcdef12345678" await client.cancelConditionalOrder({ sender, request: { one: { pairId: "BTC-PERP", triggerDirection: "above" } }, }) ``` ### Parameters **`sender`** — `Address`. The trader. **`request`** — `PerpsCancelConditionalOrderRequest`. `"all"`, `{ one: {...} }`, or `{ allForPair: { pairId } }`. ### Returns **`{ hash: Uint8Array } & TxData`** — see [`broadcastTxSync`](../app/broadcastTxSync). ### See also * [`submitConditionalOrder`](./submitConditionalOrder) * [`submitConditionalOrders`](./submitConditionalOrders) ## cancelPerpsOrder Cancel one or all perps orders. Cancelling releases the order's reserved margin back to the user's available margin and decrements `open_order_count`. If the user state becomes empty after all cancellations, it is deleted from storage. See protocol book: `perps/2-order-matching` §12. ### Signature ```ts function cancelPerpsOrder( client: Client, parameters: { sender: Address request: PerpsCancelOrderRequest }, ): Promise<{ hash: Uint8Array } & TxData> type PerpsCancelOrderRequest = | { one: string } | { oneByClientOrderId: string } | "all" ``` ### Example ```ts import { createSignerClient, createTransport, testnet, PrivateKeySigner } from "@left-curve/sdk" import type { Address } from "@left-curve/sdk" const client = createSignerClient({ chain: testnet, transport: createTransport(), signer: PrivateKeySigner.fromMnemonic(process.env.DANGO_MNEMONIC!), }) const sender: Address = "0x1234567890abcdef1234567890abcdef12345678" await client.cancelPerpsOrder({ sender, request: { one: "12345" } }) await client.cancelPerpsOrder({ sender, request: { oneByClientOrderId: "my-order-1" } }) await client.cancelPerpsOrder({ sender, request: "all" }) ``` ### Parameters **`sender`** — `Address`. The trader. **`request`** — `PerpsCancelOrderRequest`. `"all"`, `{ one: orderId }`, or `{ oneByClientOrderId }`. The `oneByClientOrderId` form lets you cancel an order in the same block it was submitted. ### Returns **`{ hash: Uint8Array } & TxData`** — see [`broadcastTxSync`](../app/broadcastTxSync). ### See also * [`submitPerpsOrder`](./submitPerpsOrder) * [`cancelConditionalOrder`](./cancelConditionalOrder) ## depositMargin Deposit `bridge/usdc` collateral into the perps account. The perps contract holds margin internally as a USD value. Deposits attach `bridge/usdc` base units as funds; the contract credits the user's `userState.margin` 1:1 at the fixed $1 settlement rate. See protocol book: `perps/1-margin` §2. ### Signature ```ts function depositMargin( client: Client, parameters: { sender: Address amount: string }, ): Promise<{ hash: Uint8Array } & TxData> ``` ### Example ```ts import { createSignerClient, createTransport, testnet, PrivateKeySigner } from "@left-curve/sdk" import type { Address } from "@left-curve/sdk" const client = createSignerClient({ chain: testnet, transport: createTransport(), signer: PrivateKeySigner.fromMnemonic(process.env.DANGO_MNEMONIC!), }) const sender: Address = "0x1234567890abcdef1234567890abcdef12345678" await client.depositMargin({ sender, amount: "1000000000" }) ``` ### Parameters **`sender`** — `Address`. The trader. **`amount`** — `string`. `bridge/usdc` amount in base units. ### Returns **`{ hash: Uint8Array } & TxData`** — see [`broadcastTxSync`](../app/broadcastTxSync). ### Notes * The deposit currency is hard-coded to `bridge/usdc` in the message body. * Asymmetric with [`withdrawMargin`](./withdrawMargin): deposit takes base units of `bridge/usdc`, withdraw takes a USD value. The asymmetry mirrors the on-chain contract. ### See also * [`withdrawMargin`](./withdrawMargin) * [`getPerpsUserState`](./getPerpsUserState) ## getAllPerpsPairStats Read 24h stats for every perps pair. ### Signature ```ts function getAllPerpsPairStats(client: Client): Promise ``` ### Example ```ts import { createPublicClient, createTransport, testnet } from "@left-curve/sdk" const client = createPublicClient({ chain: testnet, transport: createTransport() }) const stats = await client.getAllPerpsPairStats() ``` ### Returns **`PerpsPairStats[]`** — one `{ pairId, currentPrice, price24HAgo, volume24H, priceChange24H }` per pair. ### See also * [`getPerpsPairStats`](./getPerpsPairStats) — single-pair variant * [`allPerpsPairStatsSubscription`](../indexer/allPerpsPairStatsSubscription) ## getFeeRateOverride Read a per-user fee rate override. When present, the override replaces the volume-tiered fee schedule for the user on every fill. Overrides are set by the chain owner. ### Signature ```ts function getFeeRateOverride( client: Client, parameters: { user: Address height?: number }, ): Promise ``` ### Example ```ts import { createPublicClient, createTransport, testnet } from "@left-curve/sdk" import type { Address } from "@left-curve/sdk" const client = createPublicClient({ chain: testnet, transport: createTransport() }) const user: Address = "0x1234567890abcdef1234567890abcdef12345678" const override = await client.getFeeRateOverride({ user }) ``` ### Parameters **`user`** — `Address`. The trader. **`height`** — `number`, optional. Block height. Default `0` (latest). ### Returns **`FeeRateOverride | null`** — `{ makerFeeRate, takerFeeRate }`, or `null` if no override is set. The contract returns a tuple `[string, string]`; the action reshapes it into the object form. ### See also * [`getPerpsParam`](./getPerpsParam) — default fee schedules * [`setFeeShareRatio`](./setFeeShareRatio) ## getPerpsLiquidityDepth Read bid and ask depth buckets for a perps pair. `bucketSize` must match one of the granularities pre-configured on the pair (see `PerpsPairParam.bucketSizes`); mismatched values are rejected by the contract. The vault is typically the dominant maker on both sides. ### Signature ```ts function getPerpsLiquidityDepth( client: Client, parameters: { pairId: string bucketSize: string limit?: number height?: number }, ): Promise ``` ### Example ```ts import { createPublicClient, createTransport, testnet } from "@left-curve/sdk" const client = createPublicClient({ chain: testnet, transport: createTransport() }) const depth = await client.getPerpsLiquidityDepth({ pairId: "BTC-PERP", bucketSize: "10", limit: 50, }) ``` ### Parameters **`pairId`** — `string`. Perps pair identifier. **`bucketSize`** — `string`. Price bucket width. **`limit`** — `number`, optional. Maximum buckets per side. **`height`** — `number`, optional. Block height. Default `0` (latest). ### Returns **`PerpsLiquidityDepthResponse`** — `{ bids: Record, asks: Record }`. ### See also * [`getPerpsPairState`](./getPerpsPairState) — open interest and funding ## getPerpsOrdersByUser List a user's active perps orders. Returns only resting limit orders that have not been fully filled or cancelled; the count matches the user's `open_order_count` and the sum of `reserved_margin` across these orders equals `user_state.reservedMargin`. ### Signature ```ts function getPerpsOrdersByUser( client: Client, parameters: { user: Address height?: number }, ): Promise ``` ### Example ```ts import { createPublicClient, createTransport, testnet } from "@left-curve/sdk" import type { Address } from "@left-curve/sdk" const client = createPublicClient({ chain: testnet, transport: createTransport() }) const user: Address = "0x1234567890abcdef1234567890abcdef12345678" const orders = await client.getPerpsOrdersByUser({ user }) ``` ### Parameters **`user`** — `Address`. The trader. **`height`** — `number`, optional. Block height. Default `0` (latest). ### Returns **`PerpsOrdersByUserResponse`** — `Record` keyed by order id. ### See also * [`submitPerpsOrder`](./submitPerpsOrder) * [`cancelPerpsOrder`](./cancelPerpsOrder) ## getPerpsPairParam Read the parameters of a single perps pair: tick size, margin ratios, max OI, funding rate cap. These per-pair governance knobs drive every pre-trade check: `tickSize` for price alignment, `minOrderSize` for dust rejection, `maxAbsOi` for the per-side OI cap, `initialMarginRatio` and `maintenanceMarginRatio` for IM/MM, `maxAbsFundingRate` for the funding clamp, and `bucketSizes` for liquidity depth queries. See protocol book: `perps/7-risk` for calibration guidance. ### Signature ```ts function getPerpsPairParam( client: Client, parameters: { pairId: string height?: number }, ): Promise ``` ### Example ```ts import { createPublicClient, createTransport, testnet } from "@left-curve/sdk" const client = createPublicClient({ chain: testnet, transport: createTransport() }) const params = await client.getPerpsPairParam({ pairId: "BTC-PERP" }) ``` ### Parameters **`pairId`** — `string`. Perps pair identifier. **`height`** — `number`, optional. Block height. Default `0` (latest). ### Returns **`PerpsPairParam | null`** — params including `tickSize`, `minOrderSize`, `maxAbsOi`, `maxAbsFundingRate`, `initialMarginRatio`, `maintenanceMarginRatio`, vault weights, and `bucketSizes`. ### See also * [`getPerpsPairParams`](./getPerpsPairParams) * [`getPerpsParam`](./getPerpsParam) ## getPerpsPairParams Read parameters across every perps pair, paginated. ### Signature ```ts function getPerpsPairParams( client: Client, parameters?: { startAfter?: string limit?: number height?: number }, ): Promise> ``` ### Example ```ts import { createPublicClient, createTransport, testnet } from "@left-curve/sdk" const client = createPublicClient({ chain: testnet, transport: createTransport() }) const all = await client.getPerpsPairParams({ limit: 50 }) ``` ### Parameters **`startAfter`** — `string`, optional. Pair id to start after. **`limit`** — `number`, optional. Maximum entries. **`height`** — `number`, optional. Block height. Default `0` (latest). ### Returns **`Record`** — keyed by pair id. ### See also * [`getPerpsPairParam`](./getPerpsPairParam) — single-pair variant ## getPerpsPairState Read on-chain pair state — long/short OI, funding per unit, and current funding rate. `fundingPerUnit` is the pair-level running accumulator; per-position accrued funding is `size × (fundingPerUnit − entryFundingPerUnit)` and settles lazily whenever a position is touched. `fundingRate` is the clamped per-day rate finalised at the most recent funding collection. See protocol book: `perps/3-funding` §3–§4. ### Signature ```ts function getPerpsPairState( client: Client, parameters: { pairId: string height?: number }, ): Promise ``` ### Example ```ts import { createPublicClient, createTransport, testnet } from "@left-curve/sdk" const client = createPublicClient({ chain: testnet, transport: createTransport() }) const state = await client.getPerpsPairState({ pairId: "BTC-PERP" }) ``` ### Parameters **`pairId`** — `string`. Perps pair identifier. **`height`** — `number`, optional. Block height. Default `0` (latest). ### Returns **`PerpsPairState | null`** — `{ longOi, shortOi, fundingPerUnit, fundingRate }`, or `null` if the pair has no state yet. ### See also * [`getPerpsLiquidityDepth`](./getPerpsLiquidityDepth) * [`getPerpsState`](./getPerpsState) ## getPerpsPairStats Read 24h indexer-computed stats for a single perps pair. ### Signature ```ts function getPerpsPairStats( client: Client, parameters: { pairId: string }, ): Promise ``` ### Example ```ts import { createPublicClient, createTransport, testnet } from "@left-curve/sdk" const client = createPublicClient({ chain: testnet, transport: createTransport() }) const stats = await client.getPerpsPairStats({ pairId: "BTC-PERP" }) ``` ### Parameters **`pairId`** — `string`. Perps pair identifier. ### Returns **`PerpsPairStats`** — `{ pairId, currentPrice, price24HAgo, volume24H, priceChange24H }`. ### Notes * Sourced from the indexer's GraphQL schema. Indexer lag may apply. ### See also * [`getAllPerpsPairStats`](./getAllPerpsPairStats) * [`allPerpsPairStatsSubscription`](../indexer/allPerpsPairStatsSubscription) ## getPerpsParam Read the global perps parameters: max unlocks, max open orders, fee schedules, funding period, vault config. These chain-wide knobs include the volume-tiered `makerFeeRates` / `takerFeeRates`, `protocolFeeRate` (treasury cut), `liquidationFeeRate` and `liquidationBufferRatio`, the global `fundingPeriod`, vault settings (`vaultCooldownPeriod`, `vaultTotalWeight`, `vaultDepositCap`), and the referral master switch (`referralActive`, `minReferrerVolume`, `referrerCommissionRates`). Per-pair fields live in `getPerpsPairParam`. ### Signature ```ts function getPerpsParam( client: Client, parameters?: { height?: number }, ): Promise ``` ### Example ```ts import { createPublicClient, createTransport, testnet } from "@left-curve/sdk" const client = createPublicClient({ chain: testnet, transport: createTransport() }) const param = await client.getPerpsParam() ``` ### Parameters **`height`** — `number`, optional. Block height. Default `0` (latest). ### Returns **`PerpsParam`** — `{ maxUnlocks, maxOpenOrders, makerFeeRates, takerFeeRates, fundingPeriod, vaultTotalWeight, referralActive, ... }`. ### See also * [`getPerpsState`](./getPerpsState) — runtime state * [`getPerpsPairParams`](./getPerpsPairParams) ## getPerpsState Read global runtime state — last funding time, vault share supply, insurance fund balance, treasury. The insurance fund collects liquidation fees and absorbs bad debt; it can go negative when accumulated bad debt exceeds fees collected, with future fees naturally replenishing it. The vault and the insurance fund are isolated — external liquidation losses never touch vault margin. See protocol book: `perps/4-liquidation-and-adl` §7. ### Signature ```ts function getPerpsState( client: Client, parameters?: { height?: number }, ): Promise ``` ### Example ```ts import { createPublicClient, createTransport, testnet } from "@left-curve/sdk" const client = createPublicClient({ chain: testnet, transport: createTransport() }) const state = await client.getPerpsState() ``` ### Parameters **`height`** — `number`, optional. Block height. Default `0` (latest). ### Returns **`PerpsState`** — `{ lastFundingTime, vaultShareSupply, insuranceFund, treasury }`. ### See also * [`getPerpsParam`](./getPerpsParam) — configured parameters * [`getPerpsVaultState`](./getPerpsVaultState) ## getPerpsUserState Read the perps state for a single user. Returns the raw on-chain state record — deposited USD margin, vault shares, per-pair positions (size, entry price, entry funding accumulator), pending vault unlocks, reserved margin, and open-order count — without computing derived equity, PnL, or liquidation prices. Use [`getPerpsUserStateExtended`](./getPerpsUserStateExtended) for those. ### Signature ```ts function getPerpsUserState( client: Client, parameters: { user: Address height?: number }, ): Promise ``` ### Example ```ts import { createPublicClient, createTransport, testnet } from "@left-curve/sdk" import type { Address } from "@left-curve/sdk" const client = createPublicClient({ chain: testnet, transport: createTransport() }) const user: Address = "0x1234567890abcdef1234567890abcdef12345678" const state = await client.getPerpsUserState({ user }) ``` ### Parameters **`user`** — `Address`. The trader. **`height`** — `number`, optional. Block height. Default `0` (latest). ### Returns **`PerpsUserState | null`** — `{ margin, vaultShares, positions, unlocks, reservedMargin, openOrderCount }` or `null` if the user has no perps state. ### See also * [`getPerpsUserStateExtended`](./getPerpsUserStateExtended) — adds PnL, equity, liquidation prices * [`getPerpsOrdersByUser`](./getPerpsOrdersByUser) ## getPerpsUserStateExtended Read the extended perps state for a user, with optional PnL, equity, margin, and liquidation flags. Each computed field is gated by an `include_*` flag so the contract can skip unrequested work. The derivations come straight from the book: `equity = collateralValue + Σ unrealisedPnl − Σ accruedFunding`, `MM = Σ |size| × oracle × mmr`, and a user is liquidatable when `equity < MM`. See protocol book: `perps/1-margin` §4–§6. ### Signature ```ts function getPerpsUserStateExtended( client: Client, parameters: { user: Address includeEquity?: boolean includeAvailableMargin?: boolean includeMaintenanceMargin?: boolean includeUnrealizedPnl?: boolean includeUnrealizedFunding?: boolean includeLiquidationPrice?: boolean includeAll?: boolean height?: number }, ): Promise ``` ### Example ```ts import { createPublicClient, createTransport, testnet } from "@left-curve/sdk" import type { Address } from "@left-curve/sdk" const client = createPublicClient({ chain: testnet, transport: createTransport() }) const user: Address = "0x1234567890abcdef1234567890abcdef12345678" const state = await client.getPerpsUserStateExtended({ user, includeEquity: true, includeUnrealizedPnl: true, }) ``` ### Parameters **`user`** — `Address`. The trader. **`includeEquity`**, **`includeAvailableMargin`**, **`includeMaintenanceMargin`**, **`includeUnrealizedPnl`**, **`includeUnrealizedFunding`**, **`includeLiquidationPrice`**, **`includeAll`** — `boolean`, optional. Toggle each computed field; `null` when not included. **`height`** — `number`, optional. Block height. Default `0` (latest). ### Returns **`PerpsUserStateExtended | null`** — same shape as `PerpsUserState` plus `equity`, `availableMargin`, `maintenanceMargin`, and per-position `unrealizedPnl`, `unrealizedFunding`, `liquidationPrice`. Each computed field is `string | null` (null when the corresponding flag was not set). ### See also * [`getPerpsUserState`](./getPerpsUserState) — non-extended variant * [`getPerpsVaultState`](./getPerpsVaultState) ## getPerpsVaultState Read vault state — share supply, equity, and the vault's own perps positions. The vault is itself a `User` in the perps contract: it carries positions accumulated from market-making fills and computes equity by the same formula as any other user. `depositWithdrawalActive` flips to `false` when the vault's `effectiveEquity` is non-positive (catastrophic loss). See protocol book: `perps/5-vault` §4. ### Signature ```ts function getPerpsVaultState( client: Client, parameters?: { height?: number }, ): Promise ``` ### Example ```ts import { createPublicClient, createTransport, testnet } from "@left-curve/sdk" const client = createPublicClient({ chain: testnet, transport: createTransport() }) const state = await client.getPerpsVaultState() ``` ### Parameters **`height`** — `number`, optional. Block height. Default `0` (latest). ### Returns **`PerpsVaultState`** — `{ shareSupply, equity, depositWithdrawalActive, margin, positions, reservedMargin, openOrderCount }`. ### Notes * This action runs two on-chain queries internally: `getPerpsState` for `shareSupply` and `getPerpsUserStateExtended` (with `includeEquity: true`) for the vault contract's own positions. ### See also * [`vaultAddLiquidity`](./vaultAddLiquidity) * [`vaultRemoveLiquidity`](./vaultRemoveLiquidity) * [`getVaultSnapshots`](./getVaultSnapshots) ## getVaultSnapshots Read historical vault equity and share supply snapshots over a height range. ### Signature ```ts function getVaultSnapshots( client: Client, parameters?: { min?: number max?: number height?: number }, ): Promise> ``` ### Example ```ts import { createPublicClient, createTransport, testnet } from "@left-curve/sdk" const client = createPublicClient({ chain: testnet, transport: createTransport() }) const snapshots = await client.getVaultSnapshots({ min: 1_000, max: 5_000 }) ``` ### Parameters **`min`** — `number`, optional. Inclusive lower bound (converted to string before sending). **`max`** — `number`, optional. Inclusive upper bound (converted to string). **`height`** — `number`, optional. Block height for the query itself. Default `0` (latest). ### Returns **`Record`** — keyed by block height. Each snapshot is `{ equity, shareSupply }`. ### See also * [`getPerpsVaultState`](./getPerpsVaultState) ## queryPerpsCandles Read historical perps candles for a pair at a given interval. Paginated cursor-style. ### Signature ```ts function queryPerpsCandles( client: Client, parameters: { pairId: string interval: CandleIntervals after?: string first?: number earlierThan?: DateTime laterThan?: DateTime }, ): Promise> ``` ### Example ```ts import { createPublicClient, createTransport, testnet } from "@left-curve/sdk" const client = createPublicClient({ chain: testnet, transport: createTransport() }) const page = await client.queryPerpsCandles({ pairId: "BTC-PERP", interval: "ONE_MINUTE", first: 100, }) ``` ### Parameters **`pairId`** — `string`. Perps pair identifier. **`interval`** — `CandleIntervals`. Candle interval. **`after`** — `string`, optional. Cursor. **`first`** — `number`, optional. Page size. **`earlierThan`**, **`laterThan`** — `DateTime`, optional. ISO 8601 bounds. ### Returns **`GraphqlQueryResult`** — `{ pageInfo, nodes }`. ### See also * [`perpsCandlesSubscription`](../indexer/perpsCandlesSubscription) * [`queryCandles`](../dex/queryCandles) — spot equivalent ## queryPerpsEvents Read perps events — fills, liquidations, deleverages — paginated cursor-style. Optional filters by user, event type, pair, or block height. ### Signature ```ts function queryPerpsEvents( client: Client, parameters: { after?: string before?: string first?: number last?: number sortBy?: "BLOCK_HEIGHT_ASC" | "BLOCK_HEIGHT_DESC" userAddr?: string eventType?: string pairId?: string blockHeight?: number }, ): Promise> ``` ### Example ```ts import { createPublicClient, createTransport, testnet } from "@left-curve/sdk" const client = createPublicClient({ chain: testnet, transport: createTransport() }) const page = await client.queryPerpsEvents({ pairId: "BTC-PERP", eventType: "order_filled", first: 100, sortBy: "BLOCK_HEIGHT_DESC", }) ``` ### Parameters **`after`**, **`before`**, **`first`**, **`last`** — pagination cursors and sizes. **`sortBy`** — `"BLOCK_HEIGHT_ASC" | "BLOCK_HEIGHT_DESC"`, optional. **`userAddr`** — `string`, optional. Filter by trader. **`eventType`** — `string`, optional. `"order_filled"`, `"liquidated"`, or `"deleveraged"`. `order_filled` events carry a `fill_id` and are emitted for both regular and liquidation fills; `liquidated` and `deleveraged` (ADL) events carry no `fill_id` because they are position transfers at the bankruptcy price, not order-book matches. **`pairId`** — `string`, optional. Filter by pair. **`blockHeight`** — `number`, optional. Filter by block. ### Returns **`GraphqlQueryResult`** — `{ pageInfo, nodes }`. Each `PerpsEvent` has `idx`, `blockHeight`, `txHash`, `eventType`, `userAddr`, `pairId`, and a discriminated `data` payload. ### See also * [`queryPerpsCandles`](./queryPerpsCandles) ## setFeeShareRatio Opt in as a referrer and set the share ratio rebated to referees. Calling this is how a user becomes referrable: it gates which fraction of the level-1 commission the referrer rebates to each referee on every fill. The ratio is capped at 50 % on-chain and can only increase once set. Eligibility also requires `lifetimeVolume ≥ minReferrerVolume` (bypassed for users with a commission-rate override). See protocol book: `perps/6-referral` §3a. ### Signature ```ts function setFeeShareRatio( client: Client, parameters: { sender: Address shareRatio: string }, ): Promise<{ hash: Uint8Array } & TxData> ``` ### Example ```ts import { createSignerClient, createTransport, testnet, PrivateKeySigner } from "@left-curve/sdk" import type { Address } from "@left-curve/sdk" const client = createSignerClient({ chain: testnet, transport: createTransport(), signer: PrivateKeySigner.fromMnemonic(process.env.DANGO_MNEMONIC!), }) const sender: Address = "0x1234567890abcdef1234567890abcdef12345678" await client.setFeeShareRatio({ sender, shareRatio: "0.25" }) ``` ### Parameters **`sender`** — `Address`. The referrer. **`shareRatio`** — `string`. Decimal ratio (e.g. `"0.25"` for 25 %). On-chain cap is 50 %; can only be raised once set. ### Returns **`{ hash: Uint8Array } & TxData`** — see [`broadcastTxSync`](../app/broadcastTxSync). ### Notes * The input is normalized with `truncateDec` from `@left-curve/utils` before forming the message. ### See also * [`setReferral`](./setReferral) ## setReferral Set a referrer/referee mapping for fee sharing. Links the referee to a chain of up to five upstream referrers; each fill the referee pays goes through the per-level commission split described in protocol book: `perps/6-referral` §5. Constraints: a user cannot refer themselves, the referrer must already have opted in via [`setFeeShareRatio`](./setFeeShareRatio), and the relationship is immutable once stored. ### Signature ```ts function setReferral( client: Client, parameters: { sender: Address referrer: number referee: number }, ): Promise<{ hash: Uint8Array } & TxData> ``` ### Example ```ts import { createSignerClient, createTransport, testnet, PrivateKeySigner } from "@left-curve/sdk" import type { Address } from "@left-curve/sdk" const client = createSignerClient({ chain: testnet, transport: createTransport(), signer: PrivateKeySigner.fromMnemonic(process.env.DANGO_MNEMONIC!), }) const sender: Address = "0x1234567890abcdef1234567890abcdef12345678" await client.setReferral({ sender, referrer: 42, referee: 99 }) ``` ### Parameters **`sender`** — `Address`. The submitting account. **`referrer`** — `number`. Referrer's user index. **`referee`** — `number`. Referee's user index. ### Returns **`{ hash: Uint8Array } & TxData`** — see [`broadcastTxSync`](../app/broadcastTxSync). ### See also * [`setFeeShareRatio`](./setFeeShareRatio) ## submitConditionalOrder Submit a single trigger-based conditional perps order. Conditional orders are reduce-only by construction — they fire as a market order once the oracle price crosses `triggerPrice` in the chosen direction. `maxSlippage` is bounded at submission by the per-pair `max_market_slippage` cap; if governance later tightens that cap, a stale conditional is cancelled with `reason = SlippageCapTightened`. See protocol book: `perps/2-order-matching` §3b. ### Signature ```ts function submitConditionalOrder( client: Client, parameters: { sender: Address pairId: string size?: string triggerPrice: string triggerDirection: "above" | "below" maxSlippage: string }, ): Promise<{ hash: Uint8Array } & TxData> ``` ### Example ```ts import { createSignerClient, createTransport, testnet, PrivateKeySigner } from "@left-curve/sdk" import type { Address } from "@left-curve/sdk" const client = createSignerClient({ chain: testnet, transport: createTransport(), signer: PrivateKeySigner.fromMnemonic(process.env.DANGO_MNEMONIC!), }) const sender: Address = "0x1234567890abcdef1234567890abcdef12345678" await client.submitConditionalOrder({ sender, pairId: "BTC-PERP", size: "0.1", triggerPrice: "70000", triggerDirection: "above", maxSlippage: "0.005", }) ``` ### Parameters **`sender`** — `Address`. The trader. **`pairId`** — `string`. Perps pair identifier. **`size`** — `string`, optional. Position size (positive long, negative short). Omit to size against the current position at trigger time. **`triggerPrice`** — `string`. Price that activates the order. **`triggerDirection`** — `"above" | "below"`. Direction the oracle price must cross to fire. **`maxSlippage`** — `string`. Dimensionless ratio applied when the triggered market order executes. Must satisfy `≤ pair.max_market_slippage` at submission time. ### Returns **`{ hash: Uint8Array } & TxData`** — see [`broadcastTxSync`](../app/broadcastTxSync). ### See also * [`submitConditionalOrders`](./submitConditionalOrders) — batch variant * [`cancelConditionalOrder`](./cancelConditionalOrder) ## submitConditionalOrders Submit a batch of trigger-based conditional perps orders in one transaction. Each order is reduce-only by construction and fires as a market order once the oracle price crosses its `triggerPrice` in the chosen direction. See [`submitConditionalOrder`](./submitConditionalOrder) for the per-order mechanics. ### Signature ```ts function submitConditionalOrders( client: Client, parameters: { sender: Address orders: { pairId: string size?: string triggerPrice: string triggerDirection: "above" | "below" maxSlippage: string }[] }, ): Promise<{ hash: Uint8Array } & TxData> ``` ### Example ```ts import { createSignerClient, createTransport, testnet, PrivateKeySigner } from "@left-curve/sdk" import type { Address } from "@left-curve/sdk" const client = createSignerClient({ chain: testnet, transport: createTransport(), signer: PrivateKeySigner.fromMnemonic(process.env.DANGO_MNEMONIC!), }) const sender: Address = "0x1234567890abcdef1234567890abcdef12345678" await client.submitConditionalOrders({ sender, orders: [ { pairId: "BTC-PERP", size: "0.1", triggerPrice: "70000", triggerDirection: "above", maxSlippage: "0.005", }, { pairId: "BTC-PERP", size: "-0.05", triggerPrice: "60000", triggerDirection: "below", maxSlippage: "0.005", }, ], }) ``` ### Parameters **`sender`** — `Address`. The trader. **`orders`** — array of conditional order specs. See [`submitConditionalOrder`](./submitConditionalOrder) for each field's meaning. ### Returns **`{ hash: Uint8Array } & TxData`** — see [`broadcastTxSync`](../app/broadcastTxSync). ### Notes * Throws `Error("submitConditionalOrders requires at least one order")` if `orders.length === 0`. ### See also * [`submitConditionalOrder`](./submitConditionalOrder) * [`cancelConditionalOrder`](./cancelConditionalOrder) ## submitPerpsOrder Submit a market or limit perps order, optionally with take-profit and stop-loss children. Orders are decomposed into closing and opening portions against the user's existing position before matching, and each fill settles funding, PnL, and the volume-tiered fee in-place on USD margin. See protocol book: `perps/2-order-matching`. ### Signature ```ts function submitPerpsOrder( client: Client, parameters: { sender: Address pairId: string size: string kind: PerpsOrderKind reduceOnly: boolean tp?: ChildOrder sl?: ChildOrder }, ): Promise<{ hash: Uint8Array } & TxData> ``` ### Example ```ts import { createSignerClient, createTransport, testnet, PrivateKeySigner } from "@left-curve/sdk" import type { Address } from "@left-curve/sdk" const client = createSignerClient({ chain: testnet, transport: createTransport(), signer: PrivateKeySigner.fromMnemonic(process.env.DANGO_MNEMONIC!), }) const sender: Address = "0x1234567890abcdef1234567890abcdef12345678" // Market order await client.submitPerpsOrder({ sender, pairId: "BTC-PERP", size: "0.1", kind: { market: { maxSlippage: "0.005" } }, reduceOnly: false, }) // Limit order await client.submitPerpsOrder({ sender, pairId: "BTC-PERP", size: "-0.05", kind: { limit: { limitPrice: "65000", timeInForce: "GTC" }, }, reduceOnly: false, }) ``` ### Parameters **`sender`** — `Address`. The trader. **`pairId`** — `string`. Perps pair identifier. **`size`** — `string`. Signed quantity. Positive = buy (opens long / closes short), negative = sell. Zero is rejected. **`kind`** — `PerpsOrderKind`. Discriminated union: `{ market: { maxSlippage } }` or `{ limit: { limitPrice, timeInForce, clientOrderId? } }`. `timeInForce` is `"GTC"` (rest on book), `"IOC"` (immediate-or-cancel — discard unfilled), or `"POST"` (post-only — reject if it would cross). `maxSlippage` is bounded by the per-pair `max_market_slippage` cap. `clientOrderId` is not allowed with `"IOC"`. **`reduceOnly`** — `boolean`. When `true`, the order can only reduce an existing position. **`tp`** — `ChildOrder`, optional. Take-profit `{ triggerPrice, maxSlippage, size? }`. **`sl`** — `ChildOrder`, optional. Stop-loss `{ triggerPrice, maxSlippage, size? }`. ### Returns **`{ hash: Uint8Array } & TxData`** — see [`broadcastTxSync`](../app/broadcastTxSync). ### Notes * `clientOrderId` is normalized: `null`/`undefined` is stripped from both the message and the typed-data structure. * The chain rejects orders that fail pre-match checks: per-pair tick-size alignment, OI cap, slippage cap, limit-price band relative to oracle, and pre-match margin (worst-case 100 % fill IM + projected fee + reserved margin must be covered by equity). See `perps/2-order-matching` §3a, §3b, §5, §11. * Market and IOC limit orders revert with "no liquidity at acceptable price" if nothing fills; GTC remainders rest on the book and reserve `|openingSize| × limitPrice × imr` until cancelled or filled. * Reduce-only orders skip the pre-match margin and OI checks; they reserve zero margin. ### See also * [`cancelPerpsOrder`](./cancelPerpsOrder) * [`submitConditionalOrder`](./submitConditionalOrder) * [`getPerpsOrdersByUser`](./getPerpsOrdersByUser) ## vaultAddLiquidity Move USD margin from the caller's trading account into the perps counterparty vault and mint LP shares in return. The vault is the perps exchange's passive market maker; LPs share its inventory-skew-aware quoting PnL. Shares are minted via the ERC-4626 virtual shares pattern, floor-rounded — see protocol book: `perps/5-vault` §2. Top up trading margin first via [`depositMargin`](./depositMargin) if needed. ### Signature ```ts function vaultAddLiquidity( client: Client, parameters: { sender: Address amount: string minSharesToMint?: string }, ): Promise<{ hash: Uint8Array } & TxData> ``` ### Example ```ts import { createSignerClient, createTransport, testnet, PrivateKeySigner } from "@left-curve/sdk" import type { Address } from "@left-curve/sdk" const client = createSignerClient({ chain: testnet, transport: createTransport(), signer: PrivateKeySigner.fromMnemonic(process.env.DANGO_MNEMONIC!), }) const sender: Address = "0x1234567890abcdef1234567890abcdef12345678" // Deposit 1,000 USD of trading margin into the vault await client.vaultAddLiquidity({ sender, amount: "1000.000000", minSharesToMint: "950000", }) ``` ### Parameters **`sender`** — `Address`. The depositor. **`amount`** — `string`. USD value debited from the caller's trading margin (6-decimal `UsdValue` wire form). NOT base units — no funds are attached at the wallet boundary. **`minSharesToMint`** — `string`, optional. Slippage guard: revert if fewer than this many shares would be minted. ### Returns **`{ hash: Uint8Array } & TxData`** — see [`broadcastTxSync`](../app/broadcastTxSync). ### Notes * The USD amount is debited from the caller's existing trading margin — NOT attached as funds on the execute message. * Deposits revert if the vault's `effectiveEquity` is non-positive (catastrophic loss; see `perps/5-vault` §4). ### See also * [`vaultRemoveLiquidity`](./vaultRemoveLiquidity) * [`getPerpsVaultState`](./getPerpsVaultState) ## vaultRemoveLiquidity Burn vault shares to schedule a withdrawal. The corresponding cash is unlocked after the vault cooldown period. The release value is computed as `effectiveEquity × (sharesToBurn / effectiveSupply)` at burn time, but the USD is not credited immediately — a cooldown is enforced to prevent LPs from front-running known losses. See protocol book: `perps/5-vault` §3. ### Signature ```ts function vaultRemoveLiquidity( client: Client, parameters: { sender: Address sharesToBurn: string }, ): Promise<{ hash: Uint8Array } & TxData> ``` ### Example ```ts import { createSignerClient, createTransport, testnet, PrivateKeySigner } from "@left-curve/sdk" import type { Address } from "@left-curve/sdk" const client = createSignerClient({ chain: testnet, transport: createTransport(), signer: PrivateKeySigner.fromMnemonic(process.env.DANGO_MNEMONIC!), }) const sender: Address = "0x1234567890abcdef1234567890abcdef12345678" await client.vaultRemoveLiquidity({ sender, sharesToBurn: "1000000" }) ``` ### Parameters **`sender`** — `Address`. The withdrawer. **`sharesToBurn`** — `string`. Vault shares to burn, base units (`uint128`). ### Returns **`{ hash: Uint8Array } & TxData`** — see [`broadcastTxSync`](../app/broadcastTxSync). ### Notes * The actual cash release happens after `PerpsParam.vaultCooldownPeriod` and surfaces as a `PerpsUnlock` entry on the user's state. * Withdrawals revert if the vault's `effectiveEquity` is non-positive. * Total concurrent unlocks per user are capped at `PerpsParam.maxUnlocks`. ### See also * [`vaultAddLiquidity`](./vaultAddLiquidity) * [`getPerpsUserState`](./getPerpsUserState) * [`getPerpsParam`](./getPerpsParam) ## withdrawMargin Withdraw collateral from the perps account. The perps contract holds margin internally as a USD value; this action specifies a USD amount to release and the contract converts it to settlement-currency tokens (floor-rounded to base units) at the fixed $1 rate. The withdrawal is capped by the user's available margin — see protocol book: `perps/1-margin` §3. ### Signature ```ts function withdrawMargin( client: Client, parameters: { sender: Address amount: string }, ): Promise<{ hash: Uint8Array } & TxData> ``` ### Example ```ts import { createSignerClient, createTransport, testnet, PrivateKeySigner } from "@left-curve/sdk" import type { Address } from "@left-curve/sdk" const client = createSignerClient({ chain: testnet, transport: createTransport(), signer: PrivateKeySigner.fromMnemonic(process.env.DANGO_MNEMONIC!), }) const sender: Address = "0x1234567890abcdef1234567890abcdef12345678" // Withdraw 500 USD (6-decimal UsdValue wire form) await client.withdrawMargin({ sender, amount: "500.000000" }) ``` ### Parameters **`sender`** — `Address`. The trader. **`amount`** — `string`. USD value to withdraw (6-decimal `UsdValue` wire form, e.g. `"500.000000"` = 500 USD). NOT base units — the contract converts to settlement tokens internally at $1. ### Returns **`{ hash: Uint8Array } & TxData`** — see [`broadcastTxSync`](../app/broadcastTxSync). ### Notes * Asymmetric with [`depositMargin`](./depositMargin): deposit attaches `bridge/usdc` base units as funds, withdraw specifies a USD value in the message body. The asymmetry mirrors the on-chain contract. * Rejected by the chain if the requested amount exceeds `availableMargin = max(0, equity − usedMargin − reservedMargin)`. ### See also * [`depositMargin`](./depositMargin) * [`getPerpsUserState`](./getPerpsUserState) ## getPrices Read oracle prices, paginated. ### Signature ```ts function getPrices( client: Client, parameters?: { startAfter?: Denom limit?: number height?: number }, ): Promise> ``` ### Example ```ts import { createPublicClient, createTransport, testnet } from "@left-curve/sdk" const client = createPublicClient({ chain: testnet, transport: createTransport() }) const prices = await client.getPrices({ limit: 100 }) console.log(prices["bridge/usdc"]?.humanizedPrice) ``` ### Parameters **`startAfter`** — `Denom`, optional. Denom to start after. **`limit`** — `number`, optional. Maximum entries. **`height`** — `number`, optional. Block height. Default `0` (latest). ### Returns **`Record`** — each `Price` is `{ humanizedPrice, humanizedEma, precision, timestamp }`. ### See also * [`Price`](../../types/Price) ## accountSubscription Subscribe to account creation events for a single user index. WebSocket only. ### Signature ```ts function accountSubscription( client: Client, parameters: SubscriptionCallbacks<{ accounts: IndexedAccountEvent[] }> & { userIndex: number sinceBlockHeight?: number }, ): () => void ``` ### Example ```ts import { createPublicClient, createTransport, testnet } from "@left-curve/sdk" const client = createPublicClient({ chain: testnet, transport: createTransport() }) const unsubscribe = client.accountSubscription({ userIndex: 42, next: ({ accounts }) => console.log("new accounts", accounts), }) ``` ### Parameters **`userIndex`** — `number`. The user whose new accounts to watch. **`sinceBlockHeight`** — `number`, optional. Replay events from this height before going live. **`next`** — `(data) => void`. Called with new account events. **`error`**, **`complete`** — optional callbacks. ### Returns **`() => void`** — unsubscribe. ### See also * [`registerAccount`](../account-factory/registerAccount) * [`registerUser`](../account-factory/registerUser) ## allPairStatsSubscription Subscribe to live 24h stats for every spot pair. Uses WebSocket when available; falls back to HTTP polling. ### Signature ```ts function allPairStatsSubscription( client: Client, parameters: SubscriptionCallbacks<{ allPairStats: PairStats[] }> & { httpInterval?: number }, ): () => void ``` ### Example ```ts import { createPublicClient, createTransport, testnet } from "@left-curve/sdk" const client = createPublicClient({ chain: testnet, transport: createTransport() }) const unsubscribe = client.allPairStatsSubscription({ next: ({ allPairStats }) => render(allPairStats), }) ``` ### Parameters **`httpInterval`** — `number`, optional, default `5000`. Poll interval (ms) used when WS is unavailable. **`next`**, **`error`**, **`complete`** — callbacks. ### Returns **`() => void`** — unsubscribe. ### See also * [`getAllPairStats`](../dex/getAllPairStats) * [`allPerpsPairStatsSubscription`](./allPerpsPairStatsSubscription) ## allPerpsPairStatsSubscription Subscribe to live 24h stats for every perps pair. Uses WebSocket when available; falls back to HTTP polling. ### Signature ```ts function allPerpsPairStatsSubscription( client: Client, parameters: SubscriptionCallbacks<{ allPerpsPairStats: PerpsPairStats[] }> & { httpInterval?: number }, ): () => void ``` ### Example ```ts import { createPublicClient, createTransport, testnet } from "@left-curve/sdk" const client = createPublicClient({ chain: testnet, transport: createTransport() }) const unsubscribe = client.allPerpsPairStatsSubscription({ next: ({ allPerpsPairStats }) => render(allPerpsPairStats), }) ``` ### Parameters **`httpInterval`** — `number`, optional, default `5000`. Poll interval (ms) used when WS is unavailable. **`next`**, **`error`**, **`complete`** — callbacks. ### Returns **`() => void`** — unsubscribe. ### See also * [`getAllPerpsPairStats`](../perps/getAllPerpsPairStats) * [`allPairStatsSubscription`](./allPairStatsSubscription) ## blockSubscription Subscribe to new finalized blocks. WebSocket only. ### Signature ```ts function blockSubscription( client: Client, parameters: SubscriptionCallbacks<{ block: Omit }>, ): () => void ``` ### Example ```ts import { createPublicClient, createTransport, testnet } from "@left-curve/sdk" const client = createPublicClient({ chain: testnet, transport: createTransport() }) const unsubscribe = client.blockSubscription({ next: ({ block }) => console.log("new block", block.blockHeight), error: (err) => console.error(err), }) // later unsubscribe() ``` ### Parameters **`next`** — `(data: { block }) => void`. Called for each new block. The `block` payload excludes the transaction list (use [`queryBlock`](./queryBlock) for that). **`error`** — `(err: unknown) => void`, optional. **`complete`** — `() => void`, optional. ### Returns **`() => void`** — call to unsubscribe. ### Notes * Throws if the transport has `disableWs: true` or the WS client failed to connect. ### See also * [`queryBlock`](./queryBlock) * [Concepts: Subscriptions](../../concepts/subscriptions) ## candlesSubscription Subscribe to live spot candles for a pair and interval. WebSocket only. ### Signature ```ts function candlesSubscription( client: Client, parameters: SubscriptionCallbacks<{ candles: Candle[] }> & { baseDenom: Denom quoteDenom: Denom interval: CandleIntervals }, ): () => void ``` ### Example ```ts import { createPublicClient, createTransport, testnet } from "@left-curve/sdk" const client = createPublicClient({ chain: testnet, transport: createTransport() }) const unsubscribe = client.candlesSubscription({ baseDenom: "dango", quoteDenom: "bridge/usdc", interval: "ONE_MINUTE", next: ({ candles }) => updateChart(candles), }) ``` ### Parameters **`baseDenom`** — `Denom`. Base asset. **`quoteDenom`** — `Denom`. Quote asset. **`interval`** — `CandleIntervals`. Candle interval. **`next`**, **`error`**, **`complete`** — callbacks. ### Returns **`() => void`** — unsubscribe. ### See also * [`queryCandles`](../dex/queryCandles) — historical fetch * [`perpsCandlesSubscription`](./perpsCandlesSubscription) ## eventsByAddressesSubscription Subscribe to all events touching a set of addresses. WebSocket only. ### Signature ```ts function eventsByAddressesSubscription( client: Client, parameters: SubscriptionCallbacks<{ eventByAddresses: IndexedEvent[] }> & { addresses: Address[] sinceBlockHeight?: number }, ): () => void ``` ### Example ```ts import { createPublicClient, createTransport, testnet } from "@left-curve/sdk" import type { Address } from "@left-curve/sdk" const client = createPublicClient({ chain: testnet, transport: createTransport() }) const addresses: Address[] = [ "0x1234567890abcdef1234567890abcdef12345678", "0xabcdef1234567890abcdef1234567890abcdef12", ] const unsubscribe = client.eventsByAddressesSubscription({ addresses, next: ({ eventByAddresses }) => eventByAddresses.forEach((e) => console.log(e.transaction.hash, e.type)), }) ``` ### Parameters **`addresses`** — `Address[]`. Addresses to watch. **`sinceBlockHeight`** — `number`, optional. Replay from this height. **`next`**, **`error`**, **`complete`** — callbacks. ### Returns **`() => void`** — unsubscribe. ### See also * [`eventsSubscription`](./eventsSubscription) — type/data filters ## eventsSubscription Subscribe to a filtered live event stream. WebSocket only. ### Signature ```ts function eventsSubscription( client: Client, parameters: SubscriptionCallbacks<{ events: SubscriptionEvent[] }> & { sinceBlockHeight?: number filter?: EventFilter[] }, ): () => void ``` ### Example ```ts import { createPublicClient, createTransport, testnet } from "@left-curve/sdk" const client = createPublicClient({ chain: testnet, transport: createTransport() }) const unsubscribe = client.eventsSubscription({ filter: [ { type: "transfer", data: [{ path: ["sender"], checkMode: "EQUAL", value: ["0xabc..."] }], }, ], next: ({ events }) => events.forEach((e) => console.log(e.type, e.data)), }) ``` ### Parameters **`sinceBlockHeight`** — `number`, optional. Replay from this height. **`filter`** — `EventFilter[]`, optional. Each filter specifies an event type and one or more path-value matches. **`next`**, **`error`**, **`complete`** — callbacks. ### Returns **`() => void`** — unsubscribe. ### See also * [`eventsByAddressesSubscription`](./eventsByAddressesSubscription) * [`transferSubscription`](./transferSubscription) ## perpsCandlesSubscription Subscribe to live perps candles for a pair and interval. WebSocket only. ### Signature ```ts function perpsCandlesSubscription( client: Client, parameters: SubscriptionCallbacks<{ perpsCandles: PerpsCandle[] }> & { pairId: string interval: CandleIntervals }, ): () => void ``` ### Example ```ts import { createPublicClient, createTransport, testnet } from "@left-curve/sdk" const client = createPublicClient({ chain: testnet, transport: createTransport() }) const unsubscribe = client.perpsCandlesSubscription({ pairId: "BTC-PERP", interval: "ONE_MINUTE", next: ({ perpsCandles }) => updateChart(perpsCandles), }) ``` ### Parameters **`pairId`** — `string`. Perps pair identifier. **`interval`** — `CandleIntervals`. Candle interval. **`next`**, **`error`**, **`complete`** — callbacks. ### Returns **`() => void`** — unsubscribe. ### See also * [`queryPerpsCandles`](../perps/queryPerpsCandles) * [`candlesSubscription`](./candlesSubscription) ## perpsTradesSubscription Subscribe to live perps trades for a pair. Uses WebSocket when available; falls back to HTTP polling. ### Signature ```ts function perpsTradesSubscription( client: Client, parameters: SubscriptionCallbacks<{ perpsTrades: PerpsTrade }> & { pairId: string httpInterval?: number }, ): () => void ``` ### Example ```ts import { createPublicClient, createTransport, testnet } from "@left-curve/sdk" const client = createPublicClient({ chain: testnet, transport: createTransport() }) const unsubscribe = client.perpsTradesSubscription({ pairId: "BTC-PERP", next: ({ perpsTrades }) => console.log(perpsTrades.fillPrice, perpsTrades.fillSize), }) ``` ### Parameters **`pairId`** — `string`. Perps pair identifier. **`httpInterval`** — `number`, optional, default `3000`. Poll interval (ms) used when WS is unavailable. **`next`**, **`error`**, **`complete`** — callbacks. ### Returns **`() => void`** — unsubscribe. ### See also * [`queryPerpsEvents`](../perps/queryPerpsEvents) * [`tradesSubscription`](./tradesSubscription) ## queryAppSubscription Subscribe to the live result of a `queryApp` request. Uses WebSocket when available; falls back to HTTP polling. ### Signature ```ts function queryAppSubscription( client: Client, parameters: SubscriptionCallbacks<{ queryApp: { response: QueryResponse blockHeight: number } }> & { request: QueryRequest interval?: number httpInterval?: number }, ): () => void ``` ### Example ```ts import { createPublicClient, createTransport, testnet } from "@left-curve/sdk" import type { Address } from "@left-curve/sdk" const client = createPublicClient({ chain: testnet, transport: createTransport() }) const address: Address = "0x1234567890abcdef1234567890abcdef12345678" const unsubscribe = client.queryAppSubscription({ request: { balance: { address, denom: "dango" } }, next: ({ queryApp }) => { if ("balance" in queryApp.response) { console.log(queryApp.blockHeight, queryApp.response.balance.amount) } }, }) ``` ### Parameters **`request`** — `QueryRequest`. The query to evaluate on each block. **`interval`** — `number`, optional. Block interval between WS pushes (default `10`). **`httpInterval`** — `number`, optional, default `5000`. Poll interval (ms) used when WS is unavailable. **`next`**, **`error`**, **`complete`** — callbacks. ### Returns **`() => void`** — unsubscribe. ### Notes * The HTTP fallback uses [`queryApp`](../app/queryApp) under the hood and emits `blockHeight: 0` (the fallback path does not have block info). ### See also * [`queryApp`](../app/queryApp) ## queryBlock Fetch a block (by height or latest) with its transactions and cron outcomes. ### Signature ```ts function queryBlock( client: Client, parameters?: { height?: number }, ): Promise ``` ### Example ```ts import { createPublicClient, createTransport, testnet } from "@left-curve/sdk" const client = createPublicClient({ chain: testnet, transport: createTransport() }) const block = await client.queryBlock({ height: 12345 }) const latest = await client.queryBlock() ``` ### Parameters **`height`** — `number`, optional. Block height. Omit for latest. ### Returns **`IndexedBlock`** — `{ blockHeight, createdAt, hash, appHash, cronsOutcomes, transactions }`. Each `IndexedTransaction` carries hash, sender, type, messages, and gas accounting. ### See also * [`searchTxs`](./searchTxs) — paginated tx search * [`queryStatus`](../app/queryStatus) — just the chain id + latest block header ## queryIndexer Send a raw GraphQL request to the indexer and receive the typed response. ### Signature ```ts function queryIndexer( client: Client, parameters: { document: string variables?: Record }, ): Promise ``` ### Example ```ts import { createPublicClient, createTransport, testnet } from "@left-curve/sdk" const client = createPublicClient({ chain: testnet, transport: createTransport() }) const result = await client.request<{ block: { blockHeight: number } }>({ request: `query { block { blockHeight } }`, params: {}, }) ``` ### Parameters **`document`** — `string`. GraphQL query/mutation document. **`variables`** — `Record`, optional. Variables for the query. ### Returns **`T`** — the raw GraphQL `data` object. The caller asserts the shape. ### Notes * `queryIndexer` is not on the client surface — it is a building block used by every typed indexer action. For end-user code, call `client.request({ request: document, params: variables })` directly, or use the typed wrappers ([`queryBlock`](./queryBlock), [`searchTxs`](./searchTxs)). ### See also * [`queryBlock`](./queryBlock) * [`searchTxs`](./searchTxs) ## searchTxs Search indexed transactions by hash or sender, paginated. ### Signature ```ts function searchTxs( client: Client, parameters: { hash?: string senderAddress?: string after?: string before?: string first?: number last?: number sortBy?: string }, ): Promise> ``` ### Example ```ts import { createPublicClient, createTransport, testnet } from "@left-curve/sdk" const client = createPublicClient({ chain: testnet, transport: createTransport() }) const page = await client.searchTxs({ senderAddress: "0x1234567890abcdef1234567890abcdef12345678", first: 50, }) ``` ### Parameters **`hash`** — `string`, optional. Filter by tx hash. **`senderAddress`** — `string`, optional. Filter by sender. **`after`**, **`before`**, **`first`**, **`last`**, **`sortBy`** — standard GraphQL pagination. ### Returns **`GraphqlQueryResult`** — `{ pageInfo, nodes }`. ### See also * [`queryTx`](../app/queryTx) — single-tx fetch * [`queryBlock`](./queryBlock) ## tradesSubscription Subscribe to live spot trades for a pair. Uses WebSocket when available; falls back to HTTP polling. ### Signature ```ts function tradesSubscription( client: Client, parameters: SubscriptionCallbacks<{ trades: Trade }> & { baseDenom: Denom quoteDenom: Denom httpInterval?: number }, ): () => void ``` ### Example ```ts import { createPublicClient, createTransport, testnet } from "@left-curve/sdk" const client = createPublicClient({ chain: testnet, transport: createTransport() }) const unsubscribe = client.tradesSubscription({ baseDenom: "dango", quoteDenom: "bridge/usdc", next: ({ trades }) => console.log(trades.clearingPrice, trades.filledBase), }) ``` ### Parameters **`baseDenom`** — `Denom`. Base asset. **`quoteDenom`** — `Denom`. Quote asset. **`httpInterval`** — `number`, optional, default `3000`. Poll interval (ms) used when WS is unavailable. **`next`**, **`error`**, **`complete`** — callbacks. ### Returns **`() => void`** — unsubscribe. ### See also * [`queryTrades`](../dex/queryTrades) — historical fetch * [`perpsTradesSubscription`](./perpsTradesSubscription) ## transferSubscription Subscribe to live transfer events for a single username. WebSocket only. ### Signature ```ts function transferSubscription( client: Client, parameters: SubscriptionCallbacks<{ transfers: IndexedTransferEvent[] }> & { username: string sinceBlockHeight?: number }, ): () => void ``` ### Example ```ts import { createPublicClient, createTransport, testnet } from "@left-curve/sdk" const client = createPublicClient({ chain: testnet, transport: createTransport() }) const unsubscribe = client.transferSubscription({ username: "alice", next: ({ transfers }) => transfers.forEach((t) => console.log(t.fromAddress, t.amount)), }) ``` ### Parameters **`username`** — `string`. The user to watch. **`sinceBlockHeight`** — `number`, optional. Replay from this height. **`next`**, **`error`**, **`complete`** — callbacks. ### Returns **`() => void`** — unsubscribe. ### See also * [`transfer`](../app/transfer) * [`eventsByAddressesSubscription`](./eventsByAddressesSubscription) ## Addr32 Encode and decode 32-byte Hyperlane addresses. The Hyperlane wire format pads 20-byte EVM addresses to 32 bytes by left-padding with zeros. ### Signature ```ts class Addr32 implements Encoder { static from(address: Address): Addr32 static decode(buf: Uint8Array): Addr32 get address(): string encode(): Uint8Array } function toAddr32(address: `0x${string}`): Address ``` ### Example ```ts import { Addr32, toAddr32 } from "@left-curve/sdk/hyperlane" // 20-byte EVM address → 32-byte hex string const padded = toAddr32("0x1234567890abcdef1234567890abcdef12345678") // Class form for wire encoding const addr = Addr32.from("0x1234567890abcdef1234567890abcdef12345678") const bytes: Uint8Array = addr.encode() // 32 bytes const roundTrip = Addr32.decode(bytes) ``` ### API **`Addr32.from(address)`** — construct from a 20-byte `Address` (left-padded to 32 bytes). **`Addr32.decode(buf)`** — wrap a raw 32-byte buffer. **`addr.address`** — hex string of the 32 bytes (no `0x` prefix). **`addr.encode()`** — return the raw 32-byte `Uint8Array`. **`toAddr32(address)`** — utility that returns the 32-byte hex string directly (no class wrapper). ### See also * [`Message`](./Message) — uses `Addr32` for sender and recipient * [`TokenMessage`](./TokenMessage) ## IncrementalMerkleTree Hyperlane outbox-style incremental Merkle tree of depth 32 with Keccak-256 hashing. ### Signature ```ts class IncrementalMerkleTree { static create(): IncrementalMerkleTree static from(params: { branch: Uint8Array[]; count: bigint; root: Uint8Array }): IncrementalMerkleTree insert(node: Uint8Array): void root(): Uint8Array count(): bigint save(): { branch: Uint8Array[]; count: bigint; root: Uint8Array } } ``` ### Example ```ts import { IncrementalMerkleTree } from "@left-curve/sdk/hyperlane" import { keccak256 } from "@left-curve/crypto" const tree = IncrementalMerkleTree.create() tree.insert(keccak256(new Uint8Array([1, 2, 3]))) tree.insert(keccak256(new Uint8Array([4, 5, 6]))) const root: Uint8Array = tree.root() const snapshot = tree.save() ``` ### API **`create()`** — fresh empty tree. **`from(params)`** — rehydrate from a snapshot (`save()` output). **`insert(node)`** — append a leaf (must be 32 bytes). Throws if the tree is full (2^32 − 1 leaves). **`root()`** — current root. **`count()`** — number of leaves inserted (`bigint`). **`save()`** — snapshot for persistence. ### See also * [`Message`](./Message) * [`Metadata`](./Metadata) ## Message Encode a Hyperlane mailbox message. Outputs the binary layout the Hyperlane router expects. ### Signature ```ts class Message implements Encoder { version: number nonce: number originDomain: Domain sender: Addr32 destinationDomain: Domain recipient: Addr32 body: Uint8Array static from(params: Omit): Message encode(): Uint8Array } const MAILBOX_VERSION: number // 3 const HYPERLANE_DOMAIN_KEY: string // "HYPERLANE" ``` ### Example ```ts import { Addr32, Message, MAILBOX_VERSION } from "@left-curve/sdk/hyperlane" const msg = Message.from({ version: MAILBOX_VERSION, nonce: 0, originDomain: 1, sender: Addr32.from("0x1234567890abcdef1234567890abcdef12345678"), destinationDomain: 2, recipient: Addr32.from("0xabcdef1234567890abcdef1234567890abcdef12"), body: new Uint8Array([1, 2, 3]), }) const wire: Uint8Array = msg.encode() // 77 + body bytes ``` ### Encoding layout `encode()` produces a buffer of `77 + body.byteLength` bytes: | Offset | Length | Field | | ------ | ------ | ------------------------ | | 0 | 1 | `version` | | 1 | 4 | `nonce` (BE) | | 5 | 4 | `originDomain` (BE) | | 9 | 32 | `sender` | | 41 | 4 | `destinationDomain` (BE) | | 45 | 32 | `recipient` | | 77 | N | `body` | ### See also * [`Addr32`](./Addr32) * [`TokenMessage`](./TokenMessage) — common body payload ## Metadata Encode multisig ISM (Interchain Security Module) metadata: origin merkle tree address, merkle root, index, and validator signatures. ### Signature ```ts class Metadata implements Encoder { originMerkleTree: Addr32 merkleRoot: Uint8Array merkleIndex: number signatures: Uint8Array[] static from(params: Omit): Metadata encode(): Uint8Array } ``` ### Example ```ts import { Addr32, Metadata } from "@left-curve/sdk/hyperlane" const meta = Metadata.from({ originMerkleTree: Addr32.from("0x1234567890abcdef1234567890abcdef12345678"), merkleRoot: new Uint8Array(32), merkleIndex: 0, signatures: [new Uint8Array(65)], }) const wire: Uint8Array = meta.encode() ``` ### Encoding layout `encode()` produces a buffer of `68 + 65 * signatures.length` bytes: | Offset | Length | Field | | ------ | ------- | ---------------------------------------------- | | 0 | 32 | `originMerkleTree` | | 32 | 32 | `merkleRoot` | | 64 | 4 | `merkleIndex` (BE) | | 68 | 65 each | `signatures[i]` (65 bytes per ECDSA signature) | ### See also * [`Message`](./Message) * [`IncrementalMerkleTree`](./IncrementalMerkleTree) ## TokenMessage Encode a Hyperlane warp-route token message — recipient, amount, and metadata. ### Signature ```ts class TokenMessage implements Encoder { recipient: Addr32 amount: string metadata: Uint8Array static from(params: Omit): TokenMessage encode(): Uint8Array } ``` ### Example ```ts import { Addr32, TokenMessage } from "@left-curve/sdk/hyperlane" const tm = TokenMessage.from({ recipient: Addr32.from("0xabcdef1234567890abcdef1234567890abcdef12"), amount: "1000000000", metadata: new Uint8Array(), }) const wire: Uint8Array = tm.encode() // 64 + metadata bytes ``` ### Encoding layout `encode()` produces a buffer of `64 + metadata.byteLength` bytes: | Offset | Length | Field | | ------ | ------ | ---------------------- | | 0 | 32 | `recipient` | | 32 | 32 | `amount` (uint256, BE) | | 64 | N | `metadata` | ### See also * [`Addr32`](./Addr32) * [`Message`](./Message) — wraps `TokenMessage` as `body` ## getWithdrawalFee Read the withdrawal fee for a denom + remote chain pair. ### Signature ```ts function getWithdrawalFee( client: Client, parameters: { denom: Denom remote: Remote height?: number }, ): Promise ``` ### Example ```ts import { createPublicClient, createTransport, testnet } from "@left-curve/sdk" const client = createPublicClient({ chain: testnet, transport: createTransport() }) const fee = await client.gateway.getWithdrawalFee({ denom: "bridge/usdc", remote: { warp: { domain: 1, contract: "0x..." } }, }) ``` ### Parameters **`denom`** — `Denom`. Token denom. **`remote`** — `Remote`. `{ warp: { domain, contract: Addr32 } }` or `"bitcoin"`. **`height`** — `number`, optional. Block height. Default `0` (latest). ### Returns **`string`** — the withdrawal fee in base units. ### Notes * Gateway actions are namespaced under `client.gateway.*`. Other domains are flat. ### See also * [`transferRemote`](./transferRemote) * [Concepts: Clients](../../concepts/clients) — gateway namespacing ## transferRemote Send funds to a remote chain via the gateway contract. ### Signature ```ts function transferRemote( client: Client, parameters: { remote: Remote recipient: Addr32 sender: Address funds: Coins }, ): Promise<{ hash: Uint8Array } & TxData> ``` ### Example ```ts import { createSignerClient, createTransport, testnet, PrivateKeySigner } from "@left-curve/sdk" import type { Address } from "@left-curve/sdk" const client = createSignerClient({ chain: testnet, transport: createTransport(), signer: PrivateKeySigner.fromMnemonic(process.env.DANGO_MNEMONIC!), }) const sender: Address = "0x1234567890abcdef1234567890abcdef12345678" await client.gateway.transferRemote({ sender, remote: { warp: { domain: 1, contract: "0xabc..." } }, recipient: "0x000000000000000000000000abcdef1234567890abcdef1234567890abcdef12", funds: { "bridge/usdc": "100000000" }, }) ``` ### Parameters **`remote`** — `Remote`. `{ warp: { domain, contract: Addr32 } }` or the string `"bitcoin"`. **`recipient`** — `Addr32`. 32-byte hex recipient on the remote chain. **`sender`** — `Address`. The local sender. **`funds`** — `Coins`. Coins to send. ### Returns **`{ hash: Uint8Array } & TxData`** — see [`broadcastTxSync`](../app/broadcastTxSync). ### Notes * Namespaced under `client.gateway.*`. ### See also * [`getWithdrawalFee`](./getWithdrawalFee) * [Concepts: Clients](../../concepts/clients) — gateway namespacing ## batchUpdateOrders Create and/or cancel multiple limit orders in a single transaction. :::warning[DEX currently disabled] The Dango DEX is currently disabled. Calls described on this page will not execute on the live network until the DEX is enabled. ::: ### Signature ```ts function batchUpdateOrders( client: Client, parameters: { sender: Address funds?: Coins creates?: CreateOrderRequest[] cancels?: CancelOrderRequest }, ): Promise<{ hash: Uint8Array } & TxData> ``` ### Example ```ts import { createSignerClient, createTransport, testnet, PrivateKeySigner } from "@left-curve/sdk" import type { Address } from "@left-curve/sdk" const client = createSignerClient({ chain: testnet, transport: createTransport(), signer: PrivateKeySigner.fromMnemonic(process.env.DANGO_MNEMONIC!), }) const sender: Address = "0x1234567890abcdef1234567890abcdef12345678" await client.batchUpdateOrders({ sender, funds: { "bridge/usdc": "100000000" }, creates: [ { baseDenom: "dango", quoteDenom: "bridge/usdc", amount: { bid: { quote: "100000000" } }, price: { limit: "0.5" }, timeInForce: "GTC", }, ], cancels: { some: ["123", "456"] }, }) ``` ### Parameters **`sender`** — `Address`. The trader. **`funds`** — `Coins`, optional. Coins to attach for new bids/asks. **`creates`** — `CreateOrderRequest[]`, optional. New orders. Each entry has `baseDenom`, `quoteDenom`, `amount` (`{ bid }` or `{ ask }`), `price` (`{ limit }` or `{ market }`), and `timeInForce` (`"GTC"` or `"IOC"`). **`cancels`** — `CancelOrderRequest`, optional. `"all"` or `{ some: OrderId[] }`. ### Returns **`{ hash: Uint8Array } & TxData`** — see [`broadcastTxSync`](../app/broadcastTxSync). ### Notes * The EIP-712 typed-data structure is built dynamically based on whether the first create order is a bid/ask and limit/market. Mixed-side or mixed-style batches may need a custom `typedData` passed at the [`execute`](../app/execute) layer. ### See also * [`ordersByUser`](./ordersByUser) — list active orders * [`getOrder`](./getOrder) ## dexStatus Read whether the DEX is currently paused. :::warning[DEX currently disabled] The Dango DEX is currently disabled. Calls described on this page will not execute on the live network until the DEX is enabled. ::: ### Signature ```ts function dexStatus( client: Client, parameters?: { height?: number }, ): Promise ``` ### Example ```ts import { createPublicClient, createTransport, testnet } from "@left-curve/sdk" const client = createPublicClient({ chain: testnet, transport: createTransport() }) const paused = await client.dexStatus() ``` ### Parameters **`height`** — `number`, optional. Block height. Default `0` (latest). ### Returns **`boolean`** — `true` if the DEX is paused. ### See also * [`batchUpdateOrders`](./batchUpdateOrders) * [`swapExactAmountIn`](./swapExactAmountIn) ## getAllPairStats Read 24h stats for every trading pair. :::warning[DEX currently disabled] The Dango DEX is currently disabled. Calls described on this page will not execute on the live network until the DEX is enabled. ::: ### Signature ```ts function getAllPairStats(client: Client): Promise ``` ### Example ```ts import { createPublicClient, createTransport, testnet } from "@left-curve/sdk" const client = createPublicClient({ chain: testnet, transport: createTransport() }) const stats = await client.getAllPairStats() ``` ### Returns **`PairStats[]`** — one `{ baseDenom, quoteDenom, currentPrice, price24HAgo, volume24H, priceChange24H }` per pair. ### See also * [`getPairStats`](./getPairStats) — single-pair variant * [`allPairStatsSubscription`](../indexer/allPairStatsSubscription) — live updates ## getOrder Read the details of a single active limit order by id. :::warning[DEX currently disabled] The Dango DEX is currently disabled. Calls described on this page will not execute on the live network until the DEX is enabled. ::: ### Signature ```ts function getOrder( client: Client, parameters: { orderId: OrderId height?: number }, ): Promise ``` ### Example ```ts import { createPublicClient, createTransport, testnet } from "@left-curve/sdk" const client = createPublicClient({ chain: testnet, transport: createTransport() }) const order = await client.getOrder({ orderId: "12345" }) ``` ### Parameters **`orderId`** — `OrderId`. The order id. **`height`** — `number`, optional. Block height. Default `0` (latest). ### Returns **`OrderResponse`** — `{ user, baseDenom, quoteDenom, direction, price, amount, remaining }`. ### See also * [`ordersByUser`](./ordersByUser) * [`batchUpdateOrders`](./batchUpdateOrders) ## getPair Read the parameters of a single base/quote trading pair. :::warning[DEX currently disabled] The Dango DEX is currently disabled. Calls described on this page will not execute on the live network until the DEX is enabled. ::: ### Signature ```ts function getPair( client: Client, parameters: { baseDenom: Denom quoteDenom: Denom height?: number }, ): Promise ``` ### Example ```ts import { createPublicClient, createTransport, testnet } from "@left-curve/sdk" const client = createPublicClient({ chain: testnet, transport: createTransport() }) const params = await client.getPair({ baseDenom: "dango", quoteDenom: "bridge/usdc", }) ``` ### Parameters **`baseDenom`** — `Denom`. Base asset. **`quoteDenom`** — `Denom`. Quote asset. **`height`** — `number`, optional. Block height. Default `0` (latest). ### Returns **`PairParams`** — `{ lpDenom, curveInvariant, swapFeeRate, bucketSizes, minOrderSizeBase, minOrderSizeQuote }`. ### See also * [`getPairs`](./getPairs) — paginated list * [`getPairStats`](./getPairStats) — 24h indexer stats ## getPairStats Read 24h indexer-computed stats for a single pair: current price, 24h-ago price, 24h volume, and 24h change. :::warning[DEX currently disabled] The Dango DEX is currently disabled. Calls described on this page will not execute on the live network until the DEX is enabled. ::: ### Signature ```ts function getPairStats( client: Client, parameters: { baseDenom: string quoteDenom: string }, ): Promise ``` ### Example ```ts import { createPublicClient, createTransport, testnet } from "@left-curve/sdk" const client = createPublicClient({ chain: testnet, transport: createTransport() }) const stats = await client.getPairStats({ baseDenom: "dango", quoteDenom: "bridge/usdc", }) ``` ### Parameters **`baseDenom`** — `string`. Base asset denom. **`quoteDenom`** — `string`. Quote asset denom. ### Returns **`PairStats`** — `{ baseDenom, quoteDenom, currentPrice, price24HAgo, volume24H, priceChange24H }`. ### Notes * Sourced from the indexer's GraphQL schema, not the on-chain DEX contract. Indexer lag may apply. ### See also * [`getAllPairStats`](./getAllPairStats) * [`allPairStatsSubscription`](../indexer/allPairStatsSubscription) ## getPairs Enumerate all trading pairs and their parameters, paginated. :::warning[DEX currently disabled] The Dango DEX is currently disabled. Calls described on this page will not execute on the live network until the DEX is enabled. ::: ### Signature ```ts function getPairs( client: Client, parameters?: { limit?: number startAfter?: PairId height?: number }, ): Promise ``` ### Example ```ts import { createPublicClient, createTransport, testnet } from "@left-curve/sdk" const client = createPublicClient({ chain: testnet, transport: createTransport() }) const pairs = await client.getPairs({ limit: 50 }) ``` ### Parameters **`limit`** — `number`, optional. Maximum pairs to return. **`startAfter`** — `PairId`, optional. `{ baseDenom, quoteDenom }` to start after. **`height`** — `number`, optional. Block height. Default `0` (latest). ### Returns **`PairUpdate[]`** — array of `{ baseDenom, quoteDenom, params }`. ### See also * [`getPair`](./getPair) * [`getAllPairStats`](./getAllPairStats) ## ordersByUser List a user's active orders across all pairs. :::warning[DEX currently disabled] The Dango DEX is currently disabled. Calls described on this page will not execute on the live network until the DEX is enabled. ::: ### Signature ```ts function ordersByUser( client: Client, parameters: { user: Address startAfter?: OrderId limit?: number height?: number }, ): Promise> ``` ### Example ```ts import { createPublicClient, createTransport, testnet } from "@left-curve/sdk" import type { Address } from "@left-curve/sdk" const client = createPublicClient({ chain: testnet, transport: createTransport() }) const user: Address = "0x1234567890abcdef1234567890abcdef12345678" const orders = await client.ordersByUser({ user, limit: 100 }) ``` ### Parameters **`user`** — `Address`. The user to query. **`startAfter`** — `OrderId`, optional. Order id to start after (exclusive). **`limit`** — `number`, optional. Maximum entries to return. **`height`** — `number`, optional. Block height. Default `0` (latest). ### Returns **`Record`** — keyed by order id; each value is `{ baseDenom, quoteDenom, direction, price, amount, remaining }`. ### See also * [`getOrder`](./getOrder) * [`batchUpdateOrders`](./batchUpdateOrders) ## provideLiquidity Add liquidity to a passive pool. Unbalanced provision is equivalent to an internal swap to reach pool ratio, followed by a balanced provision. :::warning[DEX currently disabled] The Dango DEX is currently disabled. Calls described on this page will not execute on the live network until the DEX is enabled. ::: ### Signature ```ts function provideLiquidity( client: Client, parameters: { sender: Address baseDenom: Denom quoteDenom: Denom funds: Coins }, ): Promise<{ hash: Uint8Array } & TxData> ``` ### Example ```ts import { createSignerClient, createTransport, testnet, PrivateKeySigner } from "@left-curve/sdk" import type { Address } from "@left-curve/sdk" const client = createSignerClient({ chain: testnet, transport: createTransport(), signer: PrivateKeySigner.fromMnemonic(process.env.DANGO_MNEMONIC!), }) const sender: Address = "0x1234567890abcdef1234567890abcdef12345678" await client.provideLiquidity({ sender, baseDenom: "dango", quoteDenom: "bridge/usdc", funds: { dango: "1000000000", "bridge/usdc": "500000000", }, }) ``` ### Parameters **`sender`** — `Address`. The LP. **`baseDenom`** — `Denom`. Base asset of the pair. **`quoteDenom`** — `Denom`. Quote asset of the pair. **`funds`** — `Coins`. Coins to deposit. May include only base, only quote, or both. ### Returns **`{ hash: Uint8Array } & TxData`** — see [`broadcastTxSync`](../app/broadcastTxSync). ### See also * [`withdrawLiquidity`](./withdrawLiquidity) * [`simulateWithdrawLiquidity`](./simulateWithdrawLiquidity) ## queryCandles Read historical candles for a pair at a given interval. Paginated cursor-style. :::warning[DEX currently disabled] The Dango DEX is currently disabled. Calls described on this page will not execute on the live network until the DEX is enabled. ::: ### Signature ```ts function queryCandles( client: Client, parameters: { baseDenom: string quoteDenom: string interval: CandleIntervals after?: string first?: number earlierThan?: DateTime laterThan?: DateTime }, ): Promise> ``` ### Example ```ts import { createPublicClient, createTransport, testnet } from "@left-curve/sdk" const client = createPublicClient({ chain: testnet, transport: createTransport() }) const page = await client.queryCandles({ baseDenom: "dango", quoteDenom: "bridge/usdc", interval: "ONE_MINUTE", first: 100, }) for (const candle of page.nodes) { console.log(candle.timeStart, candle.close) } ``` ### Parameters **`baseDenom`** — `string`. Base asset. **`quoteDenom`** — `string`. Quote asset. **`interval`** — `CandleIntervals`. One of `ONE_SECOND`, `ONE_MINUTE`, `FIVE_MINUTES`, `FIFTEEN_MINUTES`, `ONE_HOUR`, `FOUR_HOURS`, `ONE_DAY`, `ONE_WEEK`. **`after`** — `string`, optional. Cursor for the next page. **`first`** — `number`, optional. Page size. **`earlierThan`** — `DateTime`, optional. ISO 8601 upper bound. **`laterThan`** — `DateTime`, optional. ISO 8601 lower bound. ### Returns **`GraphqlQueryResult`** — `{ pageInfo, nodes }`. Each `Candle` has OHLC, volumes, and time fields. ### See also * [`candlesSubscription`](../indexer/candlesSubscription) — live updates * [`queryPerpsCandles`](../perps/queryPerpsCandles) — perps equivalent ## queryTrades Read historical trades, optionally filtered by address. Paginated cursor-style. :::warning[DEX currently disabled] The Dango DEX is currently disabled. Calls described on this page will not execute on the live network until the DEX is enabled. ::: ### Signature ```ts function queryTrades( client: Client, parameters: { after?: string first?: number address?: Address }, ): Promise> ``` ### Example ```ts import { createPublicClient, createTransport, testnet } from "@left-curve/sdk" import type { Address } from "@left-curve/sdk" const client = createPublicClient({ chain: testnet, transport: createTransport() }) const user: Address = "0x1234567890abcdef1234567890abcdef12345678" const page = await client.queryTrades({ address: user, first: 50 }) ``` ### Parameters **`after`** — `string`, optional. Cursor for the next page. **`first`** — `number`, optional. Page size. **`address`** — `Address`, optional. Filter by trader. ### Returns **`GraphqlQueryResult`** — `{ pageInfo, nodes }`. Each `Trade` has `addr`, denoms, direction, clearing price, and fees. ### See also * [`tradesSubscription`](../indexer/tradesSubscription) — live updates * [`queryCandles`](./queryCandles) ## simulateSwapExactAmountIn Quote the output of a swap given an exact input amount and a route. :::warning[DEX currently disabled] The Dango DEX is currently disabled. Calls described on this page will not execute on the live network until the DEX is enabled. ::: ### Signature ```ts function simulateSwapExactAmountIn( client: Client, parameters: { input: Coin route: SwapRoute height?: number }, ): Promise ``` ### Example ```ts import { createPublicClient, createTransport, testnet } from "@left-curve/sdk" const client = createPublicClient({ chain: testnet, transport: createTransport() }) const quote = await client.simulateSwapExactAmountIn({ input: { denom: "dango", amount: "1000000000" }, route: [{ baseDenom: "dango", quoteDenom: "bridge/usdc" }], }) console.log(quote.amount) ``` ### Parameters **`input`** — `Coin`. Exact input `{ denom, amount }` in base units. **`route`** — `SwapRoute`. Array of `PairId` hops. **`height`** — `number`, optional. Block height. Default `0` (latest). ### Returns **`Coin`** — the simulated output `{ denom, amount }`. ### See also * [`swapExactAmountIn`](./swapExactAmountIn) — submit the swap * [`simulateSwapExactAmountOut`](./simulateSwapExactAmountOut) ## simulateSwapExactAmountOut Quote the input needed for a swap given an exact output amount and a route. :::warning[DEX currently disabled] The Dango DEX is currently disabled. Calls described on this page will not execute on the live network until the DEX is enabled. ::: ### Signature ```ts function simulateSwapExactAmountOut( client: Client, parameters: { output: Coin route: SwapRoute height?: number }, ): Promise ``` ### Example ```ts import { createPublicClient, createTransport, testnet } from "@left-curve/sdk" const client = createPublicClient({ chain: testnet, transport: createTransport() }) const quote = await client.simulateSwapExactAmountOut({ output: { denom: "bridge/usdc", amount: "100000000" }, route: [{ baseDenom: "dango", quoteDenom: "bridge/usdc" }], }) console.log(quote.amount) ``` ### Parameters **`output`** — `Coin`. Exact output `{ denom, amount }` in base units. **`route`** — `SwapRoute`. Array of `PairId` hops. **`height`** — `number`, optional. Block height. Default `0` (latest). ### Returns **`Coin`** — the simulated input `{ denom, amount }`. ### See also * [`swapExactAmountOut`](./swapExactAmountOut) * [`simulateSwapExactAmountIn`](./simulateSwapExactAmountIn) ## simulateWithdrawLiquidity Simulate burning a given amount of LP tokens. Returns the base/quote coins the burn would yield. :::warning[DEX currently disabled] The Dango DEX is currently disabled. Calls described on this page will not execute on the live network until the DEX is enabled. ::: ### Signature ```ts function simulateWithdrawLiquidity( client: Client, parameters: { baseDenom: Denom quoteDenom: Denom lpBurnAmount: string height?: number }, ): Promise ``` ### Example ```ts import { createPublicClient, createTransport, testnet } from "@left-curve/sdk" const client = createPublicClient({ chain: testnet, transport: createTransport() }) const [base, quote] = await client.simulateWithdrawLiquidity({ baseDenom: "dango", quoteDenom: "bridge/usdc", lpBurnAmount: "1000000", }) ``` ### Parameters **`baseDenom`** — `Denom`. Base asset of the pair. **`quoteDenom`** — `Denom`. Quote asset of the pair. **`lpBurnAmount`** — `string`. Amount of LP tokens to burn, base units. **`height`** — `number`, optional. Block height. Default `0` (latest). ### Returns **`CoinPair`** — `[Coin, Coin]` tuple of `[baseCoin, quoteCoin]`. ### See also * [`withdrawLiquidity`](./withdrawLiquidity) * [`provideLiquidity`](./provideLiquidity) ## swapExactAmountIn Submit an instant swap with an exact input amount and an optional minimum output for slippage control. :::warning[DEX currently disabled] The Dango DEX is currently disabled. Calls described on this page will not execute on the live network until the DEX is enabled. ::: ### Signature ```ts function swapExactAmountIn( client: Client, parameters: { sender: Address route: SwapRoute input: Coin minimumOutput?: string }, ): Promise<{ hash: Uint8Array } & TxData> ``` ### Example ```ts import { createSignerClient, createTransport, testnet, PrivateKeySigner } from "@left-curve/sdk" import type { Address } from "@left-curve/sdk" const client = createSignerClient({ chain: testnet, transport: createTransport(), signer: PrivateKeySigner.fromMnemonic(process.env.DANGO_MNEMONIC!), }) const sender: Address = "0x1234567890abcdef1234567890abcdef12345678" await client.swapExactAmountIn({ sender, input: { denom: "dango", amount: "1000000000" }, route: [{ baseDenom: "dango", quoteDenom: "bridge/usdc" }], minimumOutput: "490000000", }) ``` ### Parameters **`sender`** — `Address`. The trader. **`route`** — `SwapRoute`. Array of `PairId` hops. **`input`** — `Coin`. Exact input `{ denom, amount }` in base units. The denom must be the base or quote of the first pair. **`minimumOutput`** — `string`, optional. Minimum output amount in base units. The swap reverts if the realized output is below this. ### Returns **`{ hash: Uint8Array } & TxData`** — see [`broadcastTxSync`](../app/broadcastTxSync). ### See also * [`simulateSwapExactAmountIn`](./simulateSwapExactAmountIn) — quote first * [`swapExactAmountOut`](./swapExactAmountOut) ## swapExactAmountOut Submit an instant swap with an exact output amount. Excess input is refunded. :::warning[DEX currently disabled] The Dango DEX is currently disabled. Calls described on this page will not execute on the live network until the DEX is enabled. ::: ### Signature ```ts function swapExactAmountOut( client: Client, parameters: { sender: Address route: SwapRoute output: Coin input: Coin }, ): Promise<{ hash: Uint8Array } & TxData> ``` ### Example ```ts import { createSignerClient, createTransport, testnet, PrivateKeySigner } from "@left-curve/sdk" import type { Address } from "@left-curve/sdk" const client = createSignerClient({ chain: testnet, transport: createTransport(), signer: PrivateKeySigner.fromMnemonic(process.env.DANGO_MNEMONIC!), }) const sender: Address = "0x1234567890abcdef1234567890abcdef12345678" await client.swapExactAmountOut({ sender, input: { denom: "dango", amount: "2100000000" }, // generous bound output: { denom: "bridge/usdc", amount: "100000000" }, route: [{ baseDenom: "dango", quoteDenom: "bridge/usdc" }], }) ``` ### Parameters **`sender`** — `Address`. The trader. **`route`** — `SwapRoute`. Array of `PairId` hops. **`output`** — `Coin`. Exact output `{ denom, amount }`. **`input`** — `Coin`. Maximum input to send. Unused input is refunded. ### Returns **`{ hash: Uint8Array } & TxData`** — see [`broadcastTxSync`](../app/broadcastTxSync). ### See also * [`simulateSwapExactAmountOut`](./simulateSwapExactAmountOut) — quote first * [`swapExactAmountIn`](./swapExactAmountIn) ## withdrawLiquidity Withdraw passive liquidity from a pair by burning LP tokens. The redemption happens at the pool ratio. :::warning[DEX currently disabled] The Dango DEX is currently disabled. Calls described on this page will not execute on the live network until the DEX is enabled. ::: ### Signature ```ts function withdrawLiquidity( client: Client, parameters: { sender: Address baseDenom: Denom quoteDenom: Denom funds: Coins }, ): Promise<{ hash: Uint8Array } & TxData> ``` ### Example ```ts import { createSignerClient, createTransport, testnet, PrivateKeySigner } from "@left-curve/sdk" import type { Address } from "@left-curve/sdk" const client = createSignerClient({ chain: testnet, transport: createTransport(), signer: PrivateKeySigner.fromMnemonic(process.env.DANGO_MNEMONIC!), }) const sender: Address = "0x1234567890abcdef1234567890abcdef12345678" await client.withdrawLiquidity({ sender, baseDenom: "dango", quoteDenom: "bridge/usdc", funds: { "lp/dango/bridge_usdc": "1000000" }, }) ``` ### Parameters **`sender`** — `Address`. The LP. **`baseDenom`** — `Denom`. Base asset. **`quoteDenom`** — `Denom`. Quote asset. **`funds`** — `Coins`. The LP token to burn (lookup `lpDenom` via [`getPair`](./getPair)). ### Returns **`{ hash: Uint8Array } & TxData`** — see [`broadcastTxSync`](../app/broadcastTxSync). ### See also * [`simulateWithdrawLiquidity`](./simulateWithdrawLiquidity) — quote first * [`provideLiquidity`](./provideLiquidity) ## broadcastTxSync Broadcast a signed transaction via the indexer mutation and poll for inclusion. ### Signature ```ts function broadcastTxSync( client: Client, parameters: { tx: Tx | UnsignedTx }, ): Promise<{ hash: Uint8Array } & TxData> ``` ### Example ```ts import { createSignerClient, createTransport, testnet, PrivateKeySigner } from "@left-curve/sdk" const client = createSignerClient({ chain: testnet, transport: createTransport(), signer: PrivateKeySigner.fromMnemonic(process.env.DANGO_MNEMONIC!), }) // Most callers use signAndBroadcastTx instead, which builds and signs the tx. const result = await client.broadcastTxSync({ tx: { sender: "0x1234567890abcdef1234567890abcdef12345678", msgs: [{ transfer: { "0xabcdef...": { dango: "1000000" } } }], gasLimit: 200_000, credential: { /* ... */ }, data: { /* metadata */ }, }, }) ``` ### Parameters **`tx`** — `Tx | UnsignedTx`. A signed transaction. `UnsignedTx` is accepted but the chain will reject it unless the sender is a contract address. ### Returns **`{ hash: Uint8Array } & TxData`** — the transaction hash plus `TxData` fields (`code`, `gas_used`, `gas_wanted`, `log`). ### Notes * After broadcasting, the function polls `queryTx` up to 30 times at 500ms intervals (\~15 seconds). It throws if the tx never lands or if `tx_result.code !== 0`. * For end-to-end signing, use [`signAndBroadcastTx`](./signAndBroadcastTx) — it builds the metadata, simulates gas, signs, and broadcasts in one call. ### See also * [`signAndBroadcastTx`](./signAndBroadcastTx) — the typical entry point * [`queryTx`](./queryTx) — fetch a tx by hash * [Concepts: Transactions](../../concepts/transactions) ## configure Update the chain config, the app config, or both. Only the chain owner can submit. ### Signature ```ts function configure( client: Client, parameters: { sender: Address newCfg?: ChainConfig newAppCfg?: Json }, ): Promise<{ hash: Uint8Array } & TxData> ``` ### Example ```ts import { configure } from "@left-curve/sdk" import { createSignerClient, createTransport, testnet, PrivateKeySigner } from "@left-curve/sdk" import type { Address } from "@left-curve/sdk" const client = createSignerClient({ chain: testnet, transport: createTransport(), signer: PrivateKeySigner.fromMnemonic(process.env.DANGO_MNEMONIC!), }) const sender: Address = "0x1234567890abcdef1234567890abcdef12345678" await configure(client, { sender, newCfg: { owner: "0xowner...", bank: "0xbank...", taxman: "0xtax...", cronjobs: {}, permissions: { upload: "everybody", instantiate: "everybody" }, maxOrphanAge: 86400, }, }) ``` ### Parameters **`sender`** — `Address`. Must equal the chain owner. **`newCfg`** — `ChainConfig`, optional. New chain-level config. **`newAppCfg`** — `Json`, optional. New app-level config (matches `AppConfig`). ### Returns **`{ hash: Uint8Array } & TxData`** — see [`broadcastTxSync`](./broadcastTxSync). ### Notes * Not bundled into `appMutationActions`. Call as a free function: `configure(client, {...})`. ### See also * [`getAppConfig`](./getAppConfig) * [`upgrade`](./upgrade) ## execute Execute one or more contract messages in a single transaction, with optional funds attached and EIP-712 typed-data overrides. ### Signature ```ts function execute( client: Client, parameters: { sender: Address execute: ExecuteMsg | ExecuteMsg[] gasLimit?: number }, ): Promise<{ hash: Uint8Array } & TxData> type ExecuteMsg = { contract: Address msg: Json funds?: Funds typedData?: TypedDataParameter } ``` ### Example ```ts import { createSignerClient, createTransport, testnet, PrivateKeySigner } from "@left-curve/sdk" import type { Address } from "@left-curve/sdk" const client = createSignerClient({ chain: testnet, transport: createTransport(), signer: PrivateKeySigner.fromMnemonic(process.env.DANGO_MNEMONIC!), }) const sender: Address = "0x1234567890abcdef1234567890abcdef12345678" const contract: Address = "0xabcdef1234567890abcdef1234567890abcdef12" await client.execute({ sender, execute: { contract, msg: { increment: {} }, }, }) ``` ### Parameters **`sender`** — `Address`. The sender account. **`execute`** — `ExecuteMsg | ExecuteMsg[]`. One or more `{ contract, msg, funds?, typedData? }` entries. **`gasLimit`** — `number`, optional. Skip simulation and use this limit. #### `ExecuteMsg` fields **`contract`** — `Address`. The target contract. **`msg`** — `Json`. The execute message (camelCase, converted to snake\_case on the wire). **`funds`** — `Funds`, optional. Coins attached to the call. **`typedData`** — `TypedDataParameter`, optional. EIP-712 typed-data spec for the message body. Required when the message has non-trivial nested structure. ### Returns **`{ hash: Uint8Array } & TxData`** — see [`broadcastTxSync`](./broadcastTxSync). ### See also * [`instantiate`](./instantiate) — execute is for already-deployed contracts * [`signAndBroadcastTx`](./signAndBroadcastTx) ## getAppConfig Fetch the app-level configuration: contract addresses, fee rates, liquidation parameters, and minimum deposit per denom. ### Signature ```ts function getAppConfig( client: Client, parameters?: { height?: number }, ): Promise ``` ### Example ```ts import { createPublicClient, createTransport, testnet } from "@left-curve/sdk" const client = createPublicClient({ chain: testnet, transport: createTransport() }) const config = await client.getAppConfig() console.log(config.addresses.accountFactory) ``` ### Parameters **`height`** — `number`, optional. Block height to query at. Default `0` (latest). ### Returns **`AppConfig`** — the full app config. Notable fields: * `addresses` — addresses of every system contract (account factory, dex, perps, gateway, oracle, taxman, warp, hyperlane). * `makerFeeRate`, `takerFeeRate` — fee strings. * `maxLiquidationBonus`, `minLiquidationBonus`, `targetUtilizationRate`. * `minimumDeposit` — `Record`. ### Notes :::warning `getAppConfig` memoizes the result in a module-level cache. The first successful call wins; subsequent calls ignore the `height` argument and return the cached config. If you need to read the config at multiple heights, use [`queryApp`](./queryApp) directly. ::: ### See also * [`queryApp`](./queryApp) — the underlying generic query * [Concepts: Transactions](../../concepts/transactions) — how `signAndBroadcastTx` consumes the addresses ## getBalance Read the balance of a single denom for an address. ### Signature ```ts function getBalance( client: Client, parameters: { address: Address denom: Denom height?: number }, ): Promise ``` ### Example ```ts import { createPublicClient, createTransport, testnet } from "@left-curve/sdk" import { Decimal } from "@left-curve/utils" import type { Address } from "@left-curve/sdk" const client = createPublicClient({ chain: testnet, transport: createTransport() }) const address: Address = "0x1234567890abcdef1234567890abcdef12345678" const amount = await client.getBalance({ address, denom: "dango" }) // Parse with Decimal or BigInt for arithmetic const human = Decimal(amount).div(Decimal(10).pow(6)) // assuming 6 decimals ``` ### Parameters **`address`** — `Address`. The account to query. **`denom`** — `Denom`. The token denomination. **`height`** — `number`, optional. Block height to query at. Default `0` (latest). ### Returns **`string`** — the balance in base units, exactly as returned by the chain. Parse with `BigInt` or [`Decimal`](../../concepts/encoding-and-types) before doing arithmetic. ### See also * [`getBalances`](./getBalances) — all balances for an address as a `Coins` record * [`getSupply`](./getSupply) — total supply of one denom * [Concepts: Encoding & Types](../../concepts/encoding-and-types) ## getBalances Read a paginated map of every balance held by an address. ### Signature ```ts function getBalances( client: Client, parameters: { address: Address startAfter?: Denom limit?: number height?: number }, ): Promise ``` ### Example ```ts import { createPublicClient, createTransport, testnet } from "@left-curve/sdk" import type { Address } from "@left-curve/sdk" const client = createPublicClient({ chain: testnet, transport: createTransport() }) const address: Address = "0x1234567890abcdef1234567890abcdef12345678" const balances = await client.getBalances({ address, limit: 50 }) // { dango: "1500000000", "bridge/usdc": "1000000", ... } ``` ### Parameters **`address`** — `Address`. The account to query. **`startAfter`** — `Denom`, optional. Denom to start after (exclusive). Use the last denom from the previous page. **`limit`** — `number`, optional. Maximum entries to return. **`height`** — `number`, optional. Block height to query at. Default `0` (latest). ### Returns **`Coins`** — `Record`. Each value is base units as a string. Safe for arbitrary precision. ### See also * [`getBalance`](./getBalance) — single-denom variant * [`getSupplies`](./getSupplies) — chain-wide supplies * [Concepts: Encoding & Types](../../concepts/encoding-and-types) ## getCode Fetch a stored Wasm code blob by its hash. ### Signature ```ts function getCode( client: Client, parameters: { hash: Hex height?: number }, ): Promise ``` ### Example ```ts import { createPublicClient, createTransport, testnet } from "@left-curve/sdk" const client = createPublicClient({ chain: testnet, transport: createTransport() }) const code = await client.getCode({ hash: "0xabc123..." }) ``` ### Parameters **`hash`** — `Hex`. SHA-256 hash of the Wasm bytecode. **`height`** — `number`, optional. Block height to query at. Default `0` (latest). ### Returns **`CodeResponse`** — alias for `Code`. Contains the bytecode and status fields. ### See also * [`getCodes`](./getCodes) — paginated list of stored codes * [`storeCode`](./storeCode) — upload a new code ## getCodes List stored Wasm codes, paginated. ### Signature ```ts function getCodes( client: Client, parameters?: { startAfter?: string limit?: number height?: number }, ): Promise ``` ### Example ```ts import { createPublicClient, createTransport, testnet } from "@left-curve/sdk" const client = createPublicClient({ chain: testnet, transport: createTransport() }) const codes = await client.getCodes({ limit: 50 }) ``` ### Parameters **`startAfter`** — `string`, optional. Hash to start after (exclusive). **`limit`** — `number`, optional. Maximum entries to return. **`height`** — `number`, optional. Block height to query at. Default `0` (latest). ### Returns **`CodesResponse`** — `Record` keyed by hash. ### See also * [`getCode`](./getCode) — single-hash variant * [`storeCode`](./storeCode) ## getContractInfo Fetch a contract's metadata (code hash, label, admin) by address. ### Signature ```ts function getContractInfo( client: Client, parameters: { address: Address height?: number }, ): Promise ``` ### Example ```ts import { createPublicClient, createTransport, testnet } from "@left-curve/sdk" import type { Address } from "@left-curve/sdk" const client = createPublicClient({ chain: testnet, transport: createTransport() }) const contract: Address = "0x1234567890abcdef1234567890abcdef12345678" const info = await client.getContractInfo({ address: contract }) console.log(info.codeHash, info.admin) ``` ### Parameters **`address`** — `Address`. The contract to query. **`height`** — `number`, optional. Block height to query at. Default `0` (latest). ### Returns **`ContractInfo`** — `{ codeHash: Hex, label?: string, admin?: Address }`. ### See also * [`getContractsInfo`](./getContractsInfo) — paginated map across contracts * [`instantiate`](./instantiate) ## getContractsInfo List contract metadata across the chain, paginated. ### Signature ```ts function getContractsInfo( client: Client, parameters?: { startAfter?: Address limit?: number height?: number }, ): Promise ``` ### Example ```ts import { createPublicClient, createTransport, testnet } from "@left-curve/sdk" const client = createPublicClient({ chain: testnet, transport: createTransport() }) const contracts = await client.getContractsInfo({ limit: 100 }) ``` ### Parameters **`startAfter`** — `Address`, optional. Address to start after (exclusive). **`limit`** — `number`, optional. Maximum entries to return. **`height`** — `number`, optional. Block height to query at. Default `0` (latest). ### Returns **`ContractsResponse`** — `Record`. ### See also * [`getContractInfo`](./getContractInfo) — single-address variant ## getSupplies Read a paginated map of total supplies across denoms. ### Signature ```ts function getSupplies( client: Client, parameters?: { startAfter?: Denom limit?: number height?: number }, ): Promise ``` ### Example ```ts import { createPublicClient, createTransport, testnet } from "@left-curve/sdk" const client = createPublicClient({ chain: testnet, transport: createTransport() }) const supplies = await client.getSupplies({ limit: 100 }) ``` ### Parameters **`startAfter`** — `Denom`, optional. Denom to start after (exclusive). **`limit`** — `number`, optional. Maximum entries to return. **`height`** — `number`, optional. Block height to query at. Default `0` (latest). ### Returns **`Coins`** — `Record` of supplies. ### See also * [`getSupply`](./getSupply) — single-denom variant * [`getBalances`](./getBalances) ## getSupply Read the total supply of a single token. ### Signature ```ts function getSupply( client: Client, parameters: { denom: Denom height?: number }, ): Promise ``` ### Example ```ts import { createPublicClient, createTransport, testnet } from "@left-curve/sdk" const client = createPublicClient({ chain: testnet, transport: createTransport() }) const supply = await client.getSupply({ denom: "dango" }) console.log(supply.amount) ``` ### Parameters **`denom`** — `Denom`. The token denomination. **`height`** — `number`, optional. Block height to query at. Default `0` (latest). ### Returns **`Coin`** — `{ denom: Denom, amount: string }`. ### See also * [`getSupplies`](./getSupplies) — paginated supplies across denoms * [`getBalance`](./getBalance) ## instantiate Instantiate a contract from an already-uploaded code hash. Returns the derived address and the broadcast receipt. ### Signature ```ts function instantiate( client: Client, parameters: { sender: Address codeHash: Hex msg: Json salt: Uint8Array | string funds?: Funds admin?: Address gasLimit?: number typedData?: TypedDataParameter }, ): Promise<[string, { hash: Uint8Array } & TxData]> ``` ### Example ```ts import { createSignerClient, createTransport, testnet, PrivateKeySigner } from "@left-curve/sdk" import type { Address } from "@left-curve/sdk" const client = createSignerClient({ chain: testnet, transport: createTransport(), signer: PrivateKeySigner.fromMnemonic(process.env.DANGO_MNEMONIC!), }) const sender: Address = "0x1234567890abcdef1234567890abcdef12345678" const [address, receipt] = await client.instantiate({ sender, codeHash: "0xabc...123", msg: { initialValue: "0" }, salt: "counter-v1", }) ``` ### Parameters **`sender`** — `Address`. The deployer. **`codeHash`** — `Hex`. SHA-256 hash of the Wasm code (already stored via [`storeCode`](./storeCode)). **`msg`** — `Json`. Init message (camelCase). **`salt`** — `Uint8Array | string`. Deterministic salt; strings are UTF-8 encoded. The contract address is derived from `(sender, codeHash, salt)`. **`funds`** — `Funds`, optional. Coins to send into the new contract. **`admin`** — `Address`, optional. Account allowed to migrate the contract. **`gasLimit`** — `number`, optional. Override simulation. **`typedData`** — `TypedDataParameter`, optional. Override the EIP-712 message body schema. ### Returns **`[address, receipt]`** — the derived contract address (from [`computeAddress`](../../concepts/encoding-and-types)) and the broadcast receipt. ### See also * [`storeCode`](./storeCode) — upload before instantiating * [`storeCodeAndInstantiate`](./storeCodeAndInstantiate) — single-tx variant * [`migrate`](./migrate) ## migrate Migrate a contract to a new code hash. Only the contract's admin can call this. ### Signature ```ts function migrate( client: Client, parameters: { sender: Address contract: Address newCodeHash: Hex msg: Json typedData?: TypedDataParameter }, ): Promise<{ hash: Uint8Array } & TxData> ``` ### Example ```ts import { createSignerClient, createTransport, testnet, PrivateKeySigner } from "@left-curve/sdk" import type { Address } from "@left-curve/sdk" const client = createSignerClient({ chain: testnet, transport: createTransport(), signer: PrivateKeySigner.fromMnemonic(process.env.DANGO_MNEMONIC!), }) const sender: Address = "0x1234567890abcdef1234567890abcdef12345678" const contract: Address = "0xabcdef1234567890abcdef1234567890abcdef12" await client.migrate({ sender, contract, newCodeHash: "0xdef...456", msg: { migrate: {} }, }) ``` ### Parameters **`sender`** — `Address`. Must equal the contract's admin. **`contract`** — `Address`. The contract to migrate. **`newCodeHash`** — `Hex`. Hash of the new Wasm code (already stored). **`msg`** — `Json`. Migrate message (camelCase). **`typedData`** — `TypedDataParameter`, optional. Override the EIP-712 body schema. ### Returns **`{ hash: Uint8Array } & TxData`** — see [`broadcastTxSync`](./broadcastTxSync). ### See also * [`instantiate`](./instantiate) * [`getContractInfo`](./getContractInfo) — read the current admin ## queryApp Send a generic typed query against the app and receive the matching `QueryResponse` variant. ### Signature ```ts function queryApp( client: Client, parameters: { query: Json height?: number }, ): Promise ``` ### Example ```ts import { createPublicClient, createTransport, testnet } from "@left-curve/sdk" import type { Address } from "@left-curve/sdk" const client = createPublicClient({ chain: testnet, transport: createTransport() }) const address: Address = "0x1234567890abcdef1234567890abcdef12345678" const res = await client.queryApp({ query: { balance: { address, denom: "dango" } }, }) if ("balance" in res) { console.log(res.balance.amount) } ``` ### Parameters **`query`** — `Json`. A `QueryRequest` variant — e.g. `{ balance: { address, denom } }`, `{ contracts: {...} }`, `{ wasmSmart: {...} }`. **`height`** — `number`, optional. Block height to query at. Default `0` (latest). ### Returns **`QueryResponse`** — a discriminated union. Narrow with the `in` operator to extract the matching variant. ### Notes * Most callers use the typed wrappers (`getBalance`, `getContractInfo`, etc.). Use `queryApp` directly only for variants that lack a wrapper or for batched `multi` queries. * The SDK does not auto-narrow the response — you must check the variant name. ### See also * [`queryWasmSmart`](./queryWasmSmart) — typed wrapper for `wasmSmart` queries * [`queryStatus`](./queryStatus) — typed wrapper for chain status ## queryStatus Read the chain id and the latest finalized block. ### Signature ```ts function queryStatus(client: Client): Promise<{ chainId: string block: BlockInfo }> ``` ### Example ```ts import { createPublicClient, createTransport, testnet } from "@left-curve/sdk" const client = createPublicClient({ chain: testnet, transport: createTransport() }) const status = await client.queryStatus() console.log(status.chainId, status.block.height) ``` ### Returns **`{ chainId: string; block: BlockInfo }`** — `BlockInfo` is `{ height: string; timestamp: string; hash: string }`. ### See also * [`queryBlock`](../indexer/queryBlock) — block details with transactions * [`queryApp`](./queryApp) ## queryTx Fetch an indexed transaction by hash. ### Signature ```ts function queryTx( client: Client, parameters: { hash: Base64 }, ): Promise ``` ### Example ```ts import { createPublicClient, createTransport, testnet } from "@left-curve/sdk" const client = createPublicClient({ chain: testnet, transport: createTransport() }) const tx = await client.queryTx({ hash: "abc123base64..." }) if (tx) { console.log(tx.height, tx.tx_result.code) } ``` ### Parameters **`hash`** — `Base64`. The transaction hash. ### Returns **`TxResponse | null`** — `null` if the indexer has not seen the transaction. `tx_result.code === 0` means success; non-zero means failure. ### Notes * `signAndBroadcastTx` polls this internally up to 30 times at 500ms intervals. * The indexer may lag behind the chain by a block or two; expect `null` immediately after broadcasting. ### See also * [`broadcastTxSync`](./broadcastTxSync) — broadcast a signed tx * [`searchTxs`](../indexer/searchTxs) — paginated search ## queryWasmRaw Read a raw base64 value from a contract's storage at a specific key. ### Signature ```ts function queryWasmRaw( client: Client, parameters: { contract: Address key: Base64 height?: number }, ): Promise ``` ### Example ```ts import { createPublicClient, createTransport, testnet } from "@left-curve/sdk" import { encodeBase64, encodeUtf8 } from "@left-curve/encoding" import type { Address } from "@left-curve/sdk" const client = createPublicClient({ chain: testnet, transport: createTransport() }) const contract: Address = "0x1234567890abcdef1234567890abcdef12345678" const key = encodeBase64(encodeUtf8("config")) const value = await client.queryWasmRaw({ contract, key }) ``` ### Parameters **`contract`** — `Address`. The contract to read from. **`key`** — `Base64`. The storage key, base64-encoded. **`height`** — `number`, optional. Block height to query at. Default `0` (latest). ### Returns **`WasmRawResponse`** — `Base64 | undefined`. Undefined when no value is stored at the key. ### Notes * For typed queries, use [`queryWasmSmart`](./queryWasmSmart) instead — it serializes a JSON message and decodes the typed response. ### See also * [`queryWasmSmart`](./queryWasmSmart) — typed JSON queries * [`queryApp`](./queryApp) — generic query ## queryWasmSmart Send a typed JSON query to a smart contract and receive the decoded response. ### Signature ```ts function queryWasmSmart( client: Client, parameters: { contract: Address msg: Json height?: number }, ): Promise> ``` ### Example ```ts import { createPublicClient, createTransport, testnet } from "@left-curve/sdk" import type { Address } from "@left-curve/sdk" const client = createPublicClient({ chain: testnet, transport: createTransport() }) const contract: Address = "0x1234567890abcdef1234567890abcdef12345678" type Config = { owner: Address; fee: string } const config = await client.queryWasmSmart({ contract, msg: { config: {} }, }) ``` ### Parameters **`contract`** — `Address`. The contract to query. **`msg`** — `Json`. The query message. Always camelCase — the SDK converts to snake\_case on the wire. **`height`** — `number`, optional. Block height to query at. Default `0` (latest). ### Returns **`WasmSmartResponse`** — alias for the decoded `T`. The response is camelCase (converted from the contract's snake\_case). ### Notes * The generic type parameter is unchecked at runtime. Validate the response with a runtime schema (e.g. Zod) when the source is untrusted. * Throws if the response does not contain a `wasmSmart` field. ### See also * [`queryWasmRaw`](./queryWasmRaw) — raw storage values * [`queryApp`](./queryApp) — the generic query primitive ## signAndBroadcastTx Sign a list of messages with the client signer and broadcast the resulting transaction. The end-to-end entry point for every mutation. ### Signature ```ts function signAndBroadcastTx( client: Client, parameters: { sender: Address messages: Message[] gasLimit?: number typedData?: TypedDataParameter }, ): Promise<{ hash: Uint8Array } & TxData> ``` ### Example ```ts import { createSignerClient, createTransport, testnet, PrivateKeySigner } from "@left-curve/sdk" import type { Address } from "@left-curve/sdk" const client = createSignerClient({ chain: testnet, transport: createTransport(), signer: PrivateKeySigner.fromMnemonic(process.env.DANGO_MNEMONIC!), }) const sender: Address = "0x1234567890abcdef1234567890abcdef12345678" const result = await client.signAndBroadcastTx({ sender, messages: [ { transfer: { "0xabcdef1234567890abcdef1234567890abcdef12": { dango: "1000000" }, }, }, ], }) ``` ### Parameters **`sender`** — `Address`. Account that sends and pays for the transaction. **`messages`** — `Message[]`. The list of messages to execute (transfer, execute, instantiate, etc.). **`gasLimit`** — `number`, optional. Skip simulation and use this gas limit directly. When omitted, the SDK calls [`simulate`](./simulate) and applies the default 1.3× scaling. **`typedData`** — `TypedDataParameter`, optional. Pre-built EIP-712 typed data structure for the messages. When omitted, the action consumer builds it (every action like `transfer`, `execute` does this). ### Returns **`{ hash: Uint8Array } & TxData`** — see [`broadcastTxSync`](./broadcastTxSync). ### Notes * The client must have a signer; throws "client must have a signer" otherwise. * Calls [`getAccountSeenNonces`](../account-factory/getAccountSeenNonces) and [`getAccountInfo`](../account-factory/getAccountInfo) to derive the nonce and user index automatically. * Throws "account not found" if the sender has no on-chain account. ### See also * [`transfer`](./transfer), [`execute`](./execute), [`instantiate`](./instantiate) — higher-level wrappers * [Concepts: Transactions](../../concepts/transactions) ## simulate Gas-simulate an unsigned transaction. Returns the simulated gas usage scaled by a default multiplier. ### Signature ```ts function simulate( client: Client, parameters: { simulate: SimulateRequest scale?: number height?: number }, ): Promise ``` ### Example ```ts import { createPublicClient, createTransport, testnet } from "@left-curve/sdk" import type { Address } from "@left-curve/sdk" const client = createPublicClient({ chain: testnet, transport: createTransport() }) const sender: Address = "0x1234567890abcdef1234567890abcdef12345678" const { gasLimit, gasUsed } = await client.simulate({ simulate: { sender, msgs: [ { transfer: { "0xabcdef1234567890abcdef1234567890abcdef12": { dango: "1000000" }, }, }, ], data: null, }, }) ``` ### Parameters **`simulate`** — `SimulateRequest`. The unsigned transaction shape: `{ sender, msgs, data }`. **`scale`** — `number`, optional, default `1.3`. Multiplier applied to `gasUsed` before returning. **`height`** — `number`, optional. Block height to simulate at. Default `0` (latest). ### Returns **`SimulateResponse`** — `{ gasLimit, gasUsed }`. `gasUsed` is `Math.round(rawGasUsed * scale)`. ### Notes * The 1.3× scaling factor is the default safety margin used by `signAndBroadcastTx`. Pass `scale: 1` to see the unmodified estimate. * Simulation runs the messages against the latest state and may differ from the eventual on-chain result if the state changes between simulation and broadcast. ### See also * [`signAndBroadcastTx`](./signAndBroadcastTx) — uses `simulate` internally * [`broadcastTxSync`](./broadcastTxSync) ## storeCode Upload a Wasm code blob to the chain. ### Signature ```ts function storeCode( client: Client, parameters: { sender: Address code: Base64 }, ): Promise<{ hash: Uint8Array } & TxData> ``` ### Example ```ts import { createSignerClient, createTransport, testnet, PrivateKeySigner } from "@left-curve/sdk" import { encodeBase64 } from "@left-curve/encoding" import { readFileSync } from "node:fs" import type { Address } from "@left-curve/sdk" const client = createSignerClient({ chain: testnet, transport: createTransport(), signer: PrivateKeySigner.fromMnemonic(process.env.DANGO_MNEMONIC!), }) const sender: Address = "0x1234567890abcdef1234567890abcdef12345678" const wasm = readFileSync("./contract.wasm") await client.storeCode({ sender, code: encodeBase64(wasm) }) ``` ### Parameters **`sender`** — `Address`. The uploader. Subject to the chain's `upload` permission (`everybody`, `nobody`, or `somebodies`). **`code`** — `Base64`. Base64-encoded Wasm bytecode. ### Returns **`{ hash: Uint8Array } & TxData`** — see [`broadcastTxSync`](./broadcastTxSync). ### Notes * The code hash for later [`instantiate`](./instantiate) calls is SHA-256 of the raw bytes (the typed-data builder hashes the base64-decoded content). ### See also * [`storeCodeAndInstantiate`](./storeCodeAndInstantiate) — combined variant * [`instantiate`](./instantiate) * [`getCode`](./getCode) ## storeCodeAndInstantiate Upload a Wasm code blob and instantiate it in a single transaction. Returns the derived contract address. ### Signature ```ts function storeCodeAndInstantiate( client: Client, parameters: { sender: Address codeHash: Hex msg: Json salt: Uint8Array funds?: Funds code: Base64 admin?: Address typedData?: TypedDataParameter }, ): Promise<[string, { hash: Uint8Array } & TxData]> ``` ### Example ```ts import { createSignerClient, createTransport, testnet, PrivateKeySigner } from "@left-curve/sdk" import { encodeBase64 } from "@left-curve/encoding" import { readFileSync } from "node:fs" import { sha256 } from "@left-curve/crypto" import type { Address } from "@left-curve/sdk" const client = createSignerClient({ chain: testnet, transport: createTransport(), signer: PrivateKeySigner.fromMnemonic(process.env.DANGO_MNEMONIC!), }) const sender: Address = "0x1234567890abcdef1234567890abcdef12345678" const wasm = readFileSync("./contract.wasm") const codeHash = `0x${Buffer.from(sha256(wasm)).toString("hex")}` as `0x${string}` const [address, receipt] = await client.storeCodeAndInstantiate({ sender, code: encodeBase64(wasm), codeHash, msg: { initialValue: "0" }, salt: new TextEncoder().encode("counter-v1"), }) ``` ### Parameters **`sender`** — `Address`. The deployer. **`codeHash`** — `Hex`. SHA-256 of the Wasm. Must match the hash of the bytes you upload. **`msg`** — `Json`. Init message. **`salt`** — `Uint8Array`. Deterministic salt. **`funds`** — `Funds`, optional. Coins to send into the new contract. **`code`** — `Base64`. Base64-encoded Wasm bytecode. **`admin`** — `Address`, optional. Migration admin. **`typedData`** — `TypedDataParameter`, optional. ### Returns **`[address, receipt]`** — the derived contract address and the broadcast receipt. ### See also * [`storeCode`](./storeCode) — upload-only * [`instantiate`](./instantiate) — instantiate-only ## transfer Send a record of `Address -> Coins` from the sender's account. ### Signature ```ts function transfer( client: Client, parameters: { sender: Address transfer: Record }, ): Promise<{ hash: Uint8Array } & TxData> ``` ### Example ```ts import { createSignerClient, createTransport, testnet, PrivateKeySigner } from "@left-curve/sdk" import type { Address } from "@left-curve/sdk" const client = createSignerClient({ chain: testnet, transport: createTransport(), signer: PrivateKeySigner.fromMnemonic(process.env.DANGO_MNEMONIC!), }) const sender: Address = "0x1234567890abcdef1234567890abcdef12345678" await client.transfer({ sender, transfer: { "0xabcdef1234567890abcdef1234567890abcdef12": { dango: "1000000", "bridge/usdc": "5000000", }, }, }) ``` ### Parameters **`sender`** — `Address`. The funding account. **`transfer`** — `Record`. Map of recipient address to a `Coins` payload (`Record` of base-unit amounts). ### Returns **`{ hash: Uint8Array } & TxData`** — see [`broadcastTxSync`](./broadcastTxSync). ### Notes * A single call can send to multiple recipients; each recipient may receive multiple denoms. * Amounts are base units, always strings. ### See also * [`signAndBroadcastTx`](./signAndBroadcastTx) — the underlying primitive * [`getBalances`](./getBalances) ## upgrade Schedule a chain upgrade at a future block. Only the chain owner can submit. ### Signature ```ts function upgrade( client: Client, parameters: { sender: Address height: number cargoVersion: string gitTag?: string url?: string }, ): Promise<{ hash: Uint8Array } & TxData> ``` ### Example ```ts import { upgrade } from "@left-curve/sdk" import { createSignerClient, createTransport, testnet, PrivateKeySigner } from "@left-curve/sdk" import type { Address } from "@left-curve/sdk" const client = createSignerClient({ chain: testnet, transport: createTransport(), signer: PrivateKeySigner.fromMnemonic(process.env.DANGO_MNEMONIC!), }) const sender: Address = "0x1234567890abcdef1234567890abcdef12345678" await upgrade(client, { sender, height: 1_000_000, cargoVersion: "0.18.0", gitTag: "v0.18.0", }) ``` ### Parameters **`sender`** — `Address`. Must equal the chain owner. **`height`** — `number`. Block height at which the upgrade takes effect. **`cargoVersion`** — `string`. Expected `Cargo.toml` version of the target binary. **`gitTag`** — `string`, optional. Git tag of the release. **`url`** — `string`, optional. URL pointing to release notes or binaries. ### Returns **`{ hash: Uint8Array } & TxData`** — see [`broadcastTxSync`](./broadcastTxSync). ### Notes * The action is not bundled into `appMutationActions` — call it as a free function: `upgrade(client, {...})`. ### See also * [`configure`](./configure) — update chain and app configs ## createSession Sign a `SigningSessionInfo` payload with the client signer and return the credential bundle. The bundle can then drive [`createSessionSigner`](../../concepts/signers-and-authentication) for delegated signing. A session credential lets an ephemeral keypair sign transactions on the primary key's behalf until `expireAt`. The primary key signs a one-shot `SigningSessionInfo` covering the session pubkey and expiry; the chain verifies that envelope on every session-signed transaction. ### Signature ```ts function createSession( client: Client, parameters: { pubKey: Uint8Array expireAt: number }, ): Promise<{ keyHash: KeyHash authorization: StandardCredential sessionInfo: SigningSessionInfo }> ``` ### Example ```ts import { createSignerClient, createTransport, testnet, PrivateKeySigner, createSessionSigner } from "@left-curve/sdk" import { Secp256k1 } from "@left-curve/sdk" const client = createSignerClient({ chain: testnet, transport: createTransport(), signer: PrivateKeySigner.fromMnemonic(process.env.DANGO_MNEMONIC!), }) const sessionKey = Secp256k1.makeKeyPair() const expireAt = Date.now() + 24 * 60 * 60 * 1000 // 24h const bundle = await client.createSession({ pubKey: sessionKey.getPublicKey(), expireAt, }) const sessionSigner = createSessionSigner({ publicKey: sessionKey.getPublicKey(), privateKey: /* sessionKey's private bytes */ new Uint8Array(), keyHash: bundle.keyHash, sessionInfo: bundle.sessionInfo, authorization: bundle.authorization, }) ``` ### Parameters **`pubKey`** — `Uint8Array`. The session key's compressed public key. **`expireAt`** — `number`. Expiry as a Unix timestamp in milliseconds. The action converts to seconds before signing. ### Returns **`{ keyHash, authorization, sessionInfo }`** — `authorization` is the `StandardCredential` from the primary key, signing `sessionInfo`. `sessionInfo` carries the chain id, session pubkey (base64), and expiry (seconds, as string). ### Notes * The signer must implement `signArbitrary`. For session signers, only the primary signer (e.g. `PrivateKeySigner`) can create new sessions. * Throws "unsupported credential type" if the signer returns a non-standard credential. ### See also * [`createSessionSigner`](../../concepts/signers-and-authentication) — turns the bundle into a `Signer` * [Concepts: Signers & Authentication](../../concepts/signers-and-authentication) ## forgotUsername List the users associated with a key hash. Useful for username recovery flows. ### Signature ```ts function forgotUsername( client: Client, parameters: { keyHash: KeyHash limit?: number startAfter?: number height?: number }, ): Promise ``` ### Example ```ts import { createPublicClient, createTransport, testnet } from "@left-curve/sdk" import type { KeyHash } from "@left-curve/sdk" const client = createPublicClient({ chain: testnet, transport: createTransport() }) const keyHash: KeyHash = "ABCDEF0123456789..." const users = await client.forgotUsername({ keyHash }) ``` ### Parameters **`keyHash`** — `KeyHash`. Uppercase hex SHA-256 of the public key. **`limit`** — `number`, optional. Maximum users to return. **`startAfter`** — `number`, optional. User index to start after (exclusive). **`height`** — `number`, optional. Block height. Default `0` (latest). ### Returns **`User[]`** — list of users; each `User` contains `index`, `name`, `keys`, and `accounts`. ### See also * [`getUser`](./getUser) — fetch by index or name * [`getUserKeys`](./getUserKeys) ## getAccountInfo Fetch account details (index, owner, username) for a given account address. ### Signature ```ts function getAccountInfo( client: Client, parameters: { address: Address height?: number }, ): Promise ``` ### Example ```ts import { createPublicClient, createTransport, testnet } from "@left-curve/sdk" import type { Address } from "@left-curve/sdk" const client = createPublicClient({ chain: testnet, transport: createTransport() }) const address: Address = "0x1234567890abcdef1234567890abcdef12345678" const info = await client.getAccountInfo({ address }) console.log(info?.username, info?.index) ``` ### Parameters **`address`** — `Address`. The account address. **`height`** — `number`, optional. Block height. Default `0` (latest). ### Returns **`AccountDetails | null`** — `{ address, index, owner, username }` or `null` if the address is not a registered account. ### Notes * This makes two queries: one to the account factory and one to resolve the user's username via [`getUser`](./getUser). ### See also * [`getAccountStatus`](./getAccountStatus) — user state (`active`/`inactive`/`frozen`) * [`getAllAccountInfo`](./getAllAccountInfo) ## getAccountSeenNonces Read the most-recent nonces consumed by an account. Used internally by `signAndBroadcastTx` to compute the next nonce. ### Signature ```ts function getAccountSeenNonces( client: Client, parameters: { address: Address height?: number }, ): Promise<[number, number[]]> ``` ### Example ```ts import { createPublicClient, createTransport, testnet } from "@left-curve/sdk" import type { Address } from "@left-curve/sdk" const client = createPublicClient({ chain: testnet, transport: createTransport() }) const address: Address = "0x1234567890abcdef1234567890abcdef12345678" const [nextNonce, seenNonces] = await client.getAccountSeenNonces({ address }) ``` ### Parameters **`address`** — `Address`. The account. **`height`** — `number`, optional. Block height. Default `0` (latest). ### Returns **`[number, number[]]`** — tuple of `[nextNonce, seenNonces]`. `nextNonce` is `seenNonces[seenNonces.length - 1] + 1`, or `0` if no nonces have been seen. ### See also * [`signAndBroadcastTx`](../app/signAndBroadcastTx) — uses this internally * [`getAccountInfo`](./getAccountInfo) ## getAccountStatus Read the current user state (`active`, `inactive`, or `frozen`) for an account. ### Signature ```ts function getAccountStatus( client: Client, parameters: { address: Address height?: number }, ): Promise ``` ### Example ```ts import { createPublicClient, createTransport, testnet } from "@left-curve/sdk" import type { Address } from "@left-curve/sdk" const client = createPublicClient({ chain: testnet, transport: createTransport() }) const address: Address = "0x1234567890abcdef1234567890abcdef12345678" const status = await client.getAccountStatus({ address }) // "active" | "inactive" | "frozen" ``` ### Parameters **`address`** — `Address`. The account. **`height`** — `number`, optional. Block height. Default `0` (latest). ### Returns **`UserStatus`** — `"active" | "inactive" | "frozen"`. Use the `UserState` const map for comparisons. ### See also * [`getAccountInfo`](./getAccountInfo) — full account details ## getAllAccountInfo List every account in the factory, paginated. ### Signature ```ts function getAllAccountInfo( client: Client, parameters: { startAfter?: Address limit?: number height?: number }, ): Promise> ``` ### Example ```ts import { createPublicClient, createTransport, testnet } from "@left-curve/sdk" const client = createPublicClient({ chain: testnet, transport: createTransport() }) const accounts = await client.getAllAccountInfo({ limit: 100 }) ``` ### Parameters **`startAfter`** — `Address`, optional. Address to start after (exclusive). **`limit`** — `number`, optional. Maximum entries to return. **`height`** — `number`, optional. Block height. Default `0` (latest). ### Returns **`Record`** — map of address to `{ index, owner }`. ### See also * [`getAccountInfo`](./getAccountInfo) — fetch one by address (also resolves username) ## getCodeHash Read the current code hash for newly-created accounts. ### Signature ```ts function getCodeHash( client: Client, parameters?: { height?: number }, ): Promise ``` ### Example ```ts import { createPublicClient, createTransport, testnet } from "@left-curve/sdk" const client = createPublicClient({ chain: testnet, transport: createTransport() }) const codeHash = await client.getCodeHash() ``` ### Parameters **`height`** — `number`, optional. Block height. Default `0` (latest). ### Returns **`Hex`** — the SHA-256 hash of the account contract's Wasm. ### See also * [`registerUser`](./registerUser) * [`getCode`](../app/getCode) ## getNextAccountIndex Read the index that the next account for a username will receive. Use this to derive a new account's address before creating it. ### Signature ```ts function getNextAccountIndex( client: Client, parameters: { username: Username height?: number }, ): Promise ``` ### Example ```ts import { createPublicClient, createTransport, testnet } from "@left-curve/sdk" const client = createPublicClient({ chain: testnet, transport: createTransport() }) const index = await client.getNextAccountIndex({ username: "alice" }) ``` ### Parameters **`username`** — `Username`. The user owning the new account. **`height`** — `number`, optional. Block height. Default `0` (latest). ### Returns **`AccountIndex`** — `number`. The index that will be assigned. ### See also * [`registerAccount`](./registerAccount) * [`getUser`](./getUser) ## getUser Fetch a user record by index or by name. ### Signature ```ts function getUser( client: Client, parameters: { userIndexOrName: UserIndexOrName height?: number }, ): Promise type UserIndexOrName = { index: number } | { name: string } ``` ### Example ```ts import { createPublicClient, createTransport, testnet } from "@left-curve/sdk" const client = createPublicClient({ chain: testnet, transport: createTransport() }) const byIndex = await client.getUser({ userIndexOrName: { index: 42 } }) const byName = await client.getUser({ userIndexOrName: { name: "alice" } }) ``` ### Parameters **`userIndexOrName`** — `{ index: number } | { name: string }`. Discriminate by which key is present. **`height`** — `number`, optional. Block height. Default `0` (latest). ### Returns **`User`** — `{ index, name, keys, accounts }`. `keys` maps `KeyHash` to `Key`; `accounts` maps `AccountIndex` to `Address`. ### See also * [`getUserKeys`](./getUserKeys) — indexer-backed key list * [`forgotUsername`](./forgotUsername) ## getUserKeys Fetch the list of public keys associated with a user, via the indexer. ### Signature ```ts function getUserKeys( client: Client, parameters: { userIndex: number }, ): Promise ``` ### Example ```ts import { createPublicClient, createTransport, testnet } from "@left-curve/sdk" const client = createPublicClient({ chain: testnet, transport: createTransport() }) const keys = await client.getUserKeys({ userIndex: 42 }) keys.forEach((k) => console.log(k.keyType, k.keyHash)) ``` ### Parameters **`userIndex`** — `number`. The user's numeric index. ### Returns **`PublicKey[]`** — array of `{ keyHash, publicKey, keyType, createdBlockHeight, createdAt }`. ### Notes * Queries the indexer's GraphQL endpoint, not the on-chain contract. Indexer lag may apply. ### See also * [`getUser`](./getUser) — pulls `keys` from the on-chain contract * [`updateKey`](./updateKey) ## registerAccount Register an additional account for an existing user. Each user has a `BTreeMap` of up to 5 accounts; this action mints the next one and (optionally) seeds it with `funds`. The new account's address can be computed deterministically ahead of time. See protocol book: `overview/3-dango-contracts` §3. ### Signature ```ts function registerAccount( client: Client, parameters: { sender: Address funds?: Funds }, txParameters?: { gasLimit?: number }, ): Promise<{ hash: Uint8Array } & TxData> ``` ### Example ```ts import { createSignerClient, createTransport, testnet, PrivateKeySigner } from "@left-curve/sdk" import type { Address } from "@left-curve/sdk" const client = createSignerClient({ chain: testnet, transport: createTransport(), signer: PrivateKeySigner.fromMnemonic(process.env.DANGO_MNEMONIC!), }) const sender: Address = "0x1234567890abcdef1234567890abcdef12345678" await client.registerAccount({ sender }, { gasLimit: 250_000 }) ``` ### Parameters **`sender`** — `Address`. An existing account of the user. **`funds`** — `Funds`, optional. Initial coins to send into the new account. **`txParameters`** — `{ gasLimit?: number }`, optional. Positional second argument used by the action builder to forward gas overrides. ### Returns **`{ hash: Uint8Array } & TxData`** — see [`broadcastTxSync`](../app/broadcastTxSync). ### Notes * The action builder accepts `txParameters` as a positional second argument, not as a field on the first parameter. This is unique among mutations. * The new account address can be derived ahead of time via [`getNextAccountIndex`](./getNextAccountIndex) + [`computeAddress`](../../concepts/encoding-and-types). * Rejected if the user already owns 5 accounts (the per-user cap). ### See also * [`registerUser`](./registerUser) * [`getNextAccountIndex`](./getNextAccountIndex) ## registerUser Create a new user and their first account in a single transaction. Submitted by the account factory itself (sender-less from the caller's perspective). Registration requires a pre-existing deposit (≥ `min_deposit`) sent to the factory, a signed `RegisterUserData` proving control of the public key, and a chain id binding. If `referrer` is provided, the factory atomically forwards a `SetReferral` to the perps contract. The username on the new user record is immutable once set. See protocol book: `overview/3-dango-contracts` §3. ### Signature ```ts function registerUser( client: Client, parameters: { key: Key keyHash: KeyHash seed: number signature: Signature referrer?: number }, ): Promise<{ hash: Uint8Array } & TxData> ``` ### Example ```ts import { createSignerClient, createTransport, testnet, PrivateKeySigner } from "@left-curve/sdk" const signer = PrivateKeySigner.fromRandomKey() const client = createSignerClient({ chain: testnet, transport: createTransport(), signer, }) // Build the signature off-band per the chain's registration protocol. await client.registerUser({ key: { secp256k1: "base64-encoded-pubkey" }, keyHash: await signer.getKeyHash(), seed: 1, signature: { secp256k1: "base64-signature" }, }) ``` ### Parameters **`key`** — `Key`. The public key to register (`{ secp256k1 }`, `{ secp256r1 }`, or `{ ethereum }`). **`keyHash`** — `KeyHash`. SHA-256 of the public key, uppercase hex. **`seed`** — `number`. Salt input — see [`createAccountSalt`](../../concepts/encoding-and-types). **`signature`** — `Signature`. Signature proving control of the key. **`referrer`** — `number`, optional. Referrer's user index. ### Returns **`{ hash: Uint8Array } & TxData`** — see [`broadcastTxSync`](../app/broadcastTxSync). ### Notes * Unlike normal mutations, `registerUser` is submitted by the account factory contract itself. The action calls `simulate` for gas and broadcasts an unsigned tx (the contract is its own credential). * Exactly one `RegisterUser` message is allowed per transaction (anti-batching constraint). * Nonce jump on the resulting account is limited to 100 to bound the seen-nonce set. ### See also * [`registerAccount`](./registerAccount) — add more accounts to an existing user * [Concepts: Signers & Authentication](../../concepts/signers-and-authentication) ## updateKey Insert or delete a key on the calling account. Keys are stored on the user record (`User.keys: BTreeMap`) and indexed by hash for authentication lookups. Adding or removing a key here is how a user rotates credentials; the signature against the new key is verified against the registered hash on every subsequent transaction. See protocol book: `overview/3-dango-contracts` §4. ### Signature ```ts function updateKey( client: Client, parameters: { sender: Address action: "delete" | { insert: Key } keyHash: KeyHash }, ): Promise<{ hash: Uint8Array } & TxData> ``` ### Example ```ts import { createSignerClient, createTransport, testnet, PrivateKeySigner } from "@left-curve/sdk" import type { Address } from "@left-curve/sdk" const client = createSignerClient({ chain: testnet, transport: createTransport(), signer: PrivateKeySigner.fromMnemonic(process.env.DANGO_MNEMONIC!), }) const sender: Address = "0x1234567890abcdef1234567890abcdef12345678" // Add a new secp256k1 key await client.updateKey({ sender, action: { insert: { secp256k1: "base64-encoded-new-pubkey" } }, keyHash: "ABCDEF...new-hash", }) // Remove an existing key await client.updateKey({ sender, action: "delete", keyHash: "ABCDEF...old-hash", }) ``` ### Parameters **`sender`** — `Address`. The calling account. **`action`** — `"delete" | { insert: Key }`. Discriminated by string vs object. **`keyHash`** — `KeyHash`. The hash of the key being inserted or deleted. ### Returns **`{ hash: Uint8Array } & TxData`** — see [`broadcastTxSync`](../app/broadcastTxSync). ### See also * [`getUserKeys`](./getUserKeys) — list current keys * [`createSession`](./createSession) — session-key alternative ## updateUsername Change the username on the calling account's user record. Usernames live in a chain-wide unique index on the user record; the chain rejects collisions and reserves names. Note that the protocol book describes the original `name` field as immutable once set on `RegisterUser` — this action surfaces a chain-side rename path that is only available where the contract permits it. ### Signature ```ts function updateUsername( client: Client, parameters: { sender: Address username: Username }, ): Promise<{ hash: Uint8Array } & TxData> ``` ### Example ```ts import { createSignerClient, createTransport, testnet, PrivateKeySigner } from "@left-curve/sdk" import type { Address } from "@left-curve/sdk" const client = createSignerClient({ chain: testnet, transport: createTransport(), signer: PrivateKeySigner.fromMnemonic(process.env.DANGO_MNEMONIC!), }) const sender: Address = "0x1234567890abcdef1234567890abcdef12345678" await client.updateUsername({ sender, username: "new-alice" }) ``` ### Parameters **`sender`** — `Address`. The calling account. **`username`** — `Username`. New username. Must be unique chain-wide. ### Returns **`{ hash: Uint8Array } & TxData`** — see [`broadcastTxSync`](../app/broadcastTxSync). ### See also * [`getUser`](./getUser) ## Dango Rust SDK The Rust SDK is a thin GraphQL + REST client over a Dango node, plus key-management and transaction-signing utilities. It exposes: * [`HttpClient`](./api/clients/HttpClient) — queries (`query_app`, `query_store`, `simulate`, `query_block`, …), tx broadcast, and macro-generated paginators for indexer connections. * [`WsClient`](./api/clients/WsClient) and [`Session`](./api/clients/Session) — GraphQL subscriptions over `graphql-transport-ws`. * [`SingleSigner`](./api/clients/SingleSigner) — typestate signer for Dango single-signature accounts. * [`Secret`](./api/traits/Secret) trait with `Secp256k1` and `Eip712` implementations, and [`Keystore`](./api/clients/Keystore) for AES-256-GCM at-rest encryption. The SDK is a thin transport: there are no Dango action helpers. Transactions are composed by building `Message`s (re-exported by the SDK) and passing them to the signer. ### Start here * [Installation](./getting-started/installation) — add `dango-sdk` to a Cargo project. * [First call](./getting-started/first-call) — query the chain status with `HttpClient`. * [Project setup](./getting-started/project-setup) — endpoints, keys, env vars, runtime. ### Concepts * [Clients](./concepts/clients) — when to reach for `HttpClient` vs `WsClient`. * [Signers and authentication](./concepts/signers-and-authentication) — the `Secret` trait and `SingleSigner`. * [Transactions](./concepts/transactions) — compose, sign, broadcast. * [Subscriptions](./concepts/subscriptions) — multiplexed `Session` vs dedicated `WsClient::subscribe`. * [Encoding and types](./concepts/encoding-and-types) — base units, `Addr`, `Hash256`, codegen response types. * [Error handling](./concepts/error-handling) — `anyhow::Error` everywhere except subscription items, which surface `WsError`. * [Rate limits and quotas](./concepts/rate-limits) — server-side limits and how to shape the client around them. ## First call A five-minute hello-world that queries chain status with `HttpClient`. ### The program ```rust use { anyhow::Result, dango_sdk::HttpClient, grug::BlockClient, }; #[tokio::main] async fn main() -> Result<()> { let client = HttpClient::new("https://api-mainnet.dango.zone")?; let block = client.query_block(None).await?; println!("latest height: {}", block.info.height); println!("chain id: {}", block.info.chain_id); println!("timestamp: {}", block.info.timestamp); Ok(()) } ``` `BlockClient` supplies `query_block`. `HttpClient` implements it, so the method is reachable once the trait is in scope. ### What just happened 1. `HttpClient::new` wraps a `reqwest::Client` around the Dango HTTP endpoint. No network call is made. 2. `query_block(None)` hits the REST endpoint `GET /block/info` and parses the response into a `Block`. 3. Errors propagate as `anyhow::Error` via `?`. ### Querying contract state Most "what's my balance" calls go through the `QueryClientExt` blanket trait, which is automatically reachable on `HttpClient`: ```rust use { anyhow::Result, dango_sdk::HttpClient, grug::{Addr, Denom, QueryClientExt}, std::str::FromStr, }; #[tokio::main] async fn main() -> Result<()> { let client = HttpClient::new("https://api-mainnet.dango.zone")?; let address = Addr::from_str("0x0000000000000000000000000000000000000000")?; let denom = Denom::from_str("bridge/usdc")?; let balance = client.query_balance(address, denom, None).await?; println!("balance: {balance}"); Ok(()) } ``` `query_balance`, `query_app_config`, `query_wasm_smart`, and the other `QueryClientExt` methods. See [Clients](../concepts/clients) for the full list reachable on `HttpClient`. ### Next * [Project setup](./project-setup) — wiring endpoints, keys, and env vars. * [Concepts: Clients](../concepts/clients) — `HttpClient` vs `WsClient`. ## Installation Add `dango-sdk` to a Cargo project. ### Requirements * Rust 1.85+ (2024 edition). * A `tokio` runtime — every network call is async. ### Add the dependency ```toml [dependencies] dango-sdk = { git = "https://github.com/left-curve/left-curve" } tokio = { version = "1", features = ["macros", "rt-multi-thread"] } anyhow = "1" futures = "0.3" ``` `dango-sdk` is not yet published to crates.io. Pin the dependency to a tag or commit when running in production: ```toml dango-sdk = { git = "https://github.com/left-curve/left-curve", tag = "vX.Y.Z" } ``` ### Features | Feature | Default | Effect | | --------- | ------- | -------------------------------------------------------------------------------------------------------------------------------- | | `tracing` | off | Emits `tracing::debug!` events for every GraphQL request, subscription handshake, and subscription id. No public symbols change. | Enable it when wiring the SDK into an app that already uses `tracing`: ```toml dango-sdk = { git = "...", features = ["tracing"] } ``` ### What you get The crate re-exports everything users need at the root: ```rust use dango_sdk::{ HttpClient, WsClient, Session, SubscriptionStream, SubscriptionVariables, SingleSigner, Secret, Secp256k1, Eip712, Keystore, WsError, }; ``` It also re-exports `indexer_graphql_types::*`, so subscription and query types — `SubscribeBlock`, `subscribe_block::Variables`, `accounts::Variables`, `PageInfo`, … — are reachable directly from `dango_sdk`. ### Next * [First call](./first-call) — issue a query against a live Dango node. ## Project setup Wire endpoints, keys, and the async runtime for a real application. ### Endpoints Pick one HTTP endpoint and (optionally) a paired WebSocket endpoint: | Network | HTTP | WebSocket | | ---------- | -------------------------------- | -------------------------------------- | | Mainnet | `https://api-mainnet.dango.zone` | `wss://api-mainnet.dango.zone/graphql` | | Local node | `http://localhost:8080` | `ws://localhost:8080/graphql` | `HttpClient::new` accepts the bare HTTP origin; it appends `/graphql` and the REST paths internally. `WsClient::from_http_url` accepts an HTTP origin and rewrites the scheme — see [WsClient](../api/clients/WsClient). ### Configuration via environment Read endpoint and key material from the environment. Keep secrets out of source files. ```rust use { anyhow::{Context, Result}, dango_sdk::{HttpClient, Secp256k1, Secret, SingleSigner, WsClient}, grug::Addr, std::{env, str::FromStr}, }; #[derive(Debug)] struct Config { http_url: String, ws_url: String, chain_id: String, address: Addr, private_key: [u8; 32], } fn load() -> Result { Ok(Config { http_url: env::var("DANGO_HTTP_URL").context("DANGO_HTTP_URL not set")?, ws_url: env::var("DANGO_WS_URL").context("DANGO_WS_URL not set")?, chain_id: env::var("DANGO_CHAIN_ID").unwrap_or_else(|_| "dango-1".into()), address: Addr::from_str(&env::var("DANGO_ADDRESS")?)?, private_key: hex::decode(env::var("DANGO_PRIVATE_KEY")?)? .try_into() .map_err(|_| anyhow::anyhow!("DANGO_PRIVATE_KEY must be 32 bytes hex"))?, }) } #[tokio::main] async fn main() -> Result<()> { let cfg = load()?; let http = HttpClient::new(&cfg.http_url)?; let ws = WsClient::new(&cfg.ws_url)?; let secret = Secp256k1::from_bytes(cfg.private_key)?; let signer = SingleSigner::new(cfg.address, secret) .with_query_user_index(&http).await? .with_query_nonce(&http).await?; println!("ready: {}", signer.address); let _ = ws; Ok(()) } ``` ### Loading keys from an encrypted file For local CLIs, persist a 32-byte private key in an AES-256-GCM keystore file. See [Keystore](../api/clients/Keystore) for the format. ```rust use { anyhow::Result, dango_sdk::{Keystore, Secp256k1, Secret}, }; fn load_secret(path: &str, password: &str) -> Result { let bytes = Keystore::from_file(path, password)?; Secp256k1::from_bytes(bytes) } ``` Wrap the raw bytes in [`Secp256k1`](../api/traits/Secret) or [`Eip712`](../api/traits/Secret) depending on the account's signing scheme. ### Runtime Every network call is async. Use the multi-threaded runtime in real apps: ```rust #[tokio::main(flavor = "multi_thread", worker_threads = 4)] async fn main() -> anyhow::Result<()> { /* ... */ Ok(()) } ``` For libraries that own their runtime, expose `async fn` entry points and let the caller pick. ### Logging Enable the `tracing` feature when the host app uses `tracing`: ```toml dango-sdk = { git = "...", features = ["tracing"] } ``` This emits `debug!` events on every GraphQL request, response, and subscription handshake. No public symbols change. ### Next * [Concepts: Clients](../concepts/clients) — pick the right client for the job. * [Concepts: Transactions](../concepts/transactions) — compose, sign, broadcast. ## Clients **What this teaches:** when to use `HttpClient` and when to use `WsClient`, and what each one is wired to under the hood. ### Mental model The SDK ships two transports: * [`HttpClient`](../api/clients/HttpClient) — one-shot queries and transaction broadcast over GraphQL and REST. Cloneable, no live connection. * [`WsClient`](../api/clients/WsClient) — GraphQL subscriptions over `graphql-transport-ws`. Cheap factory value; the actual socket lives in a [`Session`](../api/clients/Session). There is no shared "Client" abstraction. Pick the one that matches the call pattern. ### HttpClient `HttpClient` wraps a `reqwest::Client`. It is `Debug + Clone`; cloning shares the underlying connection pool — pass it around freely. It implements four upstream query traits, which is where the queries live: * `QueryClient` — `query_app`, `query_store`, `simulate`. * `BlockClient` — `query_block`, `query_block_outcome` (REST, not GraphQL). * `BroadcastClient` — `broadcast_tx`. * `SearchTxClient` — `search_tx`. Bringing the traits into scope is what unlocks the methods: ```rust use { anyhow::Result, dango_sdk::HttpClient, grug::{BlockClient, BroadcastClient, QueryClient, SearchTxClient}, }; #[tokio::main] async fn main() -> Result<()> { let client = HttpClient::new("https://api-mainnet.dango.zone")?; let block = client.query_block(None).await?; println!("height: {}", block.info.height); Ok(()) } ``` #### Extension methods on `HttpClient` Because `HttpClient` implements `QueryClient`, the `QueryClientExt` blanket trait lights up a suite of convenience methods: | Method | What it does | | ------------------ | -------------------------------------------- | | `query_app_config` | Fetch the typed `AppConfig`. | | `query_balance` | Fetch a single denom balance for an address. | | `query_balances` | Fetch all balances for an address. | | `query_supply` | Fetch total supply for one denom. | | `query_supplies` | Fetch total supplies. | | `query_wasm_smart` | Run a typed smart query on a contract. | | `query_wasm_raw` | Read a raw storage slot from a contract. | | `query_code` | Fetch a Wasm bytecode by hash. | | `query_codes` | List uploaded code hashes. | | `query_contract` | Fetch a contract's info. | | `query_contracts` | List contracts. | See the `QueryClientExt` reference for full signatures. #### Pagination `HttpClient` ships own pagination helpers on top of the GraphQL connection types: * `paginate_all` — generic forward/backward loop driven by closures. * `paginate_accounts`, `paginate_transfers`, `paginate_transactions`, `paginate_blocks`, `paginate_events`, `paginate_messages` — typed wrappers for each indexer connection. Use the typed wrappers when the query type matches; use `paginate_all` when shaping a custom traversal. ### WsClient `WsClient` holds nothing but a parsed `Url`. Cloning is free. Two ways to consume it: * [`WsClient::connect`](../api/methods/ws-client/connect) → [`Session`](../api/clients/Session). Open one socket and multiplex multiple subscriptions over it. Drop the `Session` and every derived stream to close the connection. * [`WsClient::subscribe`](../api/methods/ws-client/subscribe). Open a dedicated socket for one subscription. Drop the stream to close. Subscription items are `Result, WsError>`. The first error variant `WsError::Closed` ends the stream. See [Subscriptions](./subscriptions) for which entry point to pick. ### Lifecycle * `HttpClient` has no `Drop` hook. Reuse one instance for the lifetime of the program. * `WsClient` is a config object; copy it freely. * `Session` runs a background `tokio` task spawned by `connect`. The last `Session` clone drops it via a `Close` command. See [Session](../api/clients/Session). ### Next * [Signers and authentication](./signers-and-authentication) — sign transactions before broadcasting. * [Subscriptions](./subscriptions) — when to use `Session::subscribe` vs `WsClient::subscribe`. ## Encoding and types **What this teaches:** which crates own which user-facing types, how numeric units are represented, and the shape of GraphQL response data. ### Where types live `dango-sdk` is thin: most user-facing types are re-exported by the SDK, `dango_types`, and `indexer_graphql_types`. | Type | Crate | Notes | | -------------------------------------------------------------------------------------- | ---------------------------------------------------- | ----------------------------------------------------------------------------------------------- | | `Addr`, `Hash256`, `Binary`, `ByteArray` | Core | Strongly-typed wrappers over fixed-size byte arrays. | | `Message`, `Tx`, `UnsignedTx`, `Block`, `BlockOutcome`, `TxOutcome` | Core | Core chain types. | | `Coins`, `Denom`, `Coin`, `Uint128`, `Uint256` | Core | Bank balances and unsigned integers. | | `NonEmpty`, `Defined`, `Undefined`, `MaybeDefined` | Core | Typestate helpers. | | `Key`, `Signature`, `SignDoc`, `Nonce`, `UserIndex` | `dango_types::auth` / `dango_types::account_factory` | Account-factory primitives. | | `PageInfo`, `::Variables`, `::ResponseData`, generated `*Nodes`/`*Edges` | `indexer_graphql_types` | GraphQL codegen surface, re-exported at the `dango_sdk` crate root. | | `SubscribeBlock`, …, `SubscribeQueryStatus` (×14) | `indexer_graphql_types` | One marker struct per subscription. Each has a snake-case module of `Variables`/`ResponseData`. | | `WsError` | `dango_sdk` | The only public typed error in the crate. | ### Base units, not human units Amounts on the wire are integers in base units (the contract's smallest denomination). `Coins::one("bridge/usdc", 100_000_000_u128)` is 100 USDC, not 100 micro-USDC. the SDK exposes `Uint128`/`Uint256` for amounts. Convert to display units at the edge — there is no SDK-side `Decimal`. ### Addresses `Addr` is a 20-byte fixed array, hex-encoded with the `0x` prefix in display form. ```rust use {grug::Addr, std::str::FromStr}; let addr = Addr::from_str("0x0000000000000000000000000000000000000000")?; println!("{addr}"); // 0x0000... let bytes: [u8; 20] = addr.into_inner(); // Ok::<(), anyhow::Error>(()) ``` ### Hashes `Hash256` is a 32-byte hash, hex-encoded. Used for transaction hashes, key hashes, and content hashes. ```rust use {grug::Hash256, std::str::FromStr}; let hash = Hash256::from_str("0x0000000000000000000000000000000000000000000000000000000000000000")?; // Ok::<(), anyhow::Error>(()) ``` ### GraphQL response types Every query and subscription generates three pieces of code: 1. A marker struct, e.g. `SubscribeTrades`. Use as the generic argument: `session.subscribe::(...)`. 2. A snake-case module — `subscribe_trades` — exposing `Variables`, `ResponseData`, and nested `*Nodes` records. 3. An `impl Variables for subscribe_trades::Variables { type Query = SubscribeTrades; }` linking the two. ```rust use dango_sdk::{SubscribeTrades, subscribe_trades}; let variables = subscribe_trades::Variables { base_denom: "dango".into(), quote_denom: "bridge/usdc".into(), }; ``` All generated types derive `Debug + Clone + PartialEq + Eq`. `Variables` types derive `Default` so partial construction with `..Default::default()` is idiomatic. ### Connection nodes Indexer queries return Relay-style connections: `{ nodes, page_info: PageInfo }`. The paginate helpers extract `nodes` and follow cursors. Each query has its own node type (`accounts::AccountsAccountsNodes`, `transfers::TransfersTransfersNodes`, …) — see the [SubscriptionVariables index](../api/subscriptions/SubscriptionVariables) for the subscription set. ### Serializing dynamic payloads Smart-contract calls and queries are JSON-encoded. the SDK provides `JsonSerExt` / `JsonDeExt` for typed encode/decode of any `Serialize`/`Deserialize` value: ```rust use grug::JsonSerExt; let json = my_struct.to_json_value()?; // Ok::<(), anyhow::Error>(()) ``` `Message::execute` and `Message::transfer` accept typed payloads directly; you rarely need raw JSON. ### Next * [Error handling](./error-handling) — `anyhow::Error` vs `WsError`. * [Subscriptions](./subscriptions) — how `Variables`/`ResponseData` plug into streams. ## Error handling **What this teaches:** the two error shapes in `dango-sdk` — `anyhow::Error` for one-shot calls, [`WsError`](../api/errors/WsError) inside subscription items — and how to recover from each. ### Mental model `dango-sdk` returns `anyhow::Result` (`Result`) from every fallible async or sync method. The trait impls on `HttpClient` set `type Error = anyhow::Error`, so the same pattern propagates from `query_app`, `broadcast_tx`, and friends. The single exception is subscriptions. A `SubscriptionStream` yields `Result, WsError>` — a typed enum because callers need to discriminate between "decode failure" and "server-side subscription error". ### `anyhow::Error`: when and how to inspect Use `?` to propagate. When you need to react to a specific failure, downcast: ```rust use { anyhow::Result, dango_sdk::HttpClient, grug::QueryClient, }; async fn try_query(http: &HttpClient) -> Result<()> { match http.simulate(my_unsigned_tx()).await { Ok(outcome) => println!("ok: gas={}", outcome.gas_used), Err(err) => { // Drill into the source chain for a specific cause type. if let Some(req_err) = err.downcast_ref::() { eprintln!("transport: {req_err}"); } else { eprintln!("other: {err:#}"); } } } Ok(()) } // fn my_unsigned_tx() -> grug::UnsignedTx { ... } ``` The `:#` format spec prints the full cause chain. Prefer `tracing::error!(?err)` in production. ### Common error sources | Where | What you'll see | | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `HttpClient::new` | URL parse error (`url::ParseError`). | | `query_app`, `simulate`, `broadcast_tx`, `paginate_*` | `reqwest::Error` for network failure; `anyhow!("no data returned …")` when GraphQL returned `errors` and no `data`; deserialization errors from `serde_json`. | | `query_block` (REST) | `reqwest::Error` plus HTTP status (`error_for_status` formats body into the message). | | `SingleSigner::new_first_address_available` | `anyhow!("no user index found for key hash …")` when the factory returns nothing. | | `Keystore::from_file` | I/O, JSON parse, or AES-GCM decryption failures. | | `WsClient::connect` | `Invalid URL scheme: …`, handshake send error, unexpected pre-`connection_ack` frame. | ### `WsError`: subscription items `SubscriptionStream` items are `Result, WsError>`: ```rust use { dango_sdk::WsError, futures::StreamExt, }; while let Some(item) = stream.next().await { match item { Ok(resp) => { if let Some(errors) = resp.errors { eprintln!("graphql payload errors: {errors:?}"); } if let Some(data) = resp.data { /* handle data */ } } Err(WsError::Closed(reason)) => { eprintln!("ws closed: {reason}"); break; } Err(WsError::Transport(msg)) => { eprintln!("transport: {msg}"); break; } Err(WsError::Subscription(payload)) => { eprintln!("subscription error: {payload}"); // Server-side error scoped to this subscription; // the stream is over but the connection may still be alive. } Err(WsError::Decode(msg)) => { eprintln!("decode: {msg}"); } } } // Ok::<(), anyhow::Error>(()) ``` `WsError::Closed` and `WsError::Transport` terminate the stream. `WsError::Subscription` is server-side: the stream ends but the parent [`Session`](../api/clients/Session) keeps running for other subscriptions. `WsError::Decode` is recoverable in principle but means the schema and the client disagree. ### Retry policy The SDK does not retry. Wrap calls with `tokio-retry`, `backon`, or a hand-rolled loop: ```rust use { anyhow::Result, dango_sdk::HttpClient, grug::SearchTxClient, std::time::Duration, tokio::time::sleep, }; async fn wait_for(http: &HttpClient, hash: grug::Hash256) -> Result<()> { for attempt in 0..40 { match http.search_tx(hash).await { Ok(_) => return Ok(()), Err(err) if attempt < 39 => { eprintln!("waiting (attempt {attempt}): {err}"); sleep(Duration::from_millis(500)).await; } Err(err) => return Err(err), } } unreachable!() } ``` Pick a bound that matches the operation. Don't retry forever — backoff is the caller's responsibility. ### Next * [Rate limits and quotas](./rate-limits) — what to do when the server returns 429. * [`WsError`](../api/errors/WsError) — the variants in detail. ## Rate limits and quotas **What this teaches:** the server-side limits on Dango HTTP and WebSocket endpoints, and how to shape Rust client code around them. ### The limits | Surface | Limit | Notes | | ----------------------- | ------------------------------------------ | ------------------------------------------------------------------ | | HTTP (GraphQL + REST) | 167 requests per 10 seconds, per source IP | Bursts above this return HTTP 429. | | WebSocket subscriptions | 30 concurrent subscriptions per connection | Each [`Session`](../api/clients/Session) counts as one connection. | These are server-enforced. The SDK has no built-in retry, queueing, or rate-limit awareness. ### Handling 429 on HTTP The standard call shape is `Result`. A 429 surfaces as `error_for_status` text inside the `anyhow::Error`. Wrap calls with an exponential backoff helper: ```rust use { anyhow::Result, dango_sdk::HttpClient, grug::BlockClient, std::time::Duration, tokio::time::sleep, }; async fn with_backoff(mut op: F) -> Result where F: FnMut() -> Fut, Fut: std::future::Future>, { let mut delay = Duration::from_millis(100); for _ in 0..8 { match op().await { Ok(v) => return Ok(v), Err(e) if e.to_string().contains("429") => { sleep(delay).await; delay = (delay * 2).min(Duration::from_secs(10)); } Err(e) => return Err(e), } } op().await } async fn run(http: &HttpClient) -> Result<()> { let block = with_backoff(|| async { http.query_block(None).await }).await?; println!("height: {}", block.info.height); Ok(()) } ``` For richer policies (jitter, retry-after parsing) reach for `backon` or `tokio-retry` — both are async-aware and `Result`-friendly. ### Sharding subscriptions The 30-per-connection cap is hard. Open multiple `Session`s when more streams are needed: ```rust use { anyhow::Result, dango_sdk::{Session, WsClient}, futures::future::try_join_all, }; async fn open_shards(url: &str, count: usize) -> Result> { let client = WsClient::new(url)?; try_join_all((0..count).map(|_| client.connect())).await } ``` Each `Session` is independent: a server-side reset on one connection does not affect the others. Route subscriptions to shards by content (e.g. `SubscribeTrades` by pair) so a single shard reset only disrupts part of the stream set. ### Avoiding hot-poll loops The `tx-sync` broadcast flow polls `search_tx` until the transaction is included. Tight loops eat the HTTP budget — sleep between attempts: ```rust use { anyhow::Result, dango_sdk::HttpClient, grug::{Hash256, SearchTxClient}, std::time::Duration, tokio::time::sleep, }; async fn wait(http: &HttpClient, hash: Hash256) -> Result<()> { for _ in 0..40 { if http.search_tx(hash).await.is_ok() { return Ok(()); } sleep(Duration::from_millis(500)).await; } anyhow::bail!("tx {hash} not included within 20 s"); } ``` Prefer a subscription (`SubscribeTransactions`) when watching many transactions at once. ### Reusing `HttpClient` `HttpClient` wraps a `reqwest::Client` which holds a connection pool. Cloning shares the pool. Avoid `HttpClient::new` per request — it spins up a fresh `reqwest::Client` each time and skips connection reuse. ### Next * [Error handling](./error-handling) — what kinds of failures the SDK surfaces. * [Subscriptions](./subscriptions) — when to migrate off polling. ## Signers and authentication **What this teaches:** how the `Secret` trait, `Secp256k1`/`Eip712`, and `SingleSigner` fit together to sign Dango transactions. ### Mental model Dango authenticates accounts by a `(user_index, nonce, signature)` tuple over the transaction's `SignDoc`. The SDK splits this into two layers: * A [`Secret`](../api/traits/Secret) holds a private key and knows how to sign a `SignDoc`. Implementations: [`Secp256k1`](../api/traits/Secret) (raw secp256k1 + SHA-256) and [`Eip712`](../api/traits/Secret) (EIP-712 typed data). * A [`SingleSigner`](../api/clients/SingleSigner) wraps a `Secret` together with the on-chain account context — `address`, `user_index`, `nonce` — and produces signed `Tx` values. ### Secrets ```rust use dango_sdk::{Secp256k1, Secret}; // Random key. let secret = Secp256k1::new_random(); // From raw bytes. let secret = Secp256k1::from_bytes([0u8; 32])?; // From a BIP-39 mnemonic and BIP-44 coin type (60 = Ethereum). use bip32::Mnemonic; let mnemonic = Mnemonic::new("abandon abandon abandon ...", Default::default())?; let secret = Secp256k1::from_mnemonic(&mnemonic, 60)?; // Ethereum-flavoured (EIP-712 typed-data signatures). use dango_sdk::Eip712; let secret = Eip712::new_random(); // Ok::<(), anyhow::Error>(()) ``` `Secret::key()` is what the account factory stores; `Secret::key_hash()` is what `Credential::Standard` references. `Eip712::key_hash` is `sha256(addr.to_string())` over the derived Ethereum address — distinct from `Secp256k1::key_hash` which hashes the compressed pubkey. ### SingleSigner — the typestate builder `SingleSigner` carries phantom-typed flags for whether `user_index` and `nonce` have been filled in. The compiler refuses to call `sign_transaction` until both are `Defined`. ```rust use { anyhow::Result, dango_sdk::{HttpClient, Secp256k1, Secret, SingleSigner}, grug::Addr, std::str::FromStr, }; async fn build_signer(http: &HttpClient) -> Result> { let address = Addr::from_str("0x0000000000000000000000000000000000000000")?; let secret = Secp256k1::new_random(); let signer = SingleSigner::new(address, secret) .with_query_user_index(http).await? .with_query_nonce(http).await?; Ok(signer) } ``` See [SingleSigner](../api/clients/SingleSigner) for the full state diagram and each transition's signature. ### Discovering an existing account When the only thing on hand is a `Secret`, ask the account factory for the first account associated with that key: ```rust use { anyhow::Result, dango_sdk::{HttpClient, Secp256k1, SingleSigner}, }; async fn for_key(http: &HttpClient, secret: Secp256k1) -> Result> { SingleSigner::new_first_address_available(http, secret, None).await } ``` This calls `QueryForgotUsernameRequest` with `limit: 1` against the account factory and resolves the user's master account. Pass an `AppConfig` reference to skip the extra query for the factory address. ### Signing a transaction `SingleSigner, Defined>` implements `Signer`: ```rust use { grug::{Message, NonEmpty, Signer}, }; let tx = signer.sign_transaction( NonEmpty::new(vec![/* messages */])?, "dango-1", 1_000_000, )?; ``` `sign_transaction` auto-increments the in-memory nonce on every successful call — a single signer can produce a stream of consecutive transactions without re-querying. See [Transactions](./transactions) for end-to-end mechanics. ### Recovering an existing private key `Keystore::from_file` returns raw `[u8; 32]`. Wrap it before signing: ```rust use dango_sdk::{Keystore, Secp256k1, Secret}; let bytes = Keystore::from_file("./key.json", "hunter2")?; let secret = Secp256k1::from_bytes(bytes)?; // Ok::<(), anyhow::Error>(()) ``` For Ethereum-flavoured signing, use `Eip712::from_bytes(bytes)?` instead. ### Next * [Transactions](./transactions) — compose messages, sign, broadcast, poll. * [SingleSigner](../api/clients/SingleSigner) — the full typestate API. ## Subscriptions **What this teaches:** the `graphql-transport-ws` model, when to multiplex over a [`Session`](../api/clients/Session), and when to open a dedicated connection with [`WsClient::subscribe`](../api/methods/ws-client/subscribe). ### Mental model `WsClient` is a cheap configuration value. Every live socket is a `Session` — an `Arc`-backed handle that owns a background `tokio` task driving one WebSocket. Subscriptions are tagged with protocol-level ids and routed back to per-stream channels. The protocol is `graphql-transport-ws` (the `graphql-ws` rewrite that `async-graphql` speaks). The SDK sends a `Ping` every 15 seconds to stay under the server's 30-second `keepalive_timeout`. ### Two entry points | Entry point | Connections | When to use | | --------------------- | ------------------------------ | --------------------------------------------------------------------- | | `Session::subscribe` | One socket, many subscriptions | Long-lived process with several streams. Pay the handshake cost once. | | `WsClient::subscribe` | One socket per subscription | One-shot stream, short-lived task, or trivial demo. | #### Multiplexed `Session` ```rust use { anyhow::Result, dango_sdk::{SubscribeBlock, SubscribeTrades, WsClient, subscribe_block, subscribe_trades}, futures::StreamExt, }; #[tokio::main] async fn main() -> Result<()> { let session = WsClient::new("wss://api-mainnet.dango.zone/graphql")? .connect() .await?; let mut blocks = session .subscribe::(subscribe_block::Variables {}) .await?; let mut trades = session .subscribe::(subscribe_trades::Variables { base_denom: "dango".into(), quote_denom: "bridge/usdc".into(), }) .await?; loop { tokio::select! { Some(item) = blocks.next() => println!("block: {item:?}"), Some(item) = trades.next() => println!("trade: {item:?}"), else => break, } } Ok(()) } ``` The `Session` and every clone close together. The connection drops when the last clone *and* every derived stream have been dropped — see [Session](../api/clients/Session). #### Dedicated `WsClient::subscribe` ```rust use { anyhow::Result, dango_sdk::{SubscribeBlock, WsClient, subscribe_block}, futures::StreamExt, }; #[tokio::main] async fn main() -> Result<()> { let ws = WsClient::new("wss://api-mainnet.dango.zone/graphql")?; let mut stream = ws .subscribe::(subscribe_block::Variables {}) .await?; while let Some(item) = stream.next().await { println!("{item:?}"); } Ok(()) } ``` Convenience: `connect` + `subscribe` + drop session when the stream ends. ### Sugar: `SubscriptionVariables` Each of the 13 codegen `Variables` types implements [`SubscriptionVariables`](../api/subscriptions/SubscriptionVariables), letting you call `vars.subscribe(&ws)` instead of `ws.subscribe::(vars)`: ```rust use { dango_sdk::{SubscriptionVariables, WsClient, subscribe_block}, futures::StreamExt, }; let ws = WsClient::new("wss://api-mainnet.dango.zone/graphql")?; let mut stream = subscribe_block::Variables {}.subscribe(&ws).await?; while let Some(item) = stream.next().await { println!("{item:?}"); } // Ok::<(), anyhow::Error>(()) ``` ### Stream item shape ```rust type Item = Result, dango_sdk::WsError>; ``` * `Err(WsError)` — transport-level or subscription-protocol failure. Inspect the variant; some are terminal. * `Ok(Response { data, errors, .. })` — a server-side payload. GraphQL errors live in `errors`; `data` may be `None` if every selection errored. ```rust while let Some(item) = stream.next().await { match item { Ok(resp) => { if let Some(errors) = resp.errors { eprintln!("graphql: {errors:?}"); } if let Some(data) = resp.data { println!("{data:?}"); } } Err(err) => { eprintln!("ws: {err}"); break; } } } // Ok::<(), anyhow::Error>(()) ``` ### Limits The server caps each connection at 30 concurrent subscriptions. When you need more, shard across multiple `WsClient`/`Session` pairs. :::warning subscription streams are `Send` but not `Sync`. Move them into a single task — don't share by reference across threads. ::: ### Next * [Rate limits and quotas](./rate-limits) — server-side limits including the 30/WS cap. * [Session](../api/clients/Session) — lifecycle and `Drop` semantics. import { Step, Steps } from 'vocs/components' ## Transactions **What this teaches:** the full path from `Message` to confirmed transaction, using only `dango-sdk` primitives. :::warning[DEX currently disabled] The Dango DEX is currently disabled. Calls described on this page will not execute on the live network until the DEX is enabled. ::: ### Mental model The Rust SDK does not ship Dango action helpers — there is no `client.transfer(...)` or `client.submit_order(...)`. Transactions are built by hand: Build a `NonEmpty>`. This is where the Dango contract knowledge lives — encode the contract message yourself (`Message::execute`, `Message::transfer`, …). Call [`SingleSigner::sign_transaction`](../api/clients/SingleSigner) to produce a `Tx`. Submit via `BroadcastClient::broadcast_tx` on `HttpClient`. Wait for inclusion with `SearchTxClient::search_tx`. ### Compose Most flows use `Message::transfer` (bank send) or `Message::execute` (smart contract call). ```rust use { grug::{Addr, Coins, Message, NonEmpty}, std::str::FromStr, }; let recipient = Addr::from_str("0x1111111111111111111111111111111111111111")?; let coins = Coins::one("bridge/usdc", 100_000_000_u128)?; // 100 USDC in base units let messages = NonEmpty::new(vec![ Message::transfer(recipient, coins)?, ])?; // Ok::<(), anyhow::Error>(()) ``` For a smart-contract call, encode the contract's typed `ExecuteMsg`: ```rust use { dango_types::dex, grug::{Coins, Message}, }; let msg = Message::execute( /* contract: */ Addr::from_str("0x...")?, /* payload: */ &dex::ExecuteMsg::SubmitOrders { /* ... */ }, /* funds: */ Coins::new(), )?; // Ok::<(), anyhow::Error>(()) ``` ### Estimate gas `HttpClient` implements `QueryClient::simulate`, which runs the transaction against the latest committed state and reports gas used: ```rust use { dango_sdk::{HttpClient, SingleSigner}, grug::{QueryClient, Signer}, }; let unsigned = signer.unsigned_transaction(messages.clone(), "dango-1")?; let outcome = http.simulate(unsigned).await?; let gas_limit = (outcome.gas_used as f64 * 1.5) as u64; // 50% buffer // Ok::<(), anyhow::Error>(()) ``` Apply your own buffer — `simulate` reports actual usage, not a recommended limit. ### Sign ```rust use grug::Signer; let tx = signer.sign_transaction(messages, "dango-1", gas_limit)?; // Ok::<(), anyhow::Error>(()) ``` `sign_transaction` consumes the current nonce and increments the in-memory value, so successive calls produce successive transactions without an extra round-trip. :::warning the nonce is incremented even when broadcast eventually fails. After a broadcast failure, call `SequencedSigner::update_nonce` to resync against the chain. ::: ### Broadcast ```rust use grug::BroadcastClient; let result = http.broadcast_tx(tx).await?; println!("hash={}", result.tx_hash); // Ok::<(), anyhow::Error>(()) ``` Broadcast is `tx-sync`: the node accepts or rejects the transaction (mempool decision) but does not wait for inclusion. Poll for the receipt next. ### Wait for inclusion ```rust use { grug::SearchTxClient, std::time::Duration, tokio::time::sleep, }; let outcome = loop { match http.search_tx(result.tx_hash).await { Ok(o) => break o, Err(_) => sleep(Duration::from_millis(500)).await, } }; println!("included at height {}, gas {}", outcome.height, outcome.outcome.gas_used); // Ok::<(), anyhow::Error>(()) ``` Wrap this loop in a bounded retry helper (`tokio-retry`, `backon`, …) — the SDK does not ship one. ### Putting it together ```rust use { anyhow::Result, dango_sdk::{HttpClient, Secp256k1, Secret, SingleSigner}, grug::{Addr, BroadcastClient, Coins, Message, NonEmpty, QueryClient, SearchTxClient, Signer}, std::{str::FromStr, time::Duration}, tokio::time::sleep, }; #[tokio::main] async fn main() -> Result<()> { let http = HttpClient::new("https://api-mainnet.dango.zone")?; let secret = Secp256k1::from_bytes([0u8; 32])?; let addr = Addr::from_str("0x0000000000000000000000000000000000000000")?; let mut signer = SingleSigner::new(addr, secret) .with_query_user_index(&http).await? .with_query_nonce(&http).await?; let recipient = Addr::from_str("0x1111111111111111111111111111111111111111")?; let messages = NonEmpty::new(vec![ Message::transfer(recipient, Coins::one("bridge/usdc", 100_000_000_u128)?)?, ])?; let unsigned = signer.unsigned_transaction(messages.clone(), "dango-1")?; let estimated = http.simulate(unsigned).await?.gas_used as u64; let tx = signer.sign_transaction(messages, "dango-1", estimated * 3 / 2)?; let result = http.broadcast_tx(tx).await?; loop { match http.search_tx(result.tx_hash).await { Ok(o) => { println!("included at {}", o.height); break; } Err(_) => sleep(Duration::from_millis(500)).await, } } Ok(()) } ``` ### Next * [Subscriptions](./subscriptions) — react to chain events instead of polling. * [Error handling](./error-handling) — distinguish broadcast-rejection, execution failure, and transport error. ## PageInfo Cursor metadata for indexer GraphQL connections. ### Definition ```rust #[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] #[serde(rename_all = "camelCase")] pub struct PageInfo { pub start_cursor: Option, pub end_cursor: Option, pub has_next_page: bool, pub has_previous_page: bool, } ``` Re-exported at the `dango_sdk` crate root from `indexer_graphql_types`. ### Fields **`start_cursor`** — `Option`. Cursor of the first node in this page. `None` when the page is empty. **`end_cursor`** — `Option`. Cursor of the last node. Pass as `after:` to fetch the next page. **`has_next_page`** — `bool`. `true` when more pages exist after this one (forward). **`has_previous_page`** — `bool`. `true` when pages exist before this one (backward). ### Construction ```rust use dango_sdk::PageInfo; let pi = PageInfo { start_cursor: Some("cursor:0".into()), end_cursor: Some("cursor:100".into()), has_next_page: true, has_previous_page: false, }; let _ = pi; ``` The codegen connection types — `accounts::AccountsAccounts`, `transfers::TransfersTransfers`, … — each have their own `PageInfo` field with the same shape. `paginate_all` accepts a closure that converts the per-query `PageInfo` into this common type so the pagination loop is type-agnostic. ### Notes * Connections follow the Relay cursor spec. Cursors are opaque strings; don't parse them. * Forward pagination uses `end_cursor` + `after:`. Backward uses `start_cursor` + `before:`. ### See also * [`paginate_all`](../methods/http-client/paginate_all). * [Concepts: Encoding and types](../../concepts/encoding-and-types) — connection shape. ## Secret A trait abstraction over a private key that can sign Dango transactions. ### Definition ```rust pub trait Secret: Sized { type Private; type Public; type Signature; fn new_random() -> Self { Self::from_rng(&mut OsRng) } fn from_rng(rng: &mut impl CryptoRngCore) -> Self; fn from_bytes(bytes: Self::Private) -> anyhow::Result; fn from_mnemonic(mnemonic: &Mnemonic, coin_type: usize) -> anyhow::Result; fn private_key(&self) -> Self::Private; fn public_key(&self) -> Self::Public; fn key(&self) -> dango_types::auth::Key; fn key_hash(&self) -> grug::Hash256; fn sign_transaction( &self, sign_doc: dango_types::auth::SignDoc, ) -> anyhow::Result; } ``` `new_random` is provided; everything else is implementor-defined. ### Implementors #### `Secp256k1` ```rust pub struct Secp256k1 { /* k256 SigningKey */ } ``` * `type Private = [u8; 32]` * `type Public = [u8; 33]` (compressed) * `type Signature = [u8; 64]` * Signs `sign_doc.to_sign_data()` (SHA-256 of the canonical encoding) with raw secp256k1. * `key()` returns `Key::Secp256k1(public_key)`. * `key_hash()` is `sha256(public_key)`. #### `Eip712` ```rust pub struct Eip712 { inner: Secp256k1, pub address: eth_utils::Address, } ``` * Same `Private`/`Public` shapes as `Secp256k1`. * `type Signature = [u8; 65]` (signature + 1-byte recovery id). * Signs the `SignDoc` as EIP-712 typed data with domain `{ name: "dango", chain_id: EIP155_CHAIN_ID, verifying_contract: }`. * `key()` returns `Key::Ethereum(self.address)`. * `key_hash()` is `sha256(Addr::from(self.address).to_string().as_bytes())` — the *string* form of the address, not the raw bytes. This differs from `Secp256k1::key_hash`, which hashes the compressed pubkey directly. * `impl From for Eip712` — derives the Ethereum address from the verifying key. > Secp256r1 is not implemented. The source has a `TODO` for it. ### Example ```rust use { anyhow::Result, bip32::Mnemonic, dango_sdk::{DEFAULT_DERIVATION_PATH, Eip712, Secp256k1, Secret}, }; fn make() -> Result<()> { // From the system RNG. let _: Secp256k1 = Secp256k1::new_random(); // From raw bytes. let _: Secp256k1 = Secp256k1::from_bytes([0u8; 32])?; // From a BIP-39 mnemonic at BIP-44 path m/44'/60'/0'/0/0. let mnemonic = Mnemonic::new( "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", Default::default(), ).map_err(|_| anyhow::anyhow!("bad mnemonic"))?; let _: Secp256k1 = Secp256k1::from_mnemonic(&mnemonic, 60)?; // Ethereum-flavoured. let _: Eip712 = Eip712::new_random(); println!("default derivation path: {DEFAULT_DERIVATION_PATH}"); Ok(()) } ``` ### Notes * `from_mnemonic` uses BIP-44 path `m/44'/{coin_type}'/0'/0/0` with an empty seed password (consistent with Terra Station and Keplr). * `Secret` is implemented privately for the two concrete types only — external implementors are possible in principle but the SDK does not expose helpers for them. ### See also * [`Keystore`](../clients/Keystore) — encrypted on-disk storage for the private key bytes. * [`SingleSigner`](../clients/SingleSigner) — uses `Secret` to sign transactions. * [Concepts: Signers and authentication](../../concepts/signers-and-authentication). ## SubscriptionStream A type alias for the pinned, boxed `Stream` returned by every subscription call. ### Definition ```rust pub type SubscriptionStream = Pin, WsError>> + Send>>; ``` `Response` is `graphql_client::Response`, with `data: Option` and `errors: Option>`. `WsError` is the SDK's WebSocket error enum. ### Construction `SubscriptionStream` is produced by: * [`WsClient::subscribe`](../methods/ws-client/subscribe) — dedicated connection. * [`Session::subscribe`](../methods/session/subscribe) — multiplexed. * [`SubscriptionVariables::subscribe`](./SubscriptionVariables) — sugar over `WsClient::subscribe`. There is no public constructor outside the SDK. ### Example ```rust use { anyhow::Result, dango_sdk::{SubscribeBlock, WsClient, WsError, subscribe_block}, futures::StreamExt, }; #[tokio::main] async fn main() -> Result<()> { let ws = WsClient::new("wss://api-mainnet.dango.zone/graphql")?; let mut stream = ws .subscribe::(subscribe_block::Variables {}) .await?; while let Some(item) = stream.next().await { match item { Ok(resp) => println!("data: {:?}", resp.data), Err(WsError::Closed(reason)) => { eprintln!("closed: {reason}"); break; } Err(err) => eprintln!("ws: {err}"), } } Ok(()) } ``` ### Notes * `Send` but **not** `Sync`. Move the stream into a single task; do not share by reference across threads. * Already pinned and boxed — no need for an outer `Box::pin` when consuming with `StreamExt::next`. * Dropping the stream sends a `Complete` to the server and frees the slot in the 30-per-connection cap. * On `WsError::Closed` or `WsError::Transport`, the stream terminates. `WsError::Subscription` terminates this stream but the parent [`Session`](../clients/Session) keeps running for other subscriptions. ### See also * [`WsError`](../errors/WsError). * [`WsClient::subscribe`](../methods/ws-client/subscribe). * [`Session::subscribe`](../methods/session/subscribe). ## SubscriptionVariables Helper trait for subscription variables with an associated subscription type. Lets users call `vars.subscribe(&ws)` instead of `ws.subscribe::(vars)`. ### Definition ```rust pub trait SubscriptionVariables: Variables { fn subscribe( self, client: &WsClient, ) -> impl std::future::Future< Output = Result< SubscriptionStream<<::Query as GraphQLQuery>::ResponseData>, anyhow::Error, >, > + Send where Self: Sized + Unpin + Send + Sync + 'static, ::Query: Unpin + Send + Sync + 'static, <::Query as GraphQLQuery>::ResponseData: DeserializeOwned + Unpin + Send + Sync + 'static; } ``` The provided method calls `client.subscribe::(self)`. ### Example ```rust use { anyhow::Result, dango_sdk::{SubscriptionVariables, WsClient, subscribe_block}, futures::StreamExt, }; #[tokio::main] async fn main() -> Result<()> { let ws = WsClient::new("wss://api-mainnet.dango.zone/graphql")?; let mut stream = subscribe_block::Variables {}.subscribe(&ws).await?; while let Some(item) = stream.next().await { println!("{item:?}"); } Ok(()) } ``` ### Implementations `SubscriptionVariables` is implemented for the 13 codegen `Variables` types under `indexer_graphql_types`. Each variant maps to a `GraphQLQuery` marker struct (`SubscribeBlock`, `SubscribeAccounts`, …) via `impl Variables for ::Variables`. | Variant module | Query type | What it streams | | ------------------------------ | --------------------------- | ------------------------------------------ | | `subscribe_block` | `SubscribeBlock` | Newly finalized blocks. | | `subscribe_accounts` | `SubscribeAccounts` | Account lifecycle events. | | `subscribe_transfers` | `SubscribeTransfers` | Bank transfers. | | `subscribe_transactions` | `SubscribeTransactions` | Transactions as they are included. | | `subscribe_messages` | `SubscribeMessages` | Per-message records from new transactions. | | `subscribe_events` | `SubscribeEvents` | Indexer events filtered by type/contract. | | `subscribe_event_by_addresses` | `SubscribeEventByAddresses` | Events filtered by a list of addresses. | | `subscribe_candles` | `SubscribeCandles` | DEX candles for a pair. | | `subscribe_perps_candles` | `SubscribePerpsCandles` | Perps candles for a pair. | | `subscribe_trades` | `SubscribeTrades` | DEX fills for a pair. | | `subscribe_perps_trades` | `SubscribePerpsTrades` | Perps fills for a pair. | | `subscribe_query_app` | `SubscribeQueryApp` | A streaming `query_app` snapshot. | | `subscribe_query_store` | `SubscribeQueryStore` | A streaming `query_store` snapshot. | | `subscribe_query_status` | `SubscribeQueryStatus` | Node status snapshots. | Each `Variables` struct derives `Default`, so partial construction is idiomatic — `subscribe_events::Variables::default()`. Most subscription schemas only expose pagination and `sort_by` arguments; filter on the streamed payload client-side. ### Notes * The trait bound mirrors `WsClient::subscribe`: `Send + Sync + 'static`, plus `Unpin` for the variables and `DeserializeOwned` for the response data. Every codegen type satisfies these out of the box. * This is sugar only. The underlying call path is identical to `client.subscribe::(vars)`. Use whichever reads better. * Calls `WsClient::subscribe` — each invocation opens a dedicated connection. To multiplex multiple subscriptions over a single socket, use [`Session::subscribe`](../methods/session/subscribe) directly. ### See also * [`WsClient::subscribe`](../methods/ws-client/subscribe). * [`SubscriptionStream`](./SubscriptionStream). * [Concepts: Subscriptions](../../concepts/subscriptions). * [Concepts: Encoding and types](../../concepts/encoding-and-types) — the underlying `Variables` trait used by codegen. ## WsClient::connect Open a WebSocket connection and return a [`Session`](../../clients/Session) that can host multiple concurrent subscriptions. ### Signature ```rust pub async fn connect(&self) -> Result; ``` ### Example ```rust use { anyhow::Result, dango_sdk::{Session, WsClient}, }; #[tokio::main] async fn main() -> Result<()> { let session: Session = WsClient::new("wss://api-mainnet.dango.zone/graphql")? .connect() .await?; let _ = session; Ok(()) } ``` ### Returns **`Session`** — a multiplexed session over the new connection. Cloning is cheap. The connection closes when the last `Session` clone and every derived stream have been dropped. ### Notes * Performs the `connection_init`/`connection_ack` handshake before returning. * Spawns a background `tokio` task that drives the socket (handles subscribe commands, pings, server messages). Requires a multi-threaded runtime — `tokio::spawn` is called internally. * Errors: * `WebSocket connection failed: …` — TLS handshake or TCP error. * `failed to send connection_init: …` — initial write failed. * `unexpected message before connection_ack: …` — server replied off-protocol. * `WebSocket closed before connection_ack` — server hung up during handshake. ### See also * [`Session::subscribe`](../session/subscribe) — open a subscription on the session. * [`subscribe`](./subscribe) — one-shot dedicated connection. * [Concepts: Subscriptions](../../../concepts/subscriptions) — multiplexed vs dedicated. ## WsClient::from\_http\_url Construct a `WsClient` from an HTTP URL, rewriting the scheme. ### Signature ```rust pub fn from_http_url(url: impl Into) -> Result; ``` ### Example ```rust use {anyhow::Result, dango_sdk::WsClient}; fn main() -> Result<()> { // https:// -> wss:// let prod = WsClient::from_http_url("https://api-mainnet.dango.zone/graphql")?; // http:// -> ws:// let dev = WsClient::from_http_url("http://localhost:8080/graphql")?; let _ = (prod, dev); Ok(()) } ``` ### Parameters **`url`** — `impl Into`. An `http://`, `https://`, `ws://`, or `wss://` URL. ### Returns **`WsClient`** — same as [`new`](./new), but with HTTP schemes rewritten. ### Notes * `http://` → `ws://`, `https://` → `wss://`. WebSocket schemes pass through unchanged. * Any other scheme errors with `Invalid URL scheme: …`. ### See also * [`new`](./new) — accept WebSocket schemes directly. ## WsClient::new Construct a `WsClient` from a `ws://` or `wss://` URL. ### Signature ```rust pub fn new(url: impl Into) -> Result; ``` ### Example ```rust use {anyhow::Result, dango_sdk::WsClient}; fn main() -> Result<()> { let client = WsClient::new("wss://api-mainnet.dango.zone/graphql")?; let _ = client; Ok(()) } ``` ### Parameters **`url`** — `impl Into`. A WebSocket URL. Must begin with `ws://` or `wss://`. ### Returns **`WsClient`** — a cheap config holder. No network call is performed. ### Notes * Returns `Err(anyhow!("Invalid URL scheme: …"))` for any non-WebSocket scheme. * Returns a `url::ParseError` (wrapped) when the URL is malformed. ### See also * [`from_http_url`](./from_http_url) — accept `http://` / `https://` and rewrite the scheme. * [`connect`](./connect) — open a connection. ## WsClient::subscribe Open a dedicated WebSocket connection and start a single subscription. ### Signature ```rust pub async fn subscribe( &self, variables: Q::Variables, ) -> Result, anyhow::Error> where Q: GraphQLQuery + Unpin + Send + Sync + 'static, Q::Variables: Unpin + Send + Sync + 'static, Q::ResponseData: DeserializeOwned + Unpin + Send + Sync + 'static; ``` ### Example ```rust use { anyhow::Result, dango_sdk::{SubscribeTrades, WsClient, subscribe_trades}, futures::StreamExt, }; #[tokio::main] async fn main() -> Result<()> { let client = WsClient::new("wss://api-mainnet.dango.zone/graphql")?; let mut stream = client .subscribe::(subscribe_trades::Variables { base_denom: "dango".into(), quote_denom: "bridge/usdc".into(), }) .await?; while let Some(item) = stream.next().await { match item { Ok(resp) => println!("trade: {resp:?}"), Err(err) => { eprintln!("ws: {err}"); break; } } } Ok(()) } ``` ### Parameters **`variables`** — `Q::Variables`. Typed variables for the subscription query. Each generated subscription has its own snake-case module with a `Variables` struct. **Type parameter `Q`** — the `GraphQLQuery` marker struct for the subscription (e.g. `SubscribeTrades`, `SubscribeBlock`). ### Returns **`SubscriptionStream`** — a boxed `Stream` of `Result, WsError>`. The connection closes when the stream is dropped. ### Notes * Convenience wrapper over [`connect`](./connect) + [`Session::subscribe`](../session/subscribe). The session is owned by the stream and dropped together. * Each call opens a new socket. Prefer `connect` + `Session::subscribe` for multi-stream programs. * Stream items are typed via the codegen `ResponseData`. See [Concepts: Encoding and types](../../../concepts/encoding-and-types) for the shape. ### See also * [`connect`](./connect) — open a multiplexed session. * [`SubscriptionStream`](../../subscriptions/SubscriptionStream) — the returned type. * [`SubscriptionVariables`](../../subscriptions/SubscriptionVariables) — sugar that flips the call order. ## SingleSigner::new Construct a bare `SingleSigner` with both `user_index` and `nonce` undefined. ### Signature ```rust impl SingleSigner, Undefined> { pub fn new(address: Addr, secret: S) -> Self; } ``` ### Example ```rust use { anyhow::Result, dango_sdk::{Secp256k1, Secret, SingleSigner}, grug::Addr, std::str::FromStr, }; fn build() -> Result> { let address = Addr::from_str("0x0000000000000000000000000000000000000000")?; let secret = Secp256k1::new_random(); Ok(SingleSigner::new(address, secret)) } ``` ### Parameters **`address`** — `Addr`. The on-chain account address. **`secret`** — `S: Secret`. The private key. ### Returns **`SingleSigner, Undefined>`** — fill both slots with `with_user_index`/`with_nonce` (or their `with_query_*` async variants) before signing. ### Notes * This is the canonical entry point when the caller already knows the address. To discover an address from a key, use [`new_first_address_available`](./new_first_address_available). * Sets `user_index` and `nonce` to `Undefined::new()`. The compiler refuses `sign_transaction` until both are `Defined`. ### See also * [`with_user_index`](./with_user_index) / [`with_query_user_index`](./with_query_user_index). * [`with_nonce`](./with_nonce) / [`with_query_nonce`](./with_query_nonce). * [`SingleSigner`](../../clients/SingleSigner) — overview. ## SingleSigner::new\_first\_address\_available Construct a `SingleSigner` by discovering the first account associated with a `Secret`'s `key_hash` via the account factory. ### Signature ```rust impl SingleSigner, Undefined> { pub async fn new_first_address_available( client: &C, secret: S, cfg: Option<&AppConfig>, ) -> anyhow::Result where C: QueryClient, anyhow::Error: From; } ``` ### Example ```rust use { anyhow::Result, dango_sdk::{HttpClient, Secp256k1, Secret, SingleSigner}, }; #[tokio::main] async fn main() -> Result<()> { let client = HttpClient::new("https://api-mainnet.dango.zone")?; let secret = Secp256k1::from_bytes([0u8; 32])?; let signer = SingleSigner::new_first_address_available(&client, secret, None) .await? .with_query_nonce(&client) .await?; println!("address: {}", signer.address); Ok(()) } ``` ### Parameters **`client`** — `&C` implementing `QueryClient`. The query client used to read the account factory. **`secret`** — `S: Secret`. The private key. Only `secret.key_hash()` is read. **`cfg`** — `Option<&AppConfig>`. Optional cached app config. When `None`, fetches it via `query_app_config`. ### Returns **`SingleSigner, Undefined>`** — fill in the nonce via [`with_nonce`](./with_nonce) or [`with_query_nonce`](./with_query_nonce) before signing. ### Notes * Calls `QueryForgotUsernameRequest { key_hash, start_after: None, limit: Some(1) }` against the account factory; takes the first match regardless of forgotten-username state. * Errors with `"no user index found for key hash …"` when no account references the key. * Resolves the master account via `QueryUserRequest(UserIndexOrName::Index(...))`. * Pass `cfg = Some(&cached)` to skip the `query_app_config` round-trip when reusing the same node across multiple calls. ### See also * [`new`](./new) — when the address is already known. * [`SingleSigner`](../../clients/SingleSigner) — typestate overview. ## SingleSigner::query\_next\_nonce Fetch the next unused nonce for this signer's account. ### Signature ```rust pub async fn query_next_nonce(&self, client: &C) -> anyhow::Result where C: QueryClient, anyhow::Error: From; ``` Available on every state of `SingleSigner`. ### Example ```rust use { anyhow::Result, dango_sdk::{HttpClient, Secp256k1, SingleSigner}, grug::Addr, std::str::FromStr, }; #[tokio::main] async fn main() -> Result<()> { let http = HttpClient::new("https://api-mainnet.dango.zone")?; let signer = SingleSigner::new( Addr::from_str("0x0000000000000000000000000000000000000000")?, Secp256k1::new_random(), ); let nonce = signer.query_next_nonce(&http).await?; println!("next nonce: {nonce}"); Ok(()) } ``` ### Parameters **`client`** — `&C` implementing `QueryClient`. ### Returns **`Nonce`** — `last_seen_nonce + 1`, or `0` if the account has never signed a transaction. ### Notes * Reads `QuerySeenNoncesRequest` on the account contract. * Borrowing variant. To flip the typestate, use [`with_query_nonce`](./with_query_nonce). ### See also * [`with_query_nonce`](./with_query_nonce). * [`update_nonce`](./update_nonce) — refresh the in-memory nonce after a broadcast failure. ## SingleSigner::query\_nonce Fetch the next unused nonce. Trait method of `SequencedSigner` (from `dango_types::signer`). ### Signature ```rust #[async_trait] impl SequencedSigner for SingleSigner> where S: Secret + Send + Sync, { async fn query_nonce(&self, client: &C) -> anyhow::Result where C: QueryClient, anyhow::Error: From; } ``` ### Example ```rust use { anyhow::Result, dango_sdk::{HttpClient, SingleSigner}, dango_types::signer::SequencedSigner, }; async fn check_nonce( http: &HttpClient, signer: &SingleSigner, ) -> Result<()> { let on_chain = signer.query_nonce(http).await?; println!("on-chain: {on_chain}, in-memory: {}", signer.nonce()); Ok(()) } ``` ### Parameters **`client`** — `&C` implementing `QueryClient`. ### Returns **`Nonce`** — same value as [`query_next_nonce`](./query_next_nonce); this is the `SequencedSigner` adapter. ### Notes * Only available when `nonce` is `Defined` (the trait bound `SequencedSigner` requires it). * Identical body to `query_next_nonce`; exposed via the trait so generic code over `SequencedSigner` can reach it. ### See also * [`query_next_nonce`](./query_next_nonce) — equivalent non-trait method. * [`update_nonce`](./update_nonce) — refresh `self.nonce` in place. ## SingleSigner::query\_user\_index Fetch this signer's `UserIndex` from the account factory. ### Signature ```rust pub async fn query_user_index(&self, client: &C) -> anyhow::Result where C: QueryClient, anyhow::Error: From; ``` Available on every state of `SingleSigner`. ### Example ```rust use { anyhow::Result, dango_sdk::{HttpClient, Secp256k1, SingleSigner}, grug::Addr, std::str::FromStr, }; #[tokio::main] async fn main() -> Result<()> { let http = HttpClient::new("https://api-mainnet.dango.zone")?; let signer = SingleSigner::new( Addr::from_str("0x0000000000000000000000000000000000000000")?, Secp256k1::new_random(), ); let idx = signer.query_user_index(&http).await?; println!("user index: {idx:?}"); Ok(()) } ``` ### Parameters **`client`** — `&C` implementing `QueryClient`. ### Returns **`UserIndex`** — the index returned by `QueryAccountRequest` for this signer's address. ### Notes * Resolves the account factory via `query_app_config::()`, then `query_wasm_smart` for `QueryAccountRequest`. * Borrowing variant; does not consume `self`. To flip the typestate, use [`with_query_user_index`](./with_query_user_index). ### See also * [`with_query_user_index`](./with_query_user_index). * [`query_next_nonce`](./query_next_nonce). ## SingleSigner::sign\_transaction Sign a set of messages and produce a broadcast-ready `Tx`. ### Signature ```rust impl Signer for SingleSigner { fn sign_transaction( &mut self, msgs: NonEmpty>, chain_id: &str, gas_limit: u64, ) -> grug::StdResult; } ``` From `impl Signer` (requires `Defined + Defined`). ### Example ```rust use { anyhow::Result, dango_sdk::{HttpClient, SingleSigner}, grug::{BroadcastClient, Coins, Message, NonEmpty, Signer}, }; async fn submit( http: &HttpClient, signer: &mut SingleSigner, ) -> Result<()> { let messages = NonEmpty::new(vec![ Message::transfer(grug::Addr::mock(1), Coins::one("bridge/usdc", 100_u128)?)?, ])?; let tx = signer.sign_transaction(messages, "dango-1", 1_000_000)?; let _ = http.broadcast_tx(tx).await?; Ok(()) } ``` ### Parameters **`msgs`** — `NonEmpty>`. The messages to include. **`chain_id`** — `&str`. Target chain id. **`gas_limit`** — `u64`. Maximum gas the transaction is allowed to consume. ### Returns **`Tx`** — `sender`, `msgs`, `gas_limit`, JSON-encoded metadata, and a JSON-encoded `Credential::Standard { key_hash, signature }`. ### Notes * **Side effect:** `*self.nonce.inner_mut() += 1`. The in-memory nonce is advanced even though the method takes `&mut self`. Successive calls produce successive transactions without re-querying. * On broadcast failure, the in-memory nonce is now ahead of the chain. Call `SequencedSigner::update_nonce` (or [`query_next_nonce`](./query_next_nonce) + [`with_nonce`](./with_nonce)) to resync. * `Eip712` signatures are EIP-712 typed-data with domain `{ name: "dango", chain_id: EIP155_CHAIN_ID, verifying_contract: }`. * Errors are `StdError` from JSON encoding or the `Secret::sign_transaction` call. ### See also * [`unsigned_transaction`](./unsigned_transaction) — same metadata, no signature. * [`update_nonce`](./update_nonce) — resync after broadcast failure. * [Concepts: Transactions](../../../concepts/transactions) — submit-and-poll flow. ## SingleSigner::unsigned\_transaction Build the `UnsignedTx` for a set of messages without producing a signature. ### Signature ```rust impl Signer for SingleSigner { fn unsigned_transaction( &self, msgs: NonEmpty>, chain_id: &str, ) -> grug::StdResult; } ``` From `impl Signer` (requires `Defined + Defined`). ### Example ```rust use { anyhow::Result, dango_sdk::{HttpClient, SingleSigner}, grug::{Coins, Message, NonEmpty, QueryClient, Signer}, }; async fn estimate( http: &HttpClient, signer: &SingleSigner, ) -> Result { let messages = NonEmpty::new(vec![ Message::transfer(grug::Addr::mock(0), Coins::one("bridge/usdc", 1_u128)?)?, ])?; let unsigned = signer.unsigned_transaction(messages, "dango-1")?; Ok(http.simulate(unsigned).await?.gas_used as u64) } ``` ### Parameters **`msgs`** — `NonEmpty>`. The messages to include in the transaction. **`chain_id`** — `&str`. The target chain id (e.g. `"dango-1"`). ### Returns **`UnsignedTx`** — `sender`, `msgs`, and a JSON-encoded `Metadata { chain_id, user_index, nonce, expiry: None }`. ### Notes * The metadata uses the signer's current `user_index` and `nonce` — values that `sign_transaction` would consume. * Pair with `QueryClient::simulate` for gas estimation; the simulation runs without nonce consumption. ### See also * [`sign_transaction`](./sign_transaction) — produce a signed `Tx`. * [`HttpClient::simulate`](../http-client/simulate). ## SingleSigner::update\_nonce Refresh `self.nonce` from the chain. Trait method of `SequencedSigner`. ### Signature ```rust #[async_trait] impl SequencedSigner for SingleSigner> where S: Secret + Send + Sync, { async fn update_nonce(&mut self, client: &C) -> anyhow::Result<()> where C: QueryClient, anyhow::Error: From; } ``` ### Example ```rust use { anyhow::Result, dango_sdk::{HttpClient, SingleSigner}, dango_types::signer::SequencedSigner, }; async fn resync( http: &HttpClient, signer: &mut SingleSigner, ) -> Result<()> { signer.update_nonce(http).await } ``` ### Parameters **`client`** — `&C` implementing `QueryClient`. ### Returns **`()`** — on success, `self.nonce` is set to the value returned by [`query_next_nonce`](./query_next_nonce). ### Notes * Use after a broadcast failure to bring the in-memory nonce back in sync with the chain (`sign_transaction` increments unconditionally). * Use after a process restart that resumed signing with a stale nonce. ### See also * [`query_nonce`](./query_nonce) — read-only counterpart. * [`sign_transaction`](./sign_transaction) — the mutation that this resyncs. ## SingleSigner::with\_nonce Consume `self` and return a `SingleSigner` with `nonce` defined. ### Signature ```rust impl> SingleSigner> { pub fn with_nonce(self, nonce: Nonce) -> SingleSigner>; } ``` ### Example ```rust use { anyhow::Result, dango_sdk::{Secp256k1, SingleSigner}, grug::Addr, std::str::FromStr, }; fn build() -> Result> { let signer = SingleSigner::new( Addr::from_str("0x0000000000000000000000000000000000000000")?, Secp256k1::new_random(), ) .with_nonce(0); Ok(signer) } ``` ### Parameters **`nonce`** — `Nonce`. The next unused nonce for this account. ### Returns **`SingleSigner>`** — the same signer with `nonce` defined. ### Notes * Use `0` for a brand-new account that has never signed a transaction. Otherwise call [`with_query_nonce`](./with_query_nonce) (or [`query_next_nonce`](./query_next_nonce) directly). * A signer with `Defined + Defined` implements `Signer`. ### See also * [`with_query_nonce`](./with_query_nonce) — fetch the next nonce from the chain. * [`query_next_nonce`](./query_next_nonce). ## SingleSigner::with\_query\_nonce Fetch the next unused nonce from the account and return a `SingleSigner` with `nonce` defined. ### Signature ```rust impl> SingleSigner> { pub async fn with_query_nonce( self, client: &C, ) -> anyhow::Result>> where C: QueryClient, anyhow::Error: From; } ``` ### Example ```rust use { anyhow::Result, dango_sdk::{HttpClient, Secp256k1, SingleSigner}, grug::Addr, std::str::FromStr, }; #[tokio::main] async fn main() -> Result<()> { let http = HttpClient::new("https://api-mainnet.dango.zone")?; let signer = SingleSigner::new( Addr::from_str("0x0000000000000000000000000000000000000000")?, Secp256k1::new_random(), ) .with_query_user_index(&http).await? .with_query_nonce(&http).await?; let _ = signer; Ok(()) } ``` ### Parameters **`client`** — `&C` implementing `QueryClient`. ### Returns **`SingleSigner>`** — the same signer with the nonce populated from the chain. ### Notes * Reads `QuerySeenNoncesRequest` on the account contract; returns `last_seen + 1`, or `0` if the account has never signed. * Errors propagate from `QueryClient::Error` via `anyhow::Error: From`. ### See also * [`with_nonce`](./with_nonce) — synchronous variant. * [`query_next_nonce`](./query_next_nonce) — same query without consuming `self`. ## SingleSigner::with\_query\_user\_index Fetch the user index from the account factory and return a `SingleSigner` with `user_index` defined. ### Signature ```rust impl> SingleSigner, N> { pub async fn with_query_user_index( self, client: &C, ) -> anyhow::Result, N>> where C: QueryClient, anyhow::Error: From; } ``` ### Example ```rust use { anyhow::Result, dango_sdk::{HttpClient, Secp256k1, SingleSigner}, grug::Addr, std::str::FromStr, }; #[tokio::main] async fn main() -> Result<()> { let http = HttpClient::new("https://api-mainnet.dango.zone")?; let signer = SingleSigner::new( Addr::from_str("0x0000000000000000000000000000000000000000")?, Secp256k1::new_random(), ) .with_query_user_index(&http) .await?; let _ = signer; Ok(()) } ``` ### Parameters **`client`** — `&C` implementing `QueryClient`. ### Returns **`SingleSigner, N>`** — the same signer with `user_index` populated from the chain. ### Notes * Wraps [`query_user_index`](./query_user_index) — same query, but consumes `self` to flip the typestate. * Errors propagate from `QueryClient::Error` via `anyhow::Error: From`. ### See also * [`with_user_index`](./with_user_index) — synchronous variant. * [`query_user_index`](./query_user_index) — same query without consuming `self`. ## SingleSigner::with\_user\_index Consume `self` and return a `SingleSigner` with `user_index` defined. ### Signature ```rust impl> SingleSigner, N> { pub fn with_user_index( self, user_index: UserIndex, ) -> SingleSigner, N>; } ``` ### Example ```rust use { anyhow::Result, dango_sdk::{Secp256k1, SingleSigner}, dango_types::account_factory::UserIndex, grug::Addr, std::str::FromStr, }; fn build() -> Result> { let signer = SingleSigner::new( Addr::from_str("0x0000000000000000000000000000000000000000")?, Secp256k1::new_random(), ) .with_user_index(42 as UserIndex); Ok(signer) } ``` ### Parameters **`user_index`** — `UserIndex`. The account-factory user index for this address. ### Returns **`SingleSigner, N>`** — the same signer with the `user_index` slot now `Defined`. The `nonce` state is preserved. ### Notes * Consumes `self`. The original signer cannot be used afterwards. * Synchronous; pairs with [`with_query_user_index`](./with_query_user_index) for the async-fetch variant. ### See also * [`with_query_user_index`](./with_query_user_index) — fetch from the account factory. * [`query_user_index`](./query_user_index) — fetch without consuming `self`. ## Session::subscribe Multiplex a new GraphQL subscription onto an existing WebSocket session. ### Signature ```rust pub async fn subscribe( &self, variables: Q::Variables, ) -> Result, anyhow::Error> where Q: GraphQLQuery + Unpin + Send + Sync + 'static, Q::Variables: Unpin + Send + Sync + 'static, Q::ResponseData: DeserializeOwned + Unpin + Send + Sync + 'static; ``` ### Example ```rust use { anyhow::Result, dango_sdk::{SubscribeBlock, SubscribeTrades, WsClient, subscribe_block, subscribe_trades}, futures::StreamExt, }; #[tokio::main] async fn main() -> Result<()> { let session = WsClient::new("wss://api-mainnet.dango.zone/graphql")? .connect() .await?; let mut blocks = session .subscribe::(subscribe_block::Variables {}) .await?; let mut trades = session .subscribe::(subscribe_trades::Variables { base_denom: "dango".into(), quote_denom: "bridge/usdc".into(), }) .await?; loop { tokio::select! { Some(item) = blocks.next() => println!("block: {item:?}"), Some(item) = trades.next() => println!("trade: {item:?}"), else => break, } } Ok(()) } ``` ### Parameters **`variables`** — `Q::Variables`. Typed variables for the subscription. **Type parameter `Q`** — the `GraphQLQuery` marker struct (e.g. `SubscribeBlock`, `SubscribeTrades`). ### Returns **`SubscriptionStream`** — a boxed `Stream` of `Result, WsError>`. Dropping the stream sends a `complete` to the server and frees the slot in the 30-per-connection limit. ### Notes * The subscription id is allocated from the session's internal `AtomicU64`. Ids are local to the session. * Returns `Err(anyhow!("session is closed"))` if the underlying background task has exited. * Multiple subscriptions on the same session run concurrently. There is no ordering guarantee across subscription ids. ### See also * [`Session`](../../clients/Session) — lifecycle and `Drop` semantics. * [`WsClient::subscribe`](../ws-client/subscribe) — dedicated-connection alternative. * [Concepts: Subscriptions](../../../concepts/subscriptions) — when to multiplex. ## Keystore::from\_file Read and decrypt a keystore file, returning the raw 32-byte private key. ### Signature ```rust pub fn from_file(filename: F, password: P) -> anyhow::Result<[u8; 32]> where F: AsRef, P: AsRef<[u8]>; ``` ### Example ```rust use { anyhow::Result, dango_sdk::{Keystore, Secp256k1, Secret}, }; fn load() -> Result { let bytes = Keystore::from_file("./key.json", "hunter2")?; Secp256k1::from_bytes(bytes) } ``` ### Parameters **`filename`** — `AsRef`. Path to the JSON keystore file. **`password`** — `AsRef<[u8]>`. Decryption password. Bytes, not necessarily UTF-8. ### Returns **`[u8; 32]`** — the raw private key bytes. Wrap with [`Secp256k1::from_bytes`](../../traits/Secret) or [`Eip712::from_bytes`](../../traits/Secret). ### Notes * The file is JSON: `{ pk, salt, nonce, ciphertext }`. PBKDF2-HMAC-SHA256 with 600 000 iterations derives the AES-256-GCM key from `password + salt`. * Wrong password produces an AES-GCM authentication-tag error; the message does not distinguish "wrong password" from "corrupted ciphertext". * File I/O errors (missing file, permission denied) propagate as `anyhow::Error`. ### See also * [`write_to_file`](./write_to_file) — counterpart. * [`Keystore`](../../clients/Keystore) — file format. * [`Secret`](../../traits/Secret) — what to wrap the bytes in. ## Keystore::write\_to\_file Encrypt a `Secret`'s private key and write a pretty-printed JSON keystore file. ### Signature ```rust pub fn write_to_file( secret: &S, filename: F, password: P, ) -> anyhow::Result where S: Secret, S::Private: AsRef<[u8]>, S::Public: Into>, F: AsRef, P: AsRef<[u8]>; ``` ### Example ```rust use { anyhow::Result, dango_sdk::{Keystore, Secp256k1, Secret}, }; fn save() -> Result { let secret = Secp256k1::new_random(); Keystore::write_to_file(&secret, "./key.json", "hunter2") } ``` ### Parameters **`secret`** — `&S: Secret`. The key to encrypt. The bounds limit this to secrets with 32-byte private keys and 33-byte compressed public keys — `Secp256k1` and `Eip712` both qualify. **`filename`** — `AsRef`. Output path. The file is overwritten if present. **`password`** — `AsRef<[u8]>`. Encryption password. ### Returns **`Keystore`** — the same value that was written to disk (with freshly generated `salt` and `nonce`). ### Notes * Each call generates a fresh 16-byte salt and 12-byte nonce from `OsRng`. Saving the same key twice yields different files. * The file contains the unencrypted compressed public key alongside the ciphertext. The private key is not recoverable without the password. * File I/O errors propagate as `anyhow::Error`. ### See also * [`from_file`](./from_file) — counterpart. * [`Keystore`](../../clients/Keystore) — file format. ## broadcast\_tx Submit a signed transaction with `tx-sync` semantics: the node accepts or rejects it from the mempool but does not wait for inclusion. ### Signature ```rust async fn broadcast_tx(&self, tx: grug::Tx) -> Result; ``` From `impl BroadcastClient for HttpClient`. ### Example ```rust use { anyhow::Result, dango_sdk::{HttpClient, SingleSigner}, grug::{BroadcastClient, NonEmpty, Signer}, }; async fn submit( http: &HttpClient, signer: &mut SingleSigner, messages: NonEmpty>, gas: u64, ) -> Result { let tx = signer.sign_transaction(messages, "dango-1", gas)?; let result = http.broadcast_tx(tx).await?; Ok(result.tx_hash) } ``` ### Parameters **`tx`** — `Tx`. A signed transaction produced by `Signer::sign_transaction`. ### Returns **`BroadcastTxOutcome`** — the mempool decision. Carries `tx_hash: Hash256` and `check_tx: CheckTxOutcome` (where `CheckTxOutcome` exposes the success/error detail). Inclusion in a block is *not* guaranteed by a successful response — poll [`search_tx`](./search_tx) until the transaction appears. ### Notes * `tx-sync` only checks `CheckTx`. Execution happens later, when the block is processed. * A successful `broadcast_tx` followed by a failed `search_tx` means the transaction failed during `DeliverTx` — inspect the outcome for the error. ### See also * [`search_tx`](./search_tx) — wait for inclusion. * [`simulate`](./simulate) — estimate gas without broadcasting. * [Concepts: Transactions](../../../concepts/transactions) — full submit-and-poll flow. ## paginate\_accounts Fetch every account in the indexer's `accounts` connection, paginating forward. ### Signature ```rust pub async fn paginate_accounts( &self, page_size: i64, variables: accounts::Variables, ) -> Result, anyhow::Error>; ``` ### Example ```rust use { anyhow::Result, dango_sdk::{HttpClient, accounts}, }; #[tokio::main] async fn main() -> Result<()> { let client = HttpClient::new("https://api-mainnet.dango.zone")?; let accounts = client .paginate_accounts(100, accounts::Variables::default()) .await?; println!("{} accounts", accounts.len()); Ok(()) } ``` ### Parameters **`page_size`** — `i64`. Number of items per request. The indexer caps `first` at 100. **`variables`** — `accounts::Variables`. Query filters. Pagination fields (`after`, `before`, `first`, `last`) are overwritten by the loop; only the filter fields you set are honored. ### Returns **`Vec`** — every node in the connection, in the indexer's natural order. ### Notes * This wrapper only does forward pagination. Use [`paginate_all`](./paginate_all) for backward traversal. * Stops when `page_info.has_next_page` is false or `end_cursor` is `None`. ### See also * [`paginate_all`](./paginate_all) — generic version with closures. * [Concepts: Encoding and types](../../../concepts/encoding-and-types) — connection node shape. ## paginate\_all Paginate through every result of a GraphQL connection using cursor-based pagination. ### Signature ```rust pub async fn paginate_all( &self, first: Option, last: Option, build_variables: BuildVariables, extract_page: ExtractPage, ) -> Result, anyhow::Error> where V: Variables + Serialize + Debug, ::ResponseData: Debug, BuildVariables: Fn(Option, Option, Option, Option) -> V, ExtractPage: Fn(::ResponseData) -> (Vec, PageInfo); ``` ### Example ```rust use { anyhow::Result, dango_sdk::{HttpClient, PageInfo, accounts}, }; #[tokio::main] async fn main() -> Result<()> { let client = HttpClient::new("https://api-mainnet.dango.zone")?; let all_accounts = client .paginate_all( Some(100), None, |after, before, first, last| accounts::Variables { after, before, first, last, ..Default::default() }, |data| { let pi = data.accounts.page_info; (data.accounts.nodes, PageInfo { start_cursor: pi.start_cursor, end_cursor: pi.end_cursor, has_next_page: pi.has_next_page, has_previous_page: pi.has_previous_page, }) }, ) .await?; println!("fetched {} accounts", all_accounts.len()); Ok(()) } ``` ### Parameters **`first`** — `Option`. Page size for forward pagination. Pass `Some(n)` and `last = None`. **`last`** — `Option`. Page size for backward pagination. Pass `Some(n)` and `first = None`. **`build_variables`** — `Fn(after, before, first, last) -> V`. Builds the query variables for a single page given the cursors. **`extract_page`** — `Fn(ResponseData) -> (Vec, PageInfo)`. Pulls the node list and pagination metadata out of the typed response. ### Returns **`Vec`** — every node in the connection, concatenated across pages. Forward pagination preserves source order; backward pagination reverses each page so the final `Vec` is oldest-first. ### Notes * Exactly one of `first` or `last` must be `Some`. Other combinations error with `"paginate_all requires exactly one of `first`or`last` to be Some"`. * Each page is a fresh GraphQL call. Mind the [HTTP rate limit](../../../concepts/rate-limits) on connections with thousands of nodes. * For the indexer's standard connections (`accounts`, `transfers`, `transactions`, `blocks`, `events`, `messages`), the typed wrappers — `paginate_accounts`, etc. — are simpler. ### See also * [`paginate_accounts`](./paginate_accounts), [`paginate_transfers`](./paginate_transfers), [`paginate_transactions`](./paginate_transactions), [`paginate_blocks`](./paginate_blocks), [`paginate_events`](./paginate_events), [`paginate_messages`](./paginate_messages) — typed wrappers. * [`PageInfo`](../../types/PageInfo) — pagination metadata type. ## paginate\_blocks Fetch every record in the indexer's `blocks` connection, paginating forward. ### Signature ```rust pub async fn paginate_blocks( &self, page_size: i64, variables: blocks::Variables, ) -> Result, anyhow::Error>; ``` ### Example ```rust use { anyhow::Result, dango_sdk::{HttpClient, blocks}, }; #[tokio::main] async fn main() -> Result<()> { let client = HttpClient::new("https://api-mainnet.dango.zone")?; let blocks = client .paginate_blocks(100, blocks::Variables::default()) .await?; println!("{} blocks", blocks.len()); Ok(()) } ``` ### Parameters **`page_size`** — `i64`. Items per request. Indexer caps `first` at 100. **`variables`** — `blocks::Variables`. Filters (height range, …); pagination fields are overwritten. ### Returns **`Vec`** — every block matching the filter. ### See also * [`paginate_all`](./paginate_all) — generic, supports backward pagination. * [`query_block`](./query_block) — single block lookup by height. ## paginate\_events Fetch every record in the indexer's `events` connection, paginating forward. ### Signature ```rust pub async fn paginate_events( &self, page_size: i64, variables: events::Variables, ) -> Result, anyhow::Error>; ``` ### Example ```rust use { anyhow::Result, dango_sdk::{HttpClient, events}, }; #[tokio::main] async fn main() -> Result<()> { let client = HttpClient::new("https://api-mainnet.dango.zone")?; let evs = client .paginate_events(100, events::Variables::default()) .await?; println!("{} events", evs.len()); Ok(()) } ``` ### Parameters **`page_size`** — `i64`. Items per request. Indexer caps `first` at 100. **`variables`** — `events::Variables`. Only pagination + `sort_by` fields are exposed by the indexer schema today; filter client-side on the returned `nodes` (e.g. `node.r#type`). Pagination cursors are overwritten by the helper. ### Returns **`Vec`** — every event matching the filter. ### See also * [`paginate_all`](./paginate_all) — generic, supports backward pagination. * [`paginate_messages`](./paginate_messages) — sibling for messages. ## paginate\_messages Fetch every record in the indexer's `messages` connection, paginating forward. ### Signature ```rust pub async fn paginate_messages( &self, page_size: i64, variables: messages::Variables, ) -> Result, anyhow::Error>; ``` ### Example ```rust use { anyhow::Result, dango_sdk::{HttpClient, messages}, }; #[tokio::main] async fn main() -> Result<()> { let client = HttpClient::new("https://api-mainnet.dango.zone")?; let msgs = client .paginate_messages(100, messages::Variables::default()) .await?; println!("{} messages", msgs.len()); Ok(()) } ``` ### Parameters **`page_size`** — `i64`. Items per request. Indexer caps `first` at 100. **`variables`** — `messages::Variables`. Filters; pagination fields are overwritten. ### Returns **`Vec`** — every message matching the filter. ### See also * [`paginate_all`](./paginate_all) — generic, supports backward pagination. * [`paginate_events`](./paginate_events) — sibling for events. ## paginate\_transactions Fetch every record in the indexer's `transactions` connection, paginating forward. ### Signature ```rust pub async fn paginate_transactions( &self, page_size: i64, variables: transactions::Variables, ) -> Result, anyhow::Error>; ``` ### Example ```rust use { anyhow::Result, dango_sdk::{HttpClient, transactions}, }; #[tokio::main] async fn main() -> Result<()> { let client = HttpClient::new("https://api-mainnet.dango.zone")?; let txs = client .paginate_transactions(100, transactions::Variables { sender_address: Some("0x0000000000000000000000000000000000000000".into()), ..Default::default() }) .await?; println!("{} transactions", txs.len()); Ok(()) } ``` ### Parameters **`page_size`** — `i64`. Items per request. Indexer caps `first` at 100. **`variables`** — `transactions::Variables`. Filters; pagination fields are overwritten by the loop. ### Returns **`Vec`** — every transaction matching the filter, in the indexer's natural order. ### See also * [`paginate_all`](./paginate_all) — generic, supports backward pagination. * [`search_tx`](./search_tx) — single transaction lookup by hash. ## paginate\_transfers Fetch every record in the indexer's `transfers` connection, paginating forward. ### Signature ```rust pub async fn paginate_transfers( &self, page_size: i64, variables: transfers::Variables, ) -> Result, anyhow::Error>; ``` ### Example ```rust use { anyhow::Result, dango_sdk::{HttpClient, transfers}, }; #[tokio::main] async fn main() -> Result<()> { let client = HttpClient::new("https://api-mainnet.dango.zone")?; let transfers = client .paginate_transfers(100, transfers::Variables { from_address: Some("0x0000000000000000000000000000000000000000".into()), ..Default::default() }) .await?; println!("{} transfers", transfers.len()); Ok(()) } ``` ### Parameters **`page_size`** — `i64`. Items per request. Indexer caps `first` at 100. **`variables`** — `transfers::Variables`. Filters (sender, recipient, denom, height range, …). Pagination fields are overwritten by the loop. ### Returns **`Vec`** — every transfer matching the filter, in the indexer's natural order. ### See also * [`paginate_all`](./paginate_all) — generic, supports backward pagination. * [`paginate_accounts`](./paginate_accounts) — sibling for accounts. ## query\_app Run an app-level query against the node at a given height. ### Signature ```rust impl QueryClient for HttpClient { type Error = anyhow::Error; type Proof = grug::Proof; async fn query_app( &self, query: Query, height: Option, ) -> Result; } ``` `Query` and `QueryResponse` are re-exported by the SDK. ### Example ```rust use { anyhow::Result, dango_sdk::HttpClient, grug::{Query, QueryClient, QueryConfigRequest}, }; #[tokio::main] async fn main() -> Result<()> { let client = HttpClient::new("https://api-mainnet.dango.zone")?; let response = client.query_app(Query::Config(QueryConfigRequest {}), None).await?; println!("{response:?}"); Ok(()) } ``` For most app-level fetches, prefer the typed `QueryClientExt` helpers — `query_app_config`, `query_balance`, `query_wasm_smart`, … — that build on top of `query_app`. ### Parameters **`query`** — `Query`. The typed query enum (`Config`, `WasmSmart`, `Balance`, …). **`height`** — `Option`. Block height to query at, or `None` for the latest committed state. ### Returns **`QueryResponse`** — the variant that matches the input `Query`. Callers usually pattern-match or unwrap into the expected variant. ### Notes * Hits the GraphQL `queryApp` resolver. Errors bubble through `anyhow::Error`. * Use a fixed `height` for snapshot consistency across multiple reads. ### See also * [`query_store`](./query_store) — raw store reads. * [`simulate`](./simulate) — dry-run a transaction. * [Concepts: Encoding and types](../../../concepts/encoding-and-types) — `Query` variants. ## query\_block Fetch a block by height. Hits the REST endpoint `GET /block/info[/{height}]`. ### Signature ```rust async fn query_block(&self, height: Option) -> Result; ``` From `impl BlockClient for HttpClient`. ### Example ```rust use { anyhow::Result, dango_sdk::HttpClient, grug::BlockClient, }; #[tokio::main] async fn main() -> Result<()> { let client = HttpClient::new("https://api-mainnet.dango.zone")?; let block = client.query_block(None).await?; println!("height: {}", block.info.height); println!("chain: {}", block.info.chain_id); Ok(()) } ``` ### Parameters **`height`** — `Option`. Block height to fetch, or `None` for the latest. ### Returns **`Block`** — block info plus the list of transactions included at that height. ### Notes * REST, not GraphQL — the request goes to `/block/info` directly. Errors include the HTTP status and response body in the `anyhow::Error` message. ### See also * [`query_block_outcome`](./query_block_outcome) — the block's execution result. * [`paginate_blocks`](./paginate_blocks) — bulk block fetch with filters. ## query\_block\_outcome Fetch a block's execution outcome. Hits the REST endpoint `GET /block/result[/{height}]`. ### Signature ```rust async fn query_block_outcome( &self, height: Option, ) -> Result; ``` From `impl BlockClient for HttpClient`. ### Example ```rust use { anyhow::Result, dango_sdk::HttpClient, grug::BlockClient, }; #[tokio::main] async fn main() -> Result<()> { let client = HttpClient::new("https://api-mainnet.dango.zone")?; let outcome = client.query_block_outcome(None).await?; println!("tx outcomes: {}", outcome.tx_outcomes.len()); Ok(()) } ``` ### Parameters **`height`** — `Option`. Block height, or `None` for the latest. ### Returns **`BlockOutcome`** — per-transaction outcomes plus block-level metadata. ### Notes * REST, not GraphQL. * The outcome is consensus-finalized; pending transactions are not reflected. ### See also * [`query_block`](./query_block) — the block itself. * [`search_tx`](./search_tx) — single-transaction outcome by hash. ## query\_store Read a raw key from the chain's state store, optionally with a Merkle proof. ### Signature ```rust async fn query_store( &self, key: Binary, height: Option, prove: bool, ) -> Result<(Option, Option), anyhow::Error>; ``` From `impl QueryClient for HttpClient`. ### Example ```rust use { anyhow::Result, dango_sdk::HttpClient, grug::{Binary, QueryClient}, }; #[tokio::main] async fn main() -> Result<()> { let client = HttpClient::new("https://api-mainnet.dango.zone")?; let key = Binary::from(b"my/key".to_vec()); let (value, proof) = client.query_store(key, None, false).await?; println!("value: {value:?}"); println!("proof: {proof:?}"); Ok(()) } ``` ### Parameters **`key`** — `Binary`. Raw storage key bytes. **`height`** — `Option`. Block height to read at, or `None` for the latest committed state. **`prove`** — `bool`. When `true`, requests a Merkle proof alongside the value. ### Returns **`(Option, Option)`** — the value at the given key (or `None` if absent) and an optional proof. The proof is returned only when `prove = true` and the node has the witness available. ### Notes * For typed contract storage, prefer `QueryClientExt::query_wasm_raw` or `query_wasm_smart` — both wrap `query_app` with the correct query variant. * Proofs are borsh-encoded `Proof` values. Decoding is handled by the SDK. ### See also * [`query_app`](./query_app) — typed app queries. * [Concepts: Encoding and types](../../../concepts/encoding-and-types) — `Binary` semantics. ## search\_tx Look up a transaction outcome by hash. ### Signature ```rust async fn search_tx(&self, hash: grug::Hash256) -> Result; ``` From `impl SearchTxClient for HttpClient`. ### Example ```rust use { anyhow::Result, dango_sdk::HttpClient, grug::{Hash256, SearchTxClient}, std::str::FromStr, }; #[tokio::main] async fn main() -> Result<()> { let client = HttpClient::new("https://api-mainnet.dango.zone")?; let hash = Hash256::from_str( "0x0000000000000000000000000000000000000000000000000000000000000000", )?; let outcome = client.search_tx(hash).await?; println!("height: {}", outcome.height); Ok(()) } ``` ### Parameters **`hash`** — `Hash256`. The transaction hash returned by [`broadcast_tx`](./broadcast_tx). ### Returns **`SearchTxOutcome`** — full outcome with `hash`, `height`, transaction body, gas usage, success/failure result, and emitted events. ### Notes * Returns `Err(anyhow!("tx not found: …"))` when the transaction isn't yet committed. Treat as "keep polling" (subject to a bounded retry). * Returns `Err(anyhow!("multiple txs found …"))` if the indexer returns more than one match — a rare consistency window. * The outcome reconstructs the `Tx` from indexer-stored fields (`messages`, `data`, `credential`) and re-parses event payloads, so this is heavier than `query_block_outcome`. ### See also * [`broadcast_tx`](./broadcast_tx) — produces the hash. * [`query_block_outcome`](./query_block_outcome) — every outcome at a height. * [Concepts: Transactions](../../../concepts/transactions) — submit-and-poll flow. ## simulate Dry-run an unsigned transaction against the latest committed state and return its outcome. ### Signature ```rust async fn simulate(&self, tx: UnsignedTx) -> Result; ``` From `impl QueryClient for HttpClient`. `UnsignedTx` and `TxOutcome` are re-exported by the SDK. ### Example ```rust use { anyhow::Result, dango_sdk::{HttpClient, SingleSigner}, grug::{NonEmpty, QueryClient, Signer}, }; async fn estimate_gas( http: &HttpClient, signer: &SingleSigner, messages: NonEmpty>, ) -> Result { let unsigned = signer.unsigned_transaction(messages, "dango-1")?; let outcome = http.simulate(unsigned).await?; Ok(outcome.gas_used as u64) } ``` ### Parameters **`tx`** — `UnsignedTx`. The transaction body (sender, messages, metadata) without a signature. ### Returns **`TxOutcome`** — the simulated outcome, including `gas_used`, `gas_limit`, the success/failure result, and emitted events. ### Notes * Simulation runs without consuming nonce or gas on chain. Authentication is skipped. * Apply your own gas buffer — the report is actual usage, not a recommended limit. * Builds `UnsignedTx` via `Signer::unsigned_transaction` to keep the `sender` and metadata consistent with what `sign_transaction` will produce. ### See also * [`broadcast_tx`](./broadcast_tx) — submit a signed version. * [Concepts: Transactions](../../../concepts/transactions) — full sign-and-broadcast flow. ## WsError The only public typed error in `dango-sdk`. Returned inside subscription stream items. ### Definition ```rust #[derive(Debug, Clone, thiserror::Error)] pub enum WsError { #[error("WebSocket connection closed: {0}")] Closed(String), #[error("WebSocket transport error: {0}")] Transport(String), #[error("subscription returned error: {0}")] Subscription(serde_json::Value), #[error("failed to decode message: {0}")] Decode(String), } ``` ### Variants **`Closed(reason)`** — the server (or the network) closed the WebSocket. `reason` is the close-frame body if present, or `"stream ended"` for unexpected EOF. Terminal: the stream emits this once, then `None`. **`Transport(message)`** — `tokio_tungstenite` reported an I/O or protocol failure. Terminal at the connection level — every subscription on the same connection fails together. **`Subscription(payload)`** — the server sent a GraphQL `error` frame for this subscription id. The payload is the raw `serde_json::Value`. Terminal for *this* stream only; sibling subscriptions on the same [`Session`](../clients/Session) keep running. **`Decode(message)`** — the SDK could not parse a server message into the expected type. Includes `Response` decoding failures (the schema and the client disagree). ### Example ```rust use { anyhow::Result, dango_sdk::{SubscribeBlock, WsClient, WsError, subscribe_block}, futures::StreamExt, }; #[tokio::main] async fn main() -> Result<()> { let ws = WsClient::new("wss://api-mainnet.dango.zone/graphql")?; let mut stream = ws .subscribe::(subscribe_block::Variables {}) .await?; while let Some(item) = stream.next().await { match item { Ok(resp) => println!("ok: {resp:?}"), Err(WsError::Closed(reason)) => { eprintln!("closed: {reason}"); break; } Err(WsError::Transport(msg)) => { eprintln!("transport: {msg}"); break; } Err(WsError::Subscription(payload)) => eprintln!("server: {payload}"), Err(WsError::Decode(msg)) => eprintln!("decode: {msg}"), } } Ok(()) } ``` ### Notes * Implements `thiserror::Error`. `Display` formats each variant per the `#[error]` attribute above. * `Clone + Debug`. The driver task may broadcast the same error to multiple subscription channels when a connection dies. * Outside subscriptions, the SDK uses `anyhow::Error`. There is no equivalent typed enum for HTTP errors. ### See also * [`SubscriptionStream`](../subscriptions/SubscriptionStream) — where this appears. * [Concepts: Error handling](../../concepts/error-handling) — recovery patterns. ## HttpClient A GraphQL + REST client over `reqwest` that talks to a Dango node. ### Setup ```rust use { anyhow::Result, dango_sdk::HttpClient, }; #[tokio::main] async fn main() -> Result<()> { let client = HttpClient::new("https://api-mainnet.dango.zone")?; let _ = client; Ok(()) } ``` `new` accepts any `reqwest::IntoUrl` and validates the URL syntactically. It does not perform a network call. ```rust pub struct HttpClient { /* private fields */ } impl HttpClient { pub fn new(url: U) -> Result where U: reqwest::IntoUrl; } ``` ### Configuration `HttpClient` does not expose constructor options. It uses `reqwest::Client::new()` internally with default settings (HTTPS, connection pool, no proxy override). For custom transports — proxies, TLS roots, timeouts — fork the source. ### Methods The methods are split between own pagination helpers and the four trait impls. Bring the trait into scope to call its methods. #### Pagination (own methods) | Method | Description | | ----------------------------------------------------------------------- | ------------------------------------------------ | | [`paginate_all`](../methods/http-client/paginate_all) | Generic forward/backward cursor pagination loop. | | [`paginate_accounts`](../methods/http-client/paginate_accounts) | All accounts, forward pagination. | | [`paginate_transfers`](../methods/http-client/paginate_transfers) | All transfers, forward pagination. | | [`paginate_transactions`](../methods/http-client/paginate_transactions) | All transactions, forward pagination. | | [`paginate_blocks`](../methods/http-client/paginate_blocks) | All blocks, forward pagination. | | [`paginate_events`](../methods/http-client/paginate_events) | All events, forward pagination. | | [`paginate_messages`](../methods/http-client/paginate_messages) | All messages, forward pagination. | #### `impl QueryClient` | Method | Description | | --------------------------------------------------- | --------------------------------------------------- | | [`query_app`](../methods/http-client/query_app) | Run an app-level query at a given height. | | [`query_store`](../methods/http-client/query_store) | Read a raw key from store, optionally with a proof. | | [`simulate`](../methods/http-client/simulate) | Dry-run an `UnsignedTx` and return its outcome. | #### `impl BlockClient` | Method | Description | | ------------------------------------------------------------------- | --------------------------------------------------------- | | [`query_block`](../methods/http-client/query_block) | Fetch a block by height (REST `/block/info`). | | [`query_block_outcome`](../methods/http-client/query_block_outcome) | Fetch a block's execution outcome (REST `/block/result`). | #### `impl BroadcastClient` | Method | Description | | ----------------------------------------------------- | ----------------------------------------------------- | | [`broadcast_tx`](../methods/http-client/broadcast_tx) | Submit a signed transaction with `tx-sync` semantics. | #### `impl SearchTxClient` | Method | Description | | ----------------------------------------------- | -------------------------------------- | | [`search_tx`](../methods/http-client/search_tx) | Look up a transaction outcome by hash. | #### Extension methods (from `QueryClientExt`) `QueryClientExt` is a blanket trait over `QueryClient`. It is reachable on `HttpClient` once `QueryClientExt` is in scope: | Method | | | ------------------ | ---------------------------- | | `query_app_config` | Typed `AppConfig` fetch. | | `query_balance` | Single denom balance. | | `query_balances` | All balances for an address. | | `query_supply` | Total supply for a denom. | | `query_supplies` | All denom supplies. | | `query_wasm_smart` | Typed contract smart query. | | `query_wasm_raw` | Raw contract storage slot. | | `query_code` | Wasm bytecode by hash. | | `query_codes` | List uploaded code hashes. | | `query_contract` | Contract info. | | `query_contracts` | List contracts. | See the `QueryClientExt` reference for full signatures. ### End-to-end example ```rust use { anyhow::Result, dango_sdk::HttpClient, grug::{BlockClient, QueryClient, QueryClientExt}, }; #[tokio::main] async fn main() -> Result<()> { let client = HttpClient::new("https://api-mainnet.dango.zone")?; let block = client.query_block(None).await?; println!("height: {}", block.info.height); let cfg = client .query_app_config::(None) .await?; println!("account factory: {}", cfg.addresses.account_factory); Ok(()) } ``` ### Notes * `HttpClient` is `Debug + Clone`. Cloning shares the underlying `reqwest::Client` connection pool — reuse a single instance. * The `tracing` feature emits `tracing::debug!` events for every GraphQL request/response. No public symbols change. * REST endpoints (`query_block`, `query_block_outcome`) hit `/block/info` and `/block/result`; GraphQL endpoints hit `/graphql`. ### See also * [WsClient](./WsClient) — subscriptions counterpart. * [Concepts: Clients](../../concepts/clients) — when to use which. * [Concepts: Transactions](../../concepts/transactions) — broadcast flow. ## Keystore An encrypted-on-disk container for a 32-byte private key, using AES-256-GCM with PBKDF2-HMAC-SHA256 key derivation. ### Setup `Keystore` does not need to be constructed directly — both static methods do the work. Reach for it through `Keystore::from_file` (load) or `Keystore::write_to_file` (save): ```rust use { anyhow::Result, dango_sdk::{Keystore, Secp256k1, Secret}, }; fn save() -> Result<()> { let secret = Secp256k1::new_random(); Keystore::write_to_file(&secret, "./key.json", "hunter2")?; Ok(()) } fn load() -> Result { let bytes = Keystore::from_file("./key.json", "hunter2")?; Secp256k1::from_bytes(bytes) } ``` ```rust #[grug::derive(Serde)] pub struct Keystore { pub pk: ByteArray<33>, pub salt: ByteArray<16>, pub nonce: ByteArray<12>, pub ciphertext: Binary, } ``` ### Configuration `Keystore` itself has no options. The KDF parameters are fixed: * PBKDF2-HMAC-SHA256 * 600 000 iterations * 16-byte salt (`OsRng`) * 12-byte AES-GCM nonce (`OsRng`) * AES-256-GCM cipher ### Methods | Method | Description | | ---------------------------------------------------- | ----------------------------------------------------------------------------------------- | | [`from_file`](../methods/keystore/from_file) | Read and decrypt a keystore file. Returns the raw `[u8; 32]` private key. | | [`write_to_file`](../methods/keystore/write_to_file) | Encrypt a `Secret` and write a pretty-printed JSON keystore file. Returns the `Keystore`. | ### End-to-end example ```rust use { anyhow::Result, dango_sdk::{Keystore, Secp256k1, Secret}, }; fn round_trip() -> Result<()> { let secret = Secp256k1::new_random(); let pubkey = secret.public_key(); Keystore::write_to_file(&secret, "./key.json", "hunter2")?; let bytes = Keystore::from_file("./key.json", "hunter2")?; let recovered = Secp256k1::from_bytes(bytes)?; assert_eq!(pubkey, recovered.public_key()); Ok(()) } ``` ### Notes * `from_file` returns the raw private key bytes, not a `Secret`. Callers wrap it in `Secp256k1::from_bytes` or `Eip712::from_bytes` depending on the account's signing scheme. * The file contains the (unencrypted) compressed public key alongside the ciphertext. Treat the file as sensitive even though the private key itself is encrypted. * `write_to_file` is only valid for secrets whose `Private` type dereferences to `[u8]` and whose `Public` type fits in `ByteArray<33>` (both `Secp256k1` and `Eip712` qualify). :::warning A wrong password fails decryption with an AES-GCM authentication-tag error — the returned `anyhow::Error` does not distinguish "wrong password" from "file corrupted". Treat all decryption errors the same. ::: ### See also * [`Secret`](../traits/Secret) — what `from_file`'s bytes feed into. * [Concepts: Signers and authentication](../../concepts/signers-and-authentication) — full key-management flow. ## Session A live WebSocket session that hosts multiple concurrent subscriptions over a single connection. ### Setup A `Session` is produced by [`WsClient::connect`](../methods/ws-client/connect); it has no public constructor. ```rust use { anyhow::Result, dango_sdk::{Session, WsClient}, }; #[tokio::main] async fn main() -> Result<()> { let session: Session = WsClient::new("wss://api-mainnet.dango.zone/graphql")? .connect() .await?; let _ = session; Ok(()) } ``` ```rust pub struct Session { inner: Arc, } ``` `Session` is `Debug + Clone`. Cloning shares the same underlying connection and command channel. ### Configuration `Session` has no configuration surface. The keepalive interval and protocol are fixed at construction time. ### Methods | Method | Description | | ------------------------------------------- | ------------------------------------------------------------------ | | [`subscribe`](../methods/session/subscribe) | Multiplex a new GraphQL subscription onto the existing connection. | ### End-to-end example ```rust use { anyhow::Result, dango_sdk::{SubscribeBlock, SubscribeTrades, WsClient, subscribe_block, subscribe_trades}, futures::StreamExt, }; #[tokio::main] async fn main() -> Result<()> { let session = WsClient::new("wss://api-mainnet.dango.zone/graphql")? .connect() .await?; let mut blocks = session .subscribe::(subscribe_block::Variables {}) .await?; let mut trades = session .subscribe::(subscribe_trades::Variables { base_denom: "dango".into(), quote_denom: "bridge/usdc".into(), }) .await?; loop { tokio::select! { Some(item) = blocks.next() => println!("block: {item:?}"), Some(item) = trades.next() => println!("trade: {item:?}"), else => break, } } // Both streams and `session` drop here; the connection is closed. Ok(()) } ``` ### Lifecycle `Session` is backed by an `Arc`. The connection stays open as long as: * at least one `Session` clone is alive, **or** * at least one [`SubscriptionStream`](../subscriptions/SubscriptionStream) derived from this session is alive. When the last `Session` clone drops, the inner `Drop` impl sends a `Close` command to the background driver task; remaining streams keep working until the server closes the connection or the stream itself is dropped. When the last stream then drops, the connection closes cleanly. :::warning holding a `Session` clone alive after every stream has been dropped keeps the connection open with no subscriptions. The server may close it for inactivity (close code 3008) despite the keepalive pings — recreate via `WsClient::connect` if needed. ::: Individual stream drops send a `complete` to the server for that subscription id, freeing the slot in the 30-per-connection cap. ### Notes * The first nine bits of every subscription id are generated from an internal `AtomicU64` per session. Ids are local to one `Session` — they do not collide across sessions. * Subscription order on the wire is the order of `subscribe` calls. The server may reply out of order. * `Session::subscribe` returns `Err` only if the command channel to the driver task is closed (i.e. the session is dead). ### See also * [WsClient](./WsClient) — what creates a `Session`. * [SubscriptionStream](../subscriptions/SubscriptionStream) — the stream type returned by `subscribe`. * [Concepts: Subscriptions](../../concepts/subscriptions) — when to use a `Session` vs `WsClient::subscribe`. ## SingleSigner A typestate builder for signing transactions against Dango's single-signature accounts. ### Setup ```rust use { anyhow::Result, dango_sdk::{HttpClient, Secp256k1, Secret, SingleSigner}, grug::Addr, std::str::FromStr, }; #[tokio::main] async fn main() -> Result<()> { let http = HttpClient::new("https://api-mainnet.dango.zone")?; let secret = Secp256k1::new_random(); let addr = Addr::from_str("0x0000000000000000000000000000000000000000")?; let signer = SingleSigner::new(addr, secret) .with_query_user_index(&http).await? .with_query_nonce(&http).await?; let _ = signer; Ok(()) } ``` ```rust pub struct SingleSigner, N = Defined> where S: Secret, I: MaybeDefined, N: MaybeDefined, { pub address: Addr, pub secret: S, pub user_index: I, pub nonce: N, } pub const DEFAULT_DERIVATION_PATH: &str = "m/44'/60'/0'/0/0"; ``` ### The typestate `SingleSigner` carries phantom-typed flags for whether `user_index` and `nonce` have been filled in: | State | `I` | `N` | Capabilities | | ---------- | ---------------------- | ------------------ | ------------------------------------------------------------------------------------ | | Bare | `Undefined` | `Undefined` | `new`, `with_user_index`, `with_query_user_index`, `with_nonce`, `with_query_nonce`. | | Index-only | `Defined` | `Undefined` | `new_first_address_available`, `user_index()`, `with_nonce`, `with_query_nonce`. | | Nonce-only | `Undefined` | `Defined` | `nonce()`, `with_user_index`, `with_query_user_index`. | | Ready | `Defined` | `Defined` | `Signer`, `SequencedSigner`, `user_index()`, `nonce()`. | `Defined` and `Undefined` are typestate helpers. The compiler enforces both fields are `Defined` before `sign_transaction` is callable. This is a common Rust idiom for builders that must reach a complete state before being usable, but it is unusual in TypeScript/Python where missing fields would be a runtime check. ### Configuration `SingleSigner` has no static configuration. The signing scheme is dictated by the `Secret` implementation (`Secp256k1` for raw secp256k1+SHA-256, `Eip712` for Ethereum typed-data). ### Methods #### Constructors | Method | Description | | ------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | | [`new`](../methods/single-signer/new) | Bare constructor — both phantom slots `Undefined`. | | [`new_first_address_available`](../methods/single-signer/new_first_address_available) | Discover the first account associated with `secret.key_hash()` via the account factory. | #### State transitions | Method | Description | | ------------------------------------------------------------------------- | ------------------------------------------------- | | [`with_user_index`](../methods/single-signer/with_user_index) | Set `user_index` from a value. | | [`with_query_user_index`](../methods/single-signer/with_query_user_index) | Set `user_index` by querying the account factory. | | [`with_nonce`](../methods/single-signer/with_nonce) | Set `nonce` from a value. | | [`with_query_nonce`](../methods/single-signer/with_query_nonce) | Set `nonce` by querying the account. | #### Queries (all states) | Method | Description | | --------------------------------------------------------------- | ---------------------------------------------------------- | | [`query_user_index`](../methods/single-signer/query_user_index) | Fetch this address's `UserIndex` from the account factory. | | [`query_next_nonce`](../methods/single-signer/query_next_nonce) | Fetch the next unused `Nonce`. | #### Accessors | Method | Description | | --------------------------- | --------------------------------- | | `user_index() -> UserIndex` | Only on `I = Defined`. | | `nonce() -> Nonce` | Only on `N = Defined`. | #### Trait impls | Trait | Where | Methods | | ----------------------------------------- | ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | `Addressable` | all states | `address()` | | `Signer` | `Defined + Defined` | [`unsigned_transaction`](../methods/single-signer/unsigned_transaction), [`sign_transaction`](../methods/single-signer/sign_transaction) | | `SequencedSigner` (`dango_types::signer`) | `Defined` | [`query_nonce`](../methods/single-signer/query_nonce), [`update_nonce`](../methods/single-signer/update_nonce) | ### End-to-end example ```rust use { anyhow::Result, dango_sdk::{HttpClient, Secp256k1, Secret, SingleSigner}, grug::{Addr, BroadcastClient, Coins, Message, NonEmpty, QueryClient, Signer}, std::str::FromStr, }; #[tokio::main] async fn main() -> Result<()> { let http = HttpClient::new("https://api-mainnet.dango.zone")?; let secret = Secp256k1::from_bytes([0u8; 32])?; let addr = Addr::from_str("0x0000000000000000000000000000000000000000")?; let mut signer = SingleSigner::new(addr, secret) .with_query_user_index(&http).await? .with_query_nonce(&http).await?; let messages = NonEmpty::new(vec![ Message::transfer( Addr::from_str("0x1111111111111111111111111111111111111111")?, Coins::one("bridge/usdc", 100_000_000_u128)?, )?, ])?; let unsigned = signer.unsigned_transaction(messages.clone(), "dango-1")?; let gas = http.simulate(unsigned).await?.gas_used as u64; let tx = signer.sign_transaction(messages, "dango-1", gas * 3 / 2)?; let outcome = http.broadcast_tx(tx).await?; println!("hash: {}", outcome.tx_hash); Ok(()) } ``` ### Notes * `sign_transaction` mutates `self.nonce` (`*self.nonce.inner_mut() += 1`) on every success. A single signer instance can produce a stream of consecutive transactions without re-querying. After a broadcast failure, call `SequencedSigner::update_nonce` to resync. * `new_first_address_available` calls `QueryForgotUsernameRequest` with `limit: Some(1)`. It returns the first user-index associated with `secret.key_hash()`, regardless of forgotten-username state. Errors with `"no user index found for key hash …"` when nothing matches. * `DEFAULT_DERIVATION_PATH` is `m/44'/60'/0'/0/0` (Ethereum coin type). Use it when constructing `Secret::from_mnemonic` with `coin_type = 60`. ### See also * [`Secret`](../traits/Secret) — the trait that supplies private keys. * [Concepts: Signers and authentication](../../concepts/signers-and-authentication) — the full mental model. * [Concepts: Transactions](../../concepts/transactions) — end-to-end signing and broadcast. ## WsClient A WebSocket factory that speaks `graphql-transport-ws` on top of `tokio-tungstenite` for Dango GraphQL subscriptions. ### Setup ```rust use { anyhow::Result, dango_sdk::WsClient, }; #[tokio::main] async fn main() -> Result<()> { let client = WsClient::new("wss://api-mainnet.dango.zone/graphql")?; let _ = client; Ok(()) } ``` ```rust pub struct WsClient { /* url: Url */ } impl WsClient { pub fn new(url: impl Into) -> Result; pub fn from_http_url(url: impl Into) -> Result; } ``` `new` accepts only `ws://` or `wss://` URLs. `from_http_url` rewrites `http://` → `ws://` and `https://` → `wss://`, passing through schemes that are already WebSocket. Both are syntactic — neither opens a connection. ### Configuration `WsClient` is a thin config holder: just a parsed `Url`. No options are exposed; the keepalive interval (15 s) is fixed. ### Methods | Method | Description | | ----------------------------------------------------- | ------------------------------------------------------------------ | | [`new`](../methods/ws-client/new) | Construct from a `ws://` or `wss://` URL. | | [`from_http_url`](../methods/ws-client/from_http_url) | Construct from an `http://`/`https://` URL, rewriting the scheme. | | [`connect`](../methods/ws-client/connect) | Open a connection and return a multiplexed [`Session`](./Session). | | [`subscribe`](../methods/ws-client/subscribe) | Open a dedicated connection for one subscription. | ### End-to-end example ```rust use { anyhow::Result, dango_sdk::{SubscribeBlock, WsClient, subscribe_block}, futures::StreamExt, }; #[tokio::main] async fn main() -> Result<()> { let client = WsClient::new("wss://api-mainnet.dango.zone/graphql")?; // One subscription, dedicated connection. let mut stream = client .subscribe::(subscribe_block::Variables {}) .await?; while let Some(item) = stream.next().await { println!("{item:?}"); } Ok(()) } ``` For many concurrent subscriptions, use `connect` and reuse a [`Session`](./Session): ```rust use { anyhow::Result, dango_sdk::{SubscribeBlock, SubscribeTrades, WsClient, subscribe_block, subscribe_trades}, futures::StreamExt, }; #[tokio::main] async fn main() -> Result<()> { let session = WsClient::new("wss://api-mainnet.dango.zone/graphql")? .connect() .await?; let mut blocks = session .subscribe::(subscribe_block::Variables {}) .await?; let mut trades = session .subscribe::(subscribe_trades::Variables { base_denom: "dango".into(), quote_denom: "bridge/usdc".into(), }) .await?; loop { tokio::select! { Some(item) = blocks.next() => println!("block: {item:?}"), Some(item) = trades.next() => println!("trade: {item:?}"), else => break, } } Ok(()) } ``` ### Notes * `WsClient` is `Debug + Clone`. Cloning is free. * `WsClient::subscribe` opens a dedicated connection. Each call costs one full handshake. Prefer [`Session::subscribe`](./Session) when streaming multiple subscriptions in one task. * The server caps each connection at 30 concurrent subscriptions. See [Rate Limits](../../concepts/rate-limits). * The SDK sends a `Ping` every 15 s on every open connection (below the server's 30 s `keepalive_timeout`). ### See also * [Session](./Session) — multiplexed connection handle. * [SubscriptionStream](../subscriptions/SubscriptionStream) — the returned stream type. * [SubscriptionVariables](../subscriptions/SubscriptionVariables) — sugar for typed variables. * [Concepts: Subscriptions](../../concepts/subscriptions) — when to use which entry point. ## Dango Python SDK Sync Python client for the Dango perpetual futures exchange. Wraps the GraphQL HTTP and WebSocket endpoints behind three classes: `Exchange` for signed actions, `Info` for read queries and subscriptions, and `Secp256k1Wallet`/`SingleSigner` for keys and nonces. The distribution package is named `dango`. Both `dango/__init__.py` and `dango/hyperliquid_compatibility/__init__.py` are empty — import everything from submodules: ```python from dango.exchange import Exchange from dango.info import Info from dango.utils.signing import Secp256k1Wallet, SingleSigner from dango.utils.types import Addr, PairId, TimeInForce from dango.utils.constants import MAINNET_API_URL, PERPS_CONTRACT_MAINNET ``` ### Where to start * [Installation](./getting-started/installation) — install the wheel and set up a virtualenv * [First call](./getting-started/first-call) — run a read query against mainnet in five minutes * [Project setup](./getting-started/project-setup) — networks, env vars, signer construction ### Reference * [Classes](./api/classes/Exchange) — `Exchange`, `Info`, `WebsocketManager`, `API`, `Secp256k1Wallet`, `SingleSigner` * Methods — per-method pages under [`Exchange`](./api/classes/Exchange#methods), [`Info`](./api/classes/Info#methods), and [`WebsocketManager`](./api/classes/WebsocketManager#methods) * [Helpers](./api/helpers/paginate_all) — module-level helpers (`paginate_all`, `dango_decimal`, `sign_doc_canonical_json`, `sign_doc_sha256`) * [Types](./api/types/Addr) — user-facing aliases, dataclasses, enums * [Errors](./api/errors/Error) — `Error`, `ClientError`, `ServerError`, `GraphQLError`, `TxFailed` ### Migrating from Hyperliquid The package ships an HL-shaped facade under `dango.hyperliquid_compatibility`. Swap your imports and most existing HL code keeps working — but there are sharp edges. Read [Migration: Hyperliquid SDK](./migration/hyperliquid) before flipping the switch. import { CodeTab, CodeTabs } from '../../../components/CodeTabs' ## Migrating from the Hyperliquid Python SDK You came from `hyperliquid-python-sdk`. The `dango` package ships an HL-shaped facade under `dango.hyperliquid_compatibility` so most of your code keeps working with one-line import swaps. **Read this page first.** There are sharp edges. ```python from hyperliquid.exchange import Exchange from hyperliquid.info import Info from hyperliquid.utils.types import Cloid, OrderType ``` ```python from dango.hyperliquid_compatibility.exchange import Exchange from dango.hyperliquid_compatibility.info import Info from dango.hyperliquid_compatibility.types import Cloid, OrderType ``` ### What will burn you #### 1. Cloid is hashed lossily — the cloid in responses is NOT the cloid you sent HL's `Cloid` is 16 bytes (128 bits). Dango's `ClientOrderId` is `Uint64` (64 bits). The compat layer accepts the same 16-byte hex string HL expects, but `Cloid.to_uint64()` collapses it via SHA-256 and keeps only the first 8 bytes: ```python from dango.hyperliquid_compatibility.types import Cloid c = Cloid("0x12345678901234567890123456789012") c.to_uint64() # 16-byte cloid → 8-byte int (deterministic, lossy) ``` **The cloid the chain records and returns is the Uint64 — NOT the original 16-byte HL cloid.** If you rely on round-tripping a `Cloid` across submit → response → cancel, you must keep your own mapping (e.g. by calling `c.to_uint64()` before submission and matching against the response). Two different 16-byte cloids can collide in the 8-byte Uint64. Probability is \~2\*\*-32 over \~4 billion cloids — negligible for any realistic workload, not zero. #### 2. `market_open` / `market_close` silently ignore `px` HL's `px` parameter is the limit price computed from oracle ± slippage. Dango's market order computes its own slippage band against the contract's mark price. The wrapper accepts `px` for signature parity but **does not use it**: ```python # `px=1500` is silently ignored ex.market_open("ETH", is_buy=True, sz=0.5, px=1500.0, slippage=0.01) ``` If your bot relies on `px` clamping at the SDK layer, you must enforce it yourself before calling. #### 3. `set_expires_after` is a no-op The wrapper stores `expires_after` on `self` but does NOT thread it into the signed metadata. HL's `expiresAfter` enforcement does not currently apply. Track this is a known gap. ```python ex.set_expires_after(1700000000000) # stored, but does nothing on the wire ``` #### 4. `Info.query_order_by_oid` ignores `user` Dango orders are keyed by `oid` alone. The wrapper accepts `user` for signature parity but does NOT forward it to the contract — passing a wrong address still returns the order: ```python info.query_order_by_oid("0xunrelated", 12345) # `user` ignored ``` ### What you must change explicitly #### Construction: `account_address` is required HL allowed `account_address=None` and defaulted to the wallet's EVM address. Dango decouples the signing key from the on-chain account. The compat `Exchange` constructor rejects `None`: ```python # Will raise ValueError Exchange(wallet, base_url="...") # Correct Exchange(wallet, base_url="...", account_address="0x...") ``` `vault_address` and `spot_meta` reject any non-None value (Dango has no vault address abstraction and no spot universe). #### Default URL is local, not mainnet HL's default `base_url` was the production API. The compat `Exchange` and `Info` default to `LOCAL_API_URL` to prevent accidental mainnet hits. Pass an explicit URL. ### Methods that wrap native equivalents | HL-compat method | Native equivalent | Notes | | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | -------------------------------------------------- | | `Exchange.order` | `submit_order` | `is_buy + sz` → signed `size` | | `Exchange.bulk_orders` (multi) | `batch_update_orders` | Single-order shortcut uses `submit_order` directly | | `Exchange.cancel` | `cancel_order(OrderId(...))` | `name` verified for parity | | `Exchange.bulk_cancel` | `batch_update_orders([CancelAction(...)])` | | | `Exchange.cancel_by_cloid` | `cancel_order(ClientOrderIdRef(...))` | Lossy hash, see above | | `Exchange.bulk_cancel_by_cloid` | `batch_update_orders` of cloid cancels | | | `Exchange.modify_order` / `bulk_modify_orders_new` | `batch_update_orders` of paired Cancel+Submit | No first-class modify on chain | | `Exchange.market_open` | `submit_market_order` | `px` ignored | | `Exchange.market_close` | `submit_market_order(reduce_only=True)` | Reads position via `Info.user_state` first | | `Exchange.set_referrer` | `set_referral` | | | `Info.user_state` | `user_state_extended` + reshape | | | `Info.open_orders` | `orders_by_user` + reshape | | | `Info.all_mids` | `all_perps_pair_stats` | Uses `currentPrice` as mid | | `Info.meta` | `pair_params` | Synthesizes `szDecimals=6` | | `Info.meta_and_asset_ctxs` | `pair_params` + `pair_states` + `all_perps_pair_stats` | | | `Info.l2_snapshot` | `pair_param` + `liquidity_depth` | Picks smallest bucket | | `Info.candles_snapshot` | `perps_candles` | HL interval string → `CandleInterval` | | `Info.user_fills` / `_by_time` | `perps_events_all(event_type="order_filled")` + dedupe | | | `Info.query_order_by_oid` | `order` | `user` ignored | | `Info.historical_orders` | `perps_events_all(event_type="order_persisted")` + `order_removed` | | | `Info.subscribe(type=...)` | `subscribe_perps_trades` / `subscribe_perps_candles` / `subscribe_user_events` / `subscribe_query_app` | Dispatch by `subscription["type"]` | | `Info.unsubscribe` | `unsubscribe` | Ignores the `subscription` arg | | `Info.disconnect_websocket` | `disconnect_websocket` | | For per-method behavior and HL-versus-native divergences, see the [HL-compat method pages](./hl-compat/exchange/order) under this section. ### Methods that diverge from upstream in subtle ways * `Info.user_state`: `assetPositions` entries report `marginUsed="0"` and `returnOnEquity="0"` — Dango doesn't track per-position margin/RoE (cross-only). `leverage.value=1` always; `maxLeverage=1` always. `cumFunding` series are always `"0"` (funding folds into realized PnL on each fill, no per-position series). * `Info.meta`: `szDecimals` is hardcoded to `6` for every asset (Dango uses a uniform settlement decimals; HL is per-asset). * `Info.meta_and_asset_ctxs`: `PerpAssetCtx.markPx`, `oraclePx`, `midPx` report the same value (the indexer's `currentPrice`). HL distinguishes the three. * `Info.l2_snapshot`: HL doesn't take a `bucket_size`; the compat layer picks the smallest from `pair_param.bucket_sizes`. `n=1` always (Dango doesn't expose per-bucket order count). `time=0` always (no server timestamp on depth queries). * `Info.candles_snapshot`: Supported intervals are `1m`, `5m`, `15m`, `1h`, `4h`, `1d`, `1w`. Other HL intervals raise `ValueError`. * `Info.user_fills` / `user_fills_by_time`: Dango emits both maker and taker rows per fill; the wrapper dedupes by `fill_id` and prefers the taker side. * `Info.subscribe(type="allMids")`: not yet implemented; raises `NotImplementedError`. * `Info.subscribe(type="activeAssetCtx" | "activeAssetData")`: `markPx`/`oraclePx` report `"0"` until parallel `pair_stats` polling is wired in. ### Methods that are not implemented Many HL methods have no Dango analog (the chain is perps-only, cross-margin only, with no spot deploys / TWAP / sub-accounts). The wrapper raises `NotImplementedError` with a one-line reason rather than silently no-op. See [Missing methods](./missing-methods) for the full list (42 on `Exchange`, 25 on `Info`). ### See also * [Missing methods](./missing-methods) — every `NotImplementedError` stub, grouped by class * [HL-compat `Exchange.order`](./hl-compat/exchange/order) — start the per-method tour ## Missing methods **What this teaches:** which Hyperliquid-shaped methods exist in the compatibility layer but raise `NotImplementedError`, so you know what to migrate to native equivalents. ### Why these are stubs The HL-compat layer ships method signatures for every public method on `hyperliquid.exchange.Exchange` and `hyperliquid.info.Info` to preserve drop-in API shape, but only a subset is wired through to Dango primitives in v1. The rest raise `NotImplementedError` rather than silently no-op. ### Exchange — not yet implemented The following methods on `dango.hyperliquid_compatibility.exchange.Exchange` currently raise `NotImplementedError`. Native alternatives, when applicable, live on `dango.exchange.Exchange`. | Method | Notes | | -------------------------------------------- | ------------------------------------------------------------ | | `update_leverage` | HL-specific isolated/cross leverage toggle — no Dango analog | | `update_isolated_margin` | HL isolated-margin model not used by Dango | | `schedule_cancel` | HL dead-man-switch behavior | | `usd_class_transfer` | HL perp/spot account classes don't exist on Dango | | `send_asset` | HL multi-asset transfer | | `vault_usd_transfer` | HL vault USD transfer | | `sub_account_transfer` | HL sub-accounts not modeled in Dango | | `sub_account_spot_transfer` | HL sub-accounts not modeled in Dango | | `approve_builder_fee` | Dango has no builder-fee marketplace | | `convert_to_multi_sig_user` | HL multi-sig accounts | | `multi_sig` | HL multi-sig signing path | | `create_sub_account` | HL sub-accounts not modeled in Dango | | `usd_transfer` | HL USD-only transfer; use native `transfer` | | `withdraw_from_bridge` | HL bridge withdrawals | | `spot_transfer` | HL spot-class transfer | | `approve_agent` | HL agent system; closest analog is sessions | | `agent_enable_dex_abstraction` | HL agent abstraction modes | | `agent_set_abstraction` | HL agent abstraction modes | | `user_dex_abstraction` | HL user abstraction state | | `user_set_abstraction` | HL user abstraction state | | `token_delegate` | HL staking | | `use_big_blocks` | HL block-sizing toggle | | `c_signer_unjail_self` | HL consensus signer ops | | `c_signer_jail_self` | HL consensus signer ops | | `c_validator_register` | HL validator ops | | `c_validator_change_profile` | HL validator ops | | `c_validator_unregister` | HL validator ops | | `noop` | HL no-op tx | | `gossip_priority_bid` | HL priority gossip auction | | `spot_deploy_register_token` | HL spot token deployment | | `spot_deploy_user_genesis` | HL spot deployment | | `spot_deploy_enable_freeze_privilege` | HL spot deployment | | `spot_deploy_freeze_user` | HL spot deployment | | `spot_deploy_revoke_freeze_privilege` | HL spot deployment | | `spot_deploy_enable_quote_token` | HL spot deployment | | `spot_deploy_token_action_inner` | HL spot deployment | | `spot_deploy_genesis` | HL spot deployment | | `spot_deploy_register_spot` | HL spot deployment | | `spot_deploy_register_hyperliquidity` | HL spot deployment | | `spot_deploy_set_deployer_trading_fee_share` | HL spot deployment | | `perp_deploy_register_asset` | HL perp deployment | | `perp_deploy_set_oracle` | HL perp deployment | ### Info — not yet implemented The following methods on `dango.hyperliquid_compatibility.info.Info` currently raise `NotImplementedError`. Native alternatives, when applicable, live on `dango.info.Info`. | Method | Notes | | ---------------------------------- | ---------------------------------------------------------- | | `spot_user_state` | Dango is perps-only | | `spot_meta` | Dango is perps-only | | `spot_meta_and_asset_ctxs` | Dango is perps-only | | `query_spot_deploy_auction_status` | HL spot auction state | | `user_staking_summary` | HL staking | | `user_staking_delegations` | HL staking | | `user_staking_rewards` | HL staking | | `delegator_history` | HL staking | | `query_user_to_multi_sig_signers` | HL multi-sig accounts | | `query_perp_deploy_auction_status` | HL perp auction state | | `query_user_dex_abstraction_state` | HL agent abstraction state | | `query_user_abstraction_state` | HL agent abstraction state | | `user_twap_slice_fills` | HL TWAP orders not modeled in Dango | | `portfolio` | HL portfolio aggregation | | `user_role` | HL role enum | | `user_rate_limit` | HL per-user rate-limit endpoint | | `extra_agents` | HL extra-agent listing | | `funding_history` | Use native indexer pagination instead | | `user_funding_history` | Use native indexer pagination instead | | `user_non_funding_ledger_updates` | HL ledger update feed | | `query_referral_state` | HL referral state | | `query_sub_accounts` | HL sub-accounts not modeled in Dango | | `frontend_open_orders` | Use [`orders_by_user`](../api/methods/info/orders_by_user) | | `user_fees` | HL fee schedule endpoint | | `query_order_by_cloid` | Cloid asymmetry — see [`hyperliquid`](./hyperliquid) | | `user_vault_equities` | HL vault accounting | ### Migration path For each stubbed method: 1. Identify what you were doing (spot vs perps, account info, staking, etc.). 2. Find the closest native equivalent on `dango.exchange.Exchange` or `dango.info.Info`. 3. Adapt to Dango's argument shape — see [Hyperliquid SDK migration](./hyperliquid) for the type-mapping cheatsheet and the Cloid asymmetry. If no Dango equivalent exists today, the feature is genuinely out of scope. ### Next * [Hyperliquid SDK migration](./hyperliquid) — the migration guide proper. * [Exchange methods](../api/classes/Exchange) — native trading API. * [Info methods](../api/classes/Info) — native read API. ## Info.all\_mids (HL-compat) HL `allMids` — coin name → mid price string. ### Signature ```python def all_mids(self, dex: str = "") -> dict[str, str] ``` ### Example ```python from dango.hyperliquid_compatibility.info import Info info = Info(base_url="https://...") mids = info.all_mids() for coin, mid in mids.items(): print(coin, mid) ``` ### Parameters **`dex`** — `str`, optional. Accepted-and-ignored. ### Returns **`dict[str, str]`** — coin name (uppercase) → mid price (HL decimal string). ### Notes * The wrapper uses `currentPrice` from `all_perps_pair_stats` as the "mid". Dango does not separately track book mid; `currentPrice` is the last trade price from the indexer. ### See also * [Native `all_perps_pair_stats`](../../../api/methods/info/all_perps_pair_stats) ## Info.candles\_snapshot (HL-compat) HL `candleSnapshot` — OHLCV candles in a time window. ### Signature ```python def candles_snapshot( self, name: str, interval: str, start: int, end: int, ) -> list[dict[str, Any]] ``` ### Example ```python from dango.hyperliquid_compatibility.info import Info info = Info(base_url="https://...") candles = info.candles_snapshot("ETH", "1m", start=1700000000000, end=1700001000000) for c in candles: print(c["t"], c["o"], c["c"]) ``` ### Parameters **`name`** — `str`. HL coin name. **`interval`** — `str`. HL form (`"1m"`, `"5m"`, `"15m"`, `"1h"`, `"4h"`, `"1d"`, `"1w"`). Other values raise `ValueError`. **`start`** — `int`. Lower bound, milliseconds since Unix epoch. **`end`** — `int`. Upper bound, milliseconds. ### Returns **`list[dict[str, Any]]`** — HL-shaped candles with single-letter keys (`T`, `t`, `s`, `i`, `o`, `c`, `h`, `l`, `v`, `n`). `n=0` always (Dango doesn't track per-candle trade count). ### Divergences from upstream * HL supports more intervals than Dango. Unsupported intervals raise `ValueError` with the supported list. ### See also * [Native `perps_candles`](../../../api/methods/info/perps_candles) * [`CandleInterval`](../../../api/types/CandleInterval) ## Info.disconnect\_websocket (HL-compat) Close the underlying WebSocket connection. ### Signature ```python def disconnect_websocket(self) -> None ``` ### Example ```python from dango.hyperliquid_compatibility.info import Info info = Info(base_url="https://...") # ... subscribe and process ... info.disconnect_websocket() ``` ### Notes * Forwards directly to the native [`disconnect_websocket`](../../../api/methods/info/disconnect_websocket). Same behavior: stop the manager, close the socket, join the daemon thread (5s timeout). * Safe to call when no subscription was ever opened. ### See also * [`unsubscribe`](./unsubscribe) — drop a single subscription ## Info.historical\_orders (HL-compat) HL `historicalOrders` — reconstruct order lifecycle from persisted + removed events. ### Signature ```python def historical_orders(self, user: str) -> list[dict[str, Any]] ``` ### Example ```python from dango.hyperliquid_compatibility.info import Info info = Info(base_url="https://...") rows = info.historical_orders("0x...") for row in rows: print(row["order"]["coin"], row["status"], row["statusTimestamp"]) ``` ### Parameters **`user`** — `str`. The user's account address. ### Returns **`list[dict[str, Any]]`** — one row per persisted order with `{order, status, statusTimestamp}`. `status` is `"open"` if the order is still active, else the reason from the matching `order_removed` event (`"filled"`, `"canceled"`, `"liquidated"`, etc.). ### Notes * Dango does not fold the lifecycle on the server. The wrapper collects every `order_persisted` event and joins by `order_id` against `order_removed` events. ### See also * [Native `perps_events_all`](../../../api/methods/info/perps_events_all) * [`ReasonForOrderRemoval`](../../../api/types/ReasonForOrderRemoval) ## Info.l2\_snapshot (HL-compat) HL `l2Book` — bid/ask depth at the finest bucket. ### Signature ```python def l2_snapshot(self, name: str) -> L2BookData ``` ### Example ```python from dango.hyperliquid_compatibility.info import Info info = Info(base_url="https://...") book = info.l2_snapshot("ETH") bids, asks = book["levels"] print(bids[:5], asks[:5]) ``` ### Parameters **`name`** — `str`. HL coin name. ### Returns **`L2BookData`** — `{coin, levels: ([bids], [asks]), time}`. Each level is `{px, sz, n}`. ### Divergences from upstream * HL doesn't take a `bucket_size`; the wrapper picks the smallest entry from `pair_param.bucket_sizes`. * `n=1` always (Dango doesn't expose per-bucket order count). * `time=0` always (no server timestamp on depth queries). * Raises `RuntimeError` if the pair has no configured `bucket_sizes`. ### See also * [Native `liquidity_depth`](../../../api/methods/info/liquidity_depth) * [Native `pair_param`](../../../api/methods/info/pair_param) ## Info.meta (HL-compat) HL `meta` — perp universe metadata. ### Signature ```python def meta(self, dex: str = "") -> Meta ``` ### Example ```python from dango.hyperliquid_compatibility.info import Info info = Info(base_url="https://...") meta = info.meta() for asset in meta["universe"]: print(asset["name"], asset["szDecimals"]) ``` ### Parameters **`dex`** — `str`, optional. Accepted-and-ignored. ### Returns **`Meta`** — `{"universe": [{"name": str, "szDecimals": int}]}`. ### Divergences from upstream * `szDecimals` is hardcoded to `6` for every asset (Dango uses a uniform settlement decimals). HL distinguishes per-asset. ### See also * [Native `pair_params`](../../../api/methods/info/pair_params) ## Info.meta\_and\_asset\_ctxs (HL-compat) HL `metaAndAssetCtxs` — meta bundled with per-asset ctx. ### Signature ```python def meta_and_asset_ctxs(self) -> list[Any] ``` ### Example ```python from dango.hyperliquid_compatibility.info import Info info = Info(base_url="https://...") meta, ctxs = info.meta_and_asset_ctxs() ``` ### Returns **`list[Any]`** — a two-element list: `[meta, list[PerpAssetCtx]]`. ### Divergences from upstream * `markPx`, `oraclePx`, `midPx` all report the same value (the indexer's `currentPrice`). HL would distinguish the three. * `szDecimals` from `meta` is hardcoded to `6`. ### Notes * HL combines this atomically in one call. Dango needs three queries (`pair_params`, `pair_states`, `all_perps_pair_stats`); the wrapper runs them sequentially. ### See also * [Native `pair_params`](../../../api/methods/info/pair_params) * [Native `pair_states`](../../../api/methods/info/pair_states) * [Native `all_perps_pair_stats`](../../../api/methods/info/all_perps_pair_stats) ## Info.name\_to\_asset (HL-compat) HL signature — coin name to integer asset index. ### Signature ```python def name_to_asset(self, name: str) -> int ``` ### Example ```python from dango.hyperliquid_compatibility.info import Info info = Info(base_url="https://...") asset_index = info.name_to_asset("ETH") ``` ### Parameters **`name`** — `str`. HL coin name. ### Returns **`int`** — the integer asset index used in HL wire shapes. ### Notes * Indexes are assigned in `pair_params` iteration order (which the indexer returns sorted by pair\_id). * Raises `KeyError` on unknown coins. ### See also * [`name_to_pair`](./name_to_pair) — coin → Dango pair\_id ## Info.name\_to\_pair (HL-compat) Translate an HL coin name to a Dango `PairId`. Raises `KeyError` on an unknown coin. ### Signature ```python def name_to_pair(self, name: str) -> PairId ``` ### Example ```python from dango.hyperliquid_compatibility.info import Info info = Info(base_url="https://...") pair = info.name_to_pair("ETH") # "perp/ethusd" ``` ### Parameters **`name`** — `str`. HL coin name (uppercase). ### Returns **`PairId`** — the Dango pair id (`perp/usd`). ### Notes * The coin resolver is built at construction from either the `meta` argument or a live `pair_params()` fetch. * This is a wrapper-only helper; HL upstream does not expose it. ### See also * [`name_to_asset`](./name_to_asset) — coin → integer asset index ## Info.open\_orders (HL-compat) HL `openOrders` — flatten Dango's keyed order map to a list. ### Signature ```python def open_orders(self, address: str, dex: str = "") -> list[dict[str, Any]] ``` ### Example ```python from dango.hyperliquid_compatibility.info import Info info = Info(base_url="https://...") for order in info.open_orders("0x..."): print(order["coin"], order["side"], order["sz"], order["limitPx"]) ``` ### Parameters **`address`** — `str`. The user's account address. **`dex`** — `str`, optional. Accepted-and-ignored. ### Returns **`list[dict[str, Any]]`** — one entry per resting order with `coin`, `side` (`"A"` ask / `"B"` bid), `limitPx`, `sz` (always positive), `oid`, `timestamp` (ms), `origSz`. ### Notes * Dango stores `size` as a signed quantity; the wrapper splits it into HL's `side` + positive `sz`. * `oid` is the Dango order id as a string. HL traders that try `int(order["oid"])` will hit `ValueError` if the id is non-numeric. ### See also * [Native `orders_by_user`](../../../api/methods/info/orders_by_user) ## Info.query\_order\_by\_oid (HL-compat) HL `orderStatus` — fetch one order by id. **`user` is accepted-and-ignored.** ### Signature ```python def query_order_by_oid(self, user: str, oid: int | str) -> dict[str, Any] ``` ### Example ```python from dango.hyperliquid_compatibility.info import Info info = Info(base_url="https://...") result = info.query_order_by_oid("0x...", 12345) if result["status"] == "order": print(result["order"]) else: print(result["status"]) # "unknownOid" ``` ### Parameters **`user`** — `str`. **Accepted-and-ignored.** Dango orders are keyed by oid alone; the wrapper does NOT forward this to the contract. **`oid`** — `int | str`. The chain order id. ### Returns **`dict[str, Any]`** — `{"status": "order", "order": }` on hit, `{"status": "unknownOid"}` on miss. ### See also * [Migration: `user` is ignored](../../hyperliquid#4-infoquery_order_by_oid-ignores-user) * [Native `order`](../../../api/methods/info/order) ## Info.subscribe (HL-compat) Dispatch an HL-shaped `Subscription` dict to the right native subscriber. ### Signature ```python def subscribe( self, subscription: Subscription, callback: Callable[[Any], None], ) -> int ``` ### Example ```python from dango.hyperliquid_compatibility.info import Info info = Info(base_url="https://...") sid = info.subscribe({"type": "trades", "coin": "ETH"}, print) sid_user = info.subscribe({"type": "userFills", "user": "0x..."}, print) ``` ### Parameters **`subscription`** — `Subscription`. One of the HL-shaped `*Subscription` TypedDicts. Dispatch is by the `type` field. **`callback`** — `Callable[[Any], None]`. Invoked with HL-shaped envelopes (`{channel, data}` for `user` / `userFills`, etc.). ### Returns **`int`** — the subscription id. ### Dispatch table | `type` | Native handler | Notes | | ------------------------------- | ------------------------------------------------------------------------- | --------------------------------------------- | | `"trades"` | `subscribe_perps_trades` | Drops maker side; dedupes by `fill_id` | | `"candle"` | `subscribe_perps_candles` | Translates HL interval string | | `"userEvents"` | `subscribe_user_events(event_types=["order_filled"])` | | | `"userFills"` | `subscribe_user_events(event_types=["order_filled"])` | Different envelope; always `isSnapshot=False` | | `"orderUpdates"` | `subscribe_user_events(event_types=["order_persisted", "order_removed"])` | Always emits a list of one | | `"l2Book"` | `subscribe_query_app(block_interval=1)` | Polling-backed; picks smallest bucket | | `"bbo"` | `subscribe_query_app(block_interval=1)` | Polling-backed, `limit=1` | | `"activeAssetCtx"` | `subscribe_query_app(block_interval=1)` | `markPx`/`oraclePx` report `"0"` (gap) | | `"activeAssetData"` | `subscribe_query_app(block_interval=1)` | Uses `multi` to bundle user + pair state | | `"allMids"` | — | Raises `NotImplementedError` | | `"userFundings"` | — | Raises (no separate funding stream) | | `"webData2"` | — | Raises (UI-specific aggregation; no analog) | | `"userNonFundingLedgerUpdates"` | — | Raises (Phase 16 deferred) | ### See also * [`unsubscribe`](./unsubscribe) — drop a subscription * [`disconnect_websocket`](./disconnect_websocket) — close the connection * [Concepts: Subscriptions](../../../concepts/subscriptions) — the WebSocket model ## Info.unsubscribe (HL-compat) Drop a subscription by id. The `subscription` argument is informational. ### Signature ```python def unsubscribe(self, subscription: Subscription, subscription_id: int) -> bool ``` ### Example ```python from dango.hyperliquid_compatibility.info import Info info = Info(base_url="https://...") sub = {"type": "trades", "coin": "ETH"} sid = info.subscribe(sub, print) # ... later ... info.unsubscribe(sub, sid) ``` ### Parameters **`subscription`** — `Subscription`. Accepted for HL signature parity; ignored — the native `unsubscribe` keys on `subscription_id` alone. **`subscription_id`** — `int`. The id returned by `subscribe`. ### Returns **`bool`** — same as the native [`unsubscribe`](../../../api/methods/info/unsubscribe). ### See also * [`subscribe`](./subscribe) — register a subscription * [`disconnect_websocket`](./disconnect_websocket) — close the connection ## Info.user\_fills (HL-compat) HL `userFills` — flat list of trade fills for one user. ### Signature ```python def user_fills(self, address: str) -> list[Fill] ``` ### Example ```python from dango.hyperliquid_compatibility.info import Info info = Info(base_url="https://...") fills = info.user_fills("0x...") for fill in fills: print(fill["coin"], fill["dir"], fill["px"], fill["sz"]) ``` ### Parameters **`address`** — `str`. The user's account address. ### Returns **`list[Fill]`** — flat list of fills with HL-shaped keys. ### Divergences from upstream * Dango emits both maker and taker rows per fill (paired by `fill_id`). The wrapper dedupes by `fill_id` and prefers the taker side (the "aggressor's view" that HL historically reports). * `tid` is synthesised from `(block_height, idx)` to keep the upstream `int` type — Dango uses string `fill_id`. * `feeToken` is hardcoded to `"USDC"` (Dango fees are settled in `bridge/usdc`). ### See also * [`user_fills_by_time`](./user_fills_by_time) — time-windowed counterpart * [Native `perps_events_all`](../../../api/methods/info/perps_events_all) ## Info.user\_fills\_by\_time (HL-compat) HL `userFillsByTime` — fills in a time range. ### Signature ```python def user_fills_by_time( self, addr: str, start: int, end: int | None = None, ) -> list[Fill] ``` ### Example ```python from dango.hyperliquid_compatibility.info import Info info = Info(base_url="https://...") fills = info.user_fills_by_time("0x...", start=1700000000000, end=1700001000000) ``` ### Parameters **`addr`** — `str`. The user's account address. **`start`** — `int`. Lower bound, milliseconds since Unix epoch. **`end`** — `int | None`, optional. Upper bound. Default: `None` (no upper bound). ### Returns **`list[Fill]`** — flat list of fills in the time range. ### Notes * The wrapper iterates `perps_events_all` newest-first (`BLOCK_HEIGHT_DESC`) and breaks as soon as an event's `createdAt < start`. On a high-volume user this avoids paginating the entire history. ### See also * [`user_fills`](./user_fills) — no time filter * [Native `perps_events_all`](../../../api/methods/info/perps_events_all) ## Info.user\_state (HL-compat) HL `clearinghouseState` — user margin and asset positions. `dex` is accepted-and-ignored. ### Signature ```python def user_state(self, address: str, dex: str = "") -> dict[str, Any] ``` ### Example ```python from dango.hyperliquid_compatibility.info import Info info = Info(base_url="https://...") state = info.user_state("0x...") print(state["marginSummary"], state["assetPositions"]) ``` ### Parameters **`address`** — `str`. The user's account address. **`dex`** — `str`, optional. Accepted-and-ignored (Dango has no builder DEX abstraction). Default: `""`. ### Returns **`dict[str, Any]`** — HL-shaped clearinghouseState: `{assetPositions, crossMarginSummary, marginSummary, withdrawable}`. Empty user returns the four keys with zero values. ### Divergences from upstream * `assetPositions[*].marginUsed` is always `"0"` (Dango doesn't track per-position margin). * `assetPositions[*].returnOnEquity` is always `"0"` (no per-position margin → no RoE). * `assetPositions[*].leverage.value` is always `1` (cross-only). * `assetPositions[*].maxLeverage` is always `1`. * `assetPositions[*].cumFunding` series are always `"0"` (funding folds into realized PnL on each fill). * `crossMarginSummary` and `marginSummary` always carry equal values (Dango is cross-margin only). ### See also * [Native `user_state_extended`](../../../api/methods/info/user_state_extended) * [Migration overview](../../hyperliquid) ## bulk\_cancel Cancel multiple open orders by chain `oid` in one batched native call. ### Signature ```python def bulk_cancel(self, cancel_requests: Iterable[dict[str, Any]]) -> dict[str, Any] ``` ### Example ```python result = exchange.bulk_cancel([ {"coin": "BTC", "oid": 12345}, {"coin": "ETH", "oid": 67890}, ]) ``` ### Parameters **`cancel_requests`** — `Iterable[dict[str, Any]]`. Each dict must have `coin` (HL coin name) and `oid` (int order id). Empty input raises `ValueError`. ### Returns `dict[str, Any]` — Hyperliquid-shaped bulk-cancel envelope. ### Notes * Each `coin` is verified via `info.name_to_pair` before dispatch — a typo fails loudly here rather than silently cancelling under an unknown pair. ### See also * [`cancel`](./cancel) — cancel a single order * [`batch_update_orders`](../../../api/methods/exchange/batch_update_orders) — native equivalent ## Exchange.bulk\_cancel\_by\_cloid (HL-compat) Cancel multiple open orders by `cloid` in one batched native call. ### Signature ```python def bulk_cancel_by_cloid( self, cancel_requests: Iterable[dict[str, Any]], ) -> dict[str, Any] ``` ### Example ```python from dango.hyperliquid_compatibility.exchange import Exchange from dango.hyperliquid_compatibility.types import Cloid ex = Exchange(wallet, base_url="...", account_address="0x...") ex.bulk_cancel_by_cloid([ {"coin": "ETH", "cloid": Cloid("0x12345678901234567890123456789012")}, {"coin": "BTC", "cloid": Cloid("0xabcdef00abcdef00abcdef00abcdef00")}, ]) ``` ### Parameters **`cancel_requests`** — `Iterable[dict[str, Any]]`. Each entry has `coin` and `cloid` (must be a `Cloid` instance — `TypeError` otherwise). Empty iterables raise `ValueError`. ### Returns **`dict[str, Any]`** — HL status envelope with `response.type="cancelByCloid"`. ### Notes * See [Migration: Cloid asymmetry](../../hyperliquid#1-cloid-is-hashed-lossily-the-cloid-in-responses-is-not-the-cloid-you-sent) for the lossy hash semantics. ### See also * [`cancel_by_cloid`](./cancel_by_cloid) — single-cancel counterpart ## Exchange.bulk\_modify\_orders\_new (HL-compat) Batch a list of cancel + submit pairs into one `batch_update_orders` call. ### Signature ```python def bulk_modify_orders_new( self, modify_requests: Iterable[dict[str, Any]], ) -> dict[str, Any] ``` ### Example ```python from dango.hyperliquid_compatibility.exchange import Exchange ex = Exchange(wallet, base_url="...", account_address="0x...") ex.bulk_modify_orders_new([ { "oid": 12345, "order": { "coin": "ETH", "is_buy": True, "sz": 0.75, "limit_px": 1490.0, "order_type": {"limit": {"tif": "Gtc"}}, "reduce_only": False, }, }, ]) ``` ### Parameters **`modify_requests`** — `Iterable[dict[str, Any]]`. Each entry has `oid` (`int | Cloid`) and `order` (an HL-shaped `OrderRequest`). Empty iterables raise `ValueError`. ### Returns **`dict[str, Any]`** — HL status envelope with `response.type="batchModify"`. One status entry per modify request. ### See also * [`modify_order`](./modify_order) — single-modify counterpart ## bulk\_orders Place multiple Hyperliquid-style orders in one batched native call. ### Signature ```python def bulk_orders( self, order_requests: Iterable[OrderRequest], *, builder: Any = None, grouping: Grouping = "na", ) -> dict[str, Any] ``` ### Example ```python from dango.hyperliquid_compatibility.exchange import Exchange result = exchange.bulk_orders([ {"coin": "BTC", "is_buy": True, "sz": 0.5, "limit_px": 65000, "order_type": {"limit": {"tif": "Gtc"}}, "reduce_only": False}, {"coin": "ETH", "is_buy": False, "sz": 2.0, "limit_px": 3200, "order_type": {"limit": {"tif": "Gtc"}}, "reduce_only": False}, ]) ``` ### Parameters **`order_requests`** — `Iterable[OrderRequest]`. List of Hyperliquid order-request dicts (same shape per item as `order()` arguments). **`builder`** — Keyword-only, unused. Raises `NotImplementedError` if non-`None` — Dango has no builder fee marketplace. **`grouping`** — Keyword-only, `Grouping`, default `"na"`. Raises `NotImplementedError` for any non-`"na"` value — the order-grouping concept is HL-specific. ### Returns `dict[str, Any]` — Hyperliquid-shaped bulk-order envelope. ### Notes * Routes to `batch_update_orders` on the native Exchange. * `cloid` per item is hashed to 64-bit via SHA-256 — see [`order`](./order). ### See also * [`order`](./order) — single-order entry * [`batch_update_orders`](../../../api/methods/exchange/batch_update_orders) — native equivalent ## cancel Cancel one open order by its on-chain `oid`. The `name` argument is verified for parity (typos fail loudly). ### Signature ```python def cancel(self, name: str, oid: int) -> dict[str, Any] ``` ### Example ```python result = exchange.cancel("BTC", oid=12345) ``` ### Parameters **`name`** — `str`. HL coin name (e.g. `"BTC"`). Used only for verification — `info.name_to_pair(name)` runs first to surface typos before the cancel is dispatched. **`oid`** — `int`. On-chain order id to cancel. ### Returns `dict[str, Any]` — Hyperliquid-shaped cancel envelope. ### Notes * Native cancel only needs the `oid`, but verifying `name` first gives a clearer error if the caller passes the wrong coin. ### See also * [`bulk_cancel`](./bulk_cancel) — cancel multiple orders in one call * [`cancel_order`](../../../api/methods/exchange/cancel_order) — native equivalent ## Exchange.cancel\_by\_cloid (HL-compat) Cancel one open order by `cloid`. Hashes the 16-byte HL cloid to Uint64. ### Signature ```python def cancel_by_cloid(self, name: str, cloid: Cloid) -> dict[str, Any] ``` ### Example ```python from dango.hyperliquid_compatibility.exchange import Exchange from dango.hyperliquid_compatibility.types import Cloid ex = Exchange(wallet, base_url="...", account_address="0x...") ex.cancel_by_cloid("ETH", Cloid("0x12345678901234567890123456789012")) ``` ### Parameters **`name`** — `str`. HL coin name (verified). **`cloid`** — `Cloid`. The HL 16-byte cloid. `Cloid.to_uint64()` produces the Uint64 the chain actually sees. ### Returns **`dict[str, Any]`** — HL status envelope with `response.type="cancelByCloid"`. :::warning the Uint64 derived from the cloid is the only thing the chain sees. If two different 16-byte cloids hash to the same Uint64 (probability \~2\*\*-32 over \~4 billion cloids), the cancel applies to whichever order claimed that Uint64 first. ::: ### See also * [`bulk_cancel_by_cloid`](./bulk_cancel_by_cloid) — multi-cancel counterpart * [Migration overview](../../hyperliquid#1-cloid-is-hashed-lossily-the-cloid-in-responses-is-not-the-cloid-you-sent) — the Cloid asymmetry ## Exchange.market\_close (HL-compat) Reduce-only market order to close the position in `coin`. **`px` is silently ignored.** ### Signature ```python def market_close( self, coin: str, *, sz: float | None = None, px: float | None = None, slippage: float = DEFAULT_SLIPPAGE, cloid: Cloid | None = None, builder: Any = None, ) -> dict[str, Any] ``` ### Example ```python from dango.hyperliquid_compatibility.exchange import Exchange ex = Exchange(wallet, base_url="...", account_address="0x...") ex.market_close("ETH") # close the full ETH position ``` ### Parameters **`coin`** — `str`. HL coin name. **`sz`** — `float | None`, optional. Size to close. `None` (default) closes the full position size (read via `info.user_state`). **`px`** — `float | None`, optional. **Accepted-and-ignored.** **`slippage`** — `float`, optional. Default: `DEFAULT_SLIPPAGE = 0.05`. **`cloid`** — `Cloid | None`, optional. Non-`None` raises `NotImplementedError`. **`builder`** — `Any`, optional. Non-`None` raises `NotImplementedError`. ### Returns **`dict[str, Any]`** — HL status envelope with `response.type="order"`. Returns an err envelope if there is no open position in `coin`, or if the position size is zero. ### Notes * Reads `assetPositions` from `info.user_state(self.account_address)` to determine which direction reduces the position. * The `szi` field (HL's signed size) drives the sign — closing a long sells (`-sz`), closing a short buys (`+sz`). ### See also * [`market_open`](./market_open) — opening counterpart * [Native `submit_market_order`](../../../api/methods/exchange/submit_market_order) ## Exchange.market\_open (HL-compat) Place a market order with a slippage cap. **`px` is silently ignored.** ### Signature ```python def market_open( self, name: str, is_buy: bool, sz: float, *, px: float | None = None, slippage: float = DEFAULT_SLIPPAGE, cloid: Cloid | None = None, builder: Any = None, ) -> dict[str, Any] ``` ### Example ```python from dango.hyperliquid_compatibility.exchange import Exchange ex = Exchange(wallet, base_url="...", account_address="0x...") ex.market_open("ETH", is_buy=True, sz=0.5, slippage=0.01) ``` ### Parameters **`name`** — `str`. HL coin name. **`is_buy`** — `bool`. **`sz`** — `float`. Always positive on the HL side. **`px`** — `float | None`, optional. **Accepted-and-ignored.** Dango computes its own slippage band against the contract's mark price. **`slippage`** — `float`, optional. Default: `DEFAULT_SLIPPAGE = 0.05` (matches HL). **`cloid`** — `Cloid | None`, optional. **Raises `NotImplementedError` if non-None** — Dango's `submit_market_order` does not accept a `client_order_id`. Use a limit order with `tif="Ioc"` if you need cloid binding for a market-equivalent. **`builder`** — `Any`, optional. Non-`None` raises `NotImplementedError`. ### Returns **`dict[str, Any]`** — HL status envelope with `response.type="order"`. ### Notes * This is the most common surprise during migration. See [Migration](../../hyperliquid#2-market_open-market_close-silently-ignore-px) for the rationale. ### See also * [`market_close`](./market_close) — reduce-only counterpart * [Native `submit_market_order`](../../../api/methods/exchange/submit_market_order) ## Exchange.modify\_order (HL-compat) Atomic cancel + replace, emulated as a single batched action. ### Signature ```python def modify_order( self, oid: int | Cloid, name: str, is_buy: bool, sz: float, limit_px: float, order_type: OrderType, reduce_only: bool = False, cloid: Cloid | None = None, ) -> dict[str, Any] ``` ### Example ```python from dango.hyperliquid_compatibility.exchange import Exchange ex = Exchange(wallet, base_url="...", account_address="0x...") ex.modify_order( oid=12345, name="ETH", is_buy=True, sz=0.75, limit_px=1490.0, order_type={"limit": {"tif": "Gtc"}}, ) ``` ### Parameters **`oid`** — `int | Cloid`. The order to cancel. `int` is the chain order id; `Cloid` triggers a Uint64-by-cloid cancel. **`name`** — `str`. HL coin name. **`is_buy`** — `bool`. **`sz`** — `float`. **`limit_px`** — `float`. **`order_type`** — `OrderType`. Only `{"limit": ...}` is supported; `{"trigger": ...}` raises. **`reduce_only`** — `bool`, optional. Default: `False`. **`cloid`** — `Cloid | None`, optional. Client id for the **new** order (independent from the `oid` being cancelled). ### Returns **`dict[str, Any]`** — HL status envelope with `response.type="batchModify"`. ### Notes * Dango has no first-class modify message. The wrapper packs a `CancelAction` + `SubmitAction` into one `batch_update_orders` call so the cancel and the new submission settle in the same block. ### See also * [`bulk_modify_orders_new`](./bulk_modify_orders_new) — multi-modify counterpart * [Native `batch_update_orders`](../../../api/methods/exchange/batch_update_orders) ## order Place a single Hyperliquid-style order. Routes to the native `submit_order`. ### Signature ```python def order( self, name: str, is_buy: bool, sz: float, limit_px: float, order_type: OrderType, reduce_only: bool = False, cloid: Cloid | None = None, builder: Any = None, ) -> dict[str, Any] ``` ### Example ```python from dango.hyperliquid_compatibility.exchange import Exchange from dango.utils.signing import Secp256k1Wallet wallet = Secp256k1Wallet.from_mnemonic("...", subaccount_index=0) exchange = Exchange( wallet, base_url="https://api-mainnet.dango.zone", account_address="0x...", ) result = exchange.order( name="BTC", is_buy=True, sz=0.5, limit_px=65000.0, order_type={"limit": {"tif": "Gtc"}}, ) ``` ### Parameters **`name`** — `str`. Hyperliquid coin name (e.g. `"BTC"`, `"ETH"`). Resolved to a Dango `pair_id` via `info.name_to_pair`. **`is_buy`** — `bool`. `True` for a long, `False` for a short. **`sz`** — `float`. Order size in base units of the coin. **`limit_px`** — `float`. Limit price. **`order_type`** — `OrderType`. Hyperliquid order-type dict, e.g. `{"limit": {"tif": "Gtc"}}` or `{"trigger": {...}}`. **`reduce_only`** — `bool`, default `False`. Reduce-only flag. **`cloid`** — `Cloid | None`, default `None`. Optional client order id (128-bit). **Hashed to a 64-bit `ClientOrderId` via SHA-256.** The cloid returned in the Dango response is the truncated value, not the original. **`builder`** — Unused. Dango has no builder-fee marketplace; pass anything (ignored). ### Returns `dict[str, Any]` — Hyperliquid-shaped order response envelope. ### Notes * See [Hyperliquid SDK migration](../../hyperliquid) for the full `Cloid` asymmetry discussion. ### See also * [`bulk_orders`](./bulk_orders) — batch many orders in one call * [`cancel`](./cancel) — cancel an open order * [`submit_order`](../../../api/methods/exchange/submit_order) — native equivalent ## Exchange.set\_expires\_after (HL-compat) Record an HL `expiresAfter` ms hint. **Currently a no-op with state storage.** ### Signature ```python def set_expires_after(self, expires_after: int | None) -> None ``` ### Example ```python from dango.hyperliquid_compatibility.exchange import Exchange ex = Exchange(wallet, base_url="...", account_address="0x...") ex.set_expires_after(1700000000000) ``` ### Parameters **`expires_after`** — `int | None`. HL's `expiresAfter` millisecond hint. Stored on `self.expires_after`. ### Returns `None`. ### Known gap The Python wrapper stores the value but does NOT currently thread it into the signed `Metadata`. HL's `expiresAfter` enforcement does not apply — txs do not auto-expire. See [Migration: set\_expires\_after is a no-op](../../hyperliquid#3-set_expires_after-is-a-no-op). ### See also * [Native `Exchange`](../../../api/classes/Exchange) — the native class has no equivalent yet ## Exchange.set\_referrer (HL-compat) Bind the signer as a referee of `code` (HL username form). ### Signature ```python def set_referrer(self, code: str) -> dict[str, Any] ``` ### Example ```python from dango.hyperliquid_compatibility.exchange import Exchange ex = Exchange(wallet, base_url="...", account_address="0x...") ex.set_referrer("alice") ``` ### Parameters **`code`** — `str`. The referrer's username. Forwarded verbatim to the native `set_referral` — username resolution happens chain-side via the account-factory contract. ### Returns **`dict[str, Any]`** — HL status envelope with `response.type="setReferrer"`. Empty `statuses` list on success; err envelope on chain-level failure. ### See also * [Native `set_referral`](../../../api/methods/exchange/set_referral) — accepts `int | str` ## First call Read public chain state in five minutes. No wallet, no signing, no env vars. ### Query the chain status ```python from dango.info import Info from dango.utils.constants import MAINNET_API_URL info = Info(MAINNET_API_URL, skip_ws=True) status = info.query_status() print(status["chainId"], status["block"]["blockHeight"]) ``` `skip_ws=True` disables the WebSocket subscription pipeline so the `Info` instance never opens a socket. Drop it when you want to subscribe. ### Read a user's positions ```python from dango.info import Info from dango.utils.constants import MAINNET_API_URL, PERPS_CONTRACT_MAINNET from dango.utils.types import Addr info = Info(MAINNET_API_URL, skip_ws=True) # The perps contract address holds the counterparty vault — it always # carries live positions, so it is a good stand-in for "any user". state = info.user_state(Addr(PERPS_CONTRACT_MAINNET)) if state: for pair_id, position in state["positions"].items(): print(pair_id, position["size"], position["entry_price"]) ``` ### List a pair's market data ```python from dango.info import Info from dango.utils.constants import MAINNET_API_URL from dango.utils.types import CandleInterval, PairId info = Info(MAINNET_API_URL, skip_ws=True) pair_id = PairId("perp/ethusd") stats = info.perps_pair_stats(pair_id) print("mid:", stats["currentPrice"], "24h vol:", stats["volume24H"]) candles = info.perps_candles(pair_id, CandleInterval.ONE_MINUTE, first=5) for candle in candles.nodes: print(candle["timeStart"], candle["open"], candle["close"]) ``` ### Subscribe to live trades ```python import time from dango.info import Info from dango.utils.constants import MAINNET_API_URL from dango.utils.types import PairId info = Info(MAINNET_API_URL) sub_id = info.subscribe_perps_trades(PairId("perp/ethusd"), print) time.sleep(15) info.unsubscribe(sub_id) info.disconnect_websocket() ``` Each callback fires once per fill with the unwrapped [`Trade`](../api/types/Trade) payload. The manager closes cleanly on `disconnect_websocket()`. ### Next * [Project setup](./project-setup) — wire up a signer to write transactions * [Concepts: Clients](../concepts/clients) — when to use `Exchange` vs `Info` vs `WebsocketManager` import { InstallTabs } from '../../../components/InstallTabs' ## Installation Install the `dango` package from PyPI. Requires Python 3.14 or newer. ### Install ### Dependencies The wheel pulls in three runtime dependencies automatically: * `eth-account` (>= 0.10) — secp256k1 primitives and BIP-39 mnemonic support * `requests` (>= 2.31) — sync HTTP transport for GraphQL queries and mutations * `websocket-client` (>= 1.5) — WebSocket transport for `graphql-transport-ws` subscriptions The SDK is sync-only. There is no asyncio surface. ### Verify ```python from dango.info import Info from dango.utils.constants import MAINNET_API_URL info = Info(MAINNET_API_URL, skip_ws=True) print(info.query_status()) ``` If the import resolves and `query_status()` returns a dict with a `chainId` key, the install is good. ### Next * [First call](./first-call) — a runnable read query * [Project setup](./project-setup) — environment variables, networks, signer construction ## Project setup Wire up a real project: pick an environment, configure a signer, hold one `Info` and one `Exchange`. ### Pick a network The SDK ships URLs and chain IDs as module constants. Import them from `dango.utils.constants`: ```python from dango.utils.constants import ( MAINNET_API_URL, TESTNET_API_URL, LOCAL_API_URL, CHAIN_ID_MAINNET, CHAIN_ID_TESTNET, PERPS_CONTRACT_MAINNET, PERPS_CONTRACT_TESTNET, ) ``` `Exchange` and `Info` default the perps contract to mainnet. Pass `perps_contract=PERPS_CONTRACT_TESTNET` (or any other deployment address) explicitly when targeting another chain. ### Store keys in environment variables Use `python-dotenv` or your orchestrator's env injection. The SDK reads no env vars on its own — you read them and pass values to the constructors. ```text title=".env" DANGO_SECRET_KEY=0x... # 32-byte hex secret DANGO_ACCOUNT_ADDRESS=0x... # Dango account address (NOT the EVM address) ``` :::warning the Dango account address is decoupled from the signing key. The same secp256k1 key can control multiple Dango accounts. Always set `DANGO_ACCOUNT_ADDRESS` explicitly — never derive it from the wallet's EVM address. ::: ### Build the signer trio ```python import os from eth_account import Account from dango.exchange import Exchange from dango.info import Info from dango.utils.constants import PERPS_CONTRACT_TESTNET, TESTNET_API_URL from dango.utils.types import Addr secret = os.environ["DANGO_SECRET_KEY"] account = Account.from_key(secret) address = Addr(os.environ["DANGO_ACCOUNT_ADDRESS"]) info = Info(TESTNET_API_URL, perps_contract=Addr(PERPS_CONTRACT_TESTNET)) exchange = Exchange( account, TESTNET_API_URL, account_address=address, info=info, perps_contract=Addr(PERPS_CONTRACT_TESTNET), ) ``` `Exchange` accepts either an `eth_account.LocalAccount` or any object implementing the [`Wallet`](../concepts/signers) protocol. Passing a `LocalAccount` triggers automatic wrapping as `Secp256k1Wallet` (raw secp256k1 over SHA-256, NOT EIP-712). Reuse the same `Info` for the `Exchange`'s read queries (chain status, simulate, broadcast, nonce, user\_index). Without `info=info`, the `Exchange` builds its own `Info` against the same `base_url` — fine, just an extra HTTP session. ### A first signed transaction ```python from dango.utils.types import PairId, TimeInForce result = exchange.submit_limit_order( PairId("perp/ethusd"), size="0.1", # signed: positive = buy, negative = sell limit_price="1500", time_in_force=TimeInForce.GTC, ) print(result) ``` `submit_limit_order` runs `simulate` → `sign` → `broadcast` and returns the BroadcastTxOutcome envelope. ### Next * [Concepts: Clients](../concepts/clients) — when to use which class * [Concepts: Transactions](../concepts/transactions) — the simulate/sign/broadcast pipeline * [Concepts: Error handling](../concepts/error-handling) — what each exception means ## Clients **What this teaches:** which of the three Python SDK classes to construct for each kind of work, and how they share state. ### The three roles | Class | Role | When to use | | ----------------------------------------------------- | --------------------------------------- | -------------------------------------------------------------- | | [`Info`](../api/classes/Info) | Read-side: queries + subscriptions | Public chain state, user state, market data, real-time streams | | [`Exchange`](../api/classes/Exchange) | Write-side: simulate + sign + broadcast | Place orders, deposit margin, manage referrals, liquidate | | [`WebsocketManager`](../api/classes/WebsocketManager) | Subscription transport | Almost never construct directly — `Info` builds one lazily | All three live under `dango.*`. The package root re-exports nothing — always import from the submodule. ```python from dango.info import Info from dango.exchange import Exchange from dango.websocket_manager import WebsocketManager # rare ``` ### The mental model `Info` is a sync GraphQL client. Every read query is one POST to `/graphql`; every subscription is one WebSocket frame on a managed connection. It is the only class you need for read-only workflows (dashboards, analytics, market makers' read path). `Exchange` subclasses `Info`'s base ([`API`](../api/classes/API)) and adds a signing pipeline. To build, sign, and broadcast a transaction it does three things: 1. Build an [`UnsignedTx`](../api/types/UnsignedTx) from your message list and the signer's current nonce. 2. Run `Info.simulate(unsigned_tx)` to learn the gas cost, then add `DEFAULT_GAS_OVERHEAD` to cover signature verification. 3. Sign the resulting `SignDoc`, wrap it into a `Tx`, and call `Info.broadcast_tx_sync(tx)`. The `Exchange` constructor takes an optional `info=` parameter so you can share one `Info` between read and write. When omitted, the `Exchange` builds its own internal `Info` over the same `base_url`. ```python from dango.exchange import Exchange from dango.info import Info from dango.utils.constants import MAINNET_API_URL from dango.utils.types import Addr info = Info(MAINNET_API_URL) exchange = Exchange( wallet, MAINNET_API_URL, account_address=Addr("0x..."), info=info, ) ``` ### Lifecycle `Info` is thread-safe for read queries — `requests.Session` handles connection pooling. For subscriptions, the first `subscribe_*` call lazily constructs a `WebsocketManager` (a daemon thread) and starts it. Call `info.disconnect_websocket()` to close cleanly: ```python info.disconnect_websocket() ``` If you want to keep the read path but never subscribe, pass `skip_ws=True` to the `Info` constructor. Calling `subscribe_*` after that raises `RuntimeError`. `Exchange` holds a [`SingleSigner`](../api/classes/SingleSigner) which tracks the next nonce. The signer increments optimistically on every `sign_tx` call — including on failed broadcasts — because the chain rejects duplicate nonces. If you broadcast outside the SDK (or your process crashes between simulate and broadcast), reset the nonce via `exchange.signer.next_nonce = N`. ### Read-only via `API` The base [`API`](../api/classes/API) class is the minimal GraphQL POST client. Both `Info` and `Exchange` subclass it. You rarely need it directly, but it is the right entry point for posting raw GraphQL documents the SDK does not wrap: ```python from dango.api import API api = API("https://api-mainnet.dango.zone") data = api.query("query { queryStatus { chainId } }") ``` ### Picking the right one * Reading? `Info`. * Writing? `Exchange`. * Streaming? `Info`. * Custom GraphQL? `API`. ### Next * [Signers & Authentication](./signers) — keys, the `Wallet` protocol, and `SingleSigner` ## Encoding & Types **What this teaches:** how Dango encodes numbers on the wire, where snake\_case vs camelCase boundaries are, and how the SDK's type aliases map to runtime values. ### Decimal strings everywhere Every USD price, USD value, quantity, and dimensionless ratio is a fixed-decimal string with 6 places of precision on the wire. The `dango_decimal()` helper produces the canonical form: ```python from decimal import Decimal from dango.utils.types import dango_decimal dango_decimal(1.5) # "1.500000" dango_decimal("1.5") # "1.500000" dango_decimal(Decimal("1.5")) # "1.500000" dango_decimal(1500) # "1500.000000" ``` If you pass a value that requires more than 6 fractional digits, `dango_decimal` raises `ValueError`. NaN and infinity raise too. Use this helper anywhere you construct a wire shape by hand; the public `Exchange` methods invoke it for you. Two HTTP-side scalars do not go through `dango_decimal`: * `Uint128` / `Uint64` — base-10 integer strings (e.g. order ids, share counts). Build with `str(int_value)`. * `Timestamp` — nanosecond-precision integer strings. ### Base units vs USD `Exchange.deposit_margin(amount)` takes `amount` as **base units** (a `Uint128`). 1.50 USDC = `1_500_000` (since `SETTLEMENT_DECIMALS = 6`). `Exchange.withdraw_margin(amount)` takes `amount` as **USD** (a `UsdValue`). 1.50 USDC = `1.5` (or `"1.5"`, or `Decimal("1.5")`). This asymmetry mirrors the on-chain contract: deposits travel inside a `Coins` map (base units), while withdrawals carry a `UsdValue` payload. The SDK does not hide this — it tracks the wire format directly. ### NewType aliases are runtime strings Every identifier alias in `dango.utils.types` (e.g. `Addr`, `PairId`, `OrderId`) is `NewType(name, str)`. At runtime they are plain `str`; at type-check time they are nominal. Construct them explicitly to keep the type checker happy: ```python from dango.utils.types import Addr, OrderId, PairId addr = Addr("0x...") pair = PairId("perp/ethusd") oid = OrderId("12345") ``` The `Uint64` and `Uint128` aliases are also `NewType(name, str)` — wire numbers, not Python `int`. `UserIndex` and `Nonce` are the exceptions: they wrap `int`. ### Enums are StrEnum `TimeInForce`, `TriggerDirection`, `CandleInterval`, `KeyType`, `AccountStatus`, `ReasonForOrderRemoval`, `PerpsEventSortBy` all inherit from `enum.StrEnum`. The `.value` attribute is the wire form: ```python from dango.utils.types import CandleInterval, TimeInForce, TriggerDirection TimeInForce.GTC.value # "GTC" TriggerDirection.ABOVE.value # "above" CandleInterval.ONE_MINUTE # "ONE_MINUTE" ``` `TimeInForce` and `KeyType` use uppercase wire values; `TriggerDirection`, `AccountStatus`, and `ReasonForOrderRemoval` use lowercase snake\_case. The `CandleInterval` enum is uppercase because it crosses the indexer GraphQL boundary (see below). ### The case convention boundary Two GraphQL endpoints back the SDK and they speak different cases: * **Smart contract queries** (`query_app_smart`) use **snake\_case** keys, because Rust serde encodes contract structs with `rename_all = "snake_case"`. * **Indexer queries** (`perps_candles`, `perps_events`, `perps_pair_stats`, `subscribe_perps_trades`, etc.) use **camelCase**. The SDK preserves both. TypedDicts for contract-side types (`UserState`, `Position`, `Param`, etc.) use snake\_case attributes. TypedDicts for indexer-side types (`PerpsCandle`, `PerpsEvent`, `Trade`, `Block`) keep camelCase verbatim and silence Ruff's `N815` warning per field. The only crossover is [`PageInfo`](../api/types/PageInfo) and [`Connection`](../api/types/Connection) — they are user-facing pagination dataclasses and use snake\_case. `_make_page_info` and `_make_connection` cross the boundary once and the rest of the code stays snake\_case. ### Frozen dataclasses for user-facing actions `SubmitAction`, `CancelAction`, `ConditionalOrderRef`, `AllForPair`, `ClientOrderIdRef` are frozen dataclasses — the ergonomic forms callers pass to `batch_update_orders` and the cancel methods. The `Exchange` translates them to the externally-tagged wire shape (`{"submit": ...}` / `{"cancel": ...}` / `{"one": ...}` / `"all"`) internally. `ClientOrderIdRef(value=7)` exists because `OrderId` is `NewType("OrderId", str)` and `ClientOrderId` is `NewType("ClientOrderId", str)` — at runtime both are strings. The dataclass wrapper disambiguates `cancel_order(7)` vs `cancel_order(OrderId("7"))` at the call site. ### Next * [Error handling](./error-handling) — exception classes and what each catches ## Error handling **What this teaches:** what each SDK exception means, where it is raised, and how to recover. ### The hierarchy Every SDK exception derives from [`Error`](../api/errors/Error): ```text Error ├── ClientError 4xx HTTP response from /graphql ├── ServerError 5xx HTTP, network failure, non-JSON body ├── GraphQLError `errors` array on the GraphQL envelope └── TxFailed broadcastTxSync returned an `err` result ``` A blanket `except Error` is the safe outer catch; the SDK never raises anything else from public methods (except for value-error / type-error cases that surface caller bugs). ### When each fires [`ClientError`](../api/errors/ClientError) — HTTP 4xx. The most common causes are malformed requests, expired sessions, or rate-limit responses (429). The message body is truncated to the first 500 chars of the response text. ```python from dango.utils.error import ClientError try: info.query_app({"bad": "shape"}) except ClientError as exc: if "429" in str(exc): # rate limited — back off and retry ... raise ``` [`ServerError`](../api/errors/ServerError) — HTTP 5xx, plus every network-level failure: DNS, connection refused, timeout, SSL, non-JSON body. Wrap retries around this: ```python import time from dango.utils.error import ServerError for attempt in range(3): try: return info.query_status() except ServerError: if attempt == 2: raise time.sleep(2 ** attempt) ``` [`GraphQLError`](../api/errors/GraphQLError) — the HTTP layer succeeded with 2xx but the GraphQL envelope carried an `errors` array, or it was missing both `data` and `errors`. The message concatenates every error's `message` with its `path`: ```text GraphQLError: invalid pair_id 'perp/xyzusd' (path=['queryApp', 'wasm_smart']) ``` Treat this as a logical error: the request reached the server but the query failed validation or execution. [`TxFailed`](../api/errors/TxFailed) — `broadcast_tx_sync` succeeded at the HTTP/GraphQL layer but the transaction was rejected by the chain. The BroadcastTxOutcome carries an `err` result. :::note as of this writing, the SDK returns the BroadcastTxOutcome envelope unchanged from `Exchange._send_action`. `TxFailed` is defined for callers and downstream tooling to raise; the SDK does not raise it automatically. Inspect the returned dict's `check_tx.code` and `result.err` fields to detect failures. ::: ### Subscription errors do not raise WebSocket subscription errors arrive through the callback as `{"_error": ...}`, not as exceptions. After an error event the subscription is terminal — the manager already dropped your callback from its dispatch table. See [Subscriptions](./subscriptions) for the full contract. ### Caller bugs: TypeError / ValueError The SDK raises plain `TypeError` / `ValueError` for clearly-invalid caller input. These are not subclasses of `Error`; they signal a bug, not a runtime failure: * `dango_decimal(NaN)` → `ValueError` * `dango_decimal({"complex": "object"})` → `TypeError` * `Exchange.deposit_margin(-100)` → `ValueError` * `Exchange.deposit_margin(True)` → `TypeError` (bool is not int) * `Exchange.submit_order(pair, 0, kind)` → `ValueError` ("order size must be non-zero") A `RuntimeError` from `SingleSigner.build_unsigned_tx` / `sign_tx` means the signer is in an incomplete state — call `query_user_index()` / `query_next_nonce()` (or use `auto_resolve`) first. ### Reading rate-limit responses The server emits 429 with a JSON body describing the limit. Read it via `ClientError`: ```python from dango.utils.error import ClientError try: info.query_status() except ClientError as exc: msg = str(exc) if "429" in msg: # parse retry-after if needed ... ``` The Python SDK has no built-in retry. Caller is responsible. ### Next * [Rate limits & quotas](./rate-limits) — what the limits are and how to back off ## Rate limits & quotas **What this teaches:** the two server-side caps that affect Python SDK users, and how to stay under them. ### The two limits | Limit | Surface | What it caps | | --------------------------------- | -------------------- | --------------------------------------------- | | **167 requests / 10 s** | HTTP `/graphql` | All POST queries and mutations from one IP | | **30 subscriptions / connection** | WebSocket `/graphql` | Simultaneous `subscribe` frames on one socket | Both limits are enforced server-side. Hitting the HTTP limit returns 429 with a `ClientError`; hitting the WebSocket limit returns an `error` frame on the subscription, which surfaces as `{"_error": ...}` in your callback. ### HTTP: 167 / 10 s Bursting is fine within the window; sustained traffic above 167 req / 10 s gets throttled. Common ways to blow the limit: * Calling `pair_param(pair_id)` in a tight loop across 50 pairs instead of `pair_params()` once. * Using `query_app_multi` is the right answer for atomic batched reads. * `paginate_all` over a high-cardinality user's events without a `page_size` ceiling. ```python # Wasteful: 50 round trips for pair_id in pairs: info.pair_param(pair_id) # Efficient: one round trip all_params = info.pair_params() ``` ### The SDK does not retry There is no built-in retry, no jittered backoff, no circuit breaker. The choice is intentional — callers know their own throughput and tolerance better than a library default would. Wrap calls yourself: ```python import time from dango.utils.error import ClientError, ServerError def with_backoff(call, max_retries=5): for attempt in range(max_retries): try: return call() except ServerError: if attempt == max_retries - 1: raise time.sleep(2 ** attempt) except ClientError as exc: if "429" not in str(exc) or attempt == max_retries - 1: raise time.sleep(2 ** attempt) balances = with_backoff(lambda: info.user_state(addr)) ``` ### WebSocket: 30 / connection The server caps each WebSocket connection at 30 simultaneous subscriptions. If you need more, shard them across multiple `Info` instances — each one lazy-builds its own `WebsocketManager`, and each manager owns one connection. ```python from dango.info import Info from dango.utils.types import PairId URL = "https://api-mainnet.dango.zone" pairs = [PairId(f"perp/{coin}usd") for coin in ("eth", "btc", "sol", ...)] shards = [Info(URL) for _ in range((len(pairs) + 29) // 30)] for idx, pair in enumerate(pairs): shards[idx // 30].subscribe_perps_trades(pair, on_trade) ``` Remember to call `info.disconnect_websocket()` on each shard at shutdown. ### Detecting a 429 Rate-limit responses arrive as HTTP 429. The SDK wraps any 4xx in `ClientError` with the response body truncated to 500 chars. Parse the message: ```python from dango.utils.error import ClientError try: info.query_status() except ClientError as exc: if "429" in str(exc): # back off ... ``` ### Detecting a subscription rate-limit error Subscription rate-limit violations surface through the callback, not as an exception. Watch for the `_error` envelope: ```python def on_event(event): if isinstance(event, dict) and "_error" in event: # server dropped the subscription; reconnect or shard ... ``` Once a subscription receives an error, the manager has already removed its callback. Re-subscribe with a fresh id (and consider whether you have crossed the 30-per-connection cap). ### Next * [Subscriptions](./subscriptions) — the WebSocket model in detail * [Error handling](./error-handling) — the exception hierarchy ## Signers & Authentication **What this teaches:** how Dango separates keys from accounts, how the `Wallet` protocol abstracts signing, and what `SingleSigner` does on top. ### Three layers The signing stack has three concerns, each owned by a different object: 1. **Key material** — `Secp256k1Wallet` holds the 32-byte secret and produces signatures. 2. **Account state** — `SingleSigner` knows the on-chain `user_index` and current `next_nonce`. 3. **Transaction pipeline** — `Exchange` orchestrates simulate, sign, broadcast. The `Wallet` protocol is the interface that `Exchange` depends on. Any object with `address`, `key`, `key_hash`, and `sign(SignDoc) -> Signature` satisfies it. Today, only `Secp256k1Wallet` ships in tree; future Passkey and Session implementations will plug in without changes to `Exchange` or `SingleSigner`. ### Key vs account Dango decouples the signing key from the on-chain account. The same secp256k1 key can control multiple Dango accounts; an account always has a fixed `user_index` (its position in the account-factory's USERS map). Every `SingleSigner` is bound to one specific `address`: ```python from dango.utils.signing import Secp256k1Wallet, SingleSigner from dango.utils.types import Addr wallet = Secp256k1Wallet.from_mnemonic("test test test ...", Addr("0xaccount")) signer = SingleSigner(wallet, Addr("0xaccount")) ``` The `wallet.address` is advisory — it is the address you handed to the wallet constructor. The authoritative binding lives on the `SingleSigner.address`. ### Constructing a wallet `Secp256k1Wallet` exposes four constructors: ```python from dango.utils.signing import Secp256k1Wallet from dango.utils.types import Addr addr = Addr("0x...") w1 = Secp256k1Wallet(bytes.fromhex("aa" * 32), addr) # raw secret w2 = Secp256k1Wallet.random(addr) # CSPRNG w3 = Secp256k1Wallet.from_bytes(bytes.fromhex("aa" * 32), addr) # explicit factory w4 = Secp256k1Wallet.from_mnemonic("test test test ...", addr) # BIP-39 mnemonic ``` The mnemonic path uses BIP-44 with coin type 60 (Ethereum's). Pass `coin_type=N` to derive at `m/44'/N'/0'/0/0` instead. The library uses `eth-account`'s unaudited HD wallet path, matching the Rust SDK's BIP-32 derivation. If you already hold an `eth_account.LocalAccount`, the `Exchange` constructor will wrap it for you: ```python from eth_account import Account from dango.exchange import Exchange from dango.utils.types import Addr account = Account.from_key("0x...") exchange = Exchange(account, base_url, account_address=Addr("0x...")) ``` Internally this calls `Secp256k1Wallet.from_eth_account(account, addr)`, which signs with `KeyType=Secp256k1` (raw secp256k1 over SHA-256), NOT with EIP-712. The Dango account address you pass is independent from the wallet's derived EVM address — they will not match. ### Nonces and SingleSigner Every signed transaction carries a `nonce`. The chain stores a sliding window of seen nonces per account and rejects duplicates. `SingleSigner` keeps a local `next_nonce` and increments it optimistically on every `sign_tx` call — even when broadcasting fails. Reusing a nonce after a failed broadcast is dangerous; the chain may have already accepted your tx. When `Exchange` constructs a signer, it auto-resolves both `user_index` and `next_nonce` by querying the chain: ```python from dango.info import Info from dango.utils.signing import Secp256k1Wallet, SingleSigner from dango.utils.types import Addr info = Info("https://api-mainnet.dango.zone") wallet = Secp256k1Wallet.from_mnemonic("...", Addr("0x...")) signer = SingleSigner.auto_resolve(wallet, Addr("0x..."), info) print(signer.user_index, signer.next_nonce) ``` For deterministic tests or recovery from a stale state, construct directly with `user_index=` and `next_nonce=` set, then bypass the auto-resolve: ```python signer = SingleSigner(wallet, addr, user_index=42, next_nonce=7) ``` ### What gets signed `sign_tx` builds a [`SignDoc`](../api/types/SignDoc), encodes it as canonical JSON (sorted keys, no whitespace, drops `None` from `data`), and hashes with SHA-256. The 32-byte digest is what the secp256k1 signature covers. The functions are exposed for tests and integration verification: ```python from dango.utils.signing import sign_doc_canonical_json, sign_doc_sha256 raw = sign_doc_canonical_json(sign_doc) digest = sign_doc_sha256(sign_doc) ``` ### Next * [Transactions](./transactions) — the full simulate/sign/broadcast pipeline ## Subscriptions **What this teaches:** how the WebSocket layer is wired, the per-connection limits, and what the callback contract is. ### One manager, one connection `Info` constructs a [`WebsocketManager`](../api/classes/WebsocketManager) lazily on the first `subscribe_*` call. The manager owns exactly one WebSocket connection over the `graphql-transport-ws` protocol and multiplexes every subscription onto it. The manager itself is a daemon `threading.Thread` — it runs `websocket-client`'s `run_forever()` and your callbacks fire on that thread. Every subscribe call returns an `int` subscription id. Hold onto it; you need it to unsubscribe. ```python from dango.info import Info from dango.utils.types import PairId info = Info("https://api-mainnet.dango.zone") sid = info.subscribe_perps_trades(PairId("perp/ethusd"), print) # ... info.unsubscribe(sid) info.disconnect_websocket() ``` ### Connection lifecycle The handshake follows the [graphql-transport-ws spec](https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md): 1. Client opens the WebSocket and sends `connection_init`. 2. Server replies with `connection_ack`. 3. Subscriptions sent before the ack are queued and flushed afterwards. The manager pings every 15 seconds (protocol-level `{"type": "ping"}` JSON, not frame-level ping) to defeat the server's 30-second idle timeout. Server pings are answered with `pong` automatically. ### Per-connection limit The server caps every WebSocket connection at **30 simultaneous subscriptions**. Past 30, the server rejects new subscribe frames with an `error` message. To shard subscriptions across that limit, construct multiple `Info` instances. Each `Info` lazy-builds its own `WebsocketManager` and therefore its own connection: ```python from dango.info import Info from dango.utils.types import PairId info_a = Info("https://api-mainnet.dango.zone") info_b = Info("https://api-mainnet.dango.zone") for pair in pairs_1_to_30: info_a.subscribe_perps_trades(pair, on_trade) for pair in pairs_31_to_60: info_b.subscribe_perps_trades(pair, on_trade) ``` ### The callback contract Callbacks fire on the WebSocket reader thread. Keep them fast — long-running work should hand off to a queue or another thread. Exceptions inside a callback are NOT caught by the manager and will crash the reader thread. Each `subscribe_*` method unwraps the GraphQL `next` payload before invoking your callback, so the argument is the inner node type: | Method | Callback argument | | ------------------------- | ----------------------------------------- | | `subscribe_perps_trades` | [`Trade`](../api/types/Trade) | | `subscribe_perps_candles` | [`PerpsCandle`](../api/types/PerpsCandle) | | `subscribe_user_events` | [`PerpsEvent`](../api/types/PerpsEvent) | | `subscribe_block` | [`Block`](../api/types/Block) | | `subscribe_query_app` | `dict[str, Any]` (the contract response) | #### Errors arrive through the callback Server-side errors do NOT raise. They arrive as `{"_error": payload}` through the same callback: ```python def on_trade(event): if "_error" in event: print("subscription failed:", event["_error"]) return print(event["fillPrice"], event["fillSize"]) ``` After an error, the subscription is terminal — the manager has already dropped the callback. To resume, call `subscribe_*` again with a new id. ### Filtering user events `subscribe_user_events` takes an optional `event_types` list. The filter rule is **(type=A AND user=X) OR (type=B AND user=X)** — the union of types intersected with the user: ```python from dango.utils.types import Addr info.subscribe_user_events( Addr("0x..."), on_event, event_types=["order_persisted", "order_removed"], ) ``` Without `event_types`, every event for the user streams through. ### Polling a query as a subscription `subscribe_query_app` re-runs a `queryApp` request every N blocks (default 10, \~10 seconds at Dango's \~1s block time). Use it to poll contract state when no native subscription exists: ```python from dango.utils.types import Addr request = { "wasm_smart": { "contract": Addr("0x..."), "msg": {"my_query": {}}, }, } info.subscribe_query_app(request, print, block_interval=1) ``` The callback receives the contract's response under `payload["response"]` (already unwrapped from the kind-keyed envelope). ### Next * [Rate limits & quotas](./rate-limits) — sharding subscriptions, 167/10s HTTP cap, 30/WS sub cap * [Error handling](./error-handling) — exception classes and when each is raised ## Transactions **What this teaches:** what happens between `exchange.submit_order(...)` and a tx hash, and what to do when something diverges. ### The pipeline Every `Exchange` method routes through one private helper, `_send_action(messages)`, which runs three steps: 1. **Simulate.** Build an [`UnsignedTx`](../api/types/UnsignedTx) from the messages + current nonce, send it to `Info.simulate`, read back `gas_used`. 2. **Sign.** Add `Exchange.DEFAULT_GAS_OVERHEAD` (770 000 — empirically measured cost of a secp256k1 verify) to the simulated `gas_used`, build a [`SignDoc`](../api/types/SignDoc), and sign it via the [`Wallet`](./signers) protocol. 3. **Broadcast.** Send the signed [`Tx`](../api/types/Tx) via `Info.broadcast_tx_sync` and return the BroadcastTxOutcome envelope. The chain rejects under-gas transactions. Simulation is the only way to compute the correct gas — never hard-code a value. ### A grounded example ```python from dango.exchange import Exchange from dango.utils.types import Addr, PairId, TimeInForce exchange = Exchange(wallet, base_url, account_address=Addr("0x...")) result = exchange.submit_limit_order( PairId("perp/ethusd"), size="0.5", # positive = buy limit_price="1500", time_in_force=TimeInForce.GTC, ) print(result["check_tx"]["events"]) ``` The returned `dict` is the raw BroadcastTxOutcome from the GraphQL endpoint. Order fills are surfaced via indexer events, not via the broadcast envelope; query `Info.orders_by_user` or subscribe to user events to confirm. ### Nonce handling `SingleSigner.next_nonce` increments on every `sign_tx` call, success or failure. The chain rejects duplicate nonces, so optimistic increment is safer than waiting for broadcast success. If a broadcast fails for a reason that does NOT consume the nonce (e.g. a network timeout before the request reached the chain), the local `next_nonce` is now one ahead. Two recovery paths: ```python # Re-fetch from chain exchange.signer.next_nonce = exchange.signer.query_next_nonce(exchange._info) # Or rewind manually if you know the tx never landed exchange.signer.next_nonce -= 1 ``` Direct mutation is intentional — the `SingleSigner` attributes are public and mutable for exactly this reason. ### Batching `batch_update_orders` packs multiple submits/cancels into one transaction. The chain enforces `1 <= len <= max_action_batch_size` (governance-tunable, fixture default 5). All actions execute atomically — either every action succeeds or none does. ```python from dango.utils.types import ( Addr, CancelAction, ClientOrderIdRef, OrderId, PairId, SubmitAction, TimeInForce, ) actions = [ SubmitAction( pair_id=PairId("perp/ethusd"), size="0.5", kind={"limit": {"limit_price": "1500", "time_in_force": "GTC", "client_order_id": None}}, ), CancelAction(spec=OrderId("12345")), CancelAction(spec=ClientOrderIdRef(value=7)), ] exchange.batch_update_orders(actions) ``` `SubmitAction` and `CancelAction` are the user-facing forms. The `Exchange` translates them into the externally-tagged wire shapes before signing. ### Direct access to the pipeline For custom messages outside the `Exchange` method set, build the pieces by hand: ```python from dango.utils.types import Addr messages = [ {"execute": {"contract": Addr("0x..."), "msg": {"my_custom": {}}, "funds": {}}}, ] unsigned = exchange.signer.build_unsigned_tx(messages, exchange._chain_id) sim = exchange._info.simulate(unsigned) signed = exchange.signer.sign_tx(messages, exchange._chain_id, int(sim["gas_used"]) + 770_000) outcome = exchange._info.broadcast_tx_sync(signed) ``` The `_info` and `_chain_id` attributes are name-mangled by convention (leading underscore) — accessing them is fine for power users who need this seam. ### Next * [Subscriptions](./subscriptions) — real-time streams * [Error handling](./error-handling) — what each exception means ## Addr A Dango account or contract address. `NewType("Addr", str)` — at runtime it is a plain `str`. ### Definition ```python Addr = NewType("Addr", str) ``` ### Construction ```python from dango.utils.types import Addr addr = Addr("0x90bc84df68d1aa59a857e04ed529e9a26edbea4f") ``` ### Notes * The `NewType` exists for type-checker enforcement. At runtime there is no class — `isinstance(addr, Addr)` does NOT work; use `isinstance(addr, str)`. * Always wrap raw strings in `Addr(...)` at call sites to keep the type checker happy. ### See also * [`PairId`](./PairId), [`OrderId`](./OrderId), [`Hash256`](./Hash256) — sibling identifier aliases ## AllForPair Cancel every conditional order on one pair. ### Definition ```python @dataclass(frozen=True) class AllForPair: pair_id: PairId ``` ### Fields **`pair_id`** — `PairId`. ### Construction ```python from dango.utils.types import AllForPair, PairId target = AllForPair(pair_id=PairId("perp/ethusd")) ``` ### See also * [`ConditionalOrderRef`](./ConditionalOrderRef) — single-order target * [`cancel_conditional_order`](../methods/exchange/cancel_conditional_order) — consumer ## Block A block payload from the `subscribe_block` stream. Keys are camelCase (indexer wire shape). ### Definition ```python class Block(TypedDict): blockHeight: int hash: str appHash: str createdAt: str cronsOutcomes: list[str] transactions: list[BlockTransaction] flattenEvents: list[BlockEvent] ``` ### Fields **`blockHeight`** — `int`. **`hash`** — `str`. Block hash. **`appHash`** — `str`. Application state root after this block. **`createdAt`** — `str`. ISO-8601 datetime. **`cronsOutcomes`** — `list[str]`. Outcomes of cron jobs that ran in this block. **`transactions`** — `list[BlockTransaction]`. Every transaction included in the block. **`flattenEvents`** — `list[BlockEvent]`. The indexer's already-flattened event stream (cron events + message events + ...). ### See also * [`subscribe_block`](../methods/info/subscribe_block) — consumer ## CancelAction User-facing form of a cancel request. Pass to [`batch_update_orders`](../methods/exchange/batch_update_orders). ### Definition ```python @dataclass(frozen=True) class CancelAction: spec: OrderId | ClientOrderIdRef | Literal["all"] ``` ### Fields **`spec`** — `OrderId | ClientOrderIdRef | Literal["all"]`. The cancel target. See [`cancel_order`](../methods/exchange/cancel_order) for the same parameter shape. ### Construction ```python from dango.utils.types import CancelAction, ClientOrderIdRef, OrderId a = CancelAction(spec=OrderId("12345")) b = CancelAction(spec=ClientOrderIdRef(value=7)) c = CancelAction(spec="all") ``` ### See also * [`SubmitAction`](./SubmitAction) — counterpart for submits * [`batch_update_orders`](../methods/exchange/batch_update_orders) — consumer ## CandleInterval OHLCV candle interval enum. Wire form is the bare uppercase name. ### Definition ```python class CandleInterval(StrEnum): ONE_SECOND = "ONE_SECOND" ONE_MINUTE = "ONE_MINUTE" FIVE_MINUTES = "FIVE_MINUTES" FIFTEEN_MINUTES = "FIFTEEN_MINUTES" ONE_HOUR = "ONE_HOUR" FOUR_HOURS = "FOUR_HOURS" ONE_DAY = "ONE_DAY" ONE_WEEK = "ONE_WEEK" ``` ### Construction ```python from dango.utils.types import CandleInterval interval = CandleInterval.ONE_MINUTE print(interval.value) # "ONE_MINUTE" ``` ### Notes * The contract uses a `strum::Display` form (`1s`/`1m`/`1h`/...). This enum is the GraphQL indexer's wire form. The two are not interchangeable. ### See also * [`perps_candles`](../methods/info/perps_candles) — historical candles * [`subscribe_perps_candles`](../methods/info/subscribe_perps_candles) — live candle stream ## ClientOrderId A client-assigned order id. `NewType("ClientOrderId", str)` — at runtime it is a plain `str` (encoding a `Uint64`). ### Definition ```python ClientOrderId = NewType("ClientOrderId", str) ``` ### Construction ```python from dango.utils.types import ClientOrderId cid = ClientOrderId("7") ``` ### Notes * Submit a limit order with `client_order_id=N` to bind a client-side id. Later cancel via `cancel_order(ClientOrderIdRef(value=N))`. * The wire form is the integer's base-10 string. For Python-side construction at call sites, use [`ClientOrderIdRef`](./ClientOrderIdRef) — it carries an `int` and the wrapper makes the cancel intent unambiguous. ### See also * [`ClientOrderIdRef`](./ClientOrderIdRef) — user-facing wrapper * [`submit_limit_order`](../methods/exchange/submit_limit_order) — accepts a `client_order_id` ## ClientOrderIdRef User-facing wrapper around a client-assigned order id. Disambiguates from `OrderId` at runtime. ### Definition ```python @dataclass(frozen=True) class ClientOrderIdRef: value: int ``` ### Fields **`value`** — `int`. The client-assigned order id (Uint64 on the wire). ### Construction ```python from dango.utils.types import ClientOrderIdRef ref = ClientOrderIdRef(value=7) ``` ### Notes * The wrapper exists because both `OrderId` and `ClientOrderId` reduce to `str` at runtime. Without it, `cancel_order(7)` would be ambiguous. * Frozen — instances are hashable and immutable. ### See also * [`cancel_order`](../methods/exchange/cancel_order) — accepts `OrderId`, `ClientOrderIdRef`, or `"all"` * [`ClientOrderId`](./ClientOrderId) — the underlying alias ## ConditionalOrderRef User-facing form of a single conditional-order cancel target. ### Definition ```python @dataclass(frozen=True) class ConditionalOrderRef: pair_id: PairId trigger_direction: TriggerDirection ``` ### Fields **`pair_id`** — `PairId`. **`trigger_direction`** — `TriggerDirection`. ### Construction ```python from dango.utils.types import ConditionalOrderRef, PairId, TriggerDirection ref = ConditionalOrderRef(pair_id=PairId("perp/ethusd"), trigger_direction=TriggerDirection.ABOVE) ``` ### Notes * A user holds at most one conditional order per `(pair_id, trigger_direction)` combination, so this tuple uniquely identifies the cancel target. ### See also * [`AllForPair`](./AllForPair) — cancel every conditional order on one pair * [`cancel_conditional_order`](../methods/exchange/cancel_conditional_order) — consumer ## Connection A page of results plus its `PageInfo` cursors. Generic over the node type. ### Definition ```python @dataclass(frozen=True) class Connection[T]: nodes: list[T] page_info: PageInfo ``` ### Fields **`nodes`** — `list[T]`. The page's items, in cursor order. **`page_info`** — `PageInfo`. Cursors and `has_next_page` / `has_previous_page` flags. ### Construction ```python from dango.info import Info from dango.utils.constants import MAINNET_API_URL from dango.utils.types import CandleInterval, PairId info = Info(MAINNET_API_URL, skip_ws=True) page = info.perps_candles(PairId("perp/ethusd"), CandleInterval.ONE_MINUTE, first=10) print(len(page.nodes), page.page_info.has_next_page) ``` ### Notes * PEP 695 generic syntax — `Connection[PerpsCandle]`, `Connection[PerpsEvent]`, etc. * Frozen dataclass: hashable and immutable. ### See also * [`PageInfo`](./PageInfo) — the cursor metadata * [`paginate_all`](../helpers/paginate_all) — walk every page ## Hash256 A 256-bit hash, uppercase hex. `NewType("Hash256", str)` — at runtime it is a plain `str`. ### Definition ```python Hash256 = NewType("Hash256", str) ``` ### Construction ```python from dango.utils.types import Hash256 h = Hash256("AABBCCDD" * 8) # 64 hex chars, uppercase ``` ### Notes * The on-chain identifier for a wallet's public key. `Secp256k1Wallet.key_hash` produces an `Hash256` from the SHA-256 of the compressed pubkey. * Wire form is uppercase hex. ### See also * [`Secp256k1Wallet`](../classes/Secp256k1Wallet) — exposes `key_hash` ## OrderId A chain-assigned order id. `NewType("OrderId", str)` — at runtime it is a plain `str` (encoding a `Uint64`). ### Definition ```python OrderId = NewType("OrderId", str) ``` ### Construction ```python from dango.utils.types import OrderId oid = OrderId("12345") ``` ### Notes * The on-chain representation is a 64-bit unsigned integer; the wire form is the integer's base-10 string. * For client-assigned order ids, see [`ClientOrderId`](./ClientOrderId) and [`ClientOrderIdRef`](./ClientOrderIdRef). ### See also * [`cancel_order`](../methods/exchange/cancel_order) — cancel by `OrderId` * [`ClientOrderIdRef`](./ClientOrderIdRef) — disambiguates from `OrderId` at runtime ## OrderRemoved Event payload for `eventType == "order_removed"`. ### Definition ```python class OrderRemoved(TypedDict): order_id: OrderId pair_id: PairId user: Addr reason: ReasonForOrderRemoval client_order_id: ClientOrderId | None ``` ### Fields **`order_id`** — `OrderId`. **`pair_id`** — `PairId`. **`user`** — `Addr`. **`reason`** — `ReasonForOrderRemoval`. Why the order left the book: `filled`, `canceled`, `liquidated`, etc. **`client_order_id`** — `ClientOrderId | None`. The client id this order was submitted with, if any. ### Construction ```python from typing import cast from dango.utils.types import OrderRemoved, PerpsEvent event: PerpsEvent = ... if event["eventType"] == "order_removed": payload = cast(OrderRemoved, event["data"]) print(payload["reason"], payload["order_id"]) ``` ### See also * [`PerpsEvent`](./PerpsEvent) — container with `data` * [`ReasonForOrderRemoval`](./ReasonForOrderRemoval) — enum ## PageInfo Cursor-pagination metadata. Mirrors the GraphQL `PageInfo` object. ### Definition ```python @dataclass(frozen=True) class PageInfo: has_previous_page: bool has_next_page: bool start_cursor: str | None end_cursor: str | None ``` ### Fields **`has_previous_page`** — `bool`. **`has_next_page`** — `bool`. Pass `after=end_cursor` to fetch the next page. **`start_cursor`** — `str | None`. Cursor of the first node in this page. **`end_cursor`** — `str | None`. Cursor of the last node in this page. ### Notes * snake\_case attributes — this is a user-facing dataclass, not a wire shape. The `_make_page_info` helper crosses the camelCase boundary once. ### See also * [`Connection`](./Connection) — the page container * [`paginate_all`](../helpers/paginate_all) — walks pages using these cursors ## PairId A perps pair identifier. `NewType("PairId", str)` — at runtime it is a plain `str`. ### Definition ```python PairId = NewType("PairId", str) ``` ### Construction ```python from dango.utils.types import PairId pair = PairId("perp/ethusd") ``` ### Notes * Dango pair ids are lowercase strings with the prefix `perp/` and the suffix `usd` (every pair settles in USD). Examples: `perp/ethusd`, `perp/btcusd`, `perp/solusd`. * Use [`Info.pair_params`](../methods/info/pair_params) to enumerate the configured pairs. ### See also * [`Addr`](./Addr) — sibling identifier alias ## PairParam Per-pair perps parameters: tick size, margin ratios, vault tuning, bucket sizes. ### Definition ```python class PairParam(TypedDict): tick_size: UsdPrice min_order_size: UsdValue max_limit_price_deviation: Dimensionless max_market_slippage: Dimensionless max_abs_oi: Quantity max_abs_funding_rate: FundingRate initial_margin_ratio: Dimensionless maintenance_margin_ratio: Dimensionless impact_size: UsdValue vault_liquidity_weight: Dimensionless vault_half_spread: Dimensionless vault_max_quote_size: Quantity vault_size_skew_factor: Dimensionless vault_spread_skew_factor: Dimensionless vault_max_skew_size: Quantity funding_rate_multiplier: Dimensionless bucket_sizes: list[UsdPrice] ``` ### Fields Covers price tick / minimum size, margin ratios, vault market-maker tuning, funding-rate caps, and the list of bid/ask aggregation `bucket_sizes` used by [`liquidity_depth`](../methods/info/liquidity_depth). ### See also * [`pair_param`](../methods/info/pair_param) — fetcher * [`PairState`](./PairState) — runtime state counterpart ## PairState Per-pair runtime state: open interest, funding accumulator, current funding rate. ### Definition ```python class PairState(TypedDict): long_oi: Quantity short_oi: Quantity funding_per_unit: FundingPerUnit funding_rate: FundingRate ``` ### Fields **`long_oi`** — `Quantity`. Long-side open interest. **`short_oi`** — `Quantity`. Short-side open interest. **`funding_per_unit`** — `FundingPerUnit`. Funding accumulator (per-unit funding accrual since pair creation). **`funding_rate`** — `FundingRate`. Current funding rate (per `Param.funding_period`). ### See also * [`pair_state`](../methods/info/pair_state) — fetcher * [`PairParam`](./PairParam) — parameters counterpart ## Param Global perps parameters. ### Definition ```python class Param(TypedDict): max_unlocks: int max_open_orders: int maker_fee_rates: RateSchedule taker_fee_rates: RateSchedule protocol_fee_rate: Dimensionless liquidation_fee_rate: Dimensionless liquidation_buffer_ratio: Dimensionless funding_period: Duration vault_total_weight: Dimensionless vault_cooldown_period: Duration referral_active: bool min_referrer_volume: UsdValue referrer_commission_rates: RateSchedule vault_deposit_cap: UsdValue | None max_action_batch_size: int ``` ### Fields The TypedDict covers fee schedules, OI/order caps, the funding period, vault weight and cooldown, referral knobs, deposit cap, and the max batch-update size enforced by the chain. ### See also * [`perps_param`](../methods/info/perps_param) — fetcher * [`State`](./State) — runtime state counterpart * [`PairParam`](./PairParam) — per-pair parameters ## PerpsCandle One OHLCV candle from the indexer. Keys are camelCase (indexer wire shape). ### Definition ```python class PerpsCandle(TypedDict): pairId: str interval: str minBlockHeight: int maxBlockHeight: int open: str high: str low: str close: str volume: str volumeUsd: str timeStart: str timeStartUnix: int timeEnd: str timeEndUnix: int ``` ### Fields **`pairId`** — `str`. **`interval`** — `str`. The wire form of `CandleInterval` (e.g. `"ONE_MINUTE"`). **`open`/`high`/`low`/`close`** — `str`. 6-decimal `BigDecimal` strings. **`volume`** — `str`. Base-asset volume. **`volumeUsd`** — `str`. Quote-asset (USD) volume. **`timeStart`/`timeEnd`** — `str`. ISO-8601 datetime strings. **`timeStartUnix`/`timeEndUnix`** — `int`. Unix **milliseconds** (despite the `Unix` suffix). ### See also * [`perps_candles`](../methods/info/perps_candles) — fetcher * [`subscribe_perps_candles`](../methods/info/subscribe_perps_candles) — live stream ## PerpsEvent One indexer event record. Keys are camelCase (indexer wire shape). ### Definition ```python class PerpsEvent(TypedDict): idx: int blockHeight: int txHash: str eventType: str userAddr: str pairId: str data: dict[str, Any] createdAt: str ``` ### Fields **`idx`** — `int`. Per-block event index. **`blockHeight`** — `int`. **`txHash`** — `str`. **`eventType`** — `str`. Variant name of the on-chain perps event enum: `order_filled`, `order_persisted`, `order_removed`, `liquidated`, `deleveraged`, `referral_set`, etc. **`userAddr`** — `str`. **`pairId`** — `str`. **`data`** — `dict[str, Any]`. The event payload. Shape varies by `eventType`; cast to the matching TypedDict (`OrderFilled`, `Liquidated`, etc.) for typed access. **`createdAt`** — `str`. ISO-8601 datetime. ### See also * [`perps_events`](../methods/info/perps_events) — paginated fetcher * [`subscribe_user_events`](../methods/info/subscribe_user_events) — live stream ## PerpsEventSortBy Sort order for indexer event queries. ### Definition ```python class PerpsEventSortBy(StrEnum): BLOCK_HEIGHT_ASC = "BLOCK_HEIGHT_ASC" BLOCK_HEIGHT_DESC = "BLOCK_HEIGHT_DESC" ``` ### Construction ```python from dango.utils.types import PerpsEventSortBy sort = PerpsEventSortBy.BLOCK_HEIGHT_DESC ``` ### Notes * The indexer only accepts these two values; no ordering by other fields. * `BLOCK_HEIGHT_DESC` is the server-side default and matches the Python kwarg default. ### See also * [`perps_events`](../methods/info/perps_events) — paginated event stream * [`perps_events_all`](../methods/info/perps_events_all) — walker over every event ## PerpsPairStats 24-hour price and volume stats for one pair. Keys are camelCase (indexer wire shape). ### Definition ```python class PerpsPairStats(TypedDict): pairId: str currentPrice: str | None price24HAgo: str | None volume24H: str priceChange24H: str | None ``` ### Fields **`pairId`** — `str`. **`currentPrice`** — `str | None`. Last trade price. `None` if no trades in the lookback window. **`price24HAgo`** — `str | None`. Price 24h ago. `None` on insufficient history. **`volume24H`** — `str`. 24-hour USD volume. Defaults to `"0"` on no trades. **`priceChange24H`** — `str | None`. Absolute price change over 24h. ### See also * [`perps_pair_stats`](../methods/info/perps_pair_stats) — single-pair fetcher * [`all_perps_pair_stats`](../methods/info/all_perps_pair_stats) — every pair in one call ## Position One open perps position. Returned inside [`UserState`](./UserState). ### Definition ```python class Position(TypedDict): size: Quantity entry_price: UsdPrice entry_funding_per_unit: FundingPerUnit conditional_order_above: ConditionalOrder | None conditional_order_below: ConditionalOrder | None ``` ### Fields **`size`** — `Quantity`. Signed quantity (positive = long, negative = short). **`entry_price`** — `UsdPrice`. Volume-weighted entry price in USD. **`entry_funding_per_unit`** — `FundingPerUnit`. Funding accumulator snapshot at entry; used to compute realized funding on close. **`conditional_order_above`** — `ConditionalOrder | None`. The take-profit / stop-loss order with `TriggerDirection.ABOVE`, if any. **`conditional_order_below`** — `ConditionalOrder | None`. The conditional order with `TriggerDirection.BELOW`, if any. ### See also * [`PositionExtended`](./PositionExtended) — same plus computed PnL / liquidation price * [`UserState`](./UserState) — container ## PositionExtended A position plus computed unrealized PnL / funding / liquidation price. ### Definition ```python class PositionExtended(TypedDict): size: Quantity entry_price: UsdPrice entry_funding_per_unit: FundingPerUnit conditional_order_above: ConditionalOrder | None conditional_order_below: ConditionalOrder | None unrealized_pnl: UsdValue | None unrealized_funding: UsdValue | None liquidation_price: UsdPrice | None ``` ### Fields Inherits [`Position`](./Position) plus: **`unrealized_pnl`** — `UsdValue | None`. Mark-vs-entry PnL. `None` when not requested. **`unrealized_funding`** — `UsdValue | None`. Funding accrued since entry. **`liquidation_price`** — `UsdPrice | None`. Price at which the position would be liquidated; only present when `include_liquidation_price=True`. ### See also * [`Position`](./Position) — base shape * [`UserStateExtended`](./UserStateExtended) — container ## QueryOrderResponse A resting limit order. The shape returned by [`Info.order`](../methods/info/order). ### Definition ```python class QueryOrderResponse(TypedDict): user: Addr pair_id: PairId size: Quantity limit_price: UsdPrice reduce_only: bool reserved_margin: UsdValue created_at: Timestamp ``` ### Fields **`user`** — `Addr`. The order owner. **`pair_id`** — `PairId`. **`size`** — `Quantity`. Signed remaining size. **`limit_price`** — `UsdPrice`. **`reduce_only`** — `bool`. **`reserved_margin`** — `UsdValue`. Margin locked behind this order. **`created_at`** — `Timestamp`. ns-precision integer string. ### See also * [`order`](../methods/info/order) — fetcher * [`QueryOrdersByUserResponseItem`](./QueryOrdersByUserResponseItem) — same fields minus `user` ## QueryOrdersByUserResponseItem One item in [`Info.orders_by_user`](../methods/info/orders_by_user)'s response map. Same shape as `QueryOrderResponse` minus `user` (the map is already keyed by user). ### Definition ```python class QueryOrdersByUserResponseItem(TypedDict): pair_id: PairId size: Quantity limit_price: UsdPrice reduce_only: bool reserved_margin: UsdValue created_at: Timestamp ``` ### Fields **`pair_id`** — `PairId`. **`size`** — `Quantity`. Signed remaining size. **`limit_price`** — `UsdPrice`. **`reduce_only`** — `bool`. **`reserved_margin`** — `UsdValue`. **`created_at`** — `Timestamp`. ns-precision integer string. ### See also * [`orders_by_user`](../methods/info/orders_by_user) — fetcher * [`QueryOrderResponse`](./QueryOrderResponse) — single-order counterpart ## ReasonForOrderRemoval Reason carried on `OrderRemoved` / `ConditionalOrderRemoved` events. ### Definition ```python class ReasonForOrderRemoval(StrEnum): FILLED = "filled" CANCELED = "canceled" POSITION_CLOSED = "position_closed" SELF_TRADE_PREVENTION = "self_trade_prevention" LIQUIDATED = "liquidated" DELEVERAGED = "deleveraged" SLIPPAGE_EXCEEDED = "slippage_exceeded" PRICE_BAND_VIOLATION = "price_band_violation" SLIPPAGE_CAP_TIGHTENED = "slippage_cap_tightened" ``` ### Construction ```python from dango.utils.types import ReasonForOrderRemoval reason = ReasonForOrderRemoval.FILLED ``` ### Notes * Wire form is lowercase snake\_case (Rust serde convention). ### See also * [`OrderRemoved`](./OrderRemoved) — event that carries this enum ## SignDoc What gets canonical-JSON encoded and SHA-256 hashed before signing. ### Definition ```python class SignDoc(TypedDict): sender: Addr gas_limit: int messages: list[Message] data: Metadata ``` ### Fields **`sender`** — `Addr`. **`gas_limit`** — `int`. **`messages`** — `list[Message]`. Note: field name is `messages`, NOT `msgs`. This differs from `UnsignedTx` / `Tx` — a faithful mirror of the Rust types. **`data`** — `Metadata`. ### Notes * `Metadata`'s `expiry` field is dropped from the canonical bytes when `None` (the chain uses `skip_serializing_none`). `sign_doc_canonical_json` performs this stripping. ### See also * [`sign_doc_canonical_json`](../helpers/sign_doc_canonical_json) — canonical encoding * [`sign_doc_sha256`](../helpers/sign_doc_sha256) — 32-byte digest * [`Tx`](./Tx) and [`UnsignedTx`](./UnsignedTx) — wire envelopes ## State Global perps runtime state. ### Definition ```python class State(TypedDict): last_funding_time: Timestamp vault_share_supply: Uint128 insurance_fund: UsdValue treasury: UsdValue ``` ### Fields **`last_funding_time`** — `Timestamp`. ns-precision integer string of the last funding rate update. **`vault_share_supply`** — `Uint128`. Total counterparty-vault LP shares minted. **`insurance_fund`** — `UsdValue`. USD reserves for bad-debt coverage. **`treasury`** — `UsdValue`. Protocol fee accumulator. ### See also * [`perps_state`](../methods/info/perps_state) — fetcher * [`Param`](./Param) — global parameters counterpart ## SubmitAction User-facing form of a submit request. Pass to [`batch_update_orders`](../methods/exchange/batch_update_orders). ### Definition ```python @dataclass(frozen=True) class SubmitAction: pair_id: PairId size: float | int | str | Decimal kind: OrderKind reduce_only: bool = False tp: ChildOrder | None = None sl: ChildOrder | None = None ``` ### Fields **`pair_id`** — `PairId`. **`size`** — `float | int | str | Decimal`. Signed quantity. Positive = buy, negative = sell. **`kind`** — `OrderKind`. The wire-shape `{"market": ...}` or `{"limit": ...}` payload. **`reduce_only`** — `bool`. Default: `False`. **`tp`** — `ChildOrder | None`. Take-profit child order. Default: `None`. **`sl`** — `ChildOrder | None`. Stop-loss child order. Default: `None`. ### Construction ```python from typing import cast from dango.utils.types import OrderKind, PairId, SubmitAction kind = cast(OrderKind, {"limit": {"limit_price": "1500.000000", "time_in_force": "GTC", "client_order_id": None}}) action = SubmitAction(pair_id=PairId("perp/ethusd"), size="0.5", kind=kind) ``` ### See also * [`CancelAction`](./CancelAction) — counterpart for cancels * [`batch_update_orders`](../methods/exchange/batch_update_orders) — consumer ## TimeInForce Limit-order time-in-force enum. ### Definition ```python class TimeInForce(StrEnum): GTC = "GTC" IOC = "IOC" POST = "POST" ``` ### Fields * `GTC` — good-till-cancelled. Resting order. * `IOC` — immediate-or-cancel. Fills what it can; cancels the rest. * `POST` — post-only. Rejected if it would cross at submission. ### Construction ```python from dango.utils.types import TimeInForce tif = TimeInForce.GTC print(tif.value) # "GTC" ``` ### Notes * `StrEnum` subclass — `tif.value` is the wire form, but the enum compares equal to its string value too (`tif == "GTC"` works). ### See also * [`submit_limit_order`](../methods/exchange/submit_limit_order) — takes a `TimeInForce` ## Trade A real-time perps trade fill from the `subscribe_perps_trades` stream. Keys are camelCase (indexer wire shape). ### Definition ```python class Trade(TypedDict): orderId: str pairId: str user: str fillPrice: str fillSize: str closingSize: str openingSize: str realizedPnl: str fee: str createdAt: str blockHeight: int tradeIdx: int fillId: str | None isMaker: bool | None ``` ### Fields **`orderId`** — `str`. The chain order id this fill belongs to. **`pairId`** — `str`. **`user`** — `str`. The user address that owns the order. **`fillPrice`/`fillSize`** — `str`. 6-decimal strings. **`closingSize`/`openingSize`** — `str`. The fill's split between closing an existing position and opening new size. **`realizedPnl`/`fee`** — `str`. Realised PnL and fee in USD. **`createdAt`** — `str`. ISO-8601 datetime. **`blockHeight`/`tradeIdx`** — `int`. **`fillId`** — `str | None`. Stable per-fill id; pairs the maker and taker rows. **`isMaker`** — `bool | None`. `True` for the maker's view of the fill, `False` for the taker's. Each match emits both rows. ### See also * [`subscribe_perps_trades`](../methods/info/subscribe_perps_trades) — consumer ## TriggerDirection Conditional-order trigger direction. ### Definition ```python class TriggerDirection(StrEnum): ABOVE = "above" BELOW = "below" ``` ### Fields * `ABOVE` — trigger when `price >= trigger_price`. * `BELOW` — trigger when `price <= trigger_price`. ### Construction ```python from dango.utils.types import TriggerDirection direction = TriggerDirection.ABOVE ``` ### Notes * Wire values are lowercase, not uppercase. ### See also * [`submit_conditional_order`](../methods/exchange/submit_conditional_order) — takes a `TriggerDirection` ## Tx A signed transaction ready for `Info.broadcast_tx_sync`. ### Definition ```python class Tx(TypedDict): sender: Addr gas_limit: int msgs: list[Message] data: Metadata credential: Credential ``` ### Fields **`sender`** — `Addr`. The signing account address. **`gas_limit`** — `int`. Gas budget (typically `simulate.gas_used + DEFAULT_GAS_OVERHEAD`). **`msgs`** — `list[Message]`. **`data`** — `Metadata`. **`credential`** — `Credential`. The wrapped signature envelope (`{"standard": {key_hash, signature}}` for secp256k1). ### See also * [`UnsignedTx`](./UnsignedTx) — unsigned counterpart * [`SignDoc`](./SignDoc) — what gets signed * [`SingleSigner.sign_tx`](../classes/SingleSigner#sign_tx) — builder ## UnsignedTx A transaction body ready for `Info.simulate`. ### Definition ```python class UnsignedTx(TypedDict): sender: Addr msgs: list[Message] data: Metadata ``` ### Fields **`sender`** — `Addr`. The signing account address. **`msgs`** — `list[Message]`. Messages to include in the transaction. **`data`** — `Metadata`. Per-tx metadata (user\_index, chain\_id, nonce, expiry). ### Construction ```python unsigned = exchange.signer.build_unsigned_tx(messages, exchange._chain_id) sim = exchange._info.simulate(unsigned) ``` ### See also * [`Tx`](./Tx) — signed counterpart * [`SignDoc`](./SignDoc) — what gets canonical-JSON encoded and signed * [`SingleSigner.build_unsigned_tx`](../classes/SingleSigner#build_unsigned_tx) — builder ## UserState A user's deposited margin, vault shares, open positions, and pending unlocks. ### Definition ```python class UserState(TypedDict): margin: UsdValue vault_shares: Uint128 positions: dict[PairId, Position] unlocks: list[Unlock] reserved_margin: UsdValue open_order_count: int ``` ### Fields **`margin`** — `UsdValue`. Total deposited margin in USD (6-decimal string). **`vault_shares`** — `Uint128`. LP shares held by this user. **`positions`** — `dict[PairId, Position]`. Open positions, keyed by pair. **`unlocks`** — `list[Unlock]`. Pending vault withdrawals awaiting cooldown. **`reserved_margin`** — `UsdValue`. Margin locked behind open orders. **`open_order_count`** — `int`. Number of resting limit orders. ### Construction ```python from dango.info import Info from dango.utils.constants import MAINNET_API_URL from dango.utils.types import Addr info = Info(MAINNET_API_URL, skip_ws=True) state: dict | None = info.user_state(Addr("0x...")) ``` ### See also * [`UserStateExtended`](./UserStateExtended) — same plus computed equity / PnL fields * [`user_state`](../methods/info/user_state) — fetcher ## UserStateExtended A user's state plus computed equity / margin / PnL fields. ### Definition ```python class UserStateExtended(TypedDict): margin: UsdValue vault_shares: Uint128 unlocks: list[Unlock] reserved_margin: UsdValue open_order_count: int equity: UsdValue | None available_margin: UsdValue | None maintenance_margin: UsdValue | None positions: dict[PairId, PositionExtended] ``` ### Fields Inherits the base [`UserState`](./UserState) fields plus: **`equity`** — `UsdValue | None`. Total account value. `None` when `include_equity=False`. **`available_margin`** — `UsdValue | None`. Margin available to open new positions / withdraw. **`maintenance_margin`** — `UsdValue | None`. Minimum margin required to avoid liquidation. **`positions`** — `dict[PairId, PositionExtended]`. Per-position `unrealized_pnl`, `unrealized_funding`, `liquidation_price` added (each may be `None` per the `include_*` flags on the query). ### Construction ```python from dango.info import Info from dango.utils.constants import MAINNET_API_URL from dango.utils.types import Addr info = Info(MAINNET_API_URL, skip_ws=True) state = info.user_state_extended(Addr("0x..."), include_liquidation_price=True) ``` ### See also * [`UserState`](./UserState) — base shape * [`user_state_extended`](../methods/info/user_state_extended) — fetcher ## run `threading.Thread` entry point. Blocks in `run_forever()` until `stop()`. ### Signature ```python def run(self) -> None ``` ### Example ```python from dango.websocket_manager import WebsocketManager manager = WebsocketManager("https://api-mainnet.dango.zone") manager.start() # spawns a daemon thread that calls `run()` ``` Call `manager.start()`, not `manager.run()`. `start()` (inherited from `threading.Thread`) launches `run()` on a fresh thread; calling `run()` directly blocks the caller. ### Notes * The thread is daemon (`daemon=True`), so it exits with the main process. * `ping_interval=0` disables `websocket-client`'s frame-level ping scheduler — the manager drives keepalive at the GraphQL protocol layer instead. ### See also * [`stop`](./stop) — signal shutdown * [`WebsocketManager`](../../classes/WebsocketManager) — overview ## stop Signal shutdown and close the underlying WebSocket. ### Signature ```python def stop(self) -> None ``` ### Example ```python from dango.websocket_manager import WebsocketManager manager = WebsocketManager("https://api-mainnet.dango.zone") manager.start() # ... do work ... manager.stop() manager.join(timeout=5.0) ``` ### Notes * Sets an internal stop event, then calls `ws.close()`. The keepalive thread wakes up on the event and exits; `run_forever()` returns shortly after. * Does NOT wait for the thread to fully exit — call `manager.join(timeout=...)` after `stop()` if you need to block. ### See also * [`run`](./run) — the thread entry point * [`Info.disconnect_websocket`](../info/disconnect_websocket) — the high-level wrapper that does stop + join ## subscribe Register a subscription. Returns an int id usable with `unsubscribe`. ### Signature ```python def subscribe( self, document: str, variables: dict[str, Any], callback: Callable[[dict[str, Any]], None], ) -> int ``` ### Example ```python from dango.websocket_manager import WebsocketManager manager = WebsocketManager("https://api-mainnet.dango.zone") manager.start() document = """ subscription Trades($pairId: String!) { perpsTrades(pairId: $pairId) { fillPrice fillSize } } """ sid = manager.subscribe(document, {"pairId": "perp/ethusd"}, print) ``` ### Parameters **`document`** — `str`. A GraphQL subscription document (`subscription { ... }`). **`variables`** — `dict[str, Any]`. Variables for the subscription. **`callback`** — `Callable[[dict[str, Any]], None]`. Invoked with each `next` message's payload. The full payload (`{"data": ..., "extensions": ...}`) is passed; the `Info` wrappers unwrap to the inner node before invoking your callback. Errors arrive as `{"_error": payload}`. ### Returns **`int`** — a subscription id. Subscriptions opened before the server's `connection_ack` are queued and flushed once the ack arrives. ### Notes * For typed unwrapping and per-channel ergonomics, prefer the `Info.subscribe_*` methods. * Callbacks fire on the WebSocket reader thread. Keep them fast. ### See also * [`unsubscribe`](./unsubscribe) — drop a subscription * [`Info`](../../classes/Info) — high-level wrappers around this transport ## unsubscribe Drop a subscription locally and tell the server to stop streaming. ### Signature ```python def unsubscribe(self, subscription_id: int) -> bool ``` ### Example ```python from dango.websocket_manager import WebsocketManager manager = WebsocketManager("https://api-mainnet.dango.zone") manager.start() sid = manager.subscribe(document, variables, print) # ... later ... manager.unsubscribe(sid) ``` ### Parameters **`subscription_id`** — `int`. The id returned by a previous `subscribe` call. ### Returns **`bool`** — `True` if the subscription was registered. `False` if the id was unknown. The `complete` frame to the server is best-effort — sending failures are swallowed (the server will drop the subscription anyway when the connection eventually closes). ### See also * [`subscribe`](./subscribe) — register a subscription * [`stop`](./stop) — close the whole connection ## all\_perps\_pair\_stats 24-hour stats for every active pair. ### Signature ```python def all_perps_pair_stats(self) -> list[PerpsPairStats] ``` ### Example ```python from dango.info import Info from dango.utils.constants import MAINNET_API_URL info = Info(MAINNET_API_URL, skip_ws=True) for stats in info.all_perps_pair_stats(): print(stats["pairId"], stats["currentPrice"], stats["volume24H"]) ``` ### Returns **`list[PerpsPairStats]`** — one entry per pair. See [`PerpsPairStats`](../../types/PerpsPairStats). ### See also * [`perps_pair_stats`](./perps_pair_stats) — single-pair lookup ## broadcast\_tx\_sync Submit a signed `Tx`. Returns the BroadcastTxOutcome envelope. ### Signature ```python def broadcast_tx_sync(self, tx: Tx) -> dict[str, Any] ``` ### Example ```python from dango.info import Info from dango.utils.constants import MAINNET_API_URL info = Info(MAINNET_API_URL, skip_ws=True) signed = exchange.signer.sign_tx(messages, exchange._chain_id, gas_limit) outcome = info.broadcast_tx_sync(signed) ``` ### Parameters **`tx`** — `Tx`. A TypedDict with `sender`, `gas_limit`, `msgs`, `data`, and `credential`. Build via [`SingleSigner.sign_tx`](../../classes/SingleSigner#sign_tx). ### Returns **`dict[str, Any]`** — the BroadcastTxOutcome envelope. Inspect `check_tx.code`, `result.err`, etc. to detect failure modes. ### Notes * This is a GraphQL **mutation**, not a query. The SDK routes it through `API.query()` because the underlying HTTP transport is the same; the GraphQL operation keyword lives in the document string. * The SDK does not auto-raise on `err` results — inspect the returned dict. ### See also * [`simulate`](./simulate) — dry-run before broadcasting * [Concepts: Transactions](../../../concepts/transactions) — the full pipeline ## disconnect\_websocket Close the WebSocket connection and clean up the manager thread. ### Signature ```python def disconnect_websocket(self) -> None ``` ### Example ```python from dango.info import Info from dango.utils.constants import MAINNET_API_URL info = Info(MAINNET_API_URL) # ... subscribe and process events ... info.disconnect_websocket() ``` ### Notes * Signals the manager to stop, calls `ws.close()`, and joins the thread with a 5-second timeout. * After disconnect, a subsequent `subscribe_*` call rebuilds a fresh `WebsocketManager` and re-opens the socket. * Safe to call when no manager was ever constructed (e.g. if you only used HTTP queries). ### See also * [`unsubscribe`](./unsubscribe) — drop a single subscription * [`WebsocketManager`](../../classes/WebsocketManager) — the underlying transport ## liquidity\_depth Aggregated bid/ask depth at a chosen price-bucket granularity. `bucket_size` must match one of the granularities pre-configured on the pair (`pair_param.bucket_sizes`). The vault is typically the dominant maker on both sides. ### Signature ```python def liquidity_depth( self, pair_id: PairId, *, bucket_size: str, limit: int | None = None, ) -> LiquidityDepthResponse ``` ### Example ```python from dango.info import Info from dango.utils.constants import MAINNET_API_URL from dango.utils.types import PairId info = Info(MAINNET_API_URL, skip_ws=True) # Fetch the smallest bucket size advertised for this pair param = info.pair_param(PairId("perp/ethusd")) bucket = min(param["bucket_sizes"], key=float) if param else "0.10000" depth = info.liquidity_depth(PairId("perp/ethusd"), bucket_size=bucket, limit=5) print(depth["bids"], depth["asks"]) ``` ### Parameters **`pair_id`** — `PairId`. **`bucket_size`** — `str`. The wire form of `UsdPrice` — a 6-decimal fixed-point string. Must match one of the entries in this pair's `pair_param.bucket_sizes`; mismatched values are rejected by the contract. **`limit`** — `int | None`, optional. Maximum number of levels per side. Default: `None` (whole book). ### Returns **`LiquidityDepthResponse`** — `{bids: dict[UsdPrice, LiquidityDepth], asks: dict[UsdPrice, LiquidityDepth]}`. Both sides are price-keyed maps. ### Notes * The SDK does not validate `bucket_size` client-side. A round-trip rejection is cheap; callers who want to avoid it should fetch `pair_param(pair_id).bucket_sizes` first. ### See also * [`pair_param`](./pair_param) — inspect `bucket_sizes` before calling ## order Fetch a single resting limit order by id. Returns `None` if the order does not exist. ### Signature ```python def order(self, order_id: OrderId) -> dict[str, Any] | None ``` ### Example ```python from dango.info import Info from dango.utils.constants import MAINNET_API_URL from dango.utils.types import OrderId info = Info("https://api-mainnet.dango.zone", skip_ws=True) result = info.order(OrderId("12345")) if result is not None: print(result["pair_id"], result["size"], result["limit_price"]) ``` ### Parameters **`order_id`** — `OrderId`. The chain-assigned order id. ### Returns **`dict[str, Any] | None`** — order details, or `None` for unknown ids. Shape matches [`QueryOrderResponse`](../../types/QueryOrderResponse) — cast if you want typed access. ### See also * [`orders_by_user`](./orders_by_user) — enumerate all of a user's orders ## orders\_by\_user All resting limit orders for one user, keyed by chain `OrderId`. Returns only orders that have not been fully filled or cancelled; the count matches the user's `open_order_count` and the sum of `reserved_margin` across these orders equals `user_state.reserved_margin`. ### Signature ```python def orders_by_user(self, user: Addr) -> dict[OrderId, dict[str, Any]] ``` ### Example ```python from dango.info import Info from dango.utils.constants import MAINNET_API_URL from dango.utils.types import Addr info = Info(MAINNET_API_URL, skip_ws=True) orders = info.orders_by_user(Addr("0x...")) for oid, order in orders.items(): print(oid, order["pair_id"], order["size"], order["limit_price"]) ``` ### Parameters **`user`** — `Addr`. The user's account address. ### Returns **`dict[OrderId, dict[str, Any]]`** — map of order id to order details. The value shape matches [`QueryOrdersByUserResponseItem`](../../types/QueryOrdersByUserResponseItem) — cast to that TypedDict for typed access. ### See also * [`order`](./order) — fetch one order by id * [`cancel_order`](../exchange/cancel_order) — cancel by `OrderId` ## pair\_param Per-pair parameters. Returns `None` if the pair is not configured. Drives every pre-trade check: `tick_size` for price alignment, `min_order_size` for dust rejection, `max_abs_oi` for the per-side OI cap, the IM/MM ratios for margin, `max_abs_funding_rate` for the funding clamp, and `bucket_sizes` for liquidity depth queries. See protocol book: `perps/7-risk` for calibration guidance. ### Signature ```python def pair_param(self, pair_id: PairId) -> PairParam | None ``` ### Example ```python from dango.info import Info from dango.utils.constants import MAINNET_API_URL from dango.utils.types import PairId info = Info(MAINNET_API_URL, skip_ws=True) param = info.pair_param(PairId("perp/ethusd")) if param is not None: print(param["tick_size"], param["bucket_sizes"]) ``` ### Parameters **`pair_id`** — `PairId`. The pair identifier (e.g. `"perp/ethusd"`). ### Returns **`PairParam | None`** — TypedDict with per-pair configuration. `None` if the pair is not configured. See [`PairParam`](../../types/PairParam) for the full field list. ### See also * [`pair_params`](./pair_params) — enumerate all pairs * [`pair_state`](./pair_state) — runtime state counterpart ## pair\_params Enumerate per-pair parameters; paginated. ### Signature ```python def pair_params( self, *, start_after: PairId | None = None, limit: int = 30, ) -> dict[PairId, PairParam] ``` ### Example ```python from dango.info import Info from dango.utils.constants import MAINNET_API_URL info = Info(MAINNET_API_URL, skip_ws=True) params = info.pair_params() for pair_id, p in params.items(): print(pair_id, p["tick_size"]) ``` ### Parameters **`start_after`** — `PairId | None`, optional. Cursor for the next page. Default: `None` (start from the first pair). **`limit`** — `int`, optional. Page size. Default: `30`. ### Returns **`dict[PairId, PairParam]`** — map of pair id to per-pair parameters, in chain-sorted order. ### See also * [`pair_param`](./pair_param) — fetch one pair * [`pair_states`](./pair_states) — enumerate runtime state ## pair\_state Per-pair runtime state: open interest, funding accumulator, current funding rate. `funding_per_unit` is the pair-level running accumulator; per-position accrued funding is `size × (funding_per_unit − entry_funding_per_unit)` and settles lazily whenever a position is touched. `funding_rate` is the clamped per-day rate finalised at the most recent funding collection. See protocol book: `perps/3-funding` §3–§4. ### Signature ```python def pair_state(self, pair_id: PairId) -> PairState | None ``` ### Example ```python from dango.info import Info from dango.utils.constants import MAINNET_API_URL from dango.utils.types import PairId info = Info(MAINNET_API_URL, skip_ws=True) state = info.pair_state(PairId("perp/ethusd")) if state is not None: print(state["long_oi"], state["short_oi"], state["funding_rate"]) ``` ### Parameters **`pair_id`** — `PairId`. The pair identifier. ### Returns **`PairState | None`** — TypedDict with runtime state. `None` if the pair is not configured. See [`PairState`](../../types/PairState). ### See also * [`pair_states`](./pair_states) — enumerate all pairs * [`pair_param`](./pair_param) — parameters counterpart ## pair\_states Enumerate per-pair runtime states; paginated. ### Signature ```python def pair_states( self, *, start_after: PairId | None = None, limit: int = 30, ) -> dict[PairId, PairState] ``` ### Example ```python from dango.info import Info from dango.utils.constants import MAINNET_API_URL info = Info(MAINNET_API_URL, skip_ws=True) states = info.pair_states() for pair_id, s in states.items(): print(pair_id, s["funding_rate"]) ``` ### Parameters **`start_after`** — `PairId | None`, optional. Cursor for the next page. Default: `None`. **`limit`** — `int`, optional. Page size. Default: `30`. ### Returns **`dict[PairId, PairState]`** — map of pair id to runtime state. ### See also * [`pair_state`](./pair_state) — fetch one pair * [`pair_params`](./pair_params) — enumerate parameters ## perps\_candles OHLCV candles for one pair at one interval; cursor-paginated. ### Signature ```python def perps_candles( self, pair_id: PairId, interval: CandleInterval, *, later_than: str | None = None, earlier_than: str | None = None, first: int | None = None, after: str | None = None, ) -> Connection[PerpsCandle] ``` ### Example ```python from dango.info import Info from dango.utils.constants import MAINNET_API_URL from dango.utils.types import CandleInterval, PairId info = Info(MAINNET_API_URL, skip_ws=True) page = info.perps_candles( PairId("perp/ethusd"), CandleInterval.ONE_MINUTE, first=10, ) for candle in page.nodes: print(candle["timeStart"], candle["open"], candle["close"]) if page.page_info.has_next_page: next_page = info.perps_candles( PairId("perp/ethusd"), CandleInterval.ONE_MINUTE, first=10, after=page.page_info.end_cursor, ) ``` ### Parameters **`pair_id`** — `PairId`. **`interval`** — `CandleInterval`. Use the enum member (e.g. `CandleInterval.ONE_MINUTE`); the wire form is `interval.value` (the bare uppercase name). **`later_than`** — `str | None`, optional. ISO-8601 datetime string. Default: `None`. **`earlier_than`** — `str | None`, optional. ISO-8601 datetime string. Default: `None`. **`first`** — `int | None`, optional. Page size. Default: `None` (server-side default). **`after`** — `str | None`, optional. Cursor from a previous `page.page_info.end_cursor`. Default: `None`. ### Returns **`Connection[PerpsCandle]`** — a page of [`PerpsCandle`](../../types/PerpsCandle) plus cursors. Keys are camelCase (indexer wire shape). ### See also * [`paginate_all`](../../helpers/paginate_all) — walk every page * [`subscribe_perps_candles`](./subscribe_perps_candles) — live stream counterpart ## perps\_events Indexer event stream with filter and sort knobs; cursor-paginated. ### Signature ```python def perps_events( self, *, user_addr: Addr | None = None, event_type: str | None = None, pair_id: PairId | None = None, block_height: int | None = None, first: int | None = None, after: str | None = None, sort_by: PerpsEventSortBy = PerpsEventSortBy.BLOCK_HEIGHT_DESC, ) -> Connection[PerpsEvent] ``` ### Example ```python from dango.info import Info from dango.utils.constants import MAINNET_API_URL from dango.utils.types import Addr, PerpsEventSortBy info = Info(MAINNET_API_URL, skip_ws=True) page = info.perps_events( user_addr=Addr("0x..."), event_type="order_filled", first=20, sort_by=PerpsEventSortBy.BLOCK_HEIGHT_DESC, ) for event in page.nodes: print(event["blockHeight"], event["eventType"], event["data"]) ``` ### Parameters **`user_addr`** — `Addr | None`, optional. **`event_type`** — `str | None`, optional. Free-form string (the indexer schema does NOT constrain it to an enum). Common values: `"order_filled"` (regular and liquidation book fills, both carrying a `fill_id`), `"liquidated"`, and `"deleveraged"` (ADL — position transfers at the bankruptcy price, no `fill_id`). **`pair_id`** — `PairId | None`, optional. **`block_height`** — `int | None`, optional. **`first`** — `int | None`, optional. Page size. **`after`** — `str | None`, optional. Cursor for the next page. **`sort_by`** — `PerpsEventSortBy`, optional. `BLOCK_HEIGHT_ASC` or `BLOCK_HEIGHT_DESC`. Default: `BLOCK_HEIGHT_DESC` (matches the indexer's server-side default). ### Returns **`Connection[PerpsEvent]`** — a page of [`PerpsEvent`](../../types/PerpsEvent) plus cursors. ### See also * [`perps_events_all`](./perps_events_all) — walk every event matching a filter * [`subscribe_user_events`](./subscribe_user_events) — live stream counterpart ## perps\_events\_all Iterate every perps event matching the filter, walking pages internally. ### Signature ```python def perps_events_all( self, *, user_addr: Addr | None = None, event_type: str | None = None, pair_id: PairId | None = None, block_height: int | None = None, sort_by: PerpsEventSortBy = PerpsEventSortBy.BLOCK_HEIGHT_DESC, page_size: int = 100, ) -> Iterator[PerpsEvent] ``` ### Example ```python from dango.info import Info from dango.utils.constants import MAINNET_API_URL from dango.utils.types import Addr info = Info(MAINNET_API_URL, skip_ws=True) for event in info.perps_events_all( user_addr=Addr("0x..."), event_type="order_filled", ): print(event["createdAt"], event["data"]) ``` ### Parameters Same filter knobs as [`perps_events`](./perps_events) plus a `page_size` for the internal walker. Default page size: `100`. ### Returns **`Iterator[PerpsEvent]`** — yields events in cursor-order (default `BLOCK_HEIGHT_DESC` — newest first). Memory stays bounded over very long histories. ### Notes * Forward-only pagination. Backward (`last`/`before`) pagination is intentionally out of scope; call `perps_events` directly if you need a tail-anchored walk. ### See also * [`perps_events`](./perps_events) — single-page version * [`paginate_all`](../../helpers/paginate_all) — generic forward-pagination helper ## perps\_pair\_stats 24-hour price/volume stats for one pair. ### Signature ```python def perps_pair_stats(self, pair_id: PairId) -> PerpsPairStats ``` ### Example ```python from dango.info import Info from dango.utils.constants import MAINNET_API_URL from dango.utils.types import PairId info = Info(MAINNET_API_URL, skip_ws=True) stats = info.perps_pair_stats(PairId("perp/ethusd")) print("mid:", stats["currentPrice"], "24h vol:", stats["volume24H"]) ``` ### Parameters **`pair_id`** — `PairId`. ### Returns **`PerpsPairStats`** — TypedDict with `pairId`, `currentPrice`, `price24HAgo`, `volume24H`, `priceChange24H`. Keys are camelCase (indexer wire shape). See [`PerpsPairStats`](../../types/PerpsPairStats). ### Notes * Some fields (`currentPrice`, `price24HAgo`, `priceChange24H`) can be `None` when the pair has no recorded trades in the lookback window. ### See also * [`all_perps_pair_stats`](./all_perps_pair_stats) — every pair in one call ## perps\_param Global perps parameters: fee schedules, OI caps, funding period, batch size, vault settings. These chain-wide knobs include the volume-tiered `maker_fee_rates` / `taker_fee_rates`, `protocol_fee_rate` (treasury cut), `liquidation_fee_rate` and `liquidation_buffer_ratio`, the global `funding_period`, vault settings (`vault_cooldown_period`, `vault_total_weight`, `vault_deposit_cap`), and the referral master switch (`referral_active`, `min_referrer_volume`, `referrer_commission_rates`). Per-pair fields live in `pair_param`. ### Signature ```python def perps_param(self) -> Param ``` ### Example ```python from dango.info import Info from dango.utils.constants import MAINNET_API_URL info = Info(MAINNET_API_URL, skip_ws=True) params = info.perps_param() print(params["max_action_batch_size"], params["funding_period"]) ``` ### Returns **`Param`** — TypedDict with global perps configuration. See [`Param`](../../types/Param) for the full field list. ### See also * [`pair_params`](./pair_params) — per-pair parameters * [`perps_state`](./perps_state) — runtime state counterpart ## perps\_state Global perps runtime state: last funding time, vault share supply, treasury, insurance fund. The insurance fund collects liquidation fees and absorbs bad debt; it can go negative when accumulated bad debt exceeds fees collected, with future fees naturally replenishing it. The vault and insurance fund are isolated — external liquidation losses never touch vault margin. See protocol book: `perps/4-liquidation-and-adl` §7. ### Signature ```python def perps_state(self) -> State ``` ### Example ```python from dango.info import Info from dango.utils.constants import MAINNET_API_URL info = Info(MAINNET_API_URL, skip_ws=True) state = info.perps_state() print(state["vault_share_supply"], state["insurance_fund"]) ``` ### Returns **`State`** — TypedDict with global runtime fields. See [`State`](../../types/State) for the full field list. ### See also * [`perps_param`](./perps_param) — global parameters counterpart * [`pair_states`](./pair_states) — per-pair runtime state ## query\_app Generic `queryApp` wrapper. Returns the raw kind-keyed envelope. ### Signature ```python def query_app( self, request: dict[str, Any], *, height: int | None = None, ) -> Any ``` ### Example ```python from dango.info import Info from dango.utils.constants import MAINNET_API_URL from dango.utils.types import Addr info = Info(MAINNET_API_URL, skip_ws=True) result = info.query_app({"wasm_smart": {"contract": Addr("0x..."), "msg": {"my_query": {}}}}) # result is `{"wasm_smart": }` — the kind-keyed envelope. ``` ### Parameters **`request`** — `dict[str, Any]`. The query request shape: `{"wasm_smart": {...}}`, `{"multi": [...]}`, `{"config": ...}`, etc. The chain accepts one variant per call. **`height`** — `int | None`, optional. Query at a specific block height. Default: `None` (latest). ### Returns **Raw kind-keyed envelope** — e.g. `{"wasm_smart": }` or `{"multi": [...]}`. Most callers want the unwrapping helpers below. ### Notes * For single-contract smart queries, prefer [`query_app_smart`](./query_app_smart), which unwraps the envelope. * For multi-query batches, prefer [`query_app_multi`](./query_app_multi). ### See also * [`query_app_smart`](./query_app_smart) — unwraps `{wasm_smart: ...}` automatically * [`query_app_multi`](./query_app_multi) — atomic batched queries ## query\_app\_multi Atomically run multiple queries at one block height. Each result is wrapped as `{"Ok": }` or `{"Err": ""}` so partial failures do not abort the batch. ### Signature ```python def query_app_multi( self, queries: list[dict[str, Any]], *, height: int | None = None, ) -> list[dict[str, Any]] ``` ### Example ```python from dango.info import Info from dango.utils.constants import MAINNET_API_URL, PERPS_CONTRACT_MAINNET from dango.utils.types import Addr info = Info(MAINNET_API_URL, skip_ws=True) results = info.query_app_multi([ {"wasm_smart": {"contract": Addr(PERPS_CONTRACT_MAINNET), "msg": {"param": {}}}}, {"wasm_smart": {"contract": Addr(PERPS_CONTRACT_MAINNET), "msg": {"state": {}}}}, ]) for r in results: if "Ok" in r: print(r["Ok"]) else: print("failed:", r["Err"]) ``` ### Parameters **`queries`** — `list[dict[str, Any]]`. A list of query request shapes; same per-element shape as [`query_app`](./query_app) accepts. **`height`** — `int | None`, optional. Pin every query in the batch to one block height. Default: `None` (latest). ### Returns **`list[dict[str, Any]]`** — one `{"Ok": }` or `{"Err": ""}` per input query, in order. Inspect per element — the SDK does NOT auto-unwrap because partial failure is by design. ### Notes * All queries in the batch execute at the same block height. Useful for atomic snapshots across multiple contracts. ### See also * [`query_app_smart`](./query_app_smart) — single-query helper with auto-unwrap ## query\_app\_smart Convenience wrapper for `{wasm_smart: ...}` queries; unwraps the envelope and returns the contract response directly. ### Signature ```python def query_app_smart( self, contract: Addr, msg: dict[str, Any], *, height: int | None = None, ) -> Any ``` ### Example ```python from dango.info import Info from dango.utils.constants import MAINNET_API_URL, PERPS_CONTRACT_MAINNET from dango.utils.types import Addr info = Info(MAINNET_API_URL, skip_ws=True) # Read the perps contract's global parameters params = info.query_app_smart(Addr(PERPS_CONTRACT_MAINNET), {"param": {}}) ``` ### Parameters **`contract`** — `Addr`. Target contract address. **`msg`** — `dict[str, Any]`. The externally-tagged query message — Rust `serde` encodes contract `QueryMsg` enums as snake\_case single-key dicts (e.g. `{"param": {}}`, `{"pair_param": {"pair_id": "..."}}`). **`height`** — `int | None`, optional. Query at a specific block height. Default: `None` (latest). ### Returns **Contract response** — the raw shape the contract emits, with the `wasm_smart` wrapper stripped. Cast to a TypedDict from `dango.utils.types` if you want a typed view. ### Notes * The typed perps wrappers (`perps_param`, `pair_param`, `user_state`, etc.) all build on this method. ### See also * [`query_app`](./query_app) — the unwrapped variant * [`query_app_multi`](./query_app_multi) — atomic batched queries ## query\_status Chain ID and latest block info. ### Signature ```python def query_status(self) -> dict[str, Any] ``` ### Example ```python from dango.info import Info from dango.utils.constants import MAINNET_API_URL info = Info(MAINNET_API_URL, skip_ws=True) status = info.query_status() print(status["chainId"], status["block"]["blockHeight"]) ``` ### Returns **`dict[str, Any]`** — `{chainId, block: {blockHeight, hash, ...}, ...}` from the indexer's `queryStatus` field. Shape follows the GraphQL schema. ### See also * [`simulate`](./simulate) — uses the chain ID resolved here * [`Exchange`](../../classes/Exchange) — auto-fetches chain ID via `query_status` at construction ## simulate Dry-run an `UnsignedTx`. Returns `gas_used`, `gas_limit`, and the simulated result. ### Signature ```python def simulate(self, tx: UnsignedTx) -> dict[str, Any] ``` ### Example ```python from dango.exchange import Exchange from dango.info import Info from dango.utils.constants import MAINNET_API_URL info = Info(MAINNET_API_URL, skip_ws=True) unsigned = exchange.signer.build_unsigned_tx(messages, exchange._chain_id) sim = info.simulate(unsigned) gas_used = int(sim["gas_used"]) ``` ### Parameters **`tx`** — `UnsignedTx`. A TypedDict with `sender`, `msgs`, and `data` (the `Metadata`). Build via [`SingleSigner.build_unsigned_tx`](../../classes/SingleSigner#build_unsigned_tx). ### Returns **`dict[str, Any]`** — `{gas_used, gas_limit, result}`. The simulate path deliberately skips signature verification, so callers must add `Exchange.DEFAULT_GAS_OVERHEAD` (770 000) on top of `gas_used` for the broadcast gas limit. ### Notes * `Exchange._send_action` runs this internally; direct callers only need it when building custom transactions outside the `Exchange` method set. ### See also * [`broadcast_tx_sync`](./broadcast_tx_sync) — submit after simulate+sign * [Concepts: Transactions](../../../concepts/transactions) — the full pipeline ## subscribe\_block Stream every newly-finalized block. ### Signature ```python def subscribe_block(self, callback: Callable[[Block], None]) -> int ``` ### Example ```python import time from dango.info import Info from dango.utils.constants import MAINNET_API_URL info = Info(MAINNET_API_URL) def on_block(block): if isinstance(block, dict) and "_error" in block: print("error:", block["_error"]) return print(block["blockHeight"], "txs:", len(block["transactions"])) sid = info.subscribe_block(on_block) time.sleep(30) info.unsubscribe(sid) info.disconnect_websocket() ``` ### Parameters **`callback`** — `Callable[[Block], None]`. Invoked once per block. The [`Block`](../../types/Block) payload includes the full transaction list and the flattened event list (`flattenEvents`). ### Returns **`int`** — the subscription id. ### See also * [Concepts: Subscriptions](../../../concepts/subscriptions) ## subscribe\_perps\_candles Stream OHLCV candles for one pair at one interval. ### Signature ```python def subscribe_perps_candles( self, pair_id: PairId, interval: CandleInterval, callback: Callable[[PerpsCandle], None], ) -> int ``` ### Example ```python import time from dango.info import Info from dango.utils.constants import MAINNET_API_URL from dango.utils.types import CandleInterval, PairId info = Info(MAINNET_API_URL) sid = info.subscribe_perps_candles( PairId("perp/ethusd"), CandleInterval.ONE_MINUTE, print, ) time.sleep(60) info.unsubscribe(sid) info.disconnect_websocket() ``` ### Parameters **`pair_id`** — `PairId`. **`interval`** — `CandleInterval`. **`callback`** — `Callable[[PerpsCandle], None]`. Invoked once per candle. The server may emit multiple candles on reconnect; the SDK fan-out delivers them one at a time. ### Returns **`int`** — the subscription id. ### See also * [`perps_candles`](./perps_candles) — historical counterpart * [Concepts: Subscriptions](../../../concepts/subscriptions) ## subscribe\_perps\_trades Stream real-time perps trade fills for one pair. ### Signature ```python def subscribe_perps_trades( self, pair_id: PairId, callback: Callable[[Trade], None], ) -> int ``` ### Example ```python import time from dango.info import Info from dango.utils.constants import MAINNET_API_URL from dango.utils.types import PairId info = Info(MAINNET_API_URL) def on_trade(trade): if isinstance(trade, dict) and "_error" in trade: print("error:", trade["_error"]) return print(trade["fillPrice"], trade["fillSize"]) sid = info.subscribe_perps_trades(PairId("perp/ethusd"), on_trade) time.sleep(15) info.unsubscribe(sid) info.disconnect_websocket() ``` ### Parameters **`pair_id`** — `PairId`. **`callback`** — `Callable[[Trade], None]`. Invoked once per fill with the unwrapped [`Trade`](../../types/Trade) payload (camelCase keys). Errors arrive as `{"_error": ...}` through the same callback. ### Returns **`int`** — the subscription id. Pass to `unsubscribe(sid)` to drop. ### Notes * Per API doc §8.2: the server replays cached recent trades on connect, then streams new fills as they happen. * Callbacks fire on the WebSocket reader thread. Keep them fast. ### See also * [`subscribe_perps_candles`](./subscribe_perps_candles) — OHLCV stream * [Concepts: Subscriptions](../../../concepts/subscriptions) — the WebSocket model ## subscribe\_query\_app Re-run a `queryApp` request every N blocks. The callback receives the contract response under `payload["response"]`. ### Signature ```python def subscribe_query_app( self, request: dict[str, Any], callback: Callable[[dict[str, Any]], None], *, block_interval: int = 10, ) -> int ``` ### Example ```python import time from dango.info import Info from dango.utils.constants import MAINNET_API_URL, PERPS_CONTRACT_MAINNET from dango.utils.types import Addr info = Info(MAINNET_API_URL) request = { "wasm_smart": { "contract": Addr(PERPS_CONTRACT_MAINNET), "msg": {"state": {}}, }, } def on_payload(payload): if "_error" in payload: print("error:", payload["_error"]) return print("block:", payload.get("blockHeight"), "state:", payload["response"]) sid = info.subscribe_query_app(request, on_payload, block_interval=5) time.sleep(30) info.unsubscribe(sid) info.disconnect_websocket() ``` ### Parameters **`request`** — `dict[str, Any]`. Same shape as [`query_app`](./query_app) accepts. **`callback`** — `Callable[[dict[str, Any]], None]`. Invoked once per polling interval with `{"blockHeight": int, "response": }`. Single-key requests are auto-unwrapped — `payload["response"]` is the contract's own response shape (not the `{wasm_smart: ...}` envelope). **`block_interval`** — `int`, optional. Polling interval in blocks. Default: `10` (\~10s at Dango's \~1s block time). ### Returns **`int`** — the subscription id. ### Notes * Use this to "subscribe to" contract state when no native subscription exists (e.g. L2 depth, user state). * For multi-query requests (`{"multi": [...]}`), the callback sees the multi list under `payload["response"]`. ### See also * [`query_app`](./query_app) — one-shot counterpart * [Concepts: Subscriptions](../../../concepts/subscriptions) ## subscribe\_user\_events Stream events for one user, optionally filtered by `event_type`. ### Signature ```python def subscribe_user_events( self, user: Addr, callback: Callable[[PerpsEvent], None], *, event_types: list[str] | None = None, ) -> int ``` ### Example ```python import time from dango.info import Info from dango.utils.constants import MAINNET_API_URL from dango.utils.types import Addr info = Info(MAINNET_API_URL) sid = info.subscribe_user_events( Addr("0x..."), print, event_types=["order_filled", "order_removed"], ) time.sleep(60) info.unsubscribe(sid) info.disconnect_websocket() ``` ### Parameters **`user`** — `Addr`. The user's account address. **`callback`** — `Callable[[PerpsEvent], None]`. Invoked once per event. **`event_types`** — `list[str] | None`, optional. Restrict to these event types. The filter rule is **(type=A AND user=X) OR (type=B AND user=X)** — the union of types intersected with the user. Default: `None` (every event for the user). ### Returns **`int`** — the subscription id. ### Notes * Event types match the chain's perps event enum: `order_filled`, `order_persisted`, `order_removed`, `liquidated`, `deleveraged`, `referral_set`, etc. ### See also * [`perps_events_all`](./perps_events_all) — historical counterpart * [Concepts: Subscriptions](../../../concepts/subscriptions) ## unsubscribe Drop a subscription locally and tell the server to stop streaming. ### Signature ```python def unsubscribe(self, subscription_id: int) -> bool ``` ### Example ```python from dango.info import Info from dango.utils.constants import MAINNET_API_URL from dango.utils.types import PairId info = Info(MAINNET_API_URL) sid = info.subscribe_perps_trades(PairId("perp/ethusd"), print) # ... later ... ok = info.unsubscribe(sid) print("dropped:", ok) ``` ### Parameters **`subscription_id`** — `int`. The id returned by a previous `subscribe_*` call. ### Returns **`bool`** — `True` if the subscription was registered and a `complete` frame was sent. `False` if the id was unknown (or no WebSocket connection was ever opened). ### Notes * Calling `unsubscribe` after `disconnect_websocket` returns `False`. * Safe to call on shutdown even if you never opened a subscription. ### See also * [`disconnect_websocket`](./disconnect_websocket) — close the whole connection ## user\_state A user's deposited margin, positions, vault shares, pending unlocks. Returns the raw on-chain state record — deposited USD margin, vault shares, per-pair positions (size, entry price, entry funding accumulator), pending vault unlocks, reserved margin, and open-order count — without computing derived equity, PnL, or liquidation prices. Use [`user_state_extended`](./user_state_extended) for those. ### Signature ```python def user_state(self, user: Addr) -> UserState | None ``` ### Example ```python from dango.info import Info from dango.utils.constants import MAINNET_API_URL from dango.utils.types import Addr info = Info(MAINNET_API_URL, skip_ws=True) state = info.user_state(Addr("0x...")) if state is not None: print(state["margin"], state["open_order_count"]) for pair_id, position in state["positions"].items(): print(pair_id, position["size"], position["entry_price"]) ``` ### Parameters **`user`** — `Addr`. The user's account address. ### Returns **`UserState | None`** — TypedDict with margin, vault shares, positions (by pair), unlocks, reserved margin, open-order count. `None` when the user has no on-chain margin record. See [`UserState`](../../types/UserState). ### See also * [`user_state_extended`](./user_state_extended) — same plus computed equity / PnL / liquidation price * [`orders_by_user`](./orders_by_user) — open orders for the user ## user\_state\_extended `user_state` plus computed equity, margin, PnL fields. Each computed field is gated by an `include_*` flag so the contract can skip the work when not needed. Derivations come straight from the book: `equity = collateral_value + Σ unrealised_pnl − Σ accrued_funding`, `MM = Σ |size| × oracle × mmr`, and a user is liquidatable when `equity < MM`. See protocol book: `perps/1-margin` §4–§6. ### Signature ```python def user_state_extended( self, user: Addr, *, include_equity: bool = True, include_available_margin: bool = True, include_maintenance_margin: bool = True, include_unrealized_pnl: bool = True, include_unrealized_funding: bool = True, include_liquidation_price: bool = False, ) -> UserStateExtended | None ``` ### Example ```python from dango.info import Info from dango.utils.constants import MAINNET_API_URL from dango.utils.types import Addr info = Info(MAINNET_API_URL, skip_ws=True) state = info.user_state_extended(Addr("0x..."), include_liquidation_price=True) if state is not None: print("equity:", state["equity"], "available:", state["available_margin"]) ``` ### Parameters **`user`** — `Addr`. The user's account address. **`include_equity`** — `bool`, optional. Default: `True`. **`include_available_margin`** — `bool`, optional. Default: `True`. **`include_maintenance_margin`** — `bool`, optional. Default: `True`. **`include_unrealized_pnl`** — `bool`, optional. Default: `True`. **`include_unrealized_funding`** — `bool`, optional. Default: `True`. **`include_liquidation_price`** — `bool`, optional. Default: `False`. ### Returns **`UserStateExtended | None`** — TypedDict including `equity`, `available_margin`, `maintenance_margin`, and per-position `unrealized_pnl` / `unrealized_funding` / `liquidation_price` (per the flags). `None` if the user has no margin record. See [`UserStateExtended`](../../types/UserStateExtended). ### Notes * The Rust `QueryMsg::UserStateExtended` variant has an additional `include_all` boolean that overrides every per-flag knob; the Python SDK deliberately omits it — there should be one canonical way to ask for everything. ### See also * [`user_state`](./user_state) — base shape without computed fields * [`liquidate`](../exchange/liquidate) — call sites that need `include_liquidation_price` ## volume A user's cumulative trading volume in USD. Used for fee-tier resolution. Drives the volume-tiered fee schedule (maker/taker rates) and the lifetime volume gate for opting in as a referrer (`min_referrer_volume`). The 30-day rolling volume of a referrer's direct referees additionally determines their commission tier — see protocol book: `perps/6-referral` §6b. ### Signature ```python def volume(self, user: Addr, *, since: int | None = None) -> str ``` ### Example ```python from dango.info import Info from dango.utils.constants import MAINNET_API_URL from dango.utils.types import Addr info = Info(MAINNET_API_URL, skip_ws=True) lifetime = info.volume(Addr("0x...")) print("lifetime volume USD:", lifetime) ``` ### Parameters **`user`** — `Addr`. The user's account address. **`since`** — `int | None`, optional. Nanosecond-timestamp lower bound (matches Dango's `Timestamp` wire format). Default: `None` (lifetime). ### Returns **`str`** — a 6-decimal `UsdValue` string. Parse with `Decimal(...)` if you need arithmetic. ### See also * [`user_state`](./user_state) — current margin + positions ## add\_liquidity Debit USD from the trading margin to mint counterparty-vault LP shares. The vault is the perps exchange's passive market maker; LPs share its inventory-skew-aware quoting PnL. Shares are minted via the ERC-4626 virtual shares pattern, floor-rounded. Use `min_shares_to_mint` as a slippage guard when the vault equity is moving. See protocol book: `perps/5-vault` §2. ### Signature ```python def add_liquidity( self, amount: float | int | str | Decimal, *, min_shares_to_mint: int | None = None, ) -> dict[str, Any] ``` ### Example ```python from dango.exchange import Exchange from dango.utils.types import Addr exchange = Exchange(wallet, base_url, account_address=Addr("0x...")) # Deposit 100 USD of margin into the LP vault exchange.add_liquidity(100) # With slippage protection: revert if minting fewer than 95 shares exchange.add_liquidity("100.00", min_shares_to_mint=95) ``` ### Parameters **`amount`** — `float | int | str | Decimal`. USD amount, canonicalised via `dango_decimal`. Must be positive (`ValueError` on zero / negative). **`min_shares_to_mint`** — `int | None`, optional. Slippage guard: revert if fewer than this many shares would be minted. `None` (default) disables the guard. `bool` is rejected (`TypeError`); negative ints are rejected (`ValueError`). ### Returns **`dict[str, Any]`** — the BroadcastTxOutcome envelope. ### Notes * The USD amount is debited from the caller's existing trading margin — NOT attached as funds on the execute message. * Vault deposits flow inside the contract; the wallet boundary is not crossed. * Deposits revert if the vault's `effective_equity` is non-positive (catastrophic loss). ### See also * [`remove_liquidity`](./remove_liquidity) — counterpart that burns shares * [`deposit_margin`](./deposit_margin) — top up margin before adding liquidity ## batch\_update\_orders Submit and cancel multiple orders atomically in one transaction. All actions execute under a single tx — either every action succeeds or none does. This is the idiomatic way to cancel-and-replace (modify) a resting limit order: pair a `CancelAction` with a `SubmitAction` in one call. Each submission still passes the per-order pre-match checks described in `perps/2-order-matching`. ### Signature ```python def batch_update_orders( self, actions: list[SubmitOrCancelAction], ) -> dict[str, Any] ``` ### Example ```python from typing import cast from dango.exchange import Exchange from dango.utils.types import ( Addr, CancelAction, ClientOrderIdRef, OrderId, OrderKind, PairId, SubmitAction, ) exchange = Exchange(wallet, base_url, account_address=Addr("0x...")) kind = cast( OrderKind, {"limit": {"limit_price": "1500.000000", "time_in_force": "GTC", "client_order_id": None}}, ) exchange.batch_update_orders([ SubmitAction(pair_id=PairId("perp/ethusd"), size="0.5", kind=kind), CancelAction(spec=OrderId("12345")), CancelAction(spec=ClientOrderIdRef(value=7)), ]) ``` ### Parameters **`actions`** — `list[SubmitOrCancelAction]`. A list of `SubmitAction` and/or `CancelAction` dataclasses. Must contain at least one action (`ValueError` on empty list). The chain enforces `1 <= len <= max_action_batch_size` (governance-tunable, fixture default 5). Over-sized batches are rejected by the chain, not by the SDK — the SDK does not track governance changes locally. ### Returns **`dict[str, Any]`** — the BroadcastTxOutcome envelope. All actions execute atomically: either every action succeeds or none does. ### Notes * Use this method to atomically cancel+replace (modify) an order: pair a `CancelAction` with a `SubmitAction` in one call. ### See also * [`submit_order`](./submit_order) — single-order submission * [`cancel_order`](./cancel_order) — single-order cancellation * [`SubmitAction`](../../types/SubmitAction) and [`CancelAction`](../../types/CancelAction) ## cancel\_conditional\_order Cancel a conditional order by `ConditionalOrderRef`, by `AllForPair`, or `"all"` for every conditional order. ### Signature ```python def cancel_conditional_order( self, spec: CancelConditionalSpec, ) -> dict[str, Any] ``` ### Example ```python from dango.exchange import Exchange from dango.utils.types import ( Addr, AllForPair, ConditionalOrderRef, PairId, TriggerDirection, ) exchange = Exchange(wallet, base_url, account_address=Addr("0x...")) # Cancel one specific conditional order exchange.cancel_conditional_order( ConditionalOrderRef(pair_id=PairId("perp/ethusd"), trigger_direction=TriggerDirection.ABOVE), ) # Cancel every conditional order on one pair exchange.cancel_conditional_order(AllForPair(pair_id=PairId("perp/ethusd"))) # Cancel every conditional order on every pair exchange.cancel_conditional_order("all") ``` ### Parameters **`spec`** — `ConditionalOrderRef | AllForPair | Literal["all"]`. The cancel target. * `ConditionalOrderRef(pair_id, trigger_direction)` cancels one specific conditional order. * `AllForPair(pair_id)` cancels every conditional order on one pair. * `"all"` cancels every conditional order for this account. ### Returns **`dict[str, Any]`** — the BroadcastTxOutcome envelope. ### See also * [`submit_conditional_order`](./submit_conditional_order) — counterpart submit * [`ConditionalOrderRef`](../../types/ConditionalOrderRef) and [`AllForPair`](../../types/AllForPair) ## cancel\_order Cancel an open order by chain `OrderId`, by `ClientOrderIdRef`, or `"all"` to cancel every open order. Cancellation releases the order's reserved margin back to the user's available margin and decrements `open_order_count`. See protocol book: `perps/2-order-matching` §12. ### Signature ```python def cancel_order( self, spec: OrderId | ClientOrderIdRef | Literal["all"], ) -> dict[str, Any] ``` ### Example ```python from dango.exchange import Exchange from dango.utils.types import Addr, ClientOrderIdRef, OrderId exchange = Exchange(wallet, base_url, account_address=Addr("0x...")) # By chain order id exchange.cancel_order(OrderId("12345")) # By client-assigned order id exchange.cancel_order(ClientOrderIdRef(value=7)) # All open orders exchange.cancel_order("all") ``` ### Parameters **`spec`** — `OrderId | ClientOrderIdRef | Literal["all"]`. The cancel target. * `OrderId("12345")` cancels by the chain-assigned id. * `ClientOrderIdRef(value=7)` cancels by the client-assigned id passed at submission. The wrapper disambiguates from a string-typed `OrderId`. * `"all"` cancels every open order for this account. ### Returns **`dict[str, Any]`** — the BroadcastTxOutcome envelope. ### Notes * The literal `"all"` is checked first, then the dataclass wrapper, then the `OrderId` path. This branch order matters because `OrderId` is `NewType("OrderId", str)` — at runtime it is a plain `str` and would otherwise match `"all"`. ### See also * [`batch_update_orders`](./batch_update_orders) — cancel many orders atomically * [`submit_limit_order`](./submit_limit_order) — submit with a `client_order_id` for later cancel-by-cloid ## deposit\_margin Deposit USDC into the perps margin sub-account. Amount is in **base units** (Uint128). The perps contract holds margin internally as a USD value; deposits attach `bridge/usdc` base units as funds and the contract credits the user's `user_state.margin` 1:1 at the fixed $1 settlement rate. See protocol book: `perps/1-margin` §2. ### Signature ```python def deposit_margin(self, amount: int) -> dict[str, Any] ``` ### Example ```python from dango.exchange import Exchange from dango.utils.types import Addr exchange = Exchange(wallet, base_url, account_address=Addr("0x...")) # 1.50 USDC = 1_500_000 base units (SETTLEMENT_DECIMALS = 6) outcome = exchange.deposit_margin(1_500_000) ``` ### Parameters **`amount`** — `int`. USDC in base units. Must be a positive `int` (not `bool`, not `float`). The settlement denom is `bridge/usdc` with 6 decimals — `1_000_000` is one USDC. Raises `TypeError` on non-int, `ValueError` on zero or negative. ### Returns **`dict[str, Any]`** — the BroadcastTxOutcome envelope. Inspect `check_tx`/`deliver_tx` (or the test-shape `code`/`hash`/`events`) for outcome details. ### Notes * The wire shape uses a `funds` map (`Coins`) — base units are correct. Use [`withdraw_margin`](./withdraw_margin) for the opposite direction, which takes USD instead. * `to=None` (the default) sends funds to the signer's own margin sub-account. v1 does not expose the `to` override. ### See also * [`withdraw_margin`](./withdraw_margin) — counterpart that takes USD instead of base units * [Concepts: Encoding & Types](../../../concepts/encoding-and-types) — base units vs USD ## liquidate Force-close an underwater user's positions. Permissionless — anyone can call. The contract closes the minimum set of positions needed to restore `equity ≥ MM × (1 + liquidation_buffer_ratio)`, starting with the largest MM contributors. Closure runs against the order book first; any unfilled remainder is auto-deleveraged (ADL) against the most profitable counter- parties at the liquidated user's bankruptcy price. Liquidation fills (book and ADL) carry zero trading fees; the liquidation fee goes to the insurance fund, not the vault. See protocol book: `perps/4-liquidation-and-adl`. ### Signature ```python def liquidate(self, user: Addr) -> dict[str, Any] ``` ### Example ```python from dango.exchange import Exchange from dango.utils.types import Addr exchange = Exchange(wallet, base_url, account_address=Addr("0x...")) exchange.liquidate(Addr("0xunderwater")) ``` ### Parameters **`user`** — `Addr`. The target trader's account address. The contract checks whether the target is actually liquidatable based on equity vs. maintenance margin; non-liquidatable targets are rejected by the chain. ### Returns **`dict[str, Any]`** — the BroadcastTxOutcome envelope. ### Notes * No client-side validation: the chain's liquidatable-or-not check is authoritative. * The contract handler at `dango/perps/src/maintain/liquidate.rs` does NOT check the caller's identity — any signer can submit this message for any trader. ### See also * [`user_state_extended`](../info/user_state_extended) — read equity / maintenance margin / liquidation price to find candidates ## remove\_liquidity Burn LP shares to schedule a counterparty-vault withdrawal (subject to cooldown). The release value is computed as `effective_equity × (shares_to_burn / effective_supply)` at burn time, but the USD is not credited immediately — the cooldown prevents LPs from front-running known losses. See protocol book: `perps/5-vault` §3. ### Signature ```python def remove_liquidity(self, shares_to_burn: int) -> dict[str, Any] ``` ### Example ```python from dango.exchange import Exchange from dango.utils.types import Addr exchange = Exchange(wallet, base_url, account_address=Addr("0x...")) # Burn 50 LP shares exchange.remove_liquidity(50) ``` ### Parameters **`shares_to_burn`** — `int`. Number of shares to burn (Uint128). Must be a positive `int` (not `bool`, not `float`). Raises `TypeError` on non-int, `ValueError` on zero / negative. ### Returns **`dict[str, Any]`** — the BroadcastTxOutcome envelope. The actual USD release happens after the vault cooldown period elapses. ### Notes * The vault cooldown period is in [`perps_param`](../info/perps_param)'s `vault_cooldown_period` field. * Pending unlocks surface in [`user_state`](../info/user_state)'s `unlocks` list. ### See also * [`add_liquidity`](./add_liquidity) — counterpart that mints shares * [`user_state`](../info/user_state) — inspect `unlocks` to track pending releases ## set\_referral Bind the signer as a referee of `referrer`. Accepts a user\_index `int` or a username `str`. The referrer must already have opted in via `SetFeeShareRatio`; a user cannot refer themselves; the relationship is immutable once stored. On every subsequent fill the referee pays, the contract walks up to 5 upstream referrers and credits each level the marginal commission rate beyond what lower levels already captured. See protocol book: `perps/6-referral` §3b, §5. ### Signature ```python def set_referral(self, referrer: int | str) -> dict[str, Any] ``` ### Example ```python from dango.exchange import Exchange from dango.utils.types import Addr exchange = Exchange(wallet, base_url, account_address=Addr("0x...")) # By user_index exchange.set_referral(42) # By username exchange.set_referral("alice") ``` ### Parameters **`referrer`** — `int | str`. The referrer's `user_index` (non-negative `int`) or `username` (non-empty `str`). * `int`: used directly as the referrer's `user_index`. Negative values are rejected (`ValueError`); `bool` is rejected (`TypeError`). * `str`: triggers a username lookup against the account-factory contract. Empty strings are rejected (`ValueError`). ### Returns **`dict[str, Any]`** — the BroadcastTxOutcome envelope. ### Notes * The signer's own `user_index` (the referee) is auto-filled from `Exchange.signer.user_index`. * Username lookup is not cached — usernames are theoretically rebindable per the contract docs, and a fresh query costs one cheap round-trip. ### See also * [Concepts: Transactions](../../../concepts/transactions) — the underlying pipeline ## submit\_conditional\_order Place a conditional (TP/SL) order. Reduce-only by construction. The order fires as a market order when the oracle price crosses `trigger_price` in the chosen direction. `max_slippage` is bounded at submission by the per-pair `max_market_slippage`; if governance later tightens that cap, a stale conditional is cancelled by the cron with `reason = SlippageCapTightened`. See protocol book: `perps/2-order-matching` §3b. ### Signature ```python def submit_conditional_order( self, pair_id: PairId, size: float | int | str | Decimal | None, trigger_price: float | int | str | Decimal, trigger_direction: TriggerDirection, max_slippage: float | int | str | Decimal, ) -> dict[str, Any] ``` ### Example ```python from dango.exchange import Exchange from dango.utils.types import Addr, PairId, TriggerDirection exchange = Exchange(wallet, base_url, account_address=Addr("0x...")) # Stop loss: close 0.5 ETH long if price drops below 1400 exchange.submit_conditional_order( PairId("perp/ethusd"), size="-0.5", # negative = sell (closes a long) trigger_price="1400", trigger_direction=TriggerDirection.BELOW, max_slippage="0.01", ) # Take profit: close the entire position when price hits 1700 exchange.submit_conditional_order( PairId("perp/ethusd"), size=None, # None = close entire position trigger_price="1700", trigger_direction=TriggerDirection.ABOVE, max_slippage="0.01", ) ``` ### Parameters **`pair_id`** — `PairId`. **`size`** — `float | int | str | Decimal | None`. Signed quantity. Negative closes a long (sells), positive closes a short (buys). `None` closes the entire position at trigger time. Zero is rejected (`ValueError`). **`trigger_price`** — `float | int | str | Decimal`. Price at which the order triggers. **`trigger_direction`** — `TriggerDirection`. `TriggerDirection.ABOVE` (trigger when price >= trigger\_price) or `TriggerDirection.BELOW`. **`max_slippage`** — `float | int | str | Decimal`. Maximum slippage when the triggered order executes; a `Dimensionless` ratio. ### Returns **`dict[str, Any]`** — the BroadcastTxOutcome envelope. ### Notes * Conditional orders are always reduce-only by construction — `reduce_only` is not a parameter. * The sign on `size` (when not `None`) is the caller's responsibility. The contract does not double-check direction against the open position. ### See also * [`cancel_conditional_order`](./cancel_conditional_order) — counterpart cancel * [`TriggerDirection`](../../types/TriggerDirection) ## submit\_limit\_order Place a limit order. Convenience wrapper over [`submit_order`](./submit_order). `limit_price` must fall within the per-pair symmetric oracle band (`|limit_price − oracle| ≤ oracle × max_limit_price_deviation`) at submission; the band is re-checked against the current oracle on every resting maker when it is encountered during matching. See protocol book: `perps/2-order-matching` §3a, §6b. ### Signature ```python def submit_limit_order( self, pair_id: PairId, size: float | int | str | Decimal, limit_price: float | str | Decimal, *, time_in_force: TimeInForce = TimeInForce.GTC, client_order_id: int | None = None, reduce_only: bool = False, tp: ChildOrder | None = None, sl: ChildOrder | None = None, ) -> dict[str, Any] ``` ### Example ```python from dango.exchange import Exchange from dango.utils.types import Addr, PairId, TimeInForce exchange = Exchange(wallet, base_url, account_address=Addr("0x...")) # A buy-side GTC limit at 1500 exchange.submit_limit_order( PairId("perp/ethusd"), size="0.5", limit_price="1500", time_in_force=TimeInForce.GTC, ) # A post-only sell with a client order id exchange.submit_limit_order( PairId("perp/ethusd"), size="-0.5", limit_price="1550", time_in_force=TimeInForce.POST, client_order_id=42, ) ``` ### Parameters **`pair_id`** — `PairId`. **`size`** — `float | int | str | Decimal`. Signed quantity (positive = buy, negative = sell). **`limit_price`** — `float | str | Decimal`. The limit price in USD; canonicalised via [`dango_decimal`](../../helpers/dango_decimal). **`time_in_force`** — `TimeInForce`, optional. `TimeInForce.GTC` (good-till-cancelled), `TimeInForce.IOC` (immediate-or-cancel), `TimeInForce.POST` (post-only). Default: `TimeInForce.GTC`. **`client_order_id`** — `int | None`, optional. Client-assigned id (Uint64). Used to later cancel via `cancel_order(ClientOrderIdRef(value=...))`. Default: `None`. **`reduce_only`** — `bool`, optional. Default: `False`. **`tp`** — `ChildOrder | None`, optional. **`sl`** — `ChildOrder | None`, optional. ### Returns **`dict[str, Any]`** — the BroadcastTxOutcome envelope. ### Notes * `time_in_force.value` (a plain `str`) is what lands on the wire; passing the enum object directly is preferred for type safety. * A POST-only order that would cross the book at submission is rejected by the chain. ### See also * [`submit_market_order`](./submit_market_order) — market-order counterpart * [`cancel_order`](./cancel_order) — cancel by `ClientOrderIdRef` * [`TimeInForce`](../../types/TimeInForce) ## submit\_market\_order Place a market order with a slippage cap. Convenience wrapper over [`submit_order`](./submit_order). Market orders are IOC: the unfilled remainder is discarded, and the order reverts entirely if nothing fills. `max_slippage` is bounded at submission by the per-pair `max_market_slippage`; the worst acceptable execution price is `oracle × (1 ± max_slippage)` for bid/ask respectively. See protocol book: `perps/2-order-matching` §3, §3b. ### Signature ```python def submit_market_order( self, pair_id: PairId, size: float | int | str | Decimal, *, max_slippage: float | str | Decimal = 0.01, reduce_only: bool = False, tp: ChildOrder | None = None, sl: ChildOrder | None = None, ) -> dict[str, Any] ``` ### Example ```python from dango.exchange import Exchange from dango.utils.types import Addr, PairId exchange = Exchange(wallet, base_url, account_address=Addr("0x...")) # Buy 0.5 ETH with up to 1% slippage (default) exchange.submit_market_order(PairId("perp/ethusd"), size="0.5") # Sell 0.5 ETH with up to 0.5% slippage exchange.submit_market_order(PairId("perp/ethusd"), size="-0.5", max_slippage="0.005") ``` ### Parameters **`pair_id`** — `PairId`. **`size`** — `float | int | str | Decimal`. Signed quantity (positive = buy, negative = sell). Zero is rejected. **`max_slippage`** — `float | str | Decimal`, optional. Maximum acceptable slippage as a dimensionless ratio. `0.01` = 1%. Default: `0.01`. **`reduce_only`** — `bool`, optional. Default: `False`. **`tp`** — `ChildOrder | None`, optional. Take-profit child order. **`sl`** — `ChildOrder | None`, optional. Stop-loss child order. ### Returns **`dict[str, Any]`** — the BroadcastTxOutcome envelope. ### Notes * `max_slippage` is a `Dimensionless` field — passes through `dango_decimal`. Precision capped at 6 places. * The market order does not carry a `client_order_id`. ### See also * [`submit_limit_order`](./submit_limit_order) — limit-order counterpart * [`submit_order`](./submit_order) — the underlying method ## submit\_order Place a single perps order. Size is signed: positive = buy, negative = sell. Orders are decomposed into closing and opening portions against the user's existing position, then matched in price-time priority. Each fill settles funding, PnL, and the volume-tiered fee in-place on USD margin. See protocol book: `perps/2-order-matching`. ### Signature ```python def submit_order( self, pair_id: PairId, size: float | int | str | Decimal, kind: OrderKind, *, reduce_only: bool = False, tp: ChildOrder | None = None, sl: ChildOrder | None = None, ) -> dict[str, Any] ``` ### Example ```python from typing import cast from dango.exchange import Exchange from dango.utils.types import Addr, OrderKind, PairId exchange = Exchange(wallet, base_url, account_address=Addr("0x...")) # A buy-side GTC limit order. kind = cast( OrderKind, {"limit": {"limit_price": "1500.000000", "time_in_force": "GTC", "client_order_id": None}}, ) exchange.submit_order(PairId("perp/ethusd"), size="0.5", kind=kind) ``` In practice, prefer the higher-level helpers [`submit_limit_order`](./submit_limit_order) and [`submit_market_order`](./submit_market_order) — they construct the `OrderKind` for you. ### Parameters **`pair_id`** — `PairId`. E.g. `PairId("perp/ethusd")`. **`size`** — `float | int | str | Decimal`. Signed quantity. Positive = buy, negative = sell. Zero is rejected (`ValueError`). Routed through [`dango_decimal`](../../helpers/dango_decimal) — precision capped at 6 places. **`kind`** — `OrderKind`. The externally-tagged wire shape: `{"market": {"max_slippage": ""}}` or `{"limit": {"limit_price": "", "time_in_force": "GTC"|"IOC"|"POST", "client_order_id": ""|None}}`. **`reduce_only`** — `bool`, optional. Default: `False`. When `True`, the order can only reduce an existing position. **`tp`** — `ChildOrder | None`, optional. Take-profit child order. Default: `None`. **`sl`** — `ChildOrder | None`, optional. Stop-loss child order. Default: `None`. ### Returns **`dict[str, Any]`** — the BroadcastTxOutcome envelope. Fills surface via indexer events; query [`orders_by_user`](../info/orders_by_user) or subscribe to user events to confirm. ### Notes * Zero-sized orders are rejected client-side. The chain would also reject them — failing early surfaces a friendlier error. * All decimal inputs collapse to `"0.000000"` if equivalent to zero, so the zero check is unambiguous across `0`, `0.0`, `"0"`, `Decimal("0")`. * The chain rejects orders that fail pre-match checks: per-pair tick-size alignment, OI cap, slippage cap, limit-price band relative to oracle, and pre-match margin (worst-case 100 % fill IM + projected fee + reserved margin must be covered by equity). See `perps/2-order-matching` §3a, §3b, §5, §11. * Market and IOC limit orders revert with "no liquidity at acceptable price" if nothing fills; GTC remainders rest on the book and reserve `|opening_size| × limit_price × imr` until cancelled or filled. ### See also * [`submit_limit_order`](./submit_limit_order) — convenience helper for limit orders * [`submit_market_order`](./submit_market_order) — convenience helper for market orders * [`batch_update_orders`](./batch_update_orders) — atomic multi-order submission ## withdraw\_margin Withdraw USDC from the perps margin sub-account. Amount is in **USD** (6-decimal `UsdValue`). Specifies a USD amount to release; the contract converts to settlement-currency base units (floor-rounded) at the fixed $1 rate. Rejected if the requested amount exceeds `available_margin = max(0, equity − used_margin − reserved_margin)`. See protocol book: `perps/1-margin` §3, §8. ### Signature ```python def withdraw_margin(self, amount: float | str | Decimal) -> dict[str, Any] ``` ### Example ```python from decimal import Decimal from dango.exchange import Exchange from dango.utils.types import Addr exchange = Exchange(wallet, base_url, account_address=Addr("0x...")) outcome = exchange.withdraw_margin(Decimal("1.5")) # 1.50 USDC ``` ### Parameters **`amount`** — `float | str | Decimal`. USDC in USD (NOT base units). Passes through `dango_decimal()` which canonicalises to a 6-decimal fixed-point string. Raises `ValueError` if precision exceeds 6 places. ### Returns **`dict[str, Any]`** — the BroadcastTxOutcome envelope. ### Notes * Asymmetric with [`deposit_margin`](./deposit_margin): deposit takes base units (Uint128), withdraw takes USD (UsdValue). The asymmetry mirrors the on-chain contract — the SDK does not paper over it. * The contract converts USD to settlement-currency base units at a fixed **$1 per unit** rate (floor-rounded to base units). See protocol book: `perps/1-margin` §3. ### See also * [`deposit_margin`](./deposit_margin) — counterpart that takes base units * [`dango_decimal`](../../helpers/dango_decimal) — the decimal-canonicalisation helper ## dango\_decimal Return the canonical fixed-decimal string form of `x`. Raises if precision is lost. ### Signature ```python def dango_decimal( x: float | int | str | Decimal, max_places: int = 6, ) -> str ``` ### Example ```python from decimal import Decimal from dango.utils.types import dango_decimal dango_decimal(1.5) # "1.500000" dango_decimal("1.5") # "1.500000" dango_decimal(Decimal("1.5")) # "1.500000" dango_decimal(1500) # "1500.000000" ``` ### Parameters **`x`** — `float | int | str | Decimal`. The value to canonicalise. `bool` and `NaN`/`Inf` are rejected. **`max_places`** — `int`, optional. Maximum allowed fractional digits. Values requiring more precision raise `ValueError`. Default: `6` (matches the Dango wire convention for `UsdValue` / `UsdPrice` / `Dimensionless` / `Quantity`). ### Returns **`str`** — a fixed-point decimal string with exactly `max_places` fractional digits. ### Notes * Used by every method that constructs a `UsdValue`/`UsdPrice`/`Dimensionless`/`Quantity` field on the wire. * For `Uint128` / `Uint64` (order ids, share counts, base-unit amounts), stringify with `str(int_value)` — not this helper. ### See also * [Concepts: Encoding & Types](../../concepts/encoding-and-types) — wire-format conventions ## paginate\_all Yield every node across all forward-paginated pages. ### Signature ```python def paginate_all[T]( fetch_page: Callable[[str | None, int], Connection[T]], *, page_size: int = 100, ) -> Iterator[T] ``` ### Example ```python from dango.info import Info, paginate_all from dango.utils.constants import MAINNET_API_URL from dango.utils.types import CandleInterval, PairId info = Info(MAINNET_API_URL, skip_ws=True) candles = paginate_all( lambda after, first: info.perps_candles( PairId("perp/ethusd"), CandleInterval.ONE_MINUTE, first=first, after=after, ), page_size=200, ) for candle in candles: print(candle["timeStart"]) ``` ### Parameters **`fetch_page`** — `Callable[[str | None, int], Connection[T]]`. A function that takes `(after_cursor, page_size)` and returns a `Connection[T]`. Match the signature of [`perps_events`](../methods/info/perps_events) / [`perps_candles`](../methods/info/perps_candles). **`page_size`** — `int`, optional. Number of nodes per request. Default: `100`. ### Returns **`Iterator[T]`** — yields every node across all pages, in cursor order. ### Notes * Forward-only. Walks via `after`/`first`; backward (`last`/`before`) pagination is intentionally out of scope. * Stops when the server reports `has_next_page=false` OR `end_cursor=null`. The latter guard prevents an infinite loop against a non-conforming server. ### See also * [`perps_events_all`](../methods/info/perps_events_all) — pre-wired walker for the events stream * [`Connection`](../types/Connection) — the page shape ## sign\_doc\_canonical\_json Encode a `SignDoc` as canonical JSON: recursive `sort_keys`, no whitespace, drops `None` from `data`. ### Signature ```python def sign_doc_canonical_json(sign_doc: SignDoc) -> bytes ``` ### Example ```python from dango.utils.signing import sign_doc_canonical_json from dango.utils.types import SignDoc raw = sign_doc_canonical_json(sign_doc) print(raw.decode("utf-8")) ``` ### Parameters **`sign_doc`** — `SignDoc`. The TypedDict with `sender`, `gas_limit`, `messages`, `data`. ### Returns **`bytes`** — UTF-8 encoded JSON with sorted keys (recursively) and no whitespace. `None` values in `data` are dropped to match the chain's canonical form (the chain's `Metadata` struct uses `skip_serializing_none`). ### Notes * Mirrors `grug::SignData::to_prehash_sign_data` exactly. Hand to [`sign_doc_sha256`](./sign_doc_sha256) for the 32-byte digest. ### See also * [`sign_doc_sha256`](./sign_doc_sha256) — the SHA-256 digest of this output * [Concepts: Signers & Authentication](../../concepts/signers) — what gets signed ## sign\_doc\_sha256 SHA-256 digest of a `SignDoc`'s canonical JSON. The 32-byte payload that gets signed. ### Signature ```python def sign_doc_sha256(sign_doc: SignDoc) -> bytes ``` ### Example ```python from dango.utils.signing import sign_doc_sha256 from dango.utils.types import SignDoc digest = sign_doc_sha256(sign_doc) assert len(digest) == 32 ``` ### Parameters **`sign_doc`** — `SignDoc`. The TypedDict with `sender`, `gas_limit`, `messages`, `data`. ### Returns **`bytes`** — exactly 32 bytes (the SHA-256 digest of `sign_doc_canonical_json(sign_doc)`). ### Notes * Exposed for test and integration code that needs to verify the digest without re-deriving the canonical-JSON contract. ### See also * [`sign_doc_canonical_json`](./sign_doc_canonical_json) — the underlying canonical encoding * [`Secp256k1Wallet.sign`](../classes/Secp256k1Wallet#sign) — the secp256k1 signature over this digest ## ClientError Raised on a 4xx HTTP response from the GraphQL endpoint. ### Definition ```python class ClientError(Error): """Raised on a 4xx HTTP response from the GraphQL endpoint.""" ``` ### When it fires Any 4xx status from `/graphql`. The most common causes: * 400 — malformed request * 401 / 403 — auth / authz failure * 429 — rate limit The exception message wraps the first 500 characters of the response body. Inspect via `str(exc)`. ### Example ```python from dango.utils.error import ClientError try: info.query_status() except ClientError as exc: if "429" in str(exc): # back off and retry ... else: raise ``` ### See also * [`Error`](./Error) — base class * [Concepts: Rate limits & quotas](../../concepts/rate-limits) — handling 429 ## Error Base class for all SDK-raised exceptions. ### Definition ```python class Error(Exception): """Base class for all SDK-raised exceptions.""" ``` ### Notes * `from dango.utils.error import Error` is the safe outer `except` for any operation that touches the SDK. * Caller-bug exceptions (`TypeError`, `ValueError`, `RuntimeError`) are NOT `Error` subclasses — they indicate a bug at the call site, not a runtime SDK failure. ### See also * [`ClientError`](./ClientError) — 4xx HTTP * [`ServerError`](./ServerError) — 5xx HTTP / network * [`GraphQLError`](./GraphQLError) — GraphQL `errors` array * [`TxFailed`](./TxFailed) — `broadcastTxSync` returned `err` * [Concepts: Error handling](../../concepts/error-handling) ## GraphQLError Raised when a GraphQL response carries a non-empty `errors` array, or is missing both `data` and `errors`. ### Definition ```python class GraphQLError(Error): """Raised when a GraphQL response carries a non-empty `errors` array.""" ``` ### When it fires The HTTP layer succeeded (2xx, valid JSON object envelope) but the query failed at the GraphQL layer. The message concatenates every error's `message` with its `path`: ```text GraphQLError: invalid pair_id 'perp/xyzusd' (path=['queryApp', 'wasm_smart']) ``` Also raised when the envelope is missing both `data` and `errors` — a server-side malformation. ### Example ```python from dango.utils.error import GraphQLError try: info.query_app({"wasm_smart": {"contract": "0xdead", "msg": {}}}) except GraphQLError as exc: print("query failed:", exc) ``` ### See also * [`Error`](./Error) — base class * [Concepts: Error handling](../../concepts/error-handling) ## ServerError Raised on a 5xx HTTP response, network-level failure, or non-JSON body from the GraphQL endpoint. ### Definition ```python class ServerError(Error): """Raised on a 5xx HTTP response from the GraphQL endpoint.""" ``` ### When it fires * 5xx HTTP status * DNS failure, connection refused, timeout, SSL error (every `requests.RequestException` is wrapped) * Response body that fails to parse as JSON * GraphQL envelope that is not a JSON object (array or scalar) ### Example ```python import time from dango.utils.error import ServerError for attempt in range(3): try: return info.query_status() except ServerError: if attempt == 2: raise time.sleep(2 ** attempt) ``` ### See also * [`Error`](./Error) — base class * [Concepts: Error handling](../../concepts/error-handling) ## TxFailed Raised when `broadcastTxSync` returns an `err` result. ### Definition ```python class TxFailed(Error): """Raised when broadcastTxSync returns an `err` result.""" ``` ### Notes * Defined for callers and downstream tooling to raise. The SDK does NOT automatically raise it — `Exchange._send_action` returns the BroadcastTxOutcome envelope unchanged. * Inspect the returned dict's `check_tx.code`, `result.err`, and similar fields to detect chain-level rejection; raise `TxFailed(...)` yourself if your application prefers exception flow. ### See also * [`Error`](./Error) — base class * [`broadcast_tx_sync`](../methods/info/broadcast_tx_sync) — the source * [Concepts: Error handling](../../concepts/error-handling) ## API Sync GraphQL POST client. Base class for [`Info`](./Info) and [`Exchange`](./Exchange). Use `API` directly when you need to post a GraphQL document the SDK does not wrap. Most callers should pick `Info` or `Exchange` instead. ### Setup ```python from dango.api import API api = API("https://api-mainnet.dango.zone") ``` ### Constructor ```python API(base_url: str, *, timeout: float | None = None) -> None ``` ### Configuration **`base_url`** — `str`. GraphQL endpoint URL; the suffix `/graphql` is appended at query time. **`timeout`** — `float | None`, optional. HTTP timeout in seconds applied to every POST. Default: no timeout. ### Methods | Method | Description | | ----------------------------- | --------------------------------------------------- | | [`query`](#query) | POST a GraphQL document and return the `data` field | | [`query_typed`](#query_typed) | Same as `query` plus a static cast on the result | #### query ```python def query( self, document: str, variables: dict[str, Any] | None = None, ) -> dict[str, Any] ``` POSTs `{"query": document, "variables": variables}` to `/graphql` and returns the GraphQL `data` envelope. Raises: * [`ServerError`](../errors/ServerError) on 5xx, network failures, or non-JSON bodies * [`ClientError`](../errors/ClientError) on 4xx * [`GraphQLError`](../errors/GraphQLError) on a non-empty `errors` array, or when both `data` and `errors` are missing ```python data = api.query(""" query Status { queryStatus { chainId block { blockHeight } } } """) print(data["queryStatus"]["chainId"]) ``` #### query\_typed ```python def query_typed[T]( self, document: str, variables: dict[str, Any] | None = None, *, response_type: type[T], ) -> T ``` Same as `query` but `cast(T, ...)`s the result. No runtime validation — the cast is for type checkers only. ### Notes * The class holds a `requests.Session` for HTTP keep-alive across queries. * `Content-Type: application/json` is set on the session at construction; no need to override per call. * Both query and mutation documents go through `query()` — the GraphQL operation keyword lives inside the document string. ### See also * [`Info`](./Info) — the read-side subclass with typed wrappers * [`Exchange`](./Exchange) — the write-side subclass * [Concepts: Error handling](../../concepts/error-handling) — exception classes raised by `query` ## Exchange Build, sign, and broadcast Dango perps transactions on behalf of one account. Subclass of [`API`](./API). ### Setup ```python from eth_account import Account from dango.exchange import Exchange from dango.utils.constants import MAINNET_API_URL from dango.utils.types import Addr account = Account.from_key("0x...") exchange = Exchange( account, MAINNET_API_URL, account_address=Addr("0x..."), ) ``` ### Constructor ```python Exchange( wallet: Wallet | LocalAccount, base_url: str, *, account_address: Addr, user_index: int | None = None, next_nonce: int | None = None, chain_id: str | None = None, timeout: float | None = None, info: Info | None = None, perps_contract: Addr | None = None, ) -> None ``` ### Configuration **`wallet`** — `Wallet | LocalAccount`. A [`Secp256k1Wallet`](./Secp256k1Wallet) (or any object satisfying the `Wallet` protocol), or an `eth_account.LocalAccount`. The `LocalAccount` branch wraps the account's raw secp256k1 secret as `Secp256k1Wallet.from_eth_account` — NOT EIP-712. **`base_url`** — `str`. GraphQL endpoint URL. Use `MAINNET_API_URL`, `TESTNET_API_URL`, or `LOCAL_API_URL` from `dango.utils.constants`. **`account_address`** — `Addr`. The Dango account this Exchange transacts as. Decoupled from the wallet's address (one key can control multiple accounts). **`user_index`** — `int | None`, optional. Skips the auto-resolution round-trip when set. Default: auto-resolved from the account-factory contract. **`next_nonce`** — `int | None`, optional. Skips the auto-resolution round-trip when set. Default: auto-resolved from the account's `seen_nonces` window. **`chain_id`** — `str | None`, optional. Skips the `query_status` round-trip. Default: auto-resolved. **`timeout`** — `float | None`, optional. HTTP timeout in seconds applied to the embedded `requests.Session`. Default: no timeout. **`info`** — `Info | None`, optional. Reuse an existing `Info` instance for the read path (simulate, broadcast, nonce, user\_index, chain\_id). Default: a fresh `Info` over the same `base_url`. **`perps_contract`** — `Addr | None`, optional. Override the perps contract address. Default: `PERPS_CONTRACT_MAINNET`. ### Class attribute **`DEFAULT_GAS_OVERHEAD: Final[int] = 770_000`** — fixed gas added on top of simulated `gas_used` to cover signature verification (which `Info.simulate` deliberately skips). Override in a subclass for tests; do not monkeypatch. ### Properties **`address -> Addr`** — the Dango account address this Exchange transacts as. **`signer -> SingleSigner`** — the underlying [`SingleSigner`](./SingleSigner); exposed for manual nonce tweaks in tests. ### Methods #### Margin | Method | Description | | -------------------------------------------------------- | ----------------------------------------------------------- | | [`deposit_margin`](../methods/exchange/deposit_margin) | Deposit USDC into the perps margin sub-account (base units) | | [`withdraw_margin`](../methods/exchange/withdraw_margin) | Withdraw USDC from the perps margin sub-account (USD) | #### Orders | Method | Description | | ---------------------------------------------------------------- | --------------------------------------------------------- | | [`submit_order`](../methods/exchange/submit_order) | Place a single perps order | | [`cancel_order`](../methods/exchange/cancel_order) | Cancel by chain `OrderId`, `ClientOrderIdRef`, or `"all"` | | [`batch_update_orders`](../methods/exchange/batch_update_orders) | Submit and/or cancel multiple orders atomically | | [`submit_market_order`](../methods/exchange/submit_market_order) | Market order with a slippage cap (convenience) | | [`submit_limit_order`](../methods/exchange/submit_limit_order) | Limit order (convenience) | #### Conditional orders (TP/SL) | Method | Description | | -------------------------------------------------------------------------- | ------------------------------------------------- | | [`submit_conditional_order`](../methods/exchange/submit_conditional_order) | Place a TP/SL order (reduce-only by construction) | | [`cancel_conditional_order`](../methods/exchange/cancel_conditional_order) | Cancel a conditional order | #### Vault | Method | Description | | ---------------------------------------------------------- | ------------------------------------ | | [`add_liquidity`](../methods/exchange/add_liquidity) | Debit USD margin to mint LP shares | | [`remove_liquidity`](../methods/exchange/remove_liquidity) | Burn LP shares (subject to cooldown) | #### Referrals & liquidation | Method | Description | | -------------------------------------------------- | ------------------------------------------ | | [`set_referral`](../methods/exchange/set_referral) | Bind the signer as referee of a referrer | | [`liquidate`](../methods/exchange/liquidate) | Force-close an underwater user's positions | ### End-to-end example ```python from eth_account import Account from dango.exchange import Exchange from dango.info import Info from dango.utils.constants import PERPS_CONTRACT_TESTNET, TESTNET_API_URL from dango.utils.types import Addr, PairId, TimeInForce account = Account.from_key("0x...") info = Info(TESTNET_API_URL, perps_contract=Addr(PERPS_CONTRACT_TESTNET)) exchange = Exchange( account, TESTNET_API_URL, account_address=Addr("0x..."), info=info, perps_contract=Addr(PERPS_CONTRACT_TESTNET), ) # Place a resting limit order. result = exchange.submit_limit_order( PairId("perp/ethusd"), size="0.5", limit_price="1500", time_in_force=TimeInForce.GTC, ) print(result) # Read back open orders and cancel. orders = info.orders_by_user(exchange.address) for oid in orders: exchange.cancel_order(oid) break ``` ### Notes * `Exchange.signer.next_nonce` increments on every `sign_tx` call, even on broadcast failure. Reset it manually if a broadcast did not reach the chain. * `Exchange` is sync. There is no asyncio surface. * The embedded `requests.Session` is thread-safe for read queries but only one transaction can be signing at a time (nonce sequencing). ### See also * [`Info`](./Info) — the read-side counterpart * [`Secp256k1Wallet`](./Secp256k1Wallet) — the only shipping wallet implementation * [Concepts: Transactions](../../concepts/transactions) — the simulate/sign/broadcast pipeline ## Info Low-level GraphQL query primitives over HTTP plus subscription transport over WebSocket. Subclass of [`API`](./API). ### Setup ```python from dango.info import Info from dango.utils.constants import MAINNET_API_URL info = Info(MAINNET_API_URL) ``` ### Constructor ```python Info( base_url: str, *, skip_ws: bool = False, timeout: float | None = None, perps_contract: Addr | None = None, ) -> None ``` ### Configuration **`base_url`** — `str`. GraphQL endpoint URL. **`skip_ws`** — `bool`, optional. When `True`, the lazy `WebsocketManager` construction is suppressed and any `subscribe_*` call raises `RuntimeError`. Default: `False`. **`timeout`** — `float | None`, optional. HTTP timeout in seconds. Default: no timeout. **`perps_contract`** — `Addr | None`, optional. Override the perps contract address. Default: `PERPS_CONTRACT_MAINNET`. ### Methods #### Chain queries | Method | Description | | -------------------------------------------------------- | --------------------------------------------- | | [`query_status`](../methods/info/query_status) | Chain ID and latest block info | | [`query_app`](../methods/info/query_app) | Generic `queryApp` wrapper (raw envelope) | | [`query_app_smart`](../methods/info/query_app_smart) | Convenience for `{wasm_smart: ...}` queries | | [`query_app_multi`](../methods/info/query_app_multi) | Atomically run multiple queries at one height | | [`simulate`](../methods/info/simulate) | Dry-run an `UnsignedTx` | | [`broadcast_tx_sync`](../methods/info/broadcast_tx_sync) | Submit a signed `Tx` | #### Perps contract queries | Method | Description | | ------------------------------------------------------------ | -------------------------------------------- | | [`perps_param`](../methods/info/perps_param) | Global perps parameters | | [`perps_state`](../methods/info/perps_state) | Global perps runtime state | | [`pair_param`](../methods/info/pair_param) | One pair's parameters | | [`pair_params`](../methods/info/pair_params) | Enumerate per-pair parameters | | [`pair_state`](../methods/info/pair_state) | One pair's runtime state | | [`pair_states`](../methods/info/pair_states) | Enumerate per-pair states | | [`liquidity_depth`](../methods/info/liquidity_depth) | Aggregated bid/ask depth | | [`user_state`](../methods/info/user_state) | User margin, positions, vault shares | | [`user_state_extended`](../methods/info/user_state_extended) | `user_state` plus computed equity/margin/PnL | | [`orders_by_user`](../methods/info/orders_by_user) | All resting orders for a user | | [`order`](../methods/info/order) | One resting order by id | | [`volume`](../methods/info/volume) | User's cumulative trading volume | #### Indexer queries | Method | Description | | -------------------------------------------------------------- | -------------------------------- | | [`perps_candles`](../methods/info/perps_candles) | OHLCV candles (cursor-paginated) | | [`perps_events`](../methods/info/perps_events) | Event stream (cursor-paginated) | | [`perps_pair_stats`](../methods/info/perps_pair_stats) | 24h stats for one pair | | [`all_perps_pair_stats`](../methods/info/all_perps_pair_stats) | 24h stats for every active pair | | [`perps_events_all`](../methods/info/perps_events_all) | Iterate every matching event | #### Subscriptions | Method | Description | | -------------------------------------------------------------------- | ---------------------------------- | | [`subscribe_perps_trades`](../methods/info/subscribe_perps_trades) | Live trade fills for one pair | | [`subscribe_perps_candles`](../methods/info/subscribe_perps_candles) | OHLCV candle stream | | [`subscribe_query_app`](../methods/info/subscribe_query_app) | Re-run a `queryApp` every N blocks | | [`subscribe_user_events`](../methods/info/subscribe_user_events) | Per-user event stream | | [`subscribe_block`](../methods/info/subscribe_block) | Every newly-finalized block | | [`unsubscribe`](../methods/info/unsubscribe) | Drop a subscription | | [`disconnect_websocket`](../methods/info/disconnect_websocket) | Close the WebSocket cleanly | ### End-to-end example ```python import time from dango.info import Info from dango.utils.constants import MAINNET_API_URL from dango.utils.types import Addr, CandleInterval, PairId info = Info(MAINNET_API_URL) # Public state status = info.query_status() pairs = info.pair_params() stats = info.all_perps_pair_stats() # Per-user state user = Addr("0x...") state = info.user_state(user) orders = info.orders_by_user(user) # Indexer candles = info.perps_candles(PairId("perp/ethusd"), CandleInterval.ONE_MINUTE, first=10) # Subscribe and drain for 30s sid = info.subscribe_perps_trades(PairId("perp/ethusd"), print) time.sleep(30) info.unsubscribe(sid) info.disconnect_websocket() ``` ### Notes * HTTP queries use a `requests.Session` — thread-safe for concurrent reads. * The `WebsocketManager` is created on the first `subscribe_*` call. Pass `skip_ws=True` if you never intend to subscribe. * The lazy manager is a daemon thread; call `disconnect_websocket()` on shutdown to close it cleanly. ### See also * [`Exchange`](./Exchange) — the write-side counterpart * [`WebsocketManager`](./WebsocketManager) — the underlying subscription transport * [Concepts: Subscriptions](../../concepts/subscriptions) — the WebSocket model ## Secp256k1Wallet Holds a 32-byte secp256k1 secret plus the Dango account address it controls. The only `Wallet`-protocol implementation that ships in tree. ### Setup ```python from dango.utils.signing import Secp256k1Wallet from dango.utils.types import Addr wallet = Secp256k1Wallet.random(Addr("0x...")) ``` ### Constructor ```python Secp256k1Wallet(secret: bytes, address: Addr) -> None ``` **`secret`** — `bytes`. Must be exactly 32 bytes with value in `[1, n-1]` for the secp256k1 curve order `n`. Raises `ValueError` on zero, on values >= `n`, or on wrong length. **`address`** — `Addr`. The Dango account address this wallet signs for. Decoupled from the key — the same secret can sign for multiple Dango accounts. ### Class methods | Method | Description | | ---------------------------------------------------------------------- | ------------------------------------------------------------------ | | `random(address) -> Secp256k1Wallet` | Generate a CSPRNG-sourced wallet | | `from_bytes(secret, address) -> Secp256k1Wallet` | Wrap an explicit 32-byte secret | | `from_mnemonic(mnemonic, address, *, coin_type=60) -> Secp256k1Wallet` | BIP-39 mnemonic + BIP-44 path `m/44'/{coin_type}'/0'/0/0` | | `from_eth_account(account, address) -> Secp256k1Wallet` | Re-use a `LocalAccount`'s secret (key\_tag=Secp256k1, NOT EIP-712) | ```python from eth_account import Account from dango.utils.signing import Secp256k1Wallet from dango.utils.types import Addr addr = Addr("0x...") w1 = Secp256k1Wallet.random(addr) w2 = Secp256k1Wallet.from_bytes(bytes.fromhex("aa" * 32), addr) w3 = Secp256k1Wallet.from_mnemonic("test test test test test test test test test test test junk", addr) w4 = Secp256k1Wallet.from_eth_account(Account.from_key("0x..."), addr) ``` ### Properties **`address -> Addr`** — the Dango account address supplied at construction. **`secret_bytes -> bytes`** — the raw 32-byte secret. Sensitive — never log or persist in plaintext. **`public_key_compressed -> bytes`** — 33-byte compressed pubkey (1-byte parity + 32-byte x). **`key -> Key`** — wire-shape `{"secp256k1": ""}`. **`key_hash -> Hash256`** — `SHA-256(compressed_pubkey)` as uppercase hex. The on-chain identifier the contract uses to look up the pubkey. ### Methods #### sign ```python def sign(self, sign_doc: SignDoc) -> Signature ``` Produces a secp256k1 signature over `SHA-256(canonical_json(sign_doc))`. Returns the 64-byte r||s in the `{"secp256k1": ""}` envelope. The trailing recovery byte from `eth_keys` is stripped — Dango verifies via the pubkey resolved from `key_hash`. ```python from dango.utils.signing import Secp256k1Wallet from dango.utils.types import Addr, SignDoc wallet = Secp256k1Wallet.random(Addr("0x...")) sig = wallet.sign(SignDoc(sender=Addr("0x..."), gas_limit=1_000_000, messages=[], data={...})) ``` ### Notes * BIP-39 mnemonics use coin type 60 (Ethereum) by default — pass `coin_type=` to derive at a different path. The library uses `eth-account`'s unaudited HD wallet path, matching the Rust SDK's BIP-32 derivation. * `from_eth_account` signs with `KeyType=Secp256k1`. The Dango account address you pass is independent from the wallet's derived EVM address and is the user's responsibility to provide. * The wallet satisfies the `@runtime_checkable` `Wallet` protocol — `isinstance(wallet, Wallet)` returns `True`. ### See also * [`SingleSigner`](./SingleSigner) — the stateful per-account signer that owns nonce sequencing * [Concepts: Signers & Authentication](../../concepts/signers) — the layered signing model ## SingleSigner Stateful per-account signer; tracks `user_index` and `next_nonce`, produces signed `Tx` envelopes. `SingleSigner` is constructed automatically by [`Exchange`](./Exchange) and exposed via `Exchange.signer`. Direct construction is supported for tests and recovery flows. ### Setup ```python from dango.utils.signing import Secp256k1Wallet, SingleSigner from dango.utils.types import Addr wallet = Secp256k1Wallet.random(Addr("0xaccount")) signer = SingleSigner(wallet, Addr("0xaccount")) ``` ### Constructor ```python SingleSigner( wallet: Wallet, address: Addr, *, user_index: int | None = None, next_nonce: int | None = None, ) -> None ``` **`wallet`** — `Wallet`. Any object satisfying the `Wallet` protocol. **`address`** — `Addr`. The Dango account address this signer is bound to. Taken explicitly (not from `wallet.address`) so the same key can sign for multiple accounts. **`user_index`** — `int | None`, optional. Pre-populate to skip the auto-resolve query. Default: unresolved; must be set before `build_unsigned_tx` / `sign_tx`. **`next_nonce`** — `int | None`, optional. Pre-populate to skip the auto-resolve query. Default: unresolved; must be set before `sign_tx`. ### Attributes (mutable) **`wallet: Wallet`**, **`address: Addr`**, **`user_index: int | None`**, **`next_nonce: int | None`** All public and mutable. Power users overwrite `next_nonce` to recover after a failed broadcast. ### Class methods #### auto\_resolve ```python @classmethod def auto_resolve( cls, wallet: Wallet, address: Addr, info: _QueryClient, ) -> SingleSigner ``` Construct a signer and populate `user_index` and `next_nonce` by querying the chain via `info.query_app_smart`. ```python from dango.info import Info from dango.utils.signing import Secp256k1Wallet, SingleSigner from dango.utils.types import Addr info = Info("https://api-mainnet.dango.zone") wallet = Secp256k1Wallet.from_mnemonic("...", Addr("0x...")) signer = SingleSigner.auto_resolve(wallet, Addr("0x..."), info) ``` ### Methods #### query\_user\_index ```python def query_user_index(self, info: _QueryClient) -> int ``` Look up `user_index` via the account-factory contract. Returns the stable index but does not mutate `self.user_index` (call `auto_resolve` or assign manually if you want both). #### query\_next\_nonce ```python def query_next_nonce(self, info: _QueryClient) -> int ``` Compute `next_nonce` from the account's seen-nonces sliding window. Returns `max(seen) + 1` or `0` if the account has never sent a transaction. #### build\_unsigned\_tx ```python def build_unsigned_tx( self, messages: list[Message], chain_id: str, ) -> UnsignedTx ``` Wrap messages plus `Metadata` into an `UnsignedTx` for `Info.simulate`. Raises `RuntimeError` if `user_index` or `next_nonce` is unresolved. #### sign\_tx ```python def sign_tx( self, messages: list[Message], chain_id: str, gas_limit: int, ) -> Tx ``` Sign and return a `Tx`. Optimistically increments `self.next_nonce` (success or failure) — the chain rejects duplicate nonces, so optimistic increment is safer than rewinding on broadcast failure. ### Notes * The `_QueryClient` parameter type is a private structural `Protocol` with a single method `query_app_smart(...)`. `Info` satisfies it structurally — pass an `Info` instance directly. * After a failed broadcast that did NOT reach the chain, manually rewind: `signer.next_nonce -= 1`, or re-fetch: `signer.next_nonce = signer.query_next_nonce(info)`. * The signer does not retry, log, or wrap exceptions — it is intentionally thin. ### See also * [`Secp256k1Wallet`](./Secp256k1Wallet) — the shipping `Wallet` implementation * [`Exchange`](./Exchange) — constructs and uses `SingleSigner` internally * [Concepts: Signers & Authentication](../../concepts/signers) — the layered signing model ## WebsocketManager Thread-based `graphql-transport-ws` subscription manager. Subclass of `threading.Thread` (`daemon=True`). In practice this class is constructed lazily by [`Info`](./Info) on the first `subscribe_*` call. Direct construction is rare — document here because the class is part of the public surface. ### Setup ```python from dango.websocket_manager import WebsocketManager manager = WebsocketManager("https://api-mainnet.dango.zone") manager.start() ``` `start()` (inherited from `threading.Thread`) launches `run()` in a daemon thread. ### Constructor ```python WebsocketManager(base_url: str) -> None ``` ### Configuration **`base_url`** — `str`. HTTP base URL; converted internally to a `wss://` (or `ws://`) URL ending in `/graphql`. ### Methods | Method | Description | | --------------------------------------------------------- | ---------------------------------------------------- | | [`run`](../methods/websocket-manager/run) | `threading.Thread` entry point (called by `start()`) | | [`stop`](../methods/websocket-manager/stop) | Signal shutdown and close the connection | | [`subscribe`](../methods/websocket-manager/subscribe) | Register a subscription | | [`unsubscribe`](../methods/websocket-manager/unsubscribe) | Drop a subscription | ### End-to-end example ```python from dango.websocket_manager import WebsocketManager manager = WebsocketManager("https://api-mainnet.dango.zone") manager.start() document = """ subscription { perpsTrades(pairId: "perp/ethusd") { fillPrice fillSize } } """ sid = manager.subscribe(document, {}, print) # ... receive events ... manager.unsubscribe(sid) manager.stop() manager.join(timeout=5.0) ``` ### Notes * The manager owns ONE WebSocket connection. The server caps each connection at 30 simultaneous subscriptions. * The manager pings every 15 seconds at the protocol layer (`{"type": "ping"}`) to defeat the server's 30-second idle timeout. * Subscriptions opened before the server's `connection_ack` are queued and flushed once the ack arrives. * Subscription errors arrive through the callback as `{"_error": payload}`, not as exceptions. After an error, the manager has already dropped the callback — the subscription is terminal. * `stop()` signals shutdown and closes the socket but does not wait for the thread to exit. Call `join(timeout=...)` if you need to block. ### See also * [`Info`](./Info) — the high-level wrapper that builds and owns one manager * [Concepts: Subscriptions](../../concepts/subscriptions) — sharding, callback contract, errors