Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 128 additions & 2 deletions src/clients/ExchangeClient.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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<unknown> {
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<PermitParams> {
Expand Down Expand Up @@ -208,6 +256,84 @@ export class ExchangeClient {
});
}

// ── TPSL Orders ─────────────────────────────────────────

async placeTpslOrder(params: PlaceTpslOrderParams): Promise<OrderResponse> {
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<OrderResponse>('/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<CancelResponse> {
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<CancelResponse>('/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<OrderResponse> {
Expand Down
9 changes: 8 additions & 1 deletion src/clients/InfoClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -98,6 +98,13 @@ export class InfoClient {
return data.orders ?? [];
}

async getOpenTpslOrders(account: string): Promise<TpslOrder[]> {
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<OrderHistoryEntry[]> {
let path = `/v1/orders?account=${account}&limit=${limit}`;
if (marketId !== undefined) path += `&market_id=${marketId}`;
Expand Down
35 changes: 35 additions & 0 deletions src/signing/domain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,38 @@ export const VERIFY_WITNESS_TYPES: Record<string, ethers.TypedDataField[]> = {
};

export const REGISTER_SIGNER_MESSAGE = 'Registering signer for RISEx';

export const PERMIT_SINGLE_TYPES: Record<string, ethers.TypedDataField[]> = {
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<string, ethers.TypedDataField[]> = {
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<string, ethers.TypedDataField[]> = {
CancelTpslOrder: [
{ name: 'account', type: 'address' },
{ name: 'orderId', type: 'string' },
{ name: 'deadline', type: 'uint32' }
]
};
2 changes: 1 addition & 1 deletion src/signing/permit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
39 changes: 39 additions & 0 deletions src/types/order.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down