From 1e3cf57c876443b9557eb95f49fdd8b29853cc81 Mon Sep 17 00:00:00 2001 From: Aleksandr Kraiz Date: Fri, 27 Jan 2023 15:45:10 +0400 Subject: [PATCH] CFD (#28) * Added new pairs type: futures * Added new pairs type: futures * added market param to pairsConfig request * OP-3268 [CFD] Deposit amount Added CFDContracts endpoint package.json version update * OP-3268 [CFD] Deposit amount CFDContractsSchema export * OP-3268 [CFD] Deposit amount CFDContractsSchema export package.json update * cfdContractsSchema rename Add getCFDHistory method package.json update * Parameter of getCFDHistory method was updated * cfdHistorySchema was updated * WS subscribtion for CFD balances, CFD Order signing functionality * cfdHistorySchema was updated package.json version 0.16.0-rc.6 was updated * package.json version 0.16.0-rc.7 was updated * Exchange and CFD history schemas was transformed package.json version 0.16.0-rc.8 was updated * Added HistoryTransactionStatus package.json version 0.16.0-rc.8 was updated * cfdContractsSchema was updated package.json version 0.16.0-rc.10 was updated * Merge conflicts fix package.json version was updated to 0.16.0-rc.12 * updated schemas, updated version * Updated CFD balance schema, updated unsubscribe with passing extra details * CFD balances schema update * Added CFD prices request * typing small fix * fixed transformation of CFD init / update responses * temporary added debug * temporary added debug. changed ver * debug, ver up * Add currentPrice to cfdBalanceSchema * Added maxAvailableLong maxAvailableShort to balances * added leverage and position status * Update isWithCode - allow string code * version up Co-authored-by: Mikhail Gladchenko Co-authored-by: Demid Co-authored-by: Dmitry Leleko --- package.json | 4 +- src/constants/cfdOrderTypes.ts | 15 ++++ src/constants/positionStatuses.ts | 9 ++ src/crypt/hashCFDOrder.ts | 31 +++++++ src/crypt/index.ts | 1 + src/crypt/signCFDOrder.ts | 83 +++++++++++++++++++ src/crypt/signCFDOrderPersonal.ts | 30 +++++++ src/services/OrionAggregator/index.ts | 65 ++++++++++++--- .../OrionAggregator/ws/MessageType.ts | 1 + .../OrionAggregator/ws/SubscriptionType.ts | 1 + src/services/OrionAggregator/ws/index.ts | 75 ++++++++++++++++- .../ws/schemas/cfdAddressUpdateSchema.ts | 33 ++++++++ .../ws/schemas/cfdBalancesSchema.ts | 46 ++++++++++ .../OrionAggregator/ws/schemas/index.ts | 1 + src/services/OrionBlockchain/index.ts | 29 +++++++ .../schemas/cfdContractsSchema.ts | 19 +++++ .../schemas/cfdHistorySchema.ts | 52 ++++++++++++ .../OrionBlockchain/schemas/historySchema.ts | 18 +++- src/services/OrionBlockchain/schemas/index.ts | 2 + src/types.ts | 49 +++++++++++ src/utils/typeHelpers.ts | 8 +- 21 files changed, 551 insertions(+), 21 deletions(-) create mode 100644 src/constants/cfdOrderTypes.ts create mode 100644 src/constants/positionStatuses.ts create mode 100644 src/crypt/hashCFDOrder.ts create mode 100644 src/crypt/signCFDOrder.ts create mode 100644 src/crypt/signCFDOrderPersonal.ts create mode 100644 src/services/OrionAggregator/ws/schemas/cfdAddressUpdateSchema.ts create mode 100644 src/services/OrionAggregator/ws/schemas/cfdBalancesSchema.ts create mode 100644 src/services/OrionBlockchain/schemas/cfdContractsSchema.ts create mode 100644 src/services/OrionBlockchain/schemas/cfdHistorySchema.ts diff --git a/package.json b/package.json index a509000..5b92dda 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "@orionprotocol/sdk", - "version": "0.15.31", + "name": "@orionprotocol/sdk", + "version": "0.16.0", "description": "Orion Protocol SDK", "main": "./lib/esm/index.js", "module": "./lib/esm/index.js", diff --git a/src/constants/cfdOrderTypes.ts b/src/constants/cfdOrderTypes.ts new file mode 100644 index 0000000..cbe5247 --- /dev/null +++ b/src/constants/cfdOrderTypes.ts @@ -0,0 +1,15 @@ +const CFD_ORDER_TYPES = { + CFDOrder: [ + { name: 'senderAddress', type: 'address' }, + { name: 'matcherAddress', type: 'address' }, + { name: 'instrumentAddress', type: 'address' }, + { name: 'amount', type: 'uint64' }, + { name: 'price', type: 'uint64' }, + { name: 'matcherFee', type: 'uint64' }, + { name: 'nonce', type: 'uint64' }, + { name: 'expiration', type: 'uint64' }, + { name: 'buySide', type: 'uint8' }, + ], +}; + +export default CFD_ORDER_TYPES; diff --git a/src/constants/positionStatuses.ts b/src/constants/positionStatuses.ts new file mode 100644 index 0000000..a4b0abe --- /dev/null +++ b/src/constants/positionStatuses.ts @@ -0,0 +1,9 @@ +const positionStatuses = [ + 'SHORT', + 'LONG', + 'CLOSED', + 'LIQUIDATED', + 'NOT_OPEN', +] as const; + +export default positionStatuses; diff --git a/src/crypt/hashCFDOrder.ts b/src/crypt/hashCFDOrder.ts new file mode 100644 index 0000000..0ee0654 --- /dev/null +++ b/src/crypt/hashCFDOrder.ts @@ -0,0 +1,31 @@ +import { ethers } from 'ethers'; +import { CFDOrder } from '../types'; + +const hashCFDOrder = (order: CFDOrder) => ethers.utils.solidityKeccak256( + [ + 'uint8', + 'address', + 'address', + 'address', + 'uint64', + 'uint64', + 'uint64', + 'uint64', + 'uint64', + 'uint8', + ], + [ + '0x03', + order.senderAddress, + order.matcherAddress, + order.instrumentAddress, + order.amount, + order.price, + order.matcherFee, + order.nonce, + order.expiration, + order.buySide ? '0x01' : '0x00', + ], +); + +export default hashCFDOrder; diff --git a/src/crypt/index.ts b/src/crypt/index.ts index 4b1f786..6a0c8d8 100644 --- a/src/crypt/index.ts +++ b/src/crypt/index.ts @@ -1,4 +1,5 @@ export { default as signCancelOrder } from './signCancelOrder'; export { default as signCancelOrderPersonal } from './signCancelOrderPersonal'; export { default as signOrder } from './signOrder'; +export { default as signCFDOrder } from './signCFDOrder'; export { default as signOrderPersonal } from './signOrderPersonal'; diff --git a/src/crypt/signCFDOrder.ts b/src/crypt/signCFDOrder.ts new file mode 100644 index 0000000..6e6118e --- /dev/null +++ b/src/crypt/signCFDOrder.ts @@ -0,0 +1,83 @@ +/* eslint-disable no-underscore-dangle */ +import { TypedDataSigner } from '@ethersproject/abstract-signer'; +import BigNumber from 'bignumber.js'; +import { ethers } from 'ethers'; +import { joinSignature, splitSignature } from 'ethers/lib/utils'; +import { INTERNAL_ORION_PRECISION } from '../constants'; +import { CFDOrder, SignedCFDOrder, SupportedChainId } from '../types'; +import normalizeNumber from '../utils/normalizeNumber'; +import getDomainData from './getDomainData'; +import signCFDOrderPersonal from "./signCFDOrderPersonal"; +import hashCFDOrder from "./hashCFDOrder"; +import CFD_ORDER_TYPES from "../constants/cfdOrderTypes"; + +const DEFAULT_EXPIRATION = 29 * 24 * 60 * 60 * 1000; // 29 days + +type SignerWithTypedDataSign = ethers.Signer & TypedDataSigner; + +export const signCFDOrder = async ( + instrumentAddress: string, + side: 'BUY' | 'SELL', + price: BigNumber.Value, + amount: BigNumber.Value, + matcherFee: BigNumber.Value, + senderAddress: string, + matcherAddress: string, + usePersonalSign: boolean, + signer: ethers.Signer, + chainId: SupportedChainId, +) => { + const nonce = Date.now(); + const expiration = nonce + DEFAULT_EXPIRATION; + + const order: CFDOrder = { + senderAddress, + matcherAddress, + instrumentAddress, + amount: normalizeNumber( + amount, + INTERNAL_ORION_PRECISION, + BigNumber.ROUND_FLOOR, + ).toNumber(), + price: normalizeNumber( + price, + INTERNAL_ORION_PRECISION, + BigNumber.ROUND_FLOOR, + ).toNumber(), + matcherFee: normalizeNumber( + matcherFee, + INTERNAL_ORION_PRECISION, + BigNumber.ROUND_CEIL, // ROUND_CEIL because we don't want get "not enough fee" error + ).toNumber(), + nonce, + expiration, + buySide: side === 'BUY' ? 1 : 0, + isPersonalSign: usePersonalSign, + }; + + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + const typedDataSigner = signer as SignerWithTypedDataSign; + const signature = usePersonalSign + ? await signCFDOrderPersonal(order, signer) + : await typedDataSigner._signTypedData( + getDomainData(chainId), + CFD_ORDER_TYPES, + order, + ); + + // https://github.com/poap-xyz/poap-fun/pull/62#issue-928290265 + // "Signature's v was always send as 27 or 28, but from Ledger was 0 or 1" + const fixedSignature = joinSignature(splitSignature(signature)); + + if (!fixedSignature) throw new Error("Can't sign order"); + + const signedOrder: SignedCFDOrder = { + ...order, + id: hashCFDOrder(order), + signature: fixedSignature, + }; + + return signedOrder; +}; + +export default signCFDOrder; diff --git a/src/crypt/signCFDOrderPersonal.ts b/src/crypt/signCFDOrderPersonal.ts new file mode 100644 index 0000000..fad91c0 --- /dev/null +++ b/src/crypt/signCFDOrderPersonal.ts @@ -0,0 +1,30 @@ +import { ethers } from 'ethers'; +import { CFDOrder } from '../types'; + +const { arrayify, joinSignature, splitSignature } = ethers.utils; + +const signCFDOrderPersonal = async (order: CFDOrder, signer: ethers.Signer) => { + const message = ethers.utils.solidityKeccak256( + [ + 'string', 'address', 'address', 'address', 'uint64', 'uint64', 'uint64', 'uint64', 'uint64', 'uint8', + ], + [ + 'order', + order.senderAddress, + order.matcherAddress, + order.instrumentAddress, + order.amount, + order.price, + order.matcherFee, + order.nonce, + order.expiration, + order.buySide, + ], + ); + const signature = await signer.signMessage(arrayify(message)); + + // NOTE: metamask broke sig.v value and we fix it in next line + return joinSignature(splitSignature(signature)); +}; + +export default signCFDOrderPersonal; diff --git a/src/services/OrionAggregator/index.ts b/src/services/OrionAggregator/index.ts index e011941..ac6b18f 100644 --- a/src/services/OrionAggregator/index.ts +++ b/src/services/OrionAggregator/index.ts @@ -9,12 +9,13 @@ import errorSchema from './schemas/errorSchema'; import placeAtomicSwapSchema from './schemas/placeAtomicSwapSchema'; import { OrionAggregatorWS } from './ws'; import { atomicSwapHistorySchema } from './schemas/atomicSwapHistorySchema'; -import { Exchange, SignedCancelOrderRequest, SignedOrder } from '../../types'; +import {Exchange, SignedCancelOrderRequest, SignedCFDOrder, SignedOrder} from '../../types'; import { pairConfigSchema } from './schemas'; import { aggregatedOrderbookSchema, exchangeOrderbookSchema, poolReservesSchema, } from './schemas/aggregatedOrderbookSchema'; import networkCodes from '../../constants/networkCodes'; +import toUpperCase from '../../utils/toUpperCase'; class OrionAggregator { private readonly apiUrl: string; @@ -33,6 +34,7 @@ class OrionAggregator { this.getTradeProfits = this.getTradeProfits.bind(this); this.placeAtomicSwap = this.placeAtomicSwap.bind(this); this.placeOrder = this.placeOrder.bind(this); + this.placeCFDOrder = this.placeCFDOrder.bind(this); this.cancelOrder = this.cancelOrder.bind(this); this.checkWhitelisted = this.checkWhitelisted.bind(this); this.getLockedBalance = this.getLockedBalance.bind(this); @@ -41,10 +43,15 @@ class OrionAggregator { this.getPoolReserves = this.getPoolReserves.bind(this); } - getPairsList = () => fetchWithValidation( - `${this.apiUrl}/api/v1/pairs/list`, - z.array(z.string()), - ); + getPairsList = (market: 'spot' | 'futures') => { + const url = new URL(`${this.apiUrl}/api/v1/pairs/list`); + url.searchParams.append('market', toUpperCase(market)); + + return fetchWithValidation( + url.toString(), + z.array(z.string()), + ); + }; getAggregatedOrderbook = (pair: string, depth = 20) => { const url = new URL(`${this.apiUrl}/api/v1/orderbook`); @@ -78,6 +85,18 @@ class OrionAggregator { ); }; + getPairConfigs = (market: 'spot' | 'futures') => { + const url = new URL(`${this.apiUrl}/api/v1/pairs/exchangeInfo`); + url.searchParams.append('market', toUpperCase(market)); + + return fetchWithValidation( + url.toString(), + exchangeInfoSchema, + undefined, + errorSchema, + ); + } + getPoolReserves = ( pair: string, exchange: Exchange, @@ -91,13 +110,6 @@ class OrionAggregator { ); }; - getPairConfigs = () => fetchWithValidation( - `${this.apiUrl}/api/v1/pairs/exchangeInfo`, - exchangeInfoSchema, - undefined, - errorSchema, - ); - getPairConfig = (assetPair: string) => fetchWithValidation( `${this.apiUrl}/api/v1/pairs/exchangeInfo/${assetPair}`, pairConfigSchema, @@ -161,6 +173,35 @@ class OrionAggregator { errorSchema, ); + placeCFDOrder = ( + signedOrder: SignedCFDOrder + ) => { + const headers = { + 'Content-Type': 'application/json', + Accept: 'application/json', + }; + + return fetchWithValidation( + `${this.apiUrl}/api/v1/order/futures`, + z.object({ + orderId: z.string(), + placementRequests: z.array( + z.object({ + amount: z.number(), + brokerAddress: z.string(), + exchange: z.string(), + }), + ).optional(), + }), + { + headers, + method: 'POST', + body: JSON.stringify(signedOrder), + }, + errorSchema, + ); + }; + getSwapInfo = ( type: 'exactSpend' | 'exactReceive', assetIn: string, diff --git a/src/services/OrionAggregator/ws/MessageType.ts b/src/services/OrionAggregator/ws/MessageType.ts index fe4bc7f..323e836 100644 --- a/src/services/OrionAggregator/ws/MessageType.ts +++ b/src/services/OrionAggregator/ws/MessageType.ts @@ -7,6 +7,7 @@ const MessageType = { ASSET_PAIRS_CONFIG_UPDATE: 'apcu', ASSET_PAIR_CONFIG_UPDATE: 'apiu', ADDRESS_UPDATE: 'au', + CFD_ADDRESS_UPDATE: 'auf', BROKER_TRADABLE_ATOMIC_SWAP_ASSETS_BALANCE_UPDATE: 'btasabu', UNSUBSCRIPTION_DONE: 'ud', } as const; diff --git a/src/services/OrionAggregator/ws/SubscriptionType.ts b/src/services/OrionAggregator/ws/SubscriptionType.ts index 66b0c0c..741dad1 100644 --- a/src/services/OrionAggregator/ws/SubscriptionType.ts +++ b/src/services/OrionAggregator/ws/SubscriptionType.ts @@ -3,6 +3,7 @@ const SubscriptionType = { ASSET_PAIR_CONFIG_UPDATES_SUBSCRIBE: 'apius', AGGREGATED_ORDER_BOOK_UPDATES_SUBSCRIBE: 'aobus', ADDRESS_UPDATES_SUBSCRIBE: 'aus', + CFD_ADDRESS_UPDATES_SUBSCRIBE: 'ausf', BROKER_TRADABLE_ATOMIC_SWAP_ASSETS_BALANCE_UPDATES_SUBSCRIBE: 'btasabus', SWAP_SUBSCRIBE: 'ss', } as const; diff --git a/src/services/OrionAggregator/ws/index.ts b/src/services/OrionAggregator/ws/index.ts index e89e4b4..fc15efb 100644 --- a/src/services/OrionAggregator/ws/index.ts +++ b/src/services/OrionAggregator/ws/index.ts @@ -11,11 +11,12 @@ import { import UnsubscriptionType from './UnsubscriptionType'; import { SwapInfoByAmountIn, SwapInfoByAmountOut, SwapInfoBase, - AssetPairUpdate, OrderbookItem, Balance, Exchange, + AssetPairUpdate, OrderbookItem, Balance, Exchange, CFDBalance, } from '../../../types'; import unsubscriptionDoneSchema from './schemas/unsubscriptionDoneSchema'; import assetPairConfigSchema from './schemas/assetPairConfigSchema'; import { fullOrderSchema, orderUpdateSchema } from './schemas/addressUpdateSchema'; +import cfdAddressUpdateSchema from './schemas/cfdAddressUpdateSchema'; // import errorSchema from './schemas/errorSchema'; const UNSUBSCRIBE = 'u'; @@ -86,13 +87,31 @@ type AddressUpdateInitial = { orders?: z.infer[] // The field is not defined if the user has no orders } +type CfdAddressUpdateUpdate = { + kind: 'update', + balances?: CFDBalance[], + order?: z.infer | z.infer +} + +type CfdAddressUpdateInitial = { + kind: 'initial', + balances: CFDBalance[], + orders?: z.infer[] // The field is not defined if the user has no orders +} + type AddressUpdateSubscription = { payload: string, callback: (data: AddressUpdateUpdate | AddressUpdateInitial) => void, } +type CfdAddressUpdateSubscription = { + payload: string, + callback: (data: CfdAddressUpdateUpdate | CfdAddressUpdateInitial) => void, +} + type Subscription = { [SubscriptionType.ADDRESS_UPDATES_SUBSCRIBE]: AddressUpdateSubscription, + [SubscriptionType.CFD_ADDRESS_UPDATES_SUBSCRIBE]: CfdAddressUpdateSubscription, [SubscriptionType.AGGREGATED_ORDER_BOOK_UPDATES_SUBSCRIBE]: AggregatedOrderbookSubscription, [SubscriptionType.ASSET_PAIRS_CONFIG_UPDATES_SUBSCRIBE]: PairsConfigSubscription, [SubscriptionType.ASSET_PAIR_CONFIG_UPDATES_SUBSCRIBE]: PairConfigSubscription, @@ -197,10 +216,11 @@ class OrionAggregatorWS { return id; } - unsubscribe(subscription: keyof typeof UnsubscriptionType | string) { + unsubscribe(subscription: keyof typeof UnsubscriptionType | string, details?: string) { this.send({ T: UNSUBSCRIBE, S: subscription, + d: details, }); if (subscription.includes('0x')) { // is wallet address (ADDRESS_UPDATE) @@ -212,6 +232,15 @@ class OrionAggregatorWS { delete this.subscriptions[SubscriptionType.ADDRESS_UPDATES_SUBSCRIBE]?.[key]; } } + + const aufSubscriptions = this.subscriptions[SubscriptionType.CFD_ADDRESS_UPDATES_SUBSCRIBE]; + if (aufSubscriptions) { + const targetAufSub = Object.entries(aufSubscriptions).find(([, value]) => value?.payload === subscription); + if (targetAufSub) { + const [key] = targetAufSub; + delete this.subscriptions[SubscriptionType.CFD_ADDRESS_UPDATES_SUBSCRIBE]?.[key]; + } + } } else if (uuidValidate(subscription)) { // is swap info subscription (contains hyphen) delete this.subscriptions[SubscriptionType.SWAP_SUBSCRIBE]?.[subscription]; @@ -277,6 +306,7 @@ class OrionAggregatorWS { initMessageSchema, pingPongMessageSchema, addressUpdateSchema, + cfdAddressUpdateSchema, assetPairsConfigSchema, assetPairConfigSchema, brokerMessageSchema, @@ -425,6 +455,47 @@ class OrionAggregatorWS { }); } break; + case MessageType.CFD_ADDRESS_UPDATE: { + switch (json.k) { // message kind + case 'i': { // initial + const fullOrders = json.o + ? json.o.reduce[]>((prev, o) => { + prev.push(o); + + return prev; + }, []) + : undefined; + + this.subscriptions[ + SubscriptionType.CFD_ADDRESS_UPDATES_SUBSCRIBE + ]?.[json.id]?.callback({ + kind: 'initial', + orders: fullOrders, + balances: json.b ?? [], + }); + } + break; + case 'u': { // update + let orderUpdate: z.infer | z.infer | undefined; + if (json.o) { + const firstOrder = json.o[0]; + orderUpdate = firstOrder; + } + + this.subscriptions[ + SubscriptionType.CFD_ADDRESS_UPDATES_SUBSCRIBE + ]?.[json.id]?.callback({ + kind: 'update', + order: orderUpdate, + balances: json.b, + }); + } + break; + default: + break; + } + } + break; case MessageType.ADDRESS_UPDATE: { const balances = json.b ? Object.entries(json.b) diff --git a/src/services/OrionAggregator/ws/schemas/cfdAddressUpdateSchema.ts b/src/services/OrionAggregator/ws/schemas/cfdAddressUpdateSchema.ts new file mode 100644 index 0000000..410e251 --- /dev/null +++ b/src/services/OrionAggregator/ws/schemas/cfdAddressUpdateSchema.ts @@ -0,0 +1,33 @@ +import { z } from "zod"; +import { fullOrderSchema, orderUpdateSchema } from "./addressUpdateSchema"; +import baseMessageSchema from "./baseMessageSchema"; +import MessageType from "../MessageType"; +import cfdBalancesSchema from "./cfdBalancesSchema"; + +const baseCfdAddressUpdate = baseMessageSchema.extend({ + id: z.string(), + T: z.literal(MessageType.CFD_ADDRESS_UPDATE), + S: z.string(), // subscription + uc: z.array(z.enum(['b', 'o'])), // update content +}); + +const updateMessageSchema = baseCfdAddressUpdate.extend({ + k: z.literal('u'), // kind of message: "u" - updates + uc: z.array(z.enum(['b', 'o'])), // update content: "o" - orders updates, "b" - balance updates + b: cfdBalancesSchema.optional(), + o: z.tuple([fullOrderSchema.or(orderUpdateSchema)]).optional(), +}); + +const initialMessageSchema = baseCfdAddressUpdate.extend({ + k: z.literal('i'), // kind of message: "i" - initial + b: cfdBalancesSchema, + o: z.array(fullOrderSchema) + .optional(), // When no orders — no field +}); + +const cfdAddressUpdateSchema = z.union([ + initialMessageSchema, + updateMessageSchema, +]); + +export default cfdAddressUpdateSchema diff --git a/src/services/OrionAggregator/ws/schemas/cfdBalancesSchema.ts b/src/services/OrionAggregator/ws/schemas/cfdBalancesSchema.ts new file mode 100644 index 0000000..c8c228c --- /dev/null +++ b/src/services/OrionAggregator/ws/schemas/cfdBalancesSchema.ts @@ -0,0 +1,46 @@ +import { z } from 'zod'; +import positionStatuses from "../../../../constants/positionStatuses"; + +const cfdBalanceSchema = z + .object({ + i: z.string(), + b: z.string(), + pnl: z.string(), + fr: z.string(), + e: z.string(), + p: z.string(), + cp: z.string(), + pp: z.string(), + r: z.string(), + m: z.string(), + mu: z.string(), + fmu: z.string(), + awb: z.string(), + mli: z.string(), + msi: z.string(), + l: z.string(), + s: z.enum(positionStatuses), + }) + .transform((obj) => ({ + instrument: obj.i, + balance: obj.b, + profitLoss: obj.pnl, + fundingRate: obj.fr, + equity: obj.e, + position: obj.p, + currentPrice: obj.cp, + positionPrice: obj.pp, + reserves: obj.r, + margin: obj.m, + marginUSD: obj.mu, + freeMarginUSD: obj.fmu, + availableWithdrawBalance: obj.awb, + maxAvailableLong: obj.mli, + maxAvailableShort: obj.msi, + leverage: obj.l, + status: obj.s, + })); + +const cfdBalancesSchema = z.array(cfdBalanceSchema); + +export default cfdBalancesSchema; diff --git a/src/services/OrionAggregator/ws/schemas/index.ts b/src/services/OrionAggregator/ws/schemas/index.ts index a0ca21d..36743dc 100644 --- a/src/services/OrionAggregator/ws/schemas/index.ts +++ b/src/services/OrionAggregator/ws/schemas/index.ts @@ -7,5 +7,6 @@ export { default as initMessageSchema } from './initMessageSchema'; export { default as pingPongMessageSchema } from './pingPongMessageSchema'; export { default as swapInfoSchema } from './swapInfoSchema'; export { default as balancesSchema } from './balancesSchema'; +export { default as cfdBalancesSchema } from './cfdBalancesSchema'; export * from './orderBookSchema'; diff --git a/src/services/OrionBlockchain/index.ts b/src/services/OrionBlockchain/index.ts index 88fa473..0f2be8f 100644 --- a/src/services/OrionBlockchain/index.ts +++ b/src/services/OrionBlockchain/index.ts @@ -10,6 +10,8 @@ import { userEarnedSchema, PairStatusEnum, pairStatusSchema, + cfdContractsSchema, + cfdHistorySchema, } from './schemas'; import redeemOrderSchema from '../OrionAggregator/schemas/redeemOrderSchema'; import { sourceAtomicHistorySchema, targetAtomicHistorySchema } from './schemas/atomicHistorySchema'; @@ -52,6 +54,12 @@ type AtomicSwapHistoryTargetQuery = AtomicSwapHistoryBaseQuery & { expiredRedeem?: 0 | 1, state?: 'REDEEMED' | 'BEFORE-REDEEM', } + +type CfdHistoryQuery = { + instrument?: string, + page?: number, + limit?: number, +} class OrionBlockchain { private readonly apiUrl: string; @@ -90,6 +98,8 @@ class OrionBlockchain { this.getBlockNumber = this.getBlockNumber.bind(this); this.getRedeemOrderBySecretHash = this.getRedeemOrderBySecretHash.bind(this); this.claimOrder = this.claimOrder.bind(this); + this.getCFDContracts = this.getCFDContracts.bind(this); + this.getCFDHistory = this.getCFDHistory.bind(this); } get orionBlockchainWsUrl() { @@ -175,6 +185,11 @@ class OrionBlockchain { z.record(z.string()).transform(makePartial), ); + getCFDPrices = () => fetchWithValidation( + `${this.apiUrl}/api/cfd/prices`, + z.record(z.string()).transform(makePartial), + ); + getTokensFee = () => fetchWithValidation( `${this.apiUrl}/api/tokensFee`, z.record(z.string()).transform(makePartial), @@ -377,6 +392,20 @@ class OrionBlockchain { body: JSON.stringify(secretHashes), }, ); + + getCFDContracts = () => fetchWithValidation( + `${this.apiUrl}/api/cfd/contracts`, + cfdContractsSchema, + ); + + getCFDHistory = (address: string, query: CfdHistoryQuery = {}) => { + const url = new URL(`${this.apiUrl}/api/cfd/deposit-withdraw/${address}`); + + Object.entries(query) + .forEach(([key, value]) => url.searchParams.append(key, value.toString())); + + return fetchWithValidation(url.toString(), cfdHistorySchema); + }; } export * as schemas from './schemas'; diff --git a/src/services/OrionBlockchain/schemas/cfdContractsSchema.ts b/src/services/OrionBlockchain/schemas/cfdContractsSchema.ts new file mode 100644 index 0000000..975f27f --- /dev/null +++ b/src/services/OrionBlockchain/schemas/cfdContractsSchema.ts @@ -0,0 +1,19 @@ +import { z } from 'zod'; + +const cfdContractsSchema = z.array(z.object({ + name: z.string(), + alias: z.string(), + address: z.string(), + leverage: z.number(), + soLevel: z.number(), + shortFR: z.number(), + longFR: z.number(), + shortFRStored: z.number(), + longFRStored: z.number(), + lastFRPriceUpdateTime: z.number(), + priceIndex: z.number(), + feePercent: z.number(), + withdrawMarginLevel: z.number(), +})); + +export default cfdContractsSchema; diff --git a/src/services/OrionBlockchain/schemas/cfdHistorySchema.ts b/src/services/OrionBlockchain/schemas/cfdHistorySchema.ts new file mode 100644 index 0000000..2391fe5 --- /dev/null +++ b/src/services/OrionBlockchain/schemas/cfdHistorySchema.ts @@ -0,0 +1,52 @@ +import { z } from 'zod'; +import { HistoryTransactionStatus } from '../../../types'; + +export enum historyTransactionType { + WITHDRAW = 'withdrawal', + DEPOSIT = 'deposit', +} + +const cfdHistoryItem = z.object({ + _id: z.string(), + __v: z.number(), + address: z.string(), + instrument: z.string(), + instrumentAddress: z.string(), + balance: z.string(), + amount: z.string(), + amountNumber: z.string(), + position: z.string(), + reason: z.enum(['WITHDRAW', 'DEPOSIT']), + positionPrice: z.string(), + fundingRate: z.string(), + transactionHash: z.string(), + blockNumber: z.number(), + createdAt: z.number(), +}); + +const cfdHistorySchema = z.object({ + success: z.boolean(), + count: z.number(), + total: z.number(), + pagination: z.object({}), + data: z.array(cfdHistoryItem), +}).transform((response) => { + return response.data.map((item) => { + const { + createdAt, reason, transactionHash, amountNumber, + } = item; + const type = historyTransactionType[reason]; + + return { + type, + date: createdAt, + token: 'USDT', + amount: amountNumber, + status: HistoryTransactionStatus.DONE, + transactionHash, + user: item.address, + }; + }); +}); + +export default cfdHistorySchema; diff --git a/src/services/OrionBlockchain/schemas/historySchema.ts b/src/services/OrionBlockchain/schemas/historySchema.ts index aa9cdd8..9aba65a 100644 --- a/src/services/OrionBlockchain/schemas/historySchema.ts +++ b/src/services/OrionBlockchain/schemas/historySchema.ts @@ -1,4 +1,5 @@ import { z } from 'zod'; +import { HistoryTransactionStatus } from '../../../types'; const historySchema = z.array(z.object( { @@ -13,6 +14,21 @@ const historySchema = z.array(z.object( user: z.string(), walletBalance: z.string().nullable().optional(), }, -)); +)).transform((response) => { + return response.map((item) => { + const { + type, createdAt, transactionHash, user, + } = item; + return { + type, + date: createdAt * 1000, + token: item.asset, + amount: item.amountNumber, + status: HistoryTransactionStatus.DONE, + transactionHash, + user, + }; + }); +}); export default historySchema; diff --git a/src/services/OrionBlockchain/schemas/index.ts b/src/services/OrionBlockchain/schemas/index.ts index 939bcf9..570f71a 100644 --- a/src/services/OrionBlockchain/schemas/index.ts +++ b/src/services/OrionBlockchain/schemas/index.ts @@ -12,3 +12,5 @@ export { default as atomicSummarySchema } from './atomicSummarySchema'; export { default as poolsLpAndStakedSchema } from './poolsLpAndStakedSchema'; export { default as userVotesSchema } from './userVotesSchema'; export { default as userEarnedSchema } from './userEarnedSchema'; +export { default as cfdContractsSchema } from './cfdContractsSchema'; +export { default as cfdHistorySchema } from './cfdHistorySchema'; diff --git a/src/types.ts b/src/types.ts index f0dd6d2..e46ecc4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -25,6 +25,27 @@ export type Balance = { wallet: string, allowance: string, } + +export type CFDBalance = { + instrument: string, + balance: string, + profitLoss: string, + fundingRate: string, + equity: string, + position: string, + currentPrice: string, + positionPrice: string, + reserves: string, + margin: string, + marginUSD: string, + freeMarginUSD: string, + availableWithdrawBalance: string, + maxAvailableLong: string, + maxAvailableShort: string, + leverage: string, + status: PositionStatus, +} + export interface Order { senderAddress: string; // address matcherAddress: string; // address @@ -39,6 +60,25 @@ export interface Order { buySide: number; // uint8, 1=buy, 0=sell isPersonalSign: boolean; // bool } + +export interface CFDOrder { + senderAddress: string; // address + matcherAddress: string; // address + instrumentAddress: string; // address + amount: number; // uint64 + price: number; // uint64 + matcherFee: number; // uint64 + nonce: number; // uint64 + expiration: number; // uint64 + buySide: number; // uint8, 1=buy, 0=sell + isPersonalSign: boolean; // bool +} + +export interface SignedCFDOrder extends CFDOrder { + id: string; // hash of Order (it's not part of order structure in smart-contract) + signature: string; // bytes +} + export interface SignedOrder extends Order { id: string; // hash of Order (it's not part of order structure in smart-contract) signature: string; // bytes @@ -188,3 +228,12 @@ export type SwapInfoByAmountOut = SwapInfoBase & { } export type SwapInfo = SwapInfoByAmountIn | SwapInfoByAmountOut; + +export enum HistoryTransactionStatus { + PENDING = 'Pending', + DONE = 'Done', + APPROVING = 'Approving', + CANCELLED = 'Cancelled', +} + +export type PositionStatus = 'SHORT' | 'LONG' | 'CLOSED' | 'LIQUIDATED' | 'NOT_OPEN'; diff --git a/src/utils/typeHelpers.ts b/src/utils/typeHelpers.ts index eb901b7..a65cbb8 100644 --- a/src/utils/typeHelpers.ts +++ b/src/utils/typeHelpers.ts @@ -3,7 +3,7 @@ type WithReason = { } type WithCodeError = Error & { - code: number; + code: number | string; } type WithMessage = { @@ -41,9 +41,9 @@ export function hasProp, K extends PropertyKey } export function isWithCode(candidate: unknown): candidate is WithCodeError { - if (!isUnknownObject(candidate)) return false; - const hasCodeProperty = hasProp(candidate, 'code') && typeof candidate.code === 'number'; - return hasCodeProperty; + if (!isUnknownObject(candidate) || !hasProp(candidate, 'code')) return false; + const type = typeof candidate.code; + return type === 'number' || type === 'string'; } export function isWithReason(candidate: unknown): candidate is WithReason {