diff --git a/.gitleaks.toml b/.gitleaks.toml new file mode 100644 index 0000000..09d74f8 --- /dev/null +++ b/.gitleaks.toml @@ -0,0 +1,14 @@ +title = "bybit-connector-ruby gitleaks configuration" + +[extend] +useDefault = true + +[[rules]] +id = "generic-api-key" + +[[rules.allowlists]] +description = "Allow documented source commit SHAs in the changelog" +condition = "AND" +regexTarget = "line" +regexes = ['''^\s*-\s+(OpenAPI|Docs):\s+`[0-9a-f]{40}`\s*$'''] +paths = ['''(^|/)CHANGELOG\.md$'''] diff --git a/CHANGELOG.md b/CHANGELOG.md index 94649e6..4c35a89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,43 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.1.2] - 2026-10-13 + +### Added +- `AccountService#demo_apply_money(...)` +- `AccountService#get_option_asset_info(...)` +- `AccountService#get_pay_info(...)` +- `AccountService#get_trade_info_for_analysis(...)` +- `AssetService#limit_order_callback(...)` +- `CryptoLoanService#get_fixed_available_inventory(...)` +- `CryptoLoanService#get_fixed_repayment_history(...)` +- `CryptoLoanService#supply_fixed(...)` +- `CryptoLoanService#get_flexible_available_inventory(...)` +- `EarnService#list_auto_savings(...)` +- `EarnService#edit_earn_auto_savings(...)` +- `MarketService#get_option_base_coins(...)` +- `PositionService#get_symbol_info(...)` +- `SpotMarginService#get_trade_coin_state(...)` +- `SpotMarginService#query_fixed_available_inventory(...)` +- `SpotMarginService#account_fixed_borrow(...)` +- `SpotMarginService#query_fixed_borrow_contracts(...)` +- `SpotMarginService#query_fixed_borrow_orders(...)` +- `SpotMarginService#query_fixed_borrow_market(...)` +- `SpotMarginService#renew_fixed_borrow(...)` +- `SpotMarginService#get_trade_flexible_available_inventory(...)` +- `SpotMarginService#get_trade_auto_repay_mode(...)` +- `SpotMarginService#query_borrow_liability(...)` +- `SpotMarginService#get_trade_max_borrowable(...)` +- `SpotMarginService#get_trade_repayment_available_amount(...)` +- `SpotMarginService#set_auto_repay_mode(...)` +- `SpotMarginService#spot_margin_set_leverage(...)` +- `SpotMarginService#get_trade_state(...)` +- `SpotMarginService#spot_margin_switch_mode(...)` +- `UserService#query_referral_code(...)` + +### Sources +- OpenAPI: `08130d57323d390dea58f15418758e2667c74ce6` +- Docs: `59e3d457dea6b419a14987c19a3aabf4ffb38238` ## [0.1.1] - 2026-07-22 diff --git a/lib/bybit/rest_api/account_service.rb b/lib/bybit/rest_api/account_service.rb index 0aafc91..a0f0977 100644 --- a/lib/bybit/rest_api/account_service.rb +++ b/lib/bybit/rest_api/account_service.rb @@ -327,6 +327,56 @@ def upgrade_to_uta_pro(**kwargs) params = Bybit::Utils::WireKeys.camelize(params) @session.sign_request(method: :post, path: '/v5/account/upgrade-to-uta', body: params) end + + # Apply demo funds + # + # POST /v5/account/demo-apply-money + # + # @option kwargs [Integer] :adjust_type Adjustment type, 0-add funds, 1-deduct funds + # @option kwargs [Array] :uta_demo_apply_money Fund application config list + # @note This endpoint requires `base_url: 'https://api-demo.bybit.com'`. + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/demo + def demo_apply_money(**kwargs) + params = Bybit::Utils::WireKeys.camelize(kwargs) + @session.sign_request(method: :post, path: '/v5/account/demo-apply-money', body: params) + end + + # Get Option Asset Info + # + # GET /v5/account/option-asset-info + # + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/account/option-asset-info + def get_option_asset_info + @session.sign_request(method: :get, path: '/v5/account/option-asset-info') + end + + # Get Pay Info + # + # GET /v5/account/pay-info + # + # @option kwargs [String] :coin Coin name, e.g. `USDT`, `BTC`. If not passed, returns all repayment info. + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/account/pay-info + def get_pay_info(**kwargs) + params = Bybit::Utils::WireKeys.camelize(kwargs) + @session.sign_request(method: :get, path: '/v5/account/pay-info', params: params) + end + + # Get Trade Info For Analysis + # + # GET /v5/account/trade-info-for-analysis + # + # @param symbol [String] Trading pair, e.g. `BTCUSDT`, `ETHUSDT`. + # @option kwargs [Integer] :start_time Query start time in milliseconds timestamp. + # @option kwargs [Integer] :end_time Query end time in milliseconds timestamp. + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/account/trade-info-for-analysis + def get_trade_info_for_analysis(symbol:, **kwargs) + params = Bybit::Utils::WireKeys.camelize(kwargs.merge(symbol: symbol)) + @session.sign_request(method: :get, path: '/v5/account/trade-info-for-analysis', params: params) + end end end end diff --git a/lib/bybit/rest_api/asset_service.rb b/lib/bybit/rest_api/asset_service.rb index 795a887..eb320d7 100644 --- a/lib/bybit/rest_api/asset_service.rb +++ b/lib/bybit/rest_api/asset_service.rb @@ -633,6 +633,23 @@ def small_asset_quote(**kwargs) params = Bybit::Utils::WireKeys.camelize(params) @session.sign_request(method: :post, path: '/v5/asset/covert/get-quote', body: params) end + + # Limit order callback notification + # + # POST /v5/asset/exchange/limit-order/callback + # + # @param exchange_tx_id [String] Convert order number + # @param option_type [Integer] Callback type: 1=filled, 2=canceled, 3=failed + # @param time_stamp [Integer] Callback timestamp (milliseconds) + # @param sign [String] Callback signature data + # @param app_id [String] appId of callback business, currently only supports: TimeResearch + # @option kwargs [String] :error_code Error code on failure + # @note This endpoint is generated from the Bybit OpenAPI specification and is not in the public documentation. + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + def limit_order_callback(exchange_tx_id:, option_type:, time_stamp:, sign:, app_id:, **kwargs) + params = Bybit::Utils::WireKeys.camelize(kwargs.merge(exchangeTxId: exchange_tx_id, optionType: option_type, timeStamp: time_stamp, sign: sign, appId: app_id)) + @session.sign_request(method: :post, path: '/v5/asset/exchange/limit-order/callback', body: params) + end end end end diff --git a/lib/bybit/rest_api/crypto_loan_service.rb b/lib/bybit/rest_api/crypto_loan_service.rb index 9d53aed..289eda1 100644 --- a/lib/bybit/rest_api/crypto_loan_service.rb +++ b/lib/bybit/rest_api/crypto_loan_service.rb @@ -370,6 +370,66 @@ def repay_flexible_with_collateral(loan_currency:, collateral_coin:, amount:, ** params = Bybit::Utils::WireKeys.camelize(params) @session.sign_request(method: :post, path: '/v5/crypto-loan-flexible/repay-collateral', body: params) end + + # Get Available Inventory + # + # GET /v5/crypto-loan-fixed/available-inventory + # + # @param currency [String] Coin name, uppercase only + # @param term [String] Fixed term 7: 7 days; 14: 14 days; 30: 30 days; 90: 90 days; 180: 180 days + # @param annual_rate [String] Customizable annual interest rate, e.g., 0.02 means 2% + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/new-crypto-loan/fixed/available-inventory + def get_fixed_available_inventory(currency:, term:, annual_rate:) + params = Bybit::Utils::WireKeys.camelize({ currency: currency, term: term, annualRate: annual_rate }) + @session.sign_request(method: :get, path: '/v5/crypto-loan-fixed/available-inventory', params: params) + end + + # Get Repayment History + # + # GET /v5/crypto-loan-fixed/repayment-history + # + # @option kwargs [String] :repay_id Repayment ID + # @option kwargs [String] :loan_currency Loan currency filter + # @option kwargs [String] :limit Page size + # @option kwargs [String] :cursor Page cursor + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/new-crypto-loan/fixed/repay-history + def get_fixed_repayment_history(**kwargs) + params = Bybit::Utils::WireKeys.camelize(kwargs) + @session.sign_request(method: :get, path: '/v5/crypto-loan-fixed/repayment-history', params: params) + end + + # Create Supply Order + # + # POST /v5/crypto-loan-fixed/supply + # + # @param order_currency [String] Currency to lend + # @param order_amount [String] Amount to lend + # @param annual_rate [String] Annual interest rate + # @param term [String] Term in days (7, 14, 30, 90, 180) + # @option kwargs [String] :available_source Fund source for supply order: + # - `0`: Funding account (default) + # - `1`: Flexible savings + # - `2`: Mixed (funding account + flexible savings) + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/new-crypto-loan/fixed/supply + def supply_fixed(order_currency:, order_amount:, annual_rate:, term:, **kwargs) + params = Bybit::Utils::WireKeys.camelize(kwargs.merge(orderCurrency: order_currency, orderAmount: order_amount, annualRate: annual_rate, term: term)) + @session.sign_request(method: :post, path: '/v5/crypto-loan-fixed/supply', body: params) + end + + # Get Available Inventory + # + # GET /v5/crypto-loan-flexible/available-inventory + # + # @param currency [String] Coin name, uppercase only + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/new-crypto-loan/flexible/available-inventory + def get_flexible_available_inventory(currency:) + params = Bybit::Utils::WireKeys.camelize({ currency: currency }) + @session.sign_request(method: :get, path: '/v5/crypto-loan-flexible/available-inventory', params: params) + end end end end diff --git a/lib/bybit/rest_api/earn_service.rb b/lib/bybit/rest_api/earn_service.rb index 280020d..f30ed65 100644 --- a/lib/bybit/rest_api/earn_service.rb +++ b/lib/bybit/rest_api/earn_service.rb @@ -1042,6 +1042,37 @@ def set_fixed_term_auto_invest(product_id:, category:, position_id:, status:, ** params = Bybit::Utils::WireKeys.camelize(params) @session.sign_request(method: :post, path: '/v5/earn/fixed-term/position/auto-invest', body: params) end + + # Get Auto Savings Settings + # + # GET /v5/earn/flexible-saving/auto-savings + # + # @option kwargs [Array] :coins Coin names to query, for example `coins=BTC&coins=ETH`. Omit for all supported coins. + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/finance/earn/flexible-saving/auto-savings + def list_auto_savings(**kwargs) + coins = kwargs[:coins] + if coins.is_a?(Array) && coins.length > 50 + raise Bybit::ConfigurationError, + 'coins supports at most 50 values' + end + params = Bybit::Utils::WireKeys.camelize(kwargs) + @session.sign_request(method: :get, path: '/v5/earn/flexible-saving/auto-savings', params: params) + end + + # Edit Auto Savings Settings + # + # POST /v5/earn/flexible-saving/auto-savings + # + # @param is_selected [Boolean] Whether to enable auto savings + # @option kwargs [String] :coin Coin to change; omit or pass an empty string to change the global setting + # @option kwargs [Boolean] :purchase_immediately When enabling, asynchronously attempt to subscribe the available Funding Account balance; ignored when disabling + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/finance/earn/flexible-saving/auto-savings + def edit_earn_auto_savings(is_selected:, **kwargs) + params = Bybit::Utils::WireKeys.camelize(kwargs.merge(isSelected: is_selected)) + @session.sign_request(method: :post, path: '/v5/earn/flexible-saving/auto-savings', body: params) + end end end end diff --git a/lib/bybit/rest_api/market_service.rb b/lib/bybit/rest_api/market_service.rb index 32e3a22..8eadb9f 100644 --- a/lib/bybit/rest_api/market_service.rb +++ b/lib/bybit/rest_api/market_service.rb @@ -350,6 +350,23 @@ def get_tickers(category:, **kwargs) params = Bybit::Utils::WireKeys.camelize(params) @session.public_request(path: '/v5/market/tickers', params: params) end + + # Get Option Base Coins + # + # GET /v5/market/option-base-coins + # + # @option kwargs [String] :underlying_type Underlying asset type. Supports comma-separated values, e.g. `0,2`. + # - `0`: Crypto + # - `1`: Commodity + # - `2`: Stock + # - `3`: Forex + # - `4`: Oil + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/market/option-base-coins + def get_option_base_coins(**kwargs) + params = Bybit::Utils::WireKeys.camelize(kwargs) + @session.public_request(path: '/v5/market/option-base-coins', params: params) + end end end end diff --git a/lib/bybit/rest_api/position_service.rb b/lib/bybit/rest_api/position_service.rb index d9c7dc9..ba46235 100644 --- a/lib/bybit/rest_api/position_service.rb +++ b/lib/bybit/rest_api/position_service.rb @@ -197,6 +197,21 @@ def switch_position_mode(category:, mode:, **kwargs) params = Bybit::Utils::WireKeys.camelize(params) @session.sign_request(method: :post, path: '/v5/position/switch-mode', body: params) end + + # Get Futures Leverage + # + # GET /v5/position/symbol-info + # + # @param category [String] Product type: + # - `linear`: USDT perpetual, USDC contract + # - `inverse`: Inverse perpetual, Inverse futures + # @option kwargs [String] :symbol Symbol name, e.g. `BTCUSDT`, uppercase only + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/position/batch-lvg + def get_symbol_info(category:, **kwargs) + params = Bybit::Utils::WireKeys.camelize(kwargs.merge(category: category)) + @session.sign_request(method: :get, path: '/v5/position/symbol-info', params: params) + end end end end diff --git a/lib/bybit/rest_api/spot_margin_service.rb b/lib/bybit/rest_api/spot_margin_service.rb index ed74cf1..d6a967e 100644 --- a/lib/bybit/rest_api/spot_margin_service.rb +++ b/lib/bybit/rest_api/spot_margin_service.rb @@ -57,6 +57,237 @@ def get_vip_margin_data(**kwargs) params = Bybit::Utils::WireKeys.camelize(params) @session.public_request(path: '/v5/spot-margin-trade/data', params: params) end + + # Get Spot Margin Coin State + # + # GET /v5/spot-margin-trade/coinstate + # + # @option kwargs [String] :currency Coin name, uppercase only. e.g. `BTC`. If not passed, returns all coin states. + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/spot-margin-uta/coinstate + def get_trade_coin_state(**kwargs) + params = Bybit::Utils::WireKeys.camelize(kwargs) + @session.sign_request(method: :get, path: '/v5/spot-margin-trade/coinstate', params: params) + end + + # Query Fixed-Rate Available Inventory + # + # GET /v5/spot-margin-trade/fixed-available-inventory + # + # @param currency [String] Borrow coin name, uppercase only. e.g. `USDT`, `BTC`. **Required.** + # @param term [String] Loan term in days. e.g. `7`, `14`, `30`, `90`, `180`. **Required.** + # @param annual_rate [String] Annual interest rate. e.g. `0.02` means 2%. **Required.** + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/spot-margin-uta/fixed-available-inventory + def query_fixed_available_inventory(currency:, term:, annual_rate:) + params = Bybit::Utils::WireKeys.camelize({ currency: currency, term: term, annualRate: annual_rate }) + @session.sign_request(method: :get, path: '/v5/spot-margin-trade/fixed-available-inventory', params: params) + end + + # Fixed-Rate Borrow + # + # POST /v5/spot-margin-trade/fixedborrow + # + # @param order_currency [String] Borrow coin name, uppercase only. e.g. `USDT`, `BTC`. + # @param order_amount [String] Borrow amount. + # @param annual_rate [String] Annual interest rate. e.g. `0.02` means 2%. + # @param term [String] Loan term in days. Supported values: `7`, `14`, `30`, `90`, `180`. + # @option kwargs [String] :repay_type Maturity handling type. + # - `1`: Auto-repay at maturity (default) + # - `2`: Convert to flexible-rate (variable-rate) loan at maturity + # @option kwargs [String] :strategy_type Order strategy. + # - `PARTIAL`: Partial fill or cancel (immediate-or-cancel) + # - `FULL`: Fill or kill (all-or-nothing) + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/spot-margin-uta/fixedborrow + def account_fixed_borrow(order_currency:, order_amount:, annual_rate:, term:, **kwargs) + params = Bybit::Utils::WireKeys.camelize(kwargs.merge(orderCurrency: order_currency, orderAmount: order_amount, annualRate: annual_rate, term: term)) + @session.sign_request(method: :post, path: '/v5/spot-margin-trade/fixedborrow', body: params) + end + + # Query Fixed-Rate Borrow Contracts + # + # GET /v5/spot-margin-trade/fixedborrow-contract-info + # + # @option kwargs [String] :order_id Filter by borrow order ID. + # @option kwargs [String] :order_currency Filter by borrow coin name, uppercase only. e.g. `USDT`. + # @option kwargs [String] :term Filter by loan term in days. e.g. `7`, `14`, `30`, `90`, `180`. + # @option kwargs [String] :limit Page size. Range: [1, 100]. Default: `10`. + # @option kwargs [String] :cursor Cursor for pagination. Use `nextPageCursor` from previous response. + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/spot-margin-uta/fixedborrow-contract-info + def query_fixed_borrow_contracts(**kwargs) + params = Bybit::Utils::WireKeys.camelize(kwargs) + @session.sign_request(method: :get, path: '/v5/spot-margin-trade/fixedborrow-contract-info', params: params) + end + + # Query Fixed-Rate Borrow Orders + # + # GET /v5/spot-margin-trade/fixedborrow-order-info + # + # @option kwargs [String] :order_id Filter by borrow order ID. + # @option kwargs [String] :order_currency Filter by borrow coin name, uppercase only. e.g. `USDT`. + # @option kwargs [String] :state Filter by order state. + # - `1`: Matching (order accepted, matching in progress) + # - `2`: Partially filled & cancelled + # - `3`: Fully filled + # - `4`: Cancelled + # @option kwargs [String] :term Filter by loan term in days. e.g. `7`, `14`, `30`, `90`, `180`. + # @option kwargs [String] :limit Page size. Range: [1, 100]. Default: `10`. + # @option kwargs [String] :cursor Cursor for pagination. Use `nextPageCursor` from previous response. + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/spot-margin-uta/fixedborrow-order-info + def query_fixed_borrow_orders(**kwargs) + params = Bybit::Utils::WireKeys.camelize(kwargs) + @session.sign_request(method: :get, path: '/v5/spot-margin-trade/fixedborrow-order-info', params: params) + end + + # Query Fixed-Rate Borrow Market + # + # GET /v5/spot-margin-trade/fixedborrow-order-quote + # + # @param order_currency [String] Borrow coin name, uppercase only. e.g. `USDT`, `BTC`. **Required.** + # @option kwargs [String] :order_by Sort field. + # - `apy`: Sort by annual interest rate + # - `term`: Sort by loan term + # - `quantity`: Sort by available quantity + # @option kwargs [String] :term Filter by loan term in days. e.g. `7`, `14`, `30`, `90`, `180`. + # @option kwargs [Integer] :sort Sort order. + # - `0`: Ascending (default) + # - `1`: Descending + # @option kwargs [Integer] :limit Page size. Range: [1, 100]. Default: `10`. + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/spot-margin-uta/fixedborrow-order-quote + def query_fixed_borrow_market(order_currency:, **kwargs) + params = Bybit::Utils::WireKeys.camelize(kwargs.merge(order_currency: order_currency)) + @session.sign_request(method: :get, path: '/v5/spot-margin-trade/fixedborrow-order-quote', params: params) + end + + # Renew Fixed-Rate Borrow + # + # POST /v5/spot-margin-trade/fixedborrow-renew + # + # @param loan_id [String] The loan contract ID (corresponds to match order ID). Required. + # @option kwargs [String] :qty Renewal amount. + # - If not provided, the full prepayment amount of the contract is used + # - Must be greater than 0 + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/spot-margin-uta/fixedborrow-renew + def renew_fixed_borrow(loan_id:, **kwargs) + params = Bybit::Utils::WireKeys.camelize(kwargs.merge(loanId: loan_id)) + @session.sign_request(method: :post, path: '/v5/spot-margin-trade/fixedborrow-renew', body: params) + end + + # Get Flexible Available Inventory + # + # GET /v5/spot-margin-trade/flexible-available-inventory + # + # @param currency [String] Coin name, uppercase only. e.g. `BTC` + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/spot-margin-uta/flexible-available-inventory + def get_trade_flexible_available_inventory(currency:) + params = Bybit::Utils::WireKeys.camelize({ currency: currency }) + @session.sign_request(method: :get, path: '/v5/spot-margin-trade/flexible-available-inventory', params: params) + end + + # Get Auto Repay Mode + # + # GET /v5/spot-margin-trade/get-auto-repay-mode + # + # @option kwargs [String] :currency Coin name, uppercase only. e.g. `ETH`. When omitted, returns settings for all currencies. + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/spot-margin-uta/get-auto-repay-mode + def get_trade_auto_repay_mode(**kwargs) + params = Bybit::Utils::WireKeys.camelize(kwargs) + @session.sign_request(method: :get, path: '/v5/spot-margin-trade/get-auto-repay-mode', params: params) + end + + # Query Borrow Liability + # + # GET /v5/spot-margin-trade/liability + # + # @param currency [String] Coin name, uppercase only. e.g. `USDT`, `BTC`. **Required.** + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/spot-margin-uta/liability + def query_borrow_liability(currency:) + params = Bybit::Utils::WireKeys.camelize({ currency: currency }) + @session.sign_request(method: :get, path: '/v5/spot-margin-trade/liability', params: params) + end + + # Get Max Borrowable Amount + # + # GET /v5/spot-margin-trade/max-borrowable + # + # @param currency [String] Coin name, uppercase only. e.g. `BTC` + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/spot-margin-uta/max-borrowable + def get_trade_max_borrowable(currency:) + params = Bybit::Utils::WireKeys.camelize({ currency: currency }) + @session.sign_request(method: :get, path: '/v5/spot-margin-trade/max-borrowable', params: params) + end + + # Get Repayment Available Amount + # + # GET /v5/spot-margin-trade/repayment-available-amount + # + # @param currency [String] Coin name, uppercase only. e.g. `BTC` + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/spot-margin-uta/repayment-available-amount + def get_trade_repayment_available_amount(currency:) + params = Bybit::Utils::WireKeys.camelize({ currency: currency }) + @session.sign_request(method: :get, path: '/v5/spot-margin-trade/repayment-available-amount', params: params) + end + + # Set Auto Repay Mode + # + # POST /v5/spot-margin-trade/set-auto-repay-mode + # + # @param auto_repay_mode [String] Auto repay mode switch: + # - `1`: Enable auto repay — Enable auto repay + # - `0`: Disable auto repay — Disable auto repay + # @option kwargs [String] :currency Coin name, uppercase only (e.g. `USDT`, `ETH`). + # If omitted, auto-repay is enabled/disabled for **all** currencies. + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/spot-margin-uta/set-auto-repay-mode + def set_auto_repay_mode(auto_repay_mode:, **kwargs) + params = Bybit::Utils::WireKeys.camelize(kwargs.merge(autoRepayMode: auto_repay_mode)) + @session.sign_request(method: :post, path: '/v5/spot-margin-trade/set-auto-repay-mode', body: params) + end + + # Set spot cross margin leverage + # + # POST /v5/spot-margin-trade/set-leverage + # + # @param leverage [String] Leverage amount, valid range is 2 to 10 + # @option kwargs [String] :currency Coin name, uppercase only (optional) + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/spot-margin-uta/set-leverage + def spot_margin_set_leverage(leverage:, **kwargs) + params = Bybit::Utils::WireKeys.camelize(kwargs.merge(leverage: leverage)) + @session.sign_request(method: :post, path: '/v5/spot-margin-trade/set-leverage', body: params) + end + + # Get Spot Margin Trade Status + # + # GET /v5/spot-margin-trade/state + # + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/spot-margin-uta/status + def get_trade_state + @session.sign_request(method: :get, path: '/v5/spot-margin-trade/state') + end + + # Toggle spot cross margin mode + # + # POST /v5/spot-margin-trade/switch-mode + # + # @param spot_margin_mode [String] Spot margin mode: 1=on, 0=off + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/spot-margin-uta/switch-mode + def spot_margin_switch_mode(spot_margin_mode:) + params = Bybit::Utils::WireKeys.camelize({ spotMarginMode: spot_margin_mode }) + @session.sign_request(method: :post, path: '/v5/spot-margin-trade/switch-mode', body: params) + end end end end diff --git a/lib/bybit/rest_api/user_service.rb b/lib/bybit/rest_api/user_service.rb index 254c6b9..e2a763d 100644 --- a/lib/bybit/rest_api/user_service.rb +++ b/lib/bybit/rest_api/user_service.rb @@ -249,6 +249,16 @@ def update_sub_api_key(subuid:, read_only:, **kwargs) params[:permissions] = permissions unless permissions.nil? @session.sign_request(method: :post, path: '/v5/user/update-sub-api', body: params) end + + # Query Referral Code + # + # GET /v5/user/invitation/code + # + # @return [Hash] Bybit V5 ApiResponse envelope (retCode / retMsg / result / retExtInfo / time). + # @see https://bybit-exchange.github.io/docs/v5/user/referral-code + def query_referral_code + @session.sign_request(method: :get, path: '/v5/user/invitation/code') + end end end end diff --git a/lib/bybit/session.rb b/lib/bybit/session.rb index 9e26642..51543ad 100644 --- a/lib/bybit/session.rb +++ b/lib/bybit/session.rb @@ -21,7 +21,8 @@ class Session def initialize(config) @config = config - @conn = config.faraday_connection || build_connection + @conn = config.faraday_connection ? config.faraday_connection.dup : build_connection + @conn.options.params_encoder = Faraday::FlatParamsEncoder end # Public unsigned endpoint — no X-BAPI-* headers attached. @@ -52,9 +53,9 @@ def dispatch(method:, path:, signed:, params:, body:) headers = build_headers(signed: signed, method: method, query_str: query_str, body_str: body_str) # Build URL manually with our own query_str so the wire bytes match - # what we signed. `req.params =` would re-serialize through - # Faraday::NestedParamsEncoder which key-orders and array-brackets - # differently, breaking the signature. + # what we signed. Faraday still parses URLs passed to `req.url`, so use + # its flat encoder to preserve repeated keys instead of collapsing them + # or adding array brackets. full_url = if !query_str.empty? && PAYLOAD_QUERY_METHODS.include?(method) "#{path}?#{query_str}" else diff --git a/lib/bybit/version.rb b/lib/bybit/version.rb index 90323fa..692ef55 100644 --- a/lib/bybit/version.rb +++ b/lib/bybit/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Bybit - VERSION = '0.1.1' + VERSION = '0.1.2' end diff --git a/spec/services/account_service_spec.rb b/spec/services/account_service_spec.rb index 312d0ef..313c36b 100644 --- a/spec/services/account_service_spec.rb +++ b/spec/services/account_service_spec.rb @@ -12,6 +12,7 @@ # Filled in by the loop below — one row per method the scanner picked up. method_table = [ { name: :batch_set_collateral, path: '/v5/account/set-collateral-switch-batch', method: :post, signed: true }, + { name: :demo_apply_money, path: '/v5/account/demo-apply-money', method: :post, signed: true }, { name: :get_borrow_history, path: '/v5/account/borrow-history', method: :get, signed: true }, { name: :get_collateral_info, path: '/v5/account/collateral-info', method: :get, signed: true }, { name: :get_dcp_info, path: '/v5/account/query-dcp-info', method: :get, signed: true }, @@ -20,8 +21,11 @@ { name: :get_wallet_balance, path: '/v5/account/wallet-balance', method: :get, signed: true }, { name: :get_instruments, path: '/v5/account/instruments-info', method: :get, signed: true }, { name: :get_mmp_state, path: '/v5/account/mmp-state', method: :get, signed: true }, + { name: :get_option_asset_info, path: '/v5/account/option-asset-info', method: :get, signed: true }, + { name: :get_pay_info, path: '/v5/account/pay-info', method: :get, signed: true }, { name: :get_smp_group, path: '/v5/account/smp-group', method: :get, signed: true }, { name: :get_transaction_log, path: '/v5/account/transaction-log', method: :get, signed: true }, + { name: :get_trade_info_for_analysis, path: '/v5/account/trade-info-for-analysis', method: :get, signed: true }, { name: :get_transferable_amount, path: '/v5/account/withdrawal', method: :get, signed: true }, { name: :get_user_settings, path: '/v5/account/user-setting-config', method: :get, signed: true }, { name: :manual_borrow, path: '/v5/account/borrow', method: :post, signed: true }, @@ -56,4 +60,43 @@ end end end + + describe '#get_trade_info_for_analysis' do + it 'requires symbol' do + expect { svc.get_trade_info_for_analysis } + .to raise_error(ArgumentError, /missing keyword: :symbol/) + end + end + + describe 'wire parameters for new endpoints' do + it 'camelizes demo fund configuration recursively' do + expect(session).to receive(:sign_request).with( + method: :post, + path: '/v5/account/demo-apply-money', + body: { adjustType: 1, utaDemoApplyMoney: [{ coin: 'USDT', amountStr: '10' }] } + ) + svc.demo_apply_money( + adjust_type: 1, + uta_demo_apply_money: [{ coin: 'USDT', amount_str: '10' }] + ) + end + + it 'sends the pay-info coin as a query parameter' do + expect(session).to receive(:sign_request).with( + method: :get, + path: '/v5/account/pay-info', + params: { coin: 'USDT' } + ) + svc.get_pay_info(coin: 'USDT') + end + + it 'sends all trade-analysis query parameters with wire names' do + expect(session).to receive(:sign_request).with( + method: :get, + path: '/v5/account/trade-info-for-analysis', + params: { symbol: 'BTCUSDT', startTime: 1, endTime: 2 } + ) + svc.get_trade_info_for_analysis(symbol: 'BTCUSDT', start_time: 1, end_time: 2) + end + end end diff --git a/spec/services/asset_service_spec.rb b/spec/services/asset_service_spec.rb index 7e5ccc4..f2b4960 100644 --- a/spec/services/asset_service_spec.rb +++ b/spec/services/asset_service_spec.rb @@ -43,6 +43,7 @@ { name: :list_sub_member_deposit_records, path: '/v5/asset/deposit/query-sub-member-record', method: :get, signed: true }, { name: :list_withdraw_addresses, path: '/v5/asset/withdraw/query-address', method: :get, signed: true }, { name: :list_withdraw_records, path: '/v5/asset/withdraw/query-record', method: :get, signed: true }, + { name: :limit_order_callback, path: '/v5/asset/exchange/limit-order/callback', method: :post, signed: true }, { name: :quote_apply, path: '/v5/asset/exchange/quote-apply', method: :post, signed: true }, { name: :send_withdraw, path: '/v5/asset/withdraw/create', method: :post, signed: true }, { name: :set_default_deposit_to_account, path: '/v5/asset/deposit/deposit-to-account', method: :post, signed: true }, @@ -74,4 +75,22 @@ end end end + + + describe '#limit_order_callback wire body' do + it 'sends every callback field with its OpenAPI wire name' do + expect(session).to receive(:sign_request).with( + method: :post, + path: '/v5/asset/exchange/limit-order/callback', + body: { + exchangeTxId: 'tx-1', optionType: 3, timeStamp: 123, + sign: 'callback-signature', appId: 'TimeResearch', errorCode: 'failed' + } + ) + svc.limit_order_callback( + exchange_tx_id: 'tx-1', option_type: 3, time_stamp: 123, + sign: 'callback-signature', app_id: 'TimeResearch', error_code: 'failed' + ) + end + end end diff --git a/spec/services/crypto_loan_service_spec.rb b/spec/services/crypto_loan_service_spec.rb index 7a08f80..69a6269 100644 --- a/spec/services/crypto_loan_service_spec.rb +++ b/spec/services/crypto_loan_service_spec.rb @@ -21,11 +21,14 @@ { name: :get_fixed_borrow_contract_info, path: '/v5/crypto-loan-fixed/borrow-contract-info', method: :get, signed: true }, { name: :get_fixed_borrow_order_info, path: '/v5/crypto-loan-fixed/borrow-order-info', method: :get, signed: true }, { name: :get_fixed_borrow_order_quote, path: '/v5/crypto-loan-fixed/borrow-order-quote', method: :get, signed: false }, + { name: :get_fixed_available_inventory, path: '/v5/crypto-loan-fixed/available-inventory', method: :get, signed: true }, + { name: :get_fixed_repayment_history, path: '/v5/crypto-loan-fixed/repayment-history', method: :get, signed: true }, { name: :get_fixed_renew_info, path: '/v5/crypto-loan-fixed/renew-info', method: :get, signed: true }, { name: :get_fixed_supply_contract_info, path: '/v5/crypto-loan-fixed/supply-contract-info', method: :get, signed: true }, { name: :get_fixed_supply_order_info, path: '/v5/crypto-loan-fixed/supply-order-info', method: :get, signed: true }, { name: :get_fixed_supply_order_quote, path: '/v5/crypto-loan-fixed/supply-order-quote', method: :get, signed: false }, { name: :get_flexible_borrow_history, path: '/v5/crypto-loan-flexible/borrow-history', method: :get, signed: true }, + { name: :get_flexible_available_inventory, path: '/v5/crypto-loan-flexible/available-inventory', method: :get, signed: true }, { name: :get_flexible_ongoing_coin, path: '/v5/crypto-loan-flexible/ongoing-coin', method: :get, signed: true }, { name: :get_flexible_repayment_history, path: '/v5/crypto-loan-flexible/repayment-history', method: :get, signed: true }, { name: :get_loanable_data, path: '/v5/crypto-loan-common/loanable-data', method: :get, signed: true }, @@ -37,6 +40,7 @@ { name: :repay_fixed_with_collateral, path: '/v5/crypto-loan-fixed/repay-collateral', method: :post, signed: true }, { name: :repay_flexible, path: '/v5/crypto-loan-flexible/repay', method: :post, signed: true }, { name: :repay_flexible_with_collateral, path: '/v5/crypto-loan-flexible/repay-collateral', method: :post, signed: true }, + { name: :supply_fixed, path: '/v5/crypto-loan-fixed/supply', method: :post, signed: true }, ].freeze method_table.each do |row| @@ -58,4 +62,51 @@ end end end + + + describe 'wire parameters for new endpoints' do + it 'sends fixed available inventory query parameters' do + expect(session).to receive(:sign_request).with( + method: :get, + path: '/v5/crypto-loan-fixed/available-inventory', + params: { currency: 'USDT', term: '7', annualRate: '0.02' } + ) + svc.get_fixed_available_inventory(currency: 'USDT', term: '7', annual_rate: '0.02') + end + + it 'camelizes fixed repayment history filters' do + expect(session).to receive(:sign_request).with( + method: :get, + path: '/v5/crypto-loan-fixed/repayment-history', + params: { repayId: 'repay-1', loanCurrency: 'USDT', limit: '10', cursor: 'next' } + ) + svc.get_fixed_repayment_history( + repay_id: 'repay-1', loan_currency: 'USDT', limit: '10', cursor: 'next' + ) + end + + it 'sends the complete fixed supply body' do + expect(session).to receive(:sign_request).with( + method: :post, + path: '/v5/crypto-loan-fixed/supply', + body: { + orderCurrency: 'USDT', orderAmount: '100', annualRate: '0.02', + term: '7', availableSource: '1' + } + ) + svc.supply_fixed( + order_currency: 'USDT', order_amount: '100', annual_rate: '0.02', + term: '7', available_source: '1' + ) + end + + it 'sends the flexible inventory currency' do + expect(session).to receive(:sign_request).with( + method: :get, + path: '/v5/crypto-loan-flexible/available-inventory', + params: { currency: 'BTC' } + ) + svc.get_flexible_available_inventory(currency: 'BTC') + end + end end diff --git a/spec/services/earn_service_spec.rb b/spec/services/earn_service_spec.rb index b5a37ea..e5b37fd 100644 --- a/spec/services/earn_service_spec.rb +++ b/spec/services/earn_service_spec.rb @@ -14,6 +14,7 @@ { name: :add_liquidity, path: '/v5/earn/liquidity-mining/add-liquidity', method: :post, signed: true }, { name: :add_margin, path: '/v5/earn/liquidity-mining/add-margin', method: :post, signed: true }, { name: :claim_liquidity_interest, path: '/v5/earn/liquidity-mining/claim-interest', method: :post, signed: true }, + { name: :edit_earn_auto_savings, path: '/v5/earn/flexible-saving/auto-savings', method: :post, signed: true }, { name: :get_advance_earn_order, path: '/v5/earn/advance/order', method: :get, signed: true }, { name: :get_advance_earn_position, path: '/v5/earn/advance/position', method: :get, signed: true }, { name: :get_advance_earn_product, path: '/v5/earn/advance/product', method: :get, signed: false }, @@ -101,4 +102,49 @@ end end end + + describe '#list_auto_savings' do + it 'sends multiple coins as repeated query parameters and signs the exact query string' do + config = Bybit::Configuration.new + config.api_key = 'test-key' + config.api_secret = 'test-secret' + config.base_url = 'https://api-testnet.bybit.com' + seen_request = nil + stubs = Faraday::Adapter::Test::Stubs.new do |stub| + stub.get(%r{/v5/earn/flexible-saving/auto-savings}) do |request| + seen_request = request + [200, { 'Content-Type' => 'application/json' }, + '{"retCode":0,"retMsg":"OK","result":{"coins":[]},"retExtInfo":{},"time":0}'] + end + end + config.faraday_connection = Faraday.new(url: config.base_url) do |connection| + connection.adapter :test, stubs + end + service = described_class.new(Bybit::Session.new(config)) + + service.list_auto_savings(coins: %w[BTC ETH]) + + query = 'coins=BTC&coins=ETH' + timestamp = seen_request.request_headers['X-BAPI-TIMESTAMP'] + expected_signature = Bybit::Authentication.sign_v5( + config.api_secret, timestamp, config.api_key, config.recv_window.to_s, query + ) + expect(seen_request.url.query).to eq(query) + expect(seen_request.request_headers['X-BAPI-SIGN']).to eq(expected_signature) + end + end + + + describe '#edit_earn_auto_savings wire body' do + it 'camelizes optional settings and keeps the boolean value' do + expect(session).to receive(:sign_request).with( + method: :post, + path: '/v5/earn/flexible-saving/auto-savings', + body: { coin: 'BTC', purchaseImmediately: true, isSelected: false } + ) + svc.edit_earn_auto_savings( + is_selected: false, coin: 'BTC', purchase_immediately: true + ) + end + end end diff --git a/spec/services/market_service_spec.rb b/spec/services/market_service_spec.rb index bd3a26c..518d066 100644 --- a/spec/services/market_service_spec.rb +++ b/spec/services/market_service_spec.rb @@ -27,6 +27,7 @@ { name: :get_open_interest, path: '/v5/market/open-interest', method: :get, signed: false }, { name: :get_order_price_limit, path: '/v5/market/price-limit', method: :get, signed: false }, { name: :get_orderbook, path: '/v5/market/orderbook', method: :get, signed: false }, + { name: :get_option_base_coins, path: '/v5/market/option-base-coins', method: :get, signed: false }, { name: :get_premium_index_price_kline, path: '/v5/market/premium-index-price-kline', method: :get, signed: false }, { name: :get_recent_public_trades, path: '/v5/market/recent-trade', method: :get, signed: false }, { name: :get_risk_limit, path: '/v5/market/risk-limit', method: :get, signed: false }, @@ -54,4 +55,15 @@ end end end + + + describe '#get_option_base_coins wire query' do + it 'camelizes the comma-separated underlying type filter' do + expect(session).to receive(:public_request).with( + path: '/v5/market/option-base-coins', + params: { underlyingType: '0,2' } + ) + svc.get_option_base_coins(underlying_type: '0,2') + end + end end diff --git a/spec/services/position_service_spec.rb b/spec/services/position_service_spec.rb index 93db33c..dab75ae 100644 --- a/spec/services/position_service_spec.rb +++ b/spec/services/position_service_spec.rb @@ -17,6 +17,7 @@ { name: :get_closed_positions, path: '/v5/position/get-closed-positions', method: :get, signed: true }, { name: :get_move_position_history, path: '/v5/position/move-history', method: :get, signed: true }, { name: :get_positions, path: '/v5/position/list', method: :get, signed: true }, + { name: :get_symbol_info, path: '/v5/position/symbol-info', method: :get, signed: true }, { name: :move_position, path: '/v5/position/move-positions', method: :post, signed: true }, { name: :set_auto_add_margin, path: '/v5/position/set-auto-add-margin', method: :post, signed: true }, { name: :set_leverage, path: '/v5/position/set-leverage', method: :post, signed: true }, @@ -43,4 +44,16 @@ end end end + + + describe '#get_symbol_info wire query' do + it 'sends category and the optional symbol' do + expect(session).to receive(:sign_request).with( + method: :get, + path: '/v5/position/symbol-info', + params: { symbol: 'BTCUSDT', category: 'linear' } + ) + svc.get_symbol_info(category: 'linear', symbol: 'BTCUSDT') + end + end end diff --git a/spec/services/spot_margin_service_spec.rb b/spec/services/spot_margin_service_spec.rb index 5707703..197fc26 100644 --- a/spec/services/spot_margin_service_spec.rb +++ b/spec/services/spot_margin_service_spec.rb @@ -14,6 +14,22 @@ { name: :get_historical_interest_rate, path: '/v5/spot-margin-trade/interest-rate-history', method: :get, signed: true }, { name: :get_position_tiers, path: '/v5/spot-margin-trade/position-tiers', method: :get, signed: true }, { name: :get_tiered_collateral_ratio, path: '/v5/spot-margin-trade/collateral', method: :get, signed: false }, + { name: :get_trade_coin_state, path: '/v5/spot-margin-trade/coinstate', method: :get, signed: true }, + { name: :query_fixed_available_inventory, path: '/v5/spot-margin-trade/fixed-available-inventory', method: :get, signed: true }, + { name: :account_fixed_borrow, path: '/v5/spot-margin-trade/fixedborrow', method: :post, signed: true }, + { name: :query_fixed_borrow_contracts, path: '/v5/spot-margin-trade/fixedborrow-contract-info', method: :get, signed: true }, + { name: :query_fixed_borrow_orders, path: '/v5/spot-margin-trade/fixedborrow-order-info', method: :get, signed: true }, + { name: :query_fixed_borrow_market, path: '/v5/spot-margin-trade/fixedborrow-order-quote', method: :get, signed: true }, + { name: :renew_fixed_borrow, path: '/v5/spot-margin-trade/fixedborrow-renew', method: :post, signed: true }, + { name: :get_trade_flexible_available_inventory, path: '/v5/spot-margin-trade/flexible-available-inventory', method: :get, signed: true }, + { name: :get_trade_auto_repay_mode, path: '/v5/spot-margin-trade/get-auto-repay-mode', method: :get, signed: true }, + { name: :query_borrow_liability, path: '/v5/spot-margin-trade/liability', method: :get, signed: true }, + { name: :get_trade_max_borrowable, path: '/v5/spot-margin-trade/max-borrowable', method: :get, signed: true }, + { name: :get_trade_repayment_available_amount, path: '/v5/spot-margin-trade/repayment-available-amount', method: :get, signed: true }, + { name: :set_auto_repay_mode, path: '/v5/spot-margin-trade/set-auto-repay-mode', method: :post, signed: true }, + { name: :spot_margin_set_leverage, path: '/v5/spot-margin-trade/set-leverage', method: :post, signed: true }, + { name: :get_trade_state, path: '/v5/spot-margin-trade/state', method: :get, signed: true }, + { name: :spot_margin_switch_mode, path: '/v5/spot-margin-trade/switch-mode', method: :post, signed: true }, { name: :get_vip_margin_data, path: '/v5/spot-margin-trade/data', method: :get, signed: false }, ].freeze @@ -36,4 +52,157 @@ end end end + + describe '#query_fixed_borrow_market' do + it 'allows order_by to be omitted' do + expect(session).to receive(:sign_request).with( + method: :get, + path: '/v5/spot-margin-trade/fixedborrow-order-quote', + params: { orderCurrency: 'USDT' } + ) + + svc.query_fixed_borrow_market(order_currency: 'USDT') + end + end + + + describe 'wire parameters for new endpoints' do + it 'sends the optional coin-state currency' do + expect(session).to receive(:sign_request).with( + method: :get, path: '/v5/spot-margin-trade/coinstate', + params: { currency: 'BTC' } + ) + svc.get_trade_coin_state(currency: 'BTC') + end + + it 'sends fixed inventory parameters' do + expect(session).to receive(:sign_request).with( + method: :get, path: '/v5/spot-margin-trade/fixed-available-inventory', + params: { currency: 'USDT', term: '7', annualRate: '0.02' } + ) + svc.query_fixed_available_inventory(currency: 'USDT', term: '7', annual_rate: '0.02') + end + + it 'sends the complete fixed borrow body' do + expect(session).to receive(:sign_request).with( + method: :post, path: '/v5/spot-margin-trade/fixedborrow', + body: { + repayType: '1', strategyType: 'FULL', orderCurrency: 'USDT', + orderAmount: '100', annualRate: '0.02', term: '7' + } + ) + svc.account_fixed_borrow( + order_currency: 'USDT', order_amount: '100', annual_rate: '0.02', term: '7', + repay_type: '1', strategy_type: 'FULL' + ) + end + + it 'camelizes fixed contract filters' do + expect(session).to receive(:sign_request).with( + method: :get, path: '/v5/spot-margin-trade/fixedborrow-contract-info', + params: { + orderId: 'order-1', orderCurrency: 'USDT', term: '7', limit: '10', cursor: 'next' + } + ) + svc.query_fixed_borrow_contracts( + order_id: 'order-1', order_currency: 'USDT', term: '7', limit: '10', cursor: 'next' + ) + end + + it 'camelizes fixed order filters' do + expect(session).to receive(:sign_request).with( + method: :get, path: '/v5/spot-margin-trade/fixedborrow-order-info', + params: { + orderId: 'order-1', orderCurrency: 'USDT', state: '3', + term: '7', limit: '10', cursor: 'next' + } + ) + svc.query_fixed_borrow_orders( + order_id: 'order-1', order_currency: 'USDT', state: '3', + term: '7', limit: '10', cursor: 'next' + ) + end + + it 'sends every fixed market query parameter' do + expect(session).to receive(:sign_request).with( + method: :get, path: '/v5/spot-margin-trade/fixedborrow-order-quote', + params: { term: '7', orderBy: 'apy', sort: 1, limit: 20, orderCurrency: 'USDT' } + ) + svc.query_fixed_borrow_market( + order_currency: 'USDT', term: '7', order_by: 'apy', sort: 1, limit: 20 + ) + end + + it 'sends the fixed renewal body' do + expect(session).to receive(:sign_request).with( + method: :post, path: '/v5/spot-margin-trade/fixedborrow-renew', + body: { qty: '10', loanId: 'loan-1' } + ) + svc.renew_fixed_borrow(loan_id: 'loan-1', qty: '10') + end + + it 'sends the flexible inventory currency' do + expect(session).to receive(:sign_request).with( + method: :get, path: '/v5/spot-margin-trade/flexible-available-inventory', + params: { currency: 'BTC' } + ) + svc.get_trade_flexible_available_inventory(currency: 'BTC') + end + + it 'sends the optional auto-repay currency query' do + expect(session).to receive(:sign_request).with( + method: :get, path: '/v5/spot-margin-trade/get-auto-repay-mode', + params: { currency: 'ETH' } + ) + svc.get_trade_auto_repay_mode(currency: 'ETH') + end + + it 'sends the liability currency' do + expect(session).to receive(:sign_request).with( + method: :get, path: '/v5/spot-margin-trade/liability', + params: { currency: 'USDT' } + ) + svc.query_borrow_liability(currency: 'USDT') + end + + it 'sends the max-borrowable currency' do + expect(session).to receive(:sign_request).with( + method: :get, path: '/v5/spot-margin-trade/max-borrowable', + params: { currency: 'BTC' } + ) + svc.get_trade_max_borrowable(currency: 'BTC') + end + + it 'sends the repayment-availability currency' do + expect(session).to receive(:sign_request).with( + method: :get, path: '/v5/spot-margin-trade/repayment-available-amount', + params: { currency: 'BTC' } + ) + svc.get_trade_repayment_available_amount(currency: 'BTC') + end + + it 'sends the auto-repay mode body' do + expect(session).to receive(:sign_request).with( + method: :post, path: '/v5/spot-margin-trade/set-auto-repay-mode', + body: { currency: 'ETH', autoRepayMode: '1' } + ) + svc.set_auto_repay_mode(auto_repay_mode: '1', currency: 'ETH') + end + + it 'sends the leverage body' do + expect(session).to receive(:sign_request).with( + method: :post, path: '/v5/spot-margin-trade/set-leverage', + body: { currency: 'BTC', leverage: '4' } + ) + svc.spot_margin_set_leverage(leverage: '4', currency: 'BTC') + end + + it 'sends the margin-mode body' do + expect(session).to receive(:sign_request).with( + method: :post, path: '/v5/spot-margin-trade/switch-mode', + body: { spotMarginMode: '1' } + ) + svc.spot_margin_switch_mode(spot_margin_mode: '1') + end + end end diff --git a/spec/services/user_service_spec.rb b/spec/services/user_service_spec.rb index 9cf0662..2f3bbf6 100644 --- a/spec/services/user_service_spec.rb +++ b/spec/services/user_service_spec.rb @@ -25,6 +25,7 @@ { name: :list_sub_api_keys, path: '/v5/user/sub-apikeys', method: :get, signed: true }, { name: :list_sub_member_uids, path: '/v5/user/query-sub-members', method: :get, signed: true }, { name: :list_sub_members, path: '/v5/user/submembers', method: :get, signed: true }, + { name: :query_referral_code, path: '/v5/user/invitation/code', method: :get, signed: true }, { name: :sign_agreement, path: '/v5/user/agreement', method: :post, signed: true }, { name: :update_api_key, path: '/v5/user/update-api', method: :post, signed: true }, { name: :update_sub_api_key, path: '/v5/user/update-sub-api', method: :post, signed: true }, diff --git a/spec/session_spec.rb b/spec/session_spec.rb index c4ebed9..5647285 100644 --- a/spec/session_spec.rb +++ b/spec/session_spec.rb @@ -13,6 +13,18 @@ end let(:session) { described_class.new(config) } + describe 'custom Faraday connection' do + it 'does not mutate the injected connection params encoder' do + connection = Faraday.new(url: config.base_url) do |faraday| + faraday.adapter :test, Faraday::Adapter::Test::Stubs.new + end + config.faraday_connection = connection + + expect { described_class.new(config) } + .not_to change { connection.options.params_encoder } + end + end + describe 'signed dispatch' do it 'signs GET query-string exactly as it appears on the wire' do url = %r{https://api-testnet\.bybit\.com/v5/account/wallet-balance}