diff --git a/ADVANCED.md b/ADVANCED.md index f4b1267..d28f801 100644 --- a/ADVANCED.md +++ b/ADVANCED.md @@ -26,7 +26,7 @@ const orion = new Orion({ // Also you can set some config as default and override it for some params const orion = new Orion("testing", { - analyticsAPI: "https://asdasd.orionprotocol.io", + analyticsAPI: "https://analytics-api.orionprotocol.io", networks: { [SupportedChainId.BSC_TESTNET]: { nodeJsonRpc: "https://data-seed-prebsc-1-s1.binance.org:8545/", diff --git a/PMM-sample.md b/PMM-sample.md new file mode 100644 index 0000000..344788e --- /dev/null +++ b/PMM-sample.md @@ -0,0 +1,260 @@ + + +
+ Orion Protocol SDK logo +

PMM example

+
+ +## Overview + +This is special example which shows how to use PMM liquidity without using SDK, only using few libraries + +```ts +import {ethers, Wallet} from "ethers"; +import {z} from "zod"; +import hmacSHA256 from "crypto-js/hmac-sha256"; +import Hex from "crypto-js/enc-hex"; + +/////////////////////////////// +/////////////////////////////// +const pmmOrderQuotationSchema = z.object({ + info: z.string().default(''), + makerAsset: z.string().default(''), + takerAsset: z.string().default(''), + maker: z.string().default(''), + allowedSender: z.string().default(''), + makingAmount: z.string().default(''), + takingAmount: z.string().default(''), +}); + +const pmmOrderSchema = z.object({ + order: pmmOrderQuotationSchema.default({}), + signature: z.string().default(''), + success: z.boolean().default(false), + error: z.string().default(''), +}); + +const orionRFQContractABI = + [ + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "info", + "type": "uint256" + }, + { + "internalType": "address", + "name": "makerAsset", + "type": "address" + }, + { + "internalType": "address", + "name": "takerAsset", + "type": "address" + }, + { + "internalType": "address", + "name": "maker", + "type": "address" + }, + { + "internalType": "address", + "name": "allowedSender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "makingAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "takingAmount", + "type": "uint256" + } + ], + "internalType": "struct OrderRFQLib.OrderRFQ", + "name": "order", + "type": "tuple" + }, + { + "internalType": "bytes", + "name": "signature", + "type": "bytes" + }, + { + "internalType": "uint256", + "name": "flagsAndAmount", + "type": "uint256" + } + ], + "name": "fillOrderRFQ", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } + ]; + +function encode_utf8(s : string) { + return unescape(encodeURIComponent(s)); +} +function sign(message : string, key: string) { + return hmacSHA256( + encode_utf8(message), + encode_utf8(key) + ).toString(Hex); +} + +function generateHeaders(body : any, method : string, path : string, timestamp : number, apiKey : string, secretKey : string) { + const sortedBody = Object.keys(body) + .sort() + .map((key) => ( + `${key}=${body[key]}` + )).join('&'); + + const payload = timestamp + method.toUpperCase() + path + sortedBody; + + const signature = sign(payload, secretKey); + + const httpOptions = { + headers: { + 'API-KEY': apiKey, + 'ACCESS-TIMESTAMP': timestamp.toString(), + 'ACCESS-SIGN': signature + } + }; + return httpOptions; +} + +// +async function RFQOrder( + tokenFrom: string, + tokenTo: string, + fromTokenAmount: string, + apiKey: string, // + secretKey: string, + wallet: string +) : Promise> { + + // NB: replace with correct URL for networks + const apiUrl = 'https://testing.orion.xyz/bsc-testnet/backend' + // const apiUrl = 'https://trade.orion.xyz/bsc-mainnet/backend' + , path = '/rfq' + , url = `${apiUrl}/api/v1/integration/pmm`+path + , headers = { + 'Content-Type': 'application/json', + } + , data = { + "baseToken":tokenFrom, // USDT + "quoteToken":tokenTo, // ORN + "amount": fromTokenAmount, // 100 + "taker": wallet, + "feeBps": 0 + } + , method = 'POST' + , timestamp = Date.now() + , signatureHeaders = generateHeaders(data, method, path, timestamp, apiKey, secretKey) + , compiledHeaders = {...headers, ...signatureHeaders.headers, } + , body = JSON.stringify(data) + ; + + let res = pmmOrderSchema.parse({}); + + try { + const result = await fetch(url,{ + headers: compiledHeaders, + method, + body + }); + + // const data = await result.text(); + // console.log(data); + + const json = await result.json(); + console.log(json); + const parseResult = pmmOrderSchema.safeParse(json); + + if(!parseResult.success) { + // Try to parse error answer + const errorSchema = z.object({error: z.object({code: z.number(), reason: z.string()})}); + + const errorParseResult = errorSchema.safeParse(json); + + if(!errorParseResult.success) + throw Error(`Unrecognized answer from aggregator: ${json}`); + + throw Error(errorParseResult.data.error.reason); + } + + res.order = parseResult.data.order; + res.signature = parseResult.data.signature; + res.error = ''; + res.success = true; + } + catch(err) { + res.error = `${err}`; + } + return res; +} + + +(async() => { + const apiKey = '958153f1-b8b9-3ec4-84eb-2147429105d9'; + const secretKey = 'secretKey'; + + // BNB testnet tokens and router + const assetORN = '0xf223eca06261145b3287a0fefd8cfad371c7eb34'; + const assetUSDT = '0xcb2951e90d8dcf16e1fa84ac0c83f48906d6a744'; + const router = '0x89357522C0ed6E557D39dC75290859246077bdfC'; + + // BNB mainnet tokens and router + // const assetORN = '0xe4ca1f75eca6214393fce1c1b316c237664eaa8e'; + // const assetUSDT = '0x55d398326f99059ff775485246999027b3197955'; + // const router = '0xcb2D40EabC4f4c92Ee993Eb3D67f7717bE476E76'; + + const rfqOrder = await RFQOrder( + assetORN, // Spending asset + assetUSDT, // Receiving asset + '1000000000', // Amount in "satoshi" of spending asset + apiKey, + secretKey, + '0x61Eed69c0d112C690fD6f44bB621357B89fBE67F' // Can be any address, ignored for now + ); + + if(!rfqOrder.success) { + console.log(rfqOrder.error); + return; + } + + // ... here you can check order prices, etc. + console.log(rfqOrder); + + // Calldata according to provided rfqOrder + const contractInterface = new ethers.Interface(orionRFQContractABI); + const calldata = contractInterface.encodeFunctionData("fillOrderRFQ", [rfqOrder.order, rfqOrder.signature, 0]); + console.log('Calldata = ', calldata); + + // Call router with this data + // Replace with your private key + const yourWalletPrivateKey = '0xf1.......'; + + const tx = { + to: router, + data: calldata + } + + const provider = new ethers.JsonRpcProvider("https://data-seed-prebsc-1-s1.binance.org:8545/"); + const signer = new ethers.Wallet(yourWalletPrivateKey, provider); + console.log('Address = ', signer.address); + const transactionResponse = await signer.sendTransaction(tx); + console.log("Transaction hash:", transactionResponse.hash); + await transactionResponse.wait(); +})(); +``` \ No newline at end of file diff --git a/README.md b/README.md index 05438c5..0fec57d 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,8 @@ +[//]: # ( Orion Protocol SDK logo)
- Orion Protocol SDK logo -

Orion Protocol SDK

+

Lumia Stream SDK

Use CEX and DEX liquidity without KYC.

@@ -14,16 +10,15 @@ ![npm bundle size (version)](https://img.shields.io/bundlephobia/minzip/@orionprotocol/sdk) [![Downloads](https://img.shields.io/npm/dm/@orionprotocol/sdk.svg)](https://www.npmjs.com/package/@orionprotocol/sdk) -Do you want to integrate the Orion protocol into your application? See [integration guide](./docs/INTEGRATION.md) +Do you want to integrate the Lumia Stream protocol into your application? See [integration guide](./docs/INTEGRATION.md) ## Overview -Orion Software Developer Kit is a set of functions and methods that allow dApp developers connect to the superior aggregated liquidity of Orion Protocol which combines orderbooks of centralized exchanges as well decentralized automatic market makers such as Uniswap or Spookyswap across several supported blockchains. -Through this connection, developers using the SDK can perform a wide range of actions, including swapping selected tokens using Orion’s aggregated liquidity, obtaining relevant market information through subscriptions, add and remove liquidity to Orion’s pools. +Lumia Stream Developer Kit, natively built into Lumia, is a set of functions and methods that allow dApp developers to connect to the superior aggregated liquidity of Lumia Stream which combines orderbooks of centralized exchanges as well as decentralized Automatic Market Makers (AMMs) such as Uniswap, PancakeSwap, and Curve, across several supported blockchains. Through this connection, developers using the SDK can perform a wide range of actions, including swapping selected tokens, obtaining relevant market information through subscriptions, and more. ## API Key -Orion’s SDK is free to use and does not require an API key or registration. Refer to integration examples for more detailed information. +Lumia Stream’s SDK is free to use and does not require an API key or registration. Refer to integration examples for more detailed information. - [Overview](#overview) - [API Key](#api-key) @@ -33,7 +28,7 @@ Orion’s SDK is free to use and does not require an API key or registration. Re - [High level methods](#high-level-methods) - [Get assets](#get-assets) - [Get pairs](#get-pairs) - - [Get Orion Bridge history](#get-orion-bridge-history) + - [Get Lumia Stream Bridge history](#get-lumia-stream-bridge-history) - [Bridge swap](#bridge-swap) - [Withdraw](#withdraw) - [Deposit](#deposit) @@ -60,6 +55,7 @@ Orion’s SDK is free to use and does not require an API key or registration. Re - [Using contracts](#using-contracts) - [Utils](#utils) - [Parsing trade transactions](#parsing-trade-transactions) +- [PMM](#pmm) ## Install @@ -144,7 +140,7 @@ const pairs = await orion.getPairs("spot"); // 'spot' // } ``` -### Get Orion Bridge history +### Get Lumia Stream Bridge history ```ts const bridgeHistory = await orion.bridge.getHistory( @@ -719,3 +715,105 @@ switch (data.type) { break; } ``` +## PMM + +PMM allows institutional traders to request RFQ orders from Lumia Stream and then fill them. + +RFQ order allows trader to fix the price for a certain time interval (up to 90 seconds, including the order settlement time interval on blockchain). + +After receiving the order (if the price of the order is satisfactory to the trader) the trader must immediately submit the transaction on behalf of his address or contract. + +For requesting RFQ-orders institutional trader should have API key and secret key. + +Please take look at code example below. + +Simple example: + +```ts +// Node.js + +import { Orion } from '@orionprotocol/sdk' +import {Wallet} from "ethers"; + +(async() => { + const apiKey = '958153f1-b8b9-3ec4-84eb-2147429105d9'; + const secretKey = 'secretKey'; + const yourWalletPrivateKey = '0x...'; + + const orion = new Orion('testing'); // Leave empty for PROD environment + const bsc = orion.getUnit('bsc'); + const wallet = new Wallet(yourWalletPrivateKey, bsc.provider); + + // This can be done only once, no need to repeat this every time + // assetToDecimals can also be useful for calculations + // const {assetToAddress, assetToDecimals} = await bsc.blockchainService.getInfo(); + const info = await bsc.blockchainService.getInfo(); + + if(!info.isOk()) + return; + + const {assetToAddress, assetToDecimals} = info.value.data; + + // Also you need to allow FRQ contract to spend tokens from your address. + // This also can be done only once. + await bsc.pmm.setAllowance(assetToAddress.ORN, '1000000000000000000', wallet); + + // Just output the PMM router contract address + console.log('Router contract address: ', await bsc.pmm.getContractAddress()); + + const rfqOrder = await bsc.aggregator.RFQOrder( + assetToAddress.ORN, // Spending asset + assetToAddress.USDT, // Receiving asset + '1000000000', // Amount in "satoshi" of spending asset + apiKey, + secretKey, + '0x61Eed69c0d112C690fD6f44bB621357B89fBE67F' // Can be any address, ignored for now + ); + + if(!rfqOrder.success) { + console.log(rfqOrder.error); + return; + } + + // ... here you can check order prices, etc. + + // Send order to blockchain + try { + const tx = await bsc.pmm.fillRFQOrder(rfqOrder, wallet); + + // If tx.hash is not empty - then transaction was sent to blockchain + console.log(tx.hash); + } + catch(err) { + console.log(err); + } +})(); +``` + +RFQ order response example description (`rfqOrder` from example above): + +``` + { + quotation: { + info: '31545611720730315633520017429', + makerAsset: '0xcb2951e90d8dcf16e1fa84ac0c83f48906d6a744', + takerAsset: '0xf223eca06261145b3287a0fefd8cfad371c7eb34', + maker: '0x1ff516e5ce789085cff86d37fc27747df852a80a', + allowedSender: '0x0000000000000000000000000000000000000000', + makingAmount: '193596929', + takingAmount: '10000000000' + }, + signature: '0x8a2f9140a3c3a5734eda763a19c54c5ac909d8a03db37d9804af9115641fd1d35896b66ca6e136c1c89e0478fb7382a4b875d0f74529c1e83601f9383d310dde1b', + success: true, + error: '' + } +``` + + +* info - can be ignored +* makerAsset - your RECEIVING asset (what you expect to receive from contract, in this case USDT) +* takerAsset - your SPENDING asset (what you're giving to contract, in this case ORN) +* maker - can be ignored for now; +* allowedSender - can be ignored for now; +* makingAmount - how much you will RECEIVE (in receiving asset's precision) +* takingAmount - how much you should SPEND (in spending asset's precision) diff --git a/jest.config.js b/jest.config.js index 8fddee8..964bf83 100644 --- a/jest.config.js +++ b/jest.config.js @@ -13,7 +13,7 @@ export default { 'ts-jest', { isolatedModules: true, - // tsconfig: 'tsconfig.json', + // tsconfig: 'tsconfig.json', useESM: true, }, ], diff --git a/package-lock.json b/package-lock.json index 7353eeb..239694a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,23 +1,24 @@ { "name": "@orionprotocol/sdk", - "version": "0.20.60", + "version": "0.20.82", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@orionprotocol/sdk", - "version": "0.20.60", + "version": "0.20.82", "hasInstallScript": true, "license": "ISC", "dependencies": { "@babel/runtime": "^7.21.0", "@ethersproject/abstract-signer": "^5.7.0", "@ethersproject/providers": "^5.7.2", - "@orionprotocol/contracts": "1.22.10", + "@orionprotocol/contracts": "1.23.3", "@types/lodash.clonedeep": "^4.5.9", "bignumber.js": "^9.1.1", "bson-objectid": "^2.0.4", "buffer": "^6.0.3", + "crypto-js": "^4.2.0", "ethers": "^6.7.1", "express": "^4.18.2", "isomorphic-ws": "^5.0.0", @@ -2421,9 +2422,9 @@ } }, "node_modules/@orionprotocol/contracts": { - "version": "1.22.10", - "resolved": "https://registry.npmjs.org/@orionprotocol/contracts/-/contracts-1.22.10.tgz", - "integrity": "sha512-c9cUkXs1Nv8p+EVTybwJqeXhecwm7xeycAVauhl6jYAqvKOx7PDCUjzE3Nh0tpi4xP3CLeABgNy8JAFYyvN1VA==" + "version": "1.23.3", + "resolved": "https://registry.npmjs.org/@orionprotocol/contracts/-/contracts-1.23.3.tgz", + "integrity": "sha512-3PBnuiUe//v7COArcm/dzFx71vxW+a9emU4PHi1zBdow+OUAa4WNb+NmNZ3AOjnx2AZKK+gWJY+zKo1zSfAOHQ==" }, "node_modules/@sinclair/typebox": { "version": "0.27.8", @@ -2638,9 +2639,9 @@ } }, "node_modules/@types/jest": { - "version": "29.5.4", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.4.tgz", - "integrity": "sha512-PhglGmhWeD46FYOVLt3X7TiWjzwuVGW9wG/4qocPevXMjCmrIc5b6db9WjeGE4QYVpUAWMDv3v0IiBwObY289A==", + "version": "29.5.12", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.12.tgz", + "integrity": "sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==", "dev": true, "dependencies": { "expect": "^29.0.0", @@ -4397,6 +4398,11 @@ "node": ">= 8" } }, + "node_modules/crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" + }, "node_modules/data-uri-to-buffer": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", @@ -13480,9 +13486,9 @@ } }, "@orionprotocol/contracts": { - "version": "1.22.10", - "resolved": "https://registry.npmjs.org/@orionprotocol/contracts/-/contracts-1.22.10.tgz", - "integrity": "sha512-c9cUkXs1Nv8p+EVTybwJqeXhecwm7xeycAVauhl6jYAqvKOx7PDCUjzE3Nh0tpi4xP3CLeABgNy8JAFYyvN1VA==" + "version": "1.23.3", + "resolved": "https://registry.npmjs.org/@orionprotocol/contracts/-/contracts-1.23.3.tgz", + "integrity": "sha512-3PBnuiUe//v7COArcm/dzFx71vxW+a9emU4PHi1zBdow+OUAa4WNb+NmNZ3AOjnx2AZKK+gWJY+zKo1zSfAOHQ==" }, "@sinclair/typebox": { "version": "0.27.8", @@ -13697,9 +13703,9 @@ } }, "@types/jest": { - "version": "29.5.4", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.4.tgz", - "integrity": "sha512-PhglGmhWeD46FYOVLt3X7TiWjzwuVGW9wG/4qocPevXMjCmrIc5b6db9WjeGE4QYVpUAWMDv3v0IiBwObY289A==", + "version": "29.5.12", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.12.tgz", + "integrity": "sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==", "dev": true, "requires": { "expect": "^29.0.0", @@ -15019,6 +15025,11 @@ "which": "^2.0.1" } }, + "crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" + }, "data-uri-to-buffer": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", diff --git a/package.json b/package.json index 12c7d9d..5bfcd1b 100644 --- a/package.json +++ b/package.json @@ -88,11 +88,12 @@ "@babel/runtime": "^7.21.0", "@ethersproject/abstract-signer": "^5.7.0", "@ethersproject/providers": "^5.7.2", - "@orionprotocol/contracts": "1.22.10", + "@orionprotocol/contracts": "1.23.3", "@types/lodash.clonedeep": "^4.5.9", "bignumber.js": "^9.1.1", "bson-objectid": "^2.0.4", "buffer": "^6.0.3", + "crypto-js": "^4.2.0", "ethers": "^6.7.1", "express": "^4.18.2", "isomorphic-ws": "^5.0.0", diff --git a/src/Orion/index.ts b/src/Orion/index.ts index 02ccd5c..8ddf534 100644 --- a/src/Orion/index.ts +++ b/src/Orion/index.ts @@ -21,10 +21,15 @@ export default class Orion { // TODO: get tradable pairs (aggregated) + public logger: ((message: string) => void) | undefined; + constructor( envOrConfig: KnownEnv | EnvConfig = 'production', - overrides?: DeepPartial + overrides?: DeepPartial, + logger?: ((message: string) => void) | undefined ) { + this.logger = logger; + let config: EnvConfig; if (typeof envOrConfig === 'string') { const envConfig = envs[envOrConfig]; @@ -33,7 +38,7 @@ export default class Orion { } this.env = envOrConfig; config = { - analyticsAPI: envConfig.analyticsAPI, + analyticsAPI: envConfig?.analyticsAPI, referralAPI: envConfig.referralAPI, networks: Object.entries(envConfig.networks).map(([chainId, networkConfig]) => { if (!isValidChainId(chainId)) throw new Error(`Invalid chainId: ${chainId}`); @@ -92,7 +97,7 @@ export default class Orion { // api: networkConfig.api, nodeJsonRpc: networkConfig.nodeJsonRpc, services: networkConfig.services, - }); + }, logger); return { ...acc, [chainId]: unit, diff --git a/src/Unit/Exchange/generateSwapCalldata.ts b/src/Unit/Exchange/generateSwapCalldata.ts index 91f3fe6..fd7fe08 100644 --- a/src/Unit/Exchange/generateSwapCalldata.ts +++ b/src/Unit/Exchange/generateSwapCalldata.ts @@ -397,7 +397,7 @@ async function shouldUseExchangeBalance( let useExchangeBalance = true; let additionalTransferAmount = 0n; - if (exchangeBalance == 0n) { + if (walletBalance >= amount || exchangeBalance == 0n) { useExchangeBalance = false; additionalTransferAmount = amount; } else { diff --git a/src/Unit/Pmm/abi/OrionRFQ.ts b/src/Unit/Pmm/abi/OrionRFQ.ts new file mode 100644 index 0000000..13d1761 --- /dev/null +++ b/src/Unit/Pmm/abi/OrionRFQ.ts @@ -0,0 +1,63 @@ +export const orionRFQContractABI = + [ + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "info", + "type": "uint256" + }, + { + "internalType": "address", + "name": "makerAsset", + "type": "address" + }, + { + "internalType": "address", + "name": "takerAsset", + "type": "address" + }, + { + "internalType": "address", + "name": "maker", + "type": "address" + }, + { + "internalType": "address", + "name": "allowedSender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "makingAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "takingAmount", + "type": "uint256" + } + ], + "internalType": "struct OrderRFQLib.OrderRFQ", + "name": "order", + "type": "tuple" + }, + { + "internalType": "bytes", + "name": "signature", + "type": "bytes" + }, + { + "internalType": "uint256", + "name": "flagsAndAmount", + "type": "uint256" + } + ], + "name": "fillOrderRFQ", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } + ]; diff --git a/src/Unit/Pmm/index.ts b/src/Unit/Pmm/index.ts new file mode 100644 index 0000000..d4f1c57 --- /dev/null +++ b/src/Unit/Pmm/index.ts @@ -0,0 +1,84 @@ +import type Unit from '../index'; +import { z } from 'zod'; +import {pmmOrderSchema} from "./schemas/order"; +import {simpleFetch} from "simple-typed-fetch"; +import {ethers, Wallet} from "ethers"; +import {BigNumber} from "bignumber.js"; +import { ERC20__factory } from '@orionprotocol/contracts/lib/ethers-v6/index.js'; +import {orionRFQContractABI} from "./abi/OrionRFQ"; + +export default class Pmm { + private readonly unit: Unit; + private readonly provider: ethers.Provider; + private contractAddress: string; + + constructor(unit: Unit) { + this.unit = unit; + this.provider = unit.provider; + this.contractAddress = ''; + // this.contractAddress = '0x89357522c0ed6e557d39dc75290859246077bdfc'; + } + + private isInitialized() : boolean { + return this.contractAddress !== ''; + } + + public async init() { + if(this.isInitialized()) + return; + const { orionPMMRouterContractAddress } = await simpleFetch(this.unit.blockchainService.getPmmInfo)(); + this.contractAddress = orionPMMRouterContractAddress; + } + + public async getContractAddress() { + await this.init(); + return this.contractAddress; + } + + public async setAllowance(token: string, amount: string, signer: Wallet) { + await this.init(); + + const bnTargetAmount = new BigNumber(amount); + const walletAddress = await signer.getAddress(); + + const tokenContract = ERC20__factory + .connect(token, this.unit.provider); + + const unsignedApproveTx = await tokenContract + .approve.populateTransaction( + this.contractAddress, + bnTargetAmount.toString() + ); + const nonce = await this.provider.getTransactionCount(walletAddress, 'pending'); + const { gasPrice, maxFeePerGas } = await this.provider.getFeeData(); + const network = await this.provider.getNetwork(); + + if (gasPrice !== null) + unsignedApproveTx.gasPrice = gasPrice; + + if(maxFeePerGas !== null) + unsignedApproveTx.maxFeePerGas = maxFeePerGas; + + unsignedApproveTx.chainId = network.chainId; + unsignedApproveTx.nonce = nonce; + unsignedApproveTx.from = walletAddress; + const gasLimit = await this.provider.estimateGas(unsignedApproveTx); + unsignedApproveTx.gasLimit = gasLimit; + + const signedTx = await signer.signTransaction(unsignedApproveTx); + const txResponse = await this.provider.broadcastTransaction(signedTx); + await txResponse.wait(); + } + + public async fillRFQOrder(order : z.infer, signer: Wallet) { + await this.init(); + + if(!order.success) + throw Error("Invalid order provided"); + + const contract = new ethers.Contract(this.contractAddress, orionRFQContractABI, signer); + + // @ts-ignore + return contract.fillOrderRFQ(order.order, order.signature, BigInt(0)); + } +} \ No newline at end of file diff --git a/src/Unit/Pmm/schemas/order.ts b/src/Unit/Pmm/schemas/order.ts new file mode 100644 index 0000000..a9727e4 --- /dev/null +++ b/src/Unit/Pmm/schemas/order.ts @@ -0,0 +1,18 @@ +import {z} from "zod"; + +export const pmmOrderQuotationSchema = z.object({ + info: z.string().default(''), + makerAsset: z.string().default(''), + takerAsset: z.string().default(''), + maker: z.string().default(''), + allowedSender: z.string().default(''), + makingAmount: z.string().default(''), + takingAmount: z.string().default(''), +}); + +export const pmmOrderSchema = z.object({ + order: pmmOrderQuotationSchema.default({}), + signature: z.string().default(''), + success: z.boolean().default(false), + error: z.string().default(''), +}); \ No newline at end of file diff --git a/src/Unit/index.ts b/src/Unit/index.ts index 33badc5..9b504cc 100644 --- a/src/Unit/index.ts +++ b/src/Unit/index.ts @@ -11,6 +11,7 @@ import Exchange from './Exchange/index.js'; import { chains, envs } from '../config'; import type { networkCodes } from '../constants/index.js'; import { IndexerService } from '../services/Indexer'; +import Pmm from "./Pmm"; type KnownConfig = { env: KnownEnv @@ -30,6 +31,8 @@ export default class Unit { public readonly aggregator: Aggregator; + public readonly pmm: Pmm; + public readonly priceFeed: PriceFeed; public readonly exchange: Exchange; @@ -38,7 +41,10 @@ export default class Unit { public readonly contracts: Record; - constructor(config: KnownConfig | VerboseUnitConfig) { + public logger: ((message: string) => void) | undefined; + + constructor(config: KnownConfig | VerboseUnitConfig, logger?: ((message: string) => void) | undefined) { + this.logger = logger; if ('env' in config) { const staticConfig = envs[config.env]; if (!staticConfig) { @@ -115,12 +121,14 @@ export default class Unit { this.aggregator = new Aggregator( this.config.services.aggregator.http, this.config.services.aggregator.ws, - this.config.basicAuth + this.config.basicAuth, + logger, ); this.priceFeed = new PriceFeed( this.config.services.priceFeed.api, this.config.basicAuth ); this.exchange = new Exchange(this); + this.pmm = new Pmm(this); } } diff --git a/src/config/chains.json b/src/config/chains.json index 275b577..3fecfd8 100644 --- a/src/config/chains.json +++ b/src/config/chains.json @@ -47,7 +47,8 @@ "rpc": "https://opbnb-mainnet-rpc.bnbchain.org", "baseCurrencyName": "BNB", "contracts": { - "WETH": "0x4200000000000000000000000000000000000006" + "WETH": "0x4200000000000000000000000000000000000006", + "curveRegistry": "" } }, "3": { @@ -192,5 +193,57 @@ "WETH": "", "curveRegistry": "" } + }, + "2525": { + "chainId": "2525", + "label": "inEVM", + "shortName": "inEVM", + "code": "inevm", + "baseCurrencyName": "INJ", + "rpc": "https://inevm.calderachain.xyz/http/", + "explorer": "https://explorer.injective.network/", + "contracts": { + "WETH": "0x4C3A213bd5e8c4BD70a8396d6F3C8302571598Cd", + "curveRegistry": "" + } + }, + "59144": { + "chainId": "59144", + "label": "Linea", + "shortName": "Linea", + "code": "linea", + "baseCurrencyName": "ETH", + "rpc": "https://rpc.linea.build/", + "explorer": "https://lineascan.build/", + "contracts": { + "WETH": "0xe5d7c2a44ffddf6b295a15c148167daaaf5cf34f", + "curveRegistry": "" + } + }, + "43114": { + "chainId": "43114", + "label": "Avalanche Network", + "shortName": "Avax", + "code": "avax", + "baseCurrencyName": "AVAX", + "rpc": "https://api.avax.network/ext/bc/C/rpc/", + "explorer": "https://snowtrace.io/", + "contracts": { + "WETH": "0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7", + "curveRegistry": "" + } + }, + "8453": { + "chainId": "8453", + "label": "Base", + "shortName": "BASE", + "code": "base", + "baseCurrencyName": "ETH", + "rpc": "https://mainnet.base.org/", + "explorer": "https://basescan.org/", + "contracts": { + "WETH": "0x4200000000000000000000000000000000000006", + "curveRegistry": "" + } } } diff --git a/src/config/envs.json b/src/config/envs.json index d8ba4d2..12e87f9 100644 --- a/src/config/envs.json +++ b/src/config/envs.json @@ -1,6 +1,5 @@ { "production": { - "analyticsAPI": "https://trade.orion.xyz/api/stats", "referralAPI": "https://trade.orion.xyz/referral-api", "networks": { "1": { @@ -129,11 +128,82 @@ "http": "/orion-indexer/" } } + }, + "2525": { + "api": "https://trade.orion.xyz/inevm-mainnet", + "services": { + "aggregator": { + "http": "/backend", + "ws": "/v1" + }, + "blockchain": { + "http": "" + }, + "priceFeed": { + "all": "/price-feed" + }, + "indexer": { + "http": "/orion-indexer/" + } + } + }, + "59144": { + "api": "https://trade.orion.xyz/linea-mainnet", + "services": { + "aggregator": { + "http": "/backend", + "ws": "/v1" + }, + "blockchain": { + "http": "" + }, + "priceFeed": { + "all": "/price-feed" + }, + "indexer": { + "http": "/orion-indexer/" + } + } + }, + "43114": { + "api": "https://trade.orion.xyz/avalanche-c-chain", + "services": { + "aggregator": { + "http": "/backend", + "ws": "/v1" + }, + "blockchain": { + "http": "" + }, + "priceFeed": { + "all": "/price-feed" + }, + "indexer": { + "http": "/orion-indexer/" + } + } + }, + "8453": { + "api": "https://trade.orion.xyz/base-mainnet", + "services": { + "aggregator": { + "http": "/backend", + "ws": "/v1" + }, + "blockchain": { + "http": "" + }, + "priceFeed": { + "all": "/price-feed" + }, + "indexer": { + "http": "/orion-indexer/" + } + } } } }, "testing": { - "analyticsAPI": "https://trade.orion.xyz/api/stats", "referralAPI": "https://testing.orion.xyz/referral-api", "networks": { "97": { @@ -248,7 +318,6 @@ } }, "staging": { - "analyticsAPI": "https://trade.orion.xyz/api/stats", "referralAPI": "https://staging.orion.xyz/referral-api", "networks": { "1": { @@ -376,11 +445,82 @@ "http": "/orion-indexer/" } } + }, + "2525": { + "api": "https://staging.orion.xyz/inevm-mainnet", + "services": { + "aggregator": { + "http": "/backend", + "ws": "/v1" + }, + "blockchain": { + "http": "" + }, + "priceFeed": { + "all": "/price-feed" + }, + "indexer": { + "http": "/orion-indexer/" + } + } + }, + "59144": { + "api": "https://staging.orion.xyz/linea-mainnet", + "services": { + "aggregator": { + "http": "/backend", + "ws": "/v1" + }, + "blockchain": { + "http": "" + }, + "priceFeed": { + "all": "/price-feed" + }, + "indexer": { + "http": "/orion-indexer/" + } + } + }, + "43114": { + "api": "https://staging.orion.xyz/avalanche-c-chain", + "services": { + "aggregator": { + "http": "/backend", + "ws": "/v1" + }, + "blockchain": { + "http": "" + }, + "priceFeed": { + "all": "/price-feed" + }, + "indexer": { + "http": "/orion-indexer/" + } + } + }, + "8453": { + "api": "https://staging.orion.xyz/base-mainnet", + "services": { + "aggregator": { + "http": "/backend", + "ws": "/v1" + }, + "blockchain": { + "http": "" + }, + "priceFeed": { + "all": "/price-feed" + }, + "indexer": { + "http": "/orion-indexer/" + } + } } } }, "experimental": { - "analyticsAPI": "https://trade.orion.xyz/api/stats", "referralAPI": "https://testing.orion.xyz/referral-api", "networks": { "97": { @@ -422,7 +562,6 @@ } }, "kucoin-production": { - "analyticsAPI": "https://trade.orion.xyz/api/stats", "referralAPI": "https://trade.orion.xyz/referral-api", "networks": { "1": { diff --git a/src/config/schemas/pureEnvSchema.ts b/src/config/schemas/pureEnvSchema.ts index 642dd0b..9c799c4 100644 --- a/src/config/schemas/pureEnvSchema.ts +++ b/src/config/schemas/pureEnvSchema.ts @@ -23,7 +23,7 @@ export const pureEnvNetworksSchema = z.object({ }); export const pureEnvPayloadSchema = z.object({ - analyticsAPI: z.string().url(), + analyticsAPI: z.string().url().optional(), referralAPI: z.string().url(), networks: z.record( z.nativeEnum(SupportedChainId), diff --git a/src/constants/chains.ts b/src/constants/chains.ts index 74135ed..1fb8ff3 100644 --- a/src/constants/chains.ts +++ b/src/constants/chains.ts @@ -17,4 +17,8 @@ export const productionChains = [ SupportedChainId.OKC, SupportedChainId.ARBITRUM, SupportedChainId.OPBNB, + SupportedChainId.INEVM, + SupportedChainId.LINEA, + SupportedChainId.AVAX, + SupportedChainId.BASE, ]; diff --git a/src/constants/networkCodes.ts b/src/constants/networkCodes.ts index d04ac04..498e149 100644 --- a/src/constants/networkCodes.ts +++ b/src/constants/networkCodes.ts @@ -1 +1 @@ -export default ['ftm', 'bsc', 'eth', 'polygon', 'okc', 'arb', 'drip', 'opbnb'] as const; +export default ['ftm', 'bsc', 'eth', 'polygon', 'okc', 'arb', 'drip', 'opbnb', 'inevm', 'linea', 'avax', 'base'] as const; diff --git a/src/constants/uppercasedNetworkCodes.ts b/src/constants/uppercasedNetworkCodes.ts index 8dc00e3..e69605f 100644 --- a/src/constants/uppercasedNetworkCodes.ts +++ b/src/constants/uppercasedNetworkCodes.ts @@ -1 +1 @@ -export default ['FTM', 'BSC', 'ETH', 'POLYGON', 'OKC', 'ARB'] as const; +export default ['FTM', 'BSC', 'ETH', 'POLYGON', 'OKC', 'ARB', 'OPBNB', 'INEVM', 'LINEA', 'AVAX', 'BASE'] as const; diff --git a/src/services/Aggregator/index.ts b/src/services/Aggregator/index.ts index 64cdc45..fd7fec8 100644 --- a/src/services/Aggregator/index.ts +++ b/src/services/Aggregator/index.ts @@ -19,6 +19,10 @@ import httpToWS from '../../utils/httpToWS.js'; import { ethers } from 'ethers'; import orderSchema from './schemas/orderSchema.js'; import { fetchWithValidation } from 'simple-typed-fetch'; +import { pmmOrderSchema } from '../../Unit/Pmm/schemas/order'; +// import hmacSHA256 from "crypto-js/hmac-sha256"; +// import Hex from "crypto-js/enc-hex"; +// const crypto = require('crypto') class Aggregator { private readonly apiUrl: string; @@ -31,11 +35,16 @@ class Aggregator { return this.apiUrl; } + public logger: ((message: string) => void) | undefined; + constructor( httpAPIUrl: string, wsAPIUrl: string, - basicAuth?: BasicAuthCredentials + basicAuth?: BasicAuthCredentials, + logger?: ((message: string) => void) | undefined ) { + this.logger = logger; + // const oaUrl = new URL(apiUrl); // const oaWsProtocol = oaUrl.protocol === 'https:' ? 'wss' : 'ws'; // const aggregatorWsUrl = `${oaWsProtocol}://${oaUrl.host + (oaUrl.pathname === '/' @@ -43,7 +52,7 @@ class Aggregator { // : oaUrl.pathname)}/v1`; this.apiUrl = httpAPIUrl; - this.ws = new AggregatorWS(httpToWS(wsAPIUrl)); + this.ws = new AggregatorWS(httpToWS(wsAPIUrl), undefined, logger); this.basicAuth = basicAuth; this.getHistoryAtomicSwaps = this.getHistoryAtomicSwaps.bind(this); @@ -52,6 +61,7 @@ class Aggregator { this.getPairsList = this.getPairsList.bind(this); this.getSwapInfo = this.getSwapInfo.bind(this); this.getTradeProfits = this.getTradeProfits.bind(this); + this.getStableCoins = this.getStableCoins.bind(this); this.placeAtomicSwap = this.placeAtomicSwap.bind(this); this.placeOrder = this.placeOrder.bind(this); this.cancelOrder = this.cancelOrder.bind(this); @@ -331,6 +341,16 @@ class Aggregator { ); }; + getStableCoins = () => { + const url = new URL(`${this.apiUrl}/api/v1/tokens/stable/`); + return fetchWithValidation( + url.toString(), + z.array(z.string()), + { headers: this.basicAuthHeaders }, + errorSchema, + ); + }; + /** * Placing atomic swap. Placement must take place on the target chain. * @param secretHash Secret hash @@ -369,6 +389,102 @@ class Aggregator { url.searchParams.append('limit', limit.toString()); return fetchWithValidation(url.toString(), atomicSwapHistorySchema, { headers: this.basicAuthHeaders }); }; + + // private encode_utf8(s: string) { + // return unescape(encodeURIComponent(s)); + // } + + // @ts-expect-error: TODO: please remove this line! + private sign(message: string, key: string) { + // return crypto.createHmac('sha256', this.encode_utf8(key)) + // .update(this.encode_utf8(message)) + // .digest('hex'); + return ''; + } + + private generateHeaders(body: any, method: string, path: string, timestamp: number, apiKey: string, secretKey: string) { + const sortedBody = Object.keys(body) + .sort() + .map((key) => ( + `${key}=${body[key]}` + )).join('&'); + + const payload = timestamp + method.toUpperCase() + path + sortedBody; + + const signature = this.sign(payload, secretKey); + + const httpOptions = { + headers: { + 'API-KEY': apiKey, + 'ACCESS-TIMESTAMP': timestamp.toString(), + 'ACCESS-SIGN': signature + } + }; + return httpOptions; + } + + public async RFQOrder( + tokenFrom: string, + tokenTo: string, + fromTokenAmount: string, + apiKey: string, // + secretKey: string, + wallet: string + ): Promise> { + // Making the order structure + const + path = '/rfq'; + const url = `${this.apiUrl}/api/v1/integration/pmm` + path; + const headers = { + 'Content-Type': 'application/json', + }; + const data = { + baseToken: tokenFrom, // USDT + quoteToken: tokenTo, // ORN + amount: fromTokenAmount, // 100 + taker: wallet, + feeBps: 0 + }; + const method = 'POST'; + const timestamp = Date.now(); + const signatureHeaders = this.generateHeaders(data, method, path, timestamp, apiKey, secretKey); + const compiledHeaders = { ...headers, ...signatureHeaders.headers, }; + const body = JSON.stringify(data) + ; + + const res = pmmOrderSchema.parse({}); + + try { + const result = await fetch(url, { + headers: compiledHeaders, + method, + body + }); + + const json = await result.json(); + const parseResult = pmmOrderSchema.safeParse(json); + + if (!parseResult.success) { + // Try to parse error answer + const errorSchema = z.object({ error: z.object({ code: z.number(), reason: z.string() }) }); + + const errorParseResult = errorSchema.safeParse(json); + + if (!errorParseResult.success) { throw Error(`Unrecognized answer from aggregator: ${json}`); } + + throw Error(errorParseResult.data.error.reason); + } + + res.order = parseResult.data.order; + res.signature = parseResult.data.signature; + res.error = ''; + res.success = true; + // return result; + } catch (err) { + res.error = `${err}`; + } + return res; + } } export * as schemas from './schemas/index.js'; export * as ws from './ws/index.js'; diff --git a/src/services/Aggregator/schemas/swapInfoSchema.ts b/src/services/Aggregator/schemas/swapInfoSchema.ts index ecbd440..aed6d09 100644 --- a/src/services/Aggregator/schemas/swapInfoSchema.ts +++ b/src/services/Aggregator/schemas/swapInfoSchema.ts @@ -49,6 +49,7 @@ const swapInfoBase = z.object({ mi: z.number().optional(), // market amount in, USD d: z.string().optional(), // difference in available amount in/out (USD) and market amount out/in (USD) in percentage }).optional(), + autoSlippage: z.number().optional(), }); const swapInfoByAmountIn = swapInfoBase.extend({ diff --git a/src/services/Aggregator/ws/index.ts b/src/services/Aggregator/ws/index.ts index 9c94a0e..2e44ee6 100644 --- a/src/services/Aggregator/ws/index.ts +++ b/src/services/Aggregator/ws/index.ts @@ -66,10 +66,11 @@ type PairConfigSubscription = { type AggregatedOrderbookSubscription = { payload: string + dc?: number callback: ( asks: OrderbookItem[], bids: OrderbookItem[], - pair: string + pair: string, ) => void errorCb?: (message: string) => void } @@ -199,9 +200,10 @@ class AggregatorWS { readonly basicAuth?: BasicAuthCredentials | undefined; - constructor(wsUrl: string, basicAuth?: BasicAuthCredentials) { + constructor(wsUrl: string, basicAuth?: BasicAuthCredentials, logger?: ((message: string) => void) | undefined) { this.wsUrl = wsUrl; this.basicAuth = basicAuth; + this.logger = logger; } private messageQueue: BufferLike[] = []; @@ -256,7 +258,7 @@ class AggregatorWS { subscription: Subscription[T], prevSubscriptionId?: string ) { - const id = type === 'aobus' + const id = type === SubscriptionType.AGGREGATED_ORDER_BOOK_UPDATES_SUBSCRIBE ? ((subscription as any).payload as string) // TODO: Refactor!!! : uuidv4(); @@ -266,6 +268,12 @@ class AggregatorWS { subRequest['T'] = type; subRequest['id'] = id; + if ('dc' in subscription) { + if (typeof subscription.dc === 'number') { + subRequest['dc'] = subscription.dc; + } + } + if ('payload' in subscription) { if (typeof subscription.payload === 'string') { console.log('subscription.payload === string', subscription.payload); @@ -394,11 +402,11 @@ class AggregatorWS { delete this.subscriptions[SubscriptionType.ASSET_PAIR_CONFIG_UPDATES_SUBSCRIBE]?.[newestSubId]; // !!! swap info subscription is uuid that contains hyphen } else if (isOrderBooksSubscription(newestSubId)) { // is pair name(AGGREGATED_ORDER_BOOK_UPDATE) - const aobSubscriptions = this.subscriptions[SubscriptionType.AGGREGATED_ORDER_BOOK_UPDATES_SUBSCRIBE]; - if (aobSubscriptions) { - const targetAobSub = Object.entries(aobSubscriptions).find(([, value]) => value?.payload === newestSubId); - if (targetAobSub) { - const [key] = targetAobSub; + const aobusSubscriptions = this.subscriptions[SubscriptionType.AGGREGATED_ORDER_BOOK_UPDATES_SUBSCRIBE]; + if (aobusSubscriptions) { + const targetAobusSub = Object.entries(aobusSubscriptions).find(([, value]) => value?.payload === newestSubId); + if (targetAobusSub) { + const [key] = targetAobusSub; delete this.subscriptions[SubscriptionType.AGGREGATED_ORDER_BOOK_UPDATES_SUBSCRIBE]?.[key]; } } @@ -558,6 +566,7 @@ class AggregatorWS { marketAmountIn: json.usd.mi, difference: json.usd.d, }, + autoSlippage: json.sl, }; switch (json.k) { // kind diff --git a/src/services/Aggregator/ws/schemas/swapInfoSchema.ts b/src/services/Aggregator/ws/schemas/swapInfoSchema.ts index 3c8a5d4..eb23200 100644 --- a/src/services/Aggregator/ws/schemas/swapInfoSchema.ts +++ b/src/services/Aggregator/ws/schemas/swapInfoSchema.ts @@ -48,6 +48,7 @@ const swapInfoSchemaBase = baseMessageSchema.extend({ mi: z.number().optional(), // market amount in, USD d: z.string().optional(), // difference in available amount in/out (USD) and market amount out/in (USD) in percentage }).optional(), + sl: z.number().optional(), }); const swapInfoSchemaByAmountIn = swapInfoSchemaBase.extend({ diff --git a/src/services/BlockchainService/index.ts b/src/services/BlockchainService/index.ts index 53ac157..ceacbcb 100644 --- a/src/services/BlockchainService/index.ts +++ b/src/services/BlockchainService/index.ts @@ -12,6 +12,7 @@ import { pairStatusSchema, pricesWithQuoteAssetSchema, referralDataSchema, + pmmSchema } from './schemas/index.js'; import type redeemOrderSchema from '../Aggregator/schemas/redeemOrderSchema.js'; import { sourceAtomicHistorySchema, targetAtomicHistorySchema } from './schemas/atomicHistorySchema.js'; @@ -82,6 +83,7 @@ class BlockchainService { this.getAuthToken = this.getAuthToken.bind(this); this.getCirculatingSupply = this.getCirculatingSupply.bind(this); this.getInfo = this.getInfo.bind(this); + this.getPmmInfo = this.getPmmInfo.bind(this); this.getPoolsConfig = this.getPoolsConfig.bind(this); this.getPoolsInfo = this.getPoolsInfo.bind(this); this.getPoolsLpAndStaked = this.getPoolsLpAndStaked.bind(this); @@ -111,6 +113,8 @@ class BlockchainService { this.getBlockNumber = this.getBlockNumber.bind(this); this.getRedeemOrderBySecretHash = this.getRedeemOrderBySecretHash.bind(this); this.claimOrder = this.claimOrder.bind(this); + this.getGasLimits = this.getGasLimits.bind(this); + this.getExchangeContractWalletBalance = this.getExchangeContractWalletBalance.bind(this); } get basicAuthHeaders() { @@ -175,6 +179,8 @@ class BlockchainService { getInfo = () => fetchWithValidation(`${this.apiUrl}/api/info`, infoSchema); + getPmmInfo = () => fetchWithValidation(`${this.apiUrl}/api/pmm-info`, pmmSchema); + getPoolsConfig = () => fetchWithValidation( `${this.apiUrl}/api/pools/config`, poolsConfigSchema, @@ -484,6 +490,18 @@ class BlockchainService { body: JSON.stringify(secretHashes), }, ); + + getGasLimits = () => fetchWithValidation( + `${this.apiUrl}/api/baseLimits`, + z.record(z.number()), + { headers: this.basicAuthHeaders } + ); + + getExchangeContractWalletBalance = (exchangeContractAddress: string) => fetchWithValidation( + `${this.apiUrl}/api/broker/getWalletBalance/${exchangeContractAddress}`, + z.record(z.string()), + { headers: this.basicAuthHeaders } + ); } export * as schemas from './schemas/index.js'; diff --git a/src/services/BlockchainService/schemas/index.ts b/src/services/BlockchainService/schemas/index.ts index 5f76ca8..886cb3a 100644 --- a/src/services/BlockchainService/schemas/index.ts +++ b/src/services/BlockchainService/schemas/index.ts @@ -13,5 +13,6 @@ export { default as poolsLpAndStakedSchema } from './poolsLpAndStakedSchema.js'; export { default as userVotesSchema } from './userVotesSchema.js'; export { default as userEarnedSchema } from './userEarnedSchema.js'; export { default as poolsV3InfoSchema } from './poolsV3InfoSchema.js'; +export { default as pmmSchema } from './pmmSchema.js'; export { pricesWithQuoteAssetSchema } from './pricesWithQuoteAssetSchema.js'; export { referralDataSchema } from './referralDataSchema.js'; \ No newline at end of file diff --git a/src/services/BlockchainService/schemas/pmmSchema.ts b/src/services/BlockchainService/schemas/pmmSchema.ts new file mode 100644 index 0000000..e56673b --- /dev/null +++ b/src/services/BlockchainService/schemas/pmmSchema.ts @@ -0,0 +1,7 @@ +import { z } from 'zod'; + +const pmmSchema = z.object({ + orionPMMRouterContractAddress: z.string() +}); + +export default pmmSchema \ No newline at end of file diff --git a/src/services/BlockchainService/schemas/referralDataSchema.ts b/src/services/BlockchainService/schemas/referralDataSchema.ts index 115e2a8..4b4674a 100644 --- a/src/services/BlockchainService/schemas/referralDataSchema.ts +++ b/src/services/BlockchainService/schemas/referralDataSchema.ts @@ -1,6 +1,6 @@ -import { z } from "zod"; +import { z } from 'zod'; export const referralDataSchema = z.object({ - referer: z.string(), + referer: z.string().nullable(), isReferral: z.boolean(), }); diff --git a/src/services/Indexer/index.ts b/src/services/Indexer/index.ts index bcdfaca..2791f8a 100644 --- a/src/services/Indexer/index.ts +++ b/src/services/Indexer/index.ts @@ -9,7 +9,7 @@ import { PoolV2InfoResponseSchema, testIncrementorSchema, veORNInfoResponseSchema, - votingInfoResponseSchema + votingInfoResponseSchema, } from './schemas'; import { fetchWithValidation } from 'simple-typed-fetch'; import { BigNumber } from 'bignumber.js'; @@ -94,6 +94,7 @@ class IndexerService { this.veORNInfo = this.veORNInfo.bind(this); this.listAmount = this.listAmount.bind(this); this.getAmountByORN = this.getAmountByORN.bind(this); + this.getAmountAt = this.getAmountAt.bind(this); this.getAmountAtCurrent = this.getAmountAtCurrent.bind(this); this.getVotingInfo = this.getVotingInfo.bind(this); } @@ -117,6 +118,23 @@ class IndexerService { }); }; + /** + * @param {number} amount - amount + * @param {number} [timestamp = Date.now()] - timestamp, defaults to current time + */ + readonly getAmountAt = ( + amount: number, + timestamp = Date.now() + ): BigNumber => { + const finalTimestamp = timestamp / 1000; + + // sqrt + return BigNumber(amount).dividedBy(this.getK(finalTimestamp)); + }; + + /** + * @deprecated since version 69 in favor of getAmountAt + */ readonly getAmountAtCurrent = (amount: number): BigNumber => { const timestamp = Date.now() / 1000; @@ -134,8 +152,7 @@ class IndexerService { const multSQRT = deltaDaysBN.dividedBy(WEEK_DAYS).sqrt(); const multCUBE = deltaDaysBN.dividedBy(alpha).pow(3); - return BigNumber(amountToken) - .multipliedBy(multSQRT.plus(multCUBE)); + return BigNumber(amountToken).multipliedBy(multSQRT.plus(multCUBE)); }; readonly getVotingInfo = (userAddress?: string) => { @@ -208,7 +225,11 @@ class IndexerService { }); }; - readonly poolV2Info = (token0: string, token1: string, address: string | undefined) => { + readonly poolV2Info = ( + token0: string, + token1: string, + address: string | undefined + ) => { return fetchWithValidation(this.apiUrl, PoolV2InfoResponseSchema, { method: 'POST', body: this.makeRPCPayload({ diff --git a/src/services/PriceFeed/ws/PriceFeedSubscription.ts b/src/services/PriceFeed/ws/PriceFeedSubscription.ts index 4969549..ad77939 100644 --- a/src/services/PriceFeed/ws/PriceFeedSubscription.ts +++ b/src/services/PriceFeed/ws/PriceFeedSubscription.ts @@ -2,7 +2,7 @@ import WebSocket from 'isomorphic-ws'; import { z } from 'zod'; import { v4 as uuidv4 } from 'uuid'; import priceFeedSubscriptions from './priceFeedSubscriptions.js'; -import { tickerInfoSchema, candleSchema } from './schemas/index.js'; +import { tickerInfoSchema, candleSchema, cexPricesSchema } from './schemas/index.js'; import priceSchema from './schemas/priceSchema.js'; import type { Json } from '../../../types.js'; import allTickersSchema from './schemas/allTickersSchema.js'; @@ -24,6 +24,10 @@ export const subscriptions = { schema: candleSchema, payload: true as const, }, + [priceFeedSubscriptions.CEX]: { + schema: cexPricesSchema, + payload: false as const, + }, }; export type SubscriptionType = keyof typeof subscriptions; diff --git a/src/services/PriceFeed/ws/priceFeedSubscriptions.ts b/src/services/PriceFeed/ws/priceFeedSubscriptions.ts index 847700d..cdd7d88 100644 --- a/src/services/PriceFeed/ws/priceFeedSubscriptions.ts +++ b/src/services/PriceFeed/ws/priceFeedSubscriptions.ts @@ -3,6 +3,7 @@ const priceFeedSubscriptions = { ALL_TICKERS: 'allTickers', LAST_PRICE: 'lastPrice', CANDLE: 'candle', + CEX: 'cexPrices' } as const; export default priceFeedSubscriptions; diff --git a/src/services/PriceFeed/ws/schemas/cexPricesSchema.ts b/src/services/PriceFeed/ws/schemas/cexPricesSchema.ts new file mode 100644 index 0000000..a12dfa6 --- /dev/null +++ b/src/services/PriceFeed/ws/schemas/cexPricesSchema.ts @@ -0,0 +1,31 @@ +import { z } from 'zod'; + +const cexPriceTickerInfoSchema = z.tuple([ + z.string(), // pair name + z.number(), // lastPrice +]).transform(([pairName, lastPrice]) => ({ + pairName:pairName.toUpperCase(), + lastPrice, +})); + +type CEXPriceTickerInfo = z.infer + +const cexPricesSchema = z.unknown().array() +.transform((tickers) => { + const data = [...tickers]; + data.shift(); + const parsedData = cexPriceTickerInfoSchema.array().parse(data); + return parsedData.reduce< + Partial< + Record< + string, + CEXPriceTickerInfo + > + > + >((prev, pairData) => ({ + ...prev, + [pairData.pairName]: pairData, + }), {}); +}); + +export default cexPricesSchema; \ No newline at end of file diff --git a/src/services/PriceFeed/ws/schemas/index.ts b/src/services/PriceFeed/ws/schemas/index.ts index 4ca9486..2e55e91 100644 --- a/src/services/PriceFeed/ws/schemas/index.ts +++ b/src/services/PriceFeed/ws/schemas/index.ts @@ -2,3 +2,4 @@ export { default as tickerInfoSchema } from './tickerInfoSchema.js'; export { default as candleSchema } from './candleSchema.js'; export { default as priceSchema } from './priceSchema.js'; export { default as allTickersSchema } from './allTickersSchema.js'; +export { default as cexPricesSchema } from './cexPricesSchema.js'; diff --git a/src/types.ts b/src/types.ts index b5f0330..6a3513a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -3,7 +3,7 @@ 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'; export type DeepPartial = T extends object ? { @@ -88,6 +88,10 @@ export enum SupportedChainId { POLYGON = '137', OKC = '66', OPBNB = '204', + INEVM = '2525', + LINEA = '59144', + AVAX = '43114', + BASE = '8453', POLYGON_TESTNET = '80001', FANTOM_TESTNET = '4002', @@ -206,6 +210,7 @@ export type SwapInfoBase = { marketAmountIn: number | undefined difference: string | undefined } | undefined + autoSlippage: number | undefined } export type SwapInfoByAmountIn = SwapInfoBase & { @@ -277,7 +282,7 @@ export type KnownEnv = typeof knownEnvs[number]; export type Json = string | number | boolean | null | Json[] | { [key: string]: Json }; export type EnvConfig = { - analyticsAPI: string + analyticsAPI: string | undefined referralAPI: string networks: Partial< Record< diff --git a/tsconfig.json b/tsconfig.json index 2bbded1..8985203 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,6 +11,7 @@ "lib" ], "compilerOptions": { + "moduleResolution": "node", "target": "esnext", "module": "ESNext", "esModuleInterop": true,