diff --git a/package-lock.json b/package-lock.json index 230235f..a06af06 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@orionprotocol/sdk", - "version": "0.20.42-rc3", + "version": "0.20.42-rc4", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@orionprotocol/sdk", - "version": "0.20.42-rc3", + "version": "0.20.42-rc4", "hasInstallScript": true, "license": "ISC", "dependencies": { diff --git a/package.json b/package.json index 89b3910..6ce7550 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@orionprotocol/sdk", - "version": "0.20.42-rc3", + "version": "0.20.42-rc4", "description": "Orion Protocol SDK", "main": "./lib/index.cjs", "module": "./lib/index.js", diff --git a/src/Unit/Exchange/swapMarket.ts b/src/Unit/Exchange/swapMarket.ts index c4c0b05..be7350a 100644 --- a/src/Unit/Exchange/swapMarket.ts +++ b/src/Unit/Exchange/swapMarket.ts @@ -4,7 +4,7 @@ import { Exchange__factory } from '@orionprotocol/contracts/lib/ethers-v6/index. import getBalances from '../../utils/getBalances.js'; import BalanceGuard from '../../BalanceGuard.js'; import getAvailableSources from '../../utils/getAvailableFundsSources.js'; -import { INTERNAL_PROTOCOL_PRECISION, NATIVE_CURRENCY_PRECISION, SWAP_THROUGH_ORION_POOL_GAS_LIMIT } from '../../constants/index.js'; +import { INTERNAL_PROTOCOL_PRECISION, NATIVE_CURRENCY_PRECISION, SWAP_THROUGH_ORION_POOL_GAS_LIMIT } from '../../constants'; import getNativeCryptocurrencyName from '../../utils/getNativeCryptocurrencyName.js'; import { calculateFeeInFeeAsset, denormalizeNumber, normalizeNumber } from '../../utils/index.js'; import { signOrder } from '../../crypt/index.js'; @@ -37,7 +37,6 @@ type PoolSwap = { export type Swap = AggregatorOrder | PoolSwap; - const isValidSingleSwap = (singleSwap: Omit & { factory: string }): singleSwap is SingleSwap => { return isValidFactory(singleSwap.factory); } diff --git a/src/crypt/hashOrder.ts b/src/crypt/hashOrder.ts index 23bc474..8a5c5d2 100644 --- a/src/crypt/hashOrder.ts +++ b/src/crypt/hashOrder.ts @@ -14,6 +14,8 @@ const hashOrder = (order: Order) => ethers.solidityPackedKeccak256( 'uint64', 'uint64', 'uint64', + 'uint64', + 'uint64', 'uint8', ], [ @@ -24,10 +26,12 @@ const hashOrder = (order: Order) => ethers.solidityPackedKeccak256( order.quoteAsset, order.matcherFeeAsset, order.amount, + order.targetChainId, order.price, order.matcherFee, order.nonce, order.expiration, + order.secretHash, order.buySide === 1 ? '0x01' : '0x00', ], ); diff --git a/src/crypt/signLockOrder.ts b/src/crypt/signLockOrder.ts new file mode 100644 index 0000000..b70c3ea --- /dev/null +++ b/src/crypt/signLockOrder.ts @@ -0,0 +1,71 @@ +import type { TypedDataSigner } from '@ethersproject/abstract-signer'; +import { ethers } from 'ethers'; +import ORDER_TYPES from '../constants/orderTypes.js'; +import type { LockOrder, SignedLockOrder, SupportedChainId } from '../types.js'; +import getDomainData from './getDomainData.js'; +import generateSecret from '../utils/generateSecret'; + +const DEFAULT_EXPIRATION = 29 * 24 * 60 * 60 * 1000; // 29 days + +type SignerWithTypedDataSign = ethers.Signer & TypedDataSigner; + +export type LockOrderProps = { + userAddress: string // адрес юзера который хочет сделать лок + senderAddress: string // broker + asset: string + amount: ethers.BigNumberish + targetChainId: ethers.BigNumberish + sign: string // подпись юзера + signer: ethers.Signer + chainId: SupportedChainId +} + +export const signLockOrder = async ({ + userAddress, + senderAddress, + sign, + amount, + targetChainId, + asset, + chainId, + signer +}: LockOrderProps) => { + const nonce = Date.now(); + const expiration = nonce + DEFAULT_EXPIRATION; + const secret = generateSecret(); + const secretHash = ethers.keccak256(secret); + + const order: LockOrder = { + user: userAddress, + sender: senderAddress, + expiration, + asset, + amount, + targetChainId, + secretHash, + sign + }; + + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + const typedDataSigner = signer as SignerWithTypedDataSign; + + const signature = await typedDataSigner.signTypedData( + getDomainData(chainId), + 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 = ethers.Signature.from(signature).serialized; + + // if (!fixedSignature) throw new Error("Can't sign order"); + + const signedOrder: SignedLockOrder = { + ...order, + signature: fixedSignature, + secret + }; + + return signedOrder; +}; diff --git a/src/crypt/signOrder.ts b/src/crypt/signOrder.ts index fca41a7..7008935 100644 --- a/src/crypt/signOrder.ts +++ b/src/crypt/signOrder.ts @@ -1,12 +1,13 @@ import type { TypedDataSigner } from '@ethersproject/abstract-signer'; import { BigNumber } from 'bignumber.js'; import { ethers } from 'ethers'; -import { INTERNAL_PROTOCOL_PRECISION } from '../constants/index.js'; +import { INTERNAL_PROTOCOL_PRECISION } from '../constants'; import ORDER_TYPES from '../constants/orderTypes.js'; import type { Order, SignedOrder, SupportedChainId } from '../types.js'; import normalizeNumber from '../utils/normalizeNumber.js'; import getDomainData from './getDomainData.js'; import hashOrder from './hashOrder.js'; +import generateSecret from '../utils/generateSecret'; const DEFAULT_EXPIRATION = 29 * 24 * 60 * 60 * 1000; // 29 days @@ -24,9 +25,12 @@ export const signOrder = async ( serviceFeeAssetAddr: string, signer: ethers.Signer, chainId: SupportedChainId, + targetChainId?: ethers.BigNumberish, ) => { const nonce = Date.now(); const expiration = nonce + DEFAULT_EXPIRATION; + const secret = generateSecret(); + const secretHash = ethers.keccak256(secret); const order: Order = { senderAddress, @@ -51,6 +55,8 @@ export const signOrder = async ( )), nonce, expiration, + secretHash, + targetChainId, buySide: side === 'BUY' ? 1 : 0, }; @@ -73,6 +79,7 @@ export const signOrder = async ( ...order, id: hashOrder(order), signature: fixedSignature, + secret }; return signedOrder; }; diff --git a/src/services/Aggregator/index.ts b/src/services/Aggregator/index.ts index e38b024..019e7cc 100644 --- a/src/services/Aggregator/index.ts +++ b/src/services/Aggregator/index.ts @@ -8,12 +8,17 @@ import errorSchema from './schemas/errorSchema.js'; import placeAtomicSwapSchema from './schemas/placeAtomicSwapSchema.js'; import { AggregatorWS } from './ws'; import { atomicSwapHistorySchema } from './schemas/atomicSwapHistorySchema.js'; -import type { BasicAuthCredentials, SignedCancelOrderRequest, SignedOrder } from '../../types.js'; +import type { + BasicAuthCredentials, + NetworkShortName, + SignedCancelOrderRequest, + SignedLockOrder, + SignedOrder +} from '../../types.js'; import { pairConfigSchema, aggregatedOrderbookSchema, exchangeOrderbookSchema, poolReservesSchema, } from './schemas/index.js'; -import type networkCodes from '../../constants/networkCodes.js'; import toUpperCase from '../../utils/toUpperCase.js'; import httpToWS from '../../utils/httpToWS.js'; import { ethers } from 'ethers'; @@ -55,6 +60,7 @@ class Aggregator { this.getTradeProfits = this.getTradeProfits.bind(this); this.placeAtomicSwap = this.placeAtomicSwap.bind(this); this.placeOrder = this.placeOrder.bind(this); + this.placeLockOrder = this.placeLockOrder.bind(this); this.cancelOrder = this.cancelOrder.bind(this); this.checkWhitelisted = this.checkWhitelisted.bind(this); this.getLockedBalance = this.getLockedBalance.bind(this); @@ -236,6 +242,39 @@ class Aggregator { ); }; + placeLockOrder = ( + signedLockOrder: SignedLockOrder, + rawExchangeRestrictions?: string | undefined, + ) => { + const headers = { + 'Content-Type': 'application/json', + Accept: 'application/json', + ...this.basicAuthHeaders, + }; + + const url = new URL(`${this.apiUrl}/api/v1/cross-chain`); + + return fetchWithValidation( + url.toString(), + 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({ ...signedLockOrder, rawExchangeRestrictions }), + }, + errorSchema, + ); + }; + cancelOrder = (signedCancelOrderRequest: SignedCancelOrderRequest) => fetchWithValidation( `${this.apiUrl}/api/v1/order`, cancelOrderSchema, @@ -291,7 +330,7 @@ class Aggregator { ); }; - getCrossChainAssetsByNetwork = (sourceChain: Uppercase) => { + getCrossChainAssetsByNetwork = (sourceChain: NetworkShortName) => { const url = new URL(`${this.apiUrl}/api/v1/cross-chain/assets`); url.searchParams.append('sourceChain', sourceChain); @@ -354,7 +393,7 @@ class Aggregator { */ placeAtomicSwap = ( secretHash: string, - sourceNetworkCode: Uppercase, + sourceNetworkCode: NetworkShortName, ) => fetchWithValidation( `${this.apiUrl}/api/v1/atomic-swap`, placeAtomicSwapSchema, @@ -376,6 +415,7 @@ class Aggregator { /** * Get placed atomic swaps. Each atomic swap received from this list has a target chain corresponding to this Aggregator * @param sender Sender address + * @param limit * @returns Fetch promise */ getHistoryAtomicSwaps = (sender: string, limit = 1000) => { diff --git a/src/services/Aggregator/ws/index.ts b/src/services/Aggregator/ws/index.ts index 69d214c..2f63237 100644 --- a/src/services/Aggregator/ws/index.ts +++ b/src/services/Aggregator/ws/index.ts @@ -82,22 +82,22 @@ type SwapInfoSubscription = { type AddressUpdateUpdate = { kind: 'update' balances: Partial< - Record< - string, - Balance + Record< + string, + Balance + > > - > order?: z.infer | z.infer | undefined } type AddressUpdateInitial = { kind: 'initial' balances: Partial< - Record< - string, - Balance + Record< + string, + Balance + > > - > orders?: Array> | undefined // The field is not defined if the user has no orders } @@ -122,22 +122,22 @@ const exclusiveSubscriptions = [ ] as const; type BufferLike = - | string - | Buffer - | DataView - | number - | ArrayBufferView - | Uint8Array - | ArrayBuffer - | SharedArrayBuffer - | readonly unknown[] - | readonly number[] - | { valueOf: () => ArrayBuffer } - | { valueOf: () => SharedArrayBuffer } - | { valueOf: () => Uint8Array } - | { valueOf: () => readonly number[] } - | { valueOf: () => string } - | { [Symbol.toPrimitive]: (hint: string) => string }; + | string + | Buffer + | DataView + | number + | ArrayBufferView + | Uint8Array + | ArrayBuffer + | SharedArrayBuffer + | readonly unknown[] + | readonly number[] + | { valueOf: () => ArrayBuffer } + | { valueOf: () => SharedArrayBuffer } + | { valueOf: () => Uint8Array } + | { valueOf: () => readonly number[] } + | { valueOf: () => string } + | { [Symbol.toPrimitive]: (hint: string) => string }; const isSubType = (subType: string): subType is keyof Subscription => Object.values(SubscriptionType).some((t) => t === subType); @@ -201,6 +201,7 @@ class AggregatorWS { } private messageQueue: BufferLike[] = []; + private sendWsMessage(message: BufferLike) { if (this.ws?.readyState === WebSocket.OPEN) { this.ws.send(message); @@ -228,6 +229,7 @@ class AggregatorWS { } private hearbeatIntervalId: ReturnType | undefined; + private setupHeartbeat() { const heartbeat = () => { if (this.isAlive) { @@ -328,11 +330,11 @@ class AggregatorWS { } /** - * Returns newest subscription id for given id. Subscription id can be changed during resubscription. - * This function ensure that old subscription id will be replaced with newest one. - * @param id Id of subscription - * @returns Newest subscription id - */ + * Returns newest subscription id for given id. Subscription id can be changed during resubscription. + * This function ensure that old subscription id will be replaced with newest one. + * @param id Id of subscription + * @returns Newest subscription id + */ getNewestSubscriptionId(id: string): string { const newId = this.subIdReplacements[id]; if (newId !== undefined && newId !== id) { diff --git a/src/types.ts b/src/types.ts index 7511476..2fd40b0 100644 --- a/src/types.ts +++ b/src/types.ts @@ -3,8 +3,10 @@ import type factories from './constants/factories.js'; import type { BigNumber } from 'bignumber.js'; import type subOrderStatuses from './constants/subOrderStatuses.js'; import type positionStatuses from './constants/positionStatuses.js'; -import type { knownEnvs } from './config/schemas/index.js'; +import type { knownEnvs } from './config/schemas'; import type getHistory from './Orion/bridge/getHistory.js'; +import type { ethers } from 'ethers'; +import type { networkCodes } from './constants'; export type DeepPartial = T extends object ? { [P in keyof T]?: DeepPartial; @@ -47,14 +49,33 @@ export type Order = { matcherFee: number // uint64 nonce: number // uint64 expiration: number // uint64 + secretHash: string // uint64 buySide: 0 | 1 // uint8, 1=buy, 0=sell + targetChainId: ethers.BigNumberish | undefined // uint64 } -export type SignedOrder = { - id: string // hash of Order (it's not part of order structure in smart-contract) +export type LockOrder = { + user: string // address // адрес юзера который хочет сделать лок + sender: string // address // broker + expiration: ethers.BigNumberish // uint64 + asset: string // address(?) + amount: ethers.BigNumberish // uint64 + targetChainId: ethers.BigNumberish // uint64 + secretHash: string // uint64 + sign: string // uint64 // подпись юзера +} + +type SignedOrderAdditionalProps = { signature: string // bytes + secret: string needWithdraw?: boolean // bool (not supported yet by smart-contract) -} & Order +} + +export type SignedOrder = SignedOrderAdditionalProps & Order & { + id: string // hash of Order (it's not part of order structure in smart-contract) +} + +export type SignedLockOrder = SignedOrderAdditionalProps & LockOrder export type CancelOrderRequest = { id: number | string @@ -99,6 +120,8 @@ export enum SupportedChainId { // BROKEN = '0', } +export type NetworkShortName = Uppercase; + const balanceTypes = ['exchange', 'wallet'] as const; export type Source = typeof balanceTypes[number]; @@ -278,22 +301,22 @@ export type EnvConfig = { analyticsAPI: string referralAPI: string networks: Partial< - Record< - SupportedChainId, - VerboseUnitConfig + Record< + SupportedChainId, + VerboseUnitConfig + > > - > } export type AggregatedAssets = Partial< - Record< - string, - Partial< - Record + Record< + string, + Partial< + Record + > > - > - >; +>; export type RedeemOrder = { sender: string @@ -433,9 +456,9 @@ type BridgeHistory = Awaited>; type BridgeHistoryItem = NonNullable; export type AtomicSwap = Partial< - Omit + Omit > & Partial< - Omit + Omit > & { sourceChainId: SupportedChainId targetChainId: SupportedChainId diff --git a/src/utils/isUppercasedNetworkCode.ts b/src/utils/isUppercasedNetworkCode.ts index 978f1cd..2cf2055 100644 --- a/src/utils/isUppercasedNetworkCode.ts +++ b/src/utils/isUppercasedNetworkCode.ts @@ -1,7 +1,8 @@ import { networkCodes } from '../constants/index.js'; import toUpperCase from './toUpperCase.js'; +import type { NetworkShortName } from '../types'; -const isUppercasedNetworkCode = (value: string): value is Uppercase => networkCodes +const isUppercasedNetworkCode = (value: string): value is NetworkShortName => networkCodes .map(toUpperCase).some((networkCode) => networkCode === value); export default isUppercasedNetworkCode;