mirror of
https://github.com/orionprotocol/sdk.git
synced 2026-03-14 06:02:36 +03:00
WS subscribtion for CFD balances, CFD Order signing functionality
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@orionprotocol/sdk",
|
||||
"version": "0.16.0-rc.5",
|
||||
"version": "0.16.0-rc.6",
|
||||
"description": "Orion Protocol SDK",
|
||||
"main": "./lib/esm/index.js",
|
||||
"module": "./lib/esm/index.js",
|
||||
|
||||
15
src/constants/cfdOrderTypes.ts
Normal file
15
src/constants/cfdOrderTypes.ts
Normal file
@@ -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;
|
||||
31
src/crypt/hashCFDOrder.ts
Normal file
31
src/crypt/hashCFDOrder.ts
Normal file
@@ -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;
|
||||
@@ -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';
|
||||
|
||||
82
src/crypt/signCFDOrder.ts
Normal file
82
src/crypt/signCFDOrder.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
/* 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;
|
||||
30
src/crypt/signCFDOrderPersonal.ts
Normal file
30
src/crypt/signCFDOrderPersonal.ts
Normal file
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -11,10 +11,11 @@ import {
|
||||
import UnsubscriptionType from './UnsubscriptionType';
|
||||
import {
|
||||
SwapInfoByAmountIn, SwapInfoByAmountOut, SwapInfoBase,
|
||||
FullOrder, OrderUpdate, AssetPairUpdate, OrderbookItem, Balance, Exchange,
|
||||
FullOrder, OrderUpdate, AssetPairUpdate, OrderbookItem, Balance, Exchange, CFDBalance,
|
||||
} from '../../../types';
|
||||
import unsubscriptionDoneSchema from './schemas/unsubscriptionDoneSchema';
|
||||
import assetPairConfigSchema from './schemas/assetPairConfigSchema';
|
||||
import cfdAddressUpdateSchema from "./schemas/cfdAddressUpdateSchema";
|
||||
// import errorSchema from './schemas/errorSchema';
|
||||
|
||||
const UNSUBSCRIBE = 'u';
|
||||
@@ -85,13 +86,31 @@ type AddressUpdateInitial = {
|
||||
orders?: FullOrder[] // The field is not defined if the user has no orders
|
||||
}
|
||||
|
||||
type CfdAddressUpdateUpdate = {
|
||||
kind: 'update',
|
||||
balances: CFDBalance[],
|
||||
order?: OrderUpdate | FullOrder
|
||||
}
|
||||
|
||||
type CfdAddressUpdateInitial = {
|
||||
kind: 'initial',
|
||||
balances: CFDBalance[],
|
||||
orders?: FullOrder[] // 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,
|
||||
@@ -211,6 +230,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];
|
||||
@@ -276,6 +304,7 @@ class OrionAggregatorWS {
|
||||
initMessageSchema,
|
||||
pingPongMessageSchema,
|
||||
addressUpdateSchema,
|
||||
cfdAddressUpdateSchema,
|
||||
assetPairsConfigSchema,
|
||||
assetPairConfigSchema,
|
||||
brokerMessageSchema,
|
||||
@@ -415,6 +444,48 @@ class OrionAggregatorWS {
|
||||
});
|
||||
}
|
||||
break;
|
||||
case MessageType.CFD_ADDRESS_UPDATE: {
|
||||
const balances = json.b ?? []
|
||||
switch (json.k) { // message kind
|
||||
case 'i': { // initial
|
||||
const fullOrders = json.o
|
||||
? json.o.reduce<FullOrder[]>((prev, o) => {
|
||||
prev.push(o);
|
||||
|
||||
return prev;
|
||||
}, [])
|
||||
: undefined;
|
||||
|
||||
this.subscriptions[
|
||||
SubscriptionType.CFD_ADDRESS_UPDATES_SUBSCRIBE
|
||||
]?.[json.id]?.callback({
|
||||
kind: 'initial',
|
||||
orders: fullOrders,
|
||||
balances,
|
||||
});
|
||||
}
|
||||
break;
|
||||
case 'u': { // update
|
||||
let orderUpdate: OrderUpdate | FullOrder | 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,
|
||||
});
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case MessageType.ADDRESS_UPDATE: {
|
||||
const balances = json.b
|
||||
? Object.entries(json.b)
|
||||
|
||||
@@ -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
|
||||
24
src/services/OrionAggregator/ws/schemas/cfdBalancesSchema.ts
Normal file
24
src/services/OrionAggregator/ws/schemas/cfdBalancesSchema.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
const cfdBalanceSchema = z.object({
|
||||
i: z.string(),
|
||||
b: z.number(),
|
||||
p: z.number(),
|
||||
pp: z.number(),
|
||||
fr: z.number(),
|
||||
sfrl: z.number(),
|
||||
lfrl: z.number(),
|
||||
})
|
||||
.transform((obj) => ({
|
||||
instrument: obj.i,
|
||||
balance: obj.b,
|
||||
position: obj.p,
|
||||
positionPrice: obj.pp,
|
||||
fundingRate: obj.fr,
|
||||
lastShortFundingRate: obj.sfrl,
|
||||
lastLongFundingRate: obj.lfrl,
|
||||
}));
|
||||
|
||||
const cfdBalancesSchema = z.array(cfdBalanceSchema)
|
||||
|
||||
export default cfdBalancesSchema;
|
||||
30
src/types.ts
30
src/types.ts
@@ -40,6 +40,17 @@ export type Balance = {
|
||||
wallet: string,
|
||||
allowance: string,
|
||||
}
|
||||
|
||||
export type CFDBalance = {
|
||||
instrument: string,
|
||||
balance: number,
|
||||
position: number,
|
||||
positionPrice: number,
|
||||
fundingRate: number,
|
||||
lastShortFundingRate: number,
|
||||
lastLongFundingRate: number,
|
||||
}
|
||||
|
||||
export interface Order {
|
||||
senderAddress: string; // address
|
||||
matcherAddress: string; // address
|
||||
@@ -54,6 +65,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
|
||||
|
||||
Reference in New Issue
Block a user