diff --git a/src/clients/ExchangeClient.ts b/src/clients/ExchangeClient.ts index 3c8ac44..cd71d14 100644 --- a/src/clients/ExchangeClient.ts +++ b/src/clients/ExchangeClient.ts @@ -1,11 +1,12 @@ import { ethers } from 'ethers'; import { InfoClient } from './InfoClient.js'; -import { createPermitParams } from '../signing/permit.js'; +import { createPermitParams, hexToBase64 } from '../signing/permit.js'; import { createRegisterSignerSignatures } from '../signing/signer.js'; import { encodeOrder, encodeCancelOrder, encodeCancelAll, encodeLeverage, encodeMarginMode, encodeIsolatedMargin } from '../signing/encoder.js'; +import { PERMIT_SINGLE_TYPES, PLACE_TPSL_TYPES, CANCEL_TPSL_TYPES } from '../signing/domain.js'; import { Side, OrderType, TimeInForce, StpMode, MarginMode } from '../types/common.js'; import type { ExchangeClientOptions, Eip712Domain } from '../types/config.js'; -import type { OrderParams, CancelParams, OrderResponse, CancelResponse, CancelAllResponse } from '../types/order.js'; +import type { OrderParams, CancelParams, OrderResponse, CancelResponse, CancelAllResponse, ApprovePermitSingleParams, PlaceTpslOrderParams, CancelTpslOrderParams } from '../types/order.js'; import type { PermitParams, NonceState, RegisterSignerResult } from '../types/auth.js'; export class ExchangeClient { @@ -138,6 +139,53 @@ export class ExchangeClient { }); } + // ── TPSL Authentication ─────────────────────────────────── + + /** + * Approves a budget for the operator hub to execute TPSL orders on your behalf. + */ + async approvePermitSingle(params: ApprovePermitSingleParams): Promise { + this.assertInit(); + this.assertAccountKey(); + + const sysConfig = await this.info.getSystemConfig(); + const operatorHub = sysConfig.addresses?.operator_hub as string | undefined; + if (!operatorHub) throw new Error('OperatorHub address not found in system config'); + + const nonceState = params.nonce ?? await this.getNonceState(); + const budget = BigInt(Math.ceil(params.budgetUsd)) * 10n ** 18n; // WAD + const allowanceExpiry = Math.floor(Date.now() / 1000) + (params.allowanceExpirySeconds ?? (30 * 24 * 3600 - 3600)); + const nonceBitmapIndex = nonceState.current_bitmap_index; + + const signatureHex = await this.accountWallet!.signTypedData( + { + name: this.domain.name, + version: this.domain.version, + chainId: this.domain.chainId, + verifyingContract: this.domain.verifyingContract, + }, + PERMIT_SINGLE_TYPES, + { + account: this.account, + operator: operatorHub, + budget, + allowanceExpiry, + nonceAnchor: Number(nonceState.nonce_anchor), + nonceBitmap: nonceBitmapIndex, + } + ); + + return this.info['http'].post('/v1/auth/approve-single', { + account: this.account, + operator: operatorHub, + budget: budget.toString(), + allowance_expiry: allowanceExpiry, + nonce_anchor: nonceState.nonce_anchor, + nonce_bitmap_index: nonceBitmapIndex, + signature: hexToBase64(signatureHex), + }); + } + // ── Orders ────────────────────────────────────────────── private async createPermit(hash: string, nonce?: NonceState): Promise { @@ -208,6 +256,84 @@ export class ExchangeClient { }); } + // ── TPSL Orders ───────────────────────────────────────── + + async placeTpslOrder(params: PlaceTpslOrderParams): Promise { + this.assertInit(); + const deadline = Math.floor(Date.now() / 1000) + (params.deadlineSeconds ?? 3600); + + const message = { + account: this.account, + marketId: params.market_id, + side: params.side === 'BUY' ? 0 : 1, + size: params.size, + stopType: params.stop_type === 'TAKE_PROFIT' ? 0 : 1, + stopPrice: params.stop_price, + limitPrice: params.limit_price, + orderType: params.order_type, + stopPriceOption: params.stop_price_option, + tif: params.tif === 'GTC' ? 0 : 1, // fallback mapping + deadline, + }; + + const signatureHex = await this.signerWallet.signTypedData( + { + name: this.domain.name, + version: this.domain.version, + chainId: this.domain.chainId, + verifyingContract: this.domain.verifyingContract, + }, + PLACE_TPSL_TYPES, + message + ); + + return this.info['http'].post('/v1/orders/tpsl', { + account: this.account, + market_id: String(params.market_id), + side: params.side, + size: params.size, + stop_type: params.stop_type, + order_type: params.order_type, + stop_price: params.stop_price, + limit_price: params.limit_price, + stop_price_option: params.stop_price_option, + tif: params.tif, + signer: this.signer, + signature: hexToBase64(signatureHex), + deadline, + }); + } + + async cancelTpslOrder(params: CancelTpslOrderParams): Promise { + this.assertInit(); + const deadline = Math.floor(Date.now() / 1000) + (params.deadlineSeconds ?? 3600); + + const message = { + account: this.account, + orderId: params.order_id, + deadline + }; + + const signatureHex = await this.signerWallet.signTypedData( + { + name: this.domain.name, + version: this.domain.version, + chainId: this.domain.chainId, + verifyingContract: this.domain.verifyingContract, + }, + CANCEL_TPSL_TYPES, + message + ); + + return this.info['http'].post('/v1/orders/tpsl/cancel', { + order_id: params.order_id, + account: this.account, + signer: this.signer, + signature: hexToBase64(signatureHex), + deadline + }); + } + // ── Convenience order methods ─────────────────────────── async marketBuy(marketId: number, sizeSteps: number): Promise { diff --git a/src/clients/InfoClient.ts b/src/clients/InfoClient.ts index 4c2f534..263833c 100644 --- a/src/clients/InfoClient.ts +++ b/src/clients/InfoClient.ts @@ -3,7 +3,7 @@ import { DEFAULT_BASE_URL } from '../utils/constants.js'; import type { ClientOptions, SystemConfig, Eip712Domain } from '../types/config.js'; import type { Market, Orderbook, Trade, Candle, FundingRate } from '../types/market.js'; import type { Balance, Position, FundingPayment, Transfer, RealizedPnl } from '../types/account.js'; -import type { OpenOrder, OrderHistoryEntry, Fill } from '../types/order.js'; +import type { OpenOrder, OrderHistoryEntry, Fill, TpslOrder } from '../types/order.js'; import type { SessionKeyStatus, SignerInfo, NonceState } from '../types/auth.js'; export class InfoClient { @@ -98,6 +98,13 @@ export class InfoClient { return data.orders ?? []; } + async getOpenTpslOrders(account: string): Promise { + const data = await this.http.get<{ orders: TpslOrder[] }>( + `/v1/orders/tpsl?account=${account}&statuses=TPSL_ORDER_STATUS_ACCEPTED` + ); + return data.orders ?? []; + } + async getOrderHistory(account: string, marketId?: number, limit = 50): Promise { let path = `/v1/orders?account=${account}&limit=${limit}`; if (marketId !== undefined) path += `&market_id=${marketId}`; diff --git a/src/signing/domain.ts b/src/signing/domain.ts index 4cc7fed..023828a 100644 --- a/src/signing/domain.ts +++ b/src/signing/domain.ts @@ -40,3 +40,38 @@ export const VERIFY_WITNESS_TYPES: Record = { }; export const REGISTER_SIGNER_MESSAGE = 'Registering signer for RISEx'; + +export const PERMIT_SINGLE_TYPES: Record = { + PermitSingle: [ + { name: 'account', type: 'address' }, + { name: 'operator', type: 'address' }, + { name: 'budget', type: 'uint96' }, + { name: 'allowanceExpiry', type: 'uint32' }, + { name: 'nonceAnchor', type: 'uint48' }, + { name: 'nonceBitmap', type: 'uint8' }, + ], +}; + +export const PLACE_TPSL_TYPES: Record = { + PlaceTpslOrder: [ + { name: 'account', type: 'address' }, + { name: 'marketId', type: 'uint64' }, + { name: 'side', type: 'uint8' }, + { name: 'size', type: 'string' }, + { name: 'stopType', type: 'uint8' }, + { name: 'stopPrice', type: 'string' }, + { name: 'limitPrice', type: 'string' }, + { name: 'orderType', type: 'uint8' }, + { name: 'stopPriceOption', type: 'uint8' }, + { name: 'tif', type: 'uint8' }, + { name: 'deadline', type: 'uint32' }, + ], +}; + +export const CANCEL_TPSL_TYPES: Record = { + CancelTpslOrder: [ + { name: 'account', type: 'address' }, + { name: 'orderId', type: 'string' }, + { name: 'deadline', type: 'uint32' } + ] +}; diff --git a/src/signing/permit.ts b/src/signing/permit.ts index 7a13d53..eb1142c 100644 --- a/src/signing/permit.ts +++ b/src/signing/permit.ts @@ -7,7 +7,7 @@ import type { Eip712Domain } from '../types/config.js'; /** * Convert a hex signature string to base64. */ -function hexToBase64(hex: string): string { +export function hexToBase64(hex: string): string { const clean = hex.startsWith('0x') ? hex.slice(2) : hex; const bytes = Buffer.from(clean, 'hex'); return bytes.toString('base64'); diff --git a/src/types/order.ts b/src/types/order.ts index 62c724d..2c02f00 100644 --- a/src/types/order.ts +++ b/src/types/order.ts @@ -36,6 +36,45 @@ export interface TpSlParams { [key: string]: unknown; } +export interface ApprovePermitSingleParams { + budgetUsd: number; + allowanceExpirySeconds?: number; + nonce?: NonceState; +} + +export interface PlaceTpslOrderParams { + market_id: number; + side: 'BUY' | 'SELL'; + size: string; + stop_type: 'TAKE_PROFIT' | 'STOP_LOSS'; + order_type: number; + stop_price: string; + limit_price: string; + stop_price_option: number; + tif: string; + nonce?: NonceState; + deadlineSeconds?: number; +} + +export interface CancelTpslOrderParams { + order_id: string; + nonce?: NonceState; + deadlineSeconds?: number; +} + +export interface TpslOrder { + order_id: string; + market_id: number; + account: string; + side: string; + size: string; + stop_type: string; + stop_price: string; + limit_price: string; + status: string; + [key: string]: unknown; +} + export interface OrderResponse { order_id: string; sc_order_id: string;