From 6f5b537c476803338bdef98f18ad026eb041ec17 Mon Sep 17 00:00:00 2001 From: KS Date: Sun, 10 Mar 2024 20:28:00 +0300 Subject: [PATCH] added PMM --- README.md | 93 ++++++++++++++++ jest.config.js | 2 +- package-lock.json | 40 +++++-- package.json | 4 +- src/Unit/Pmm/abi/OrionRFQ.ts | 63 +++++++++++ src/Unit/Pmm/index.ts | 78 +++++++++++++ src/Unit/Pmm/schemas/order.ts | 18 +++ src/Unit/index.ts | 4 + src/services/Aggregator/index.ts | 103 ++++++++++++++++++ src/services/BlockchainService/index.ts | 4 + .../BlockchainService/schemas/index.ts | 1 + .../BlockchainService/schemas/pmmSchema.ts | 7 ++ tsconfig.json | 1 + 13 files changed, 408 insertions(+), 10 deletions(-) create mode 100644 src/Unit/Pmm/abi/OrionRFQ.ts create mode 100644 src/Unit/Pmm/index.ts create mode 100644 src/Unit/Pmm/schemas/order.ts create mode 100644 src/services/BlockchainService/schemas/pmmSchema.ts diff --git a/README.md b/README.md index 05438c5..0d2fa18 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,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 @@ -719,3 +720,95 @@ switch (data.type) { break; } ``` +## PMM + +PMM allows institutional traders to request RFQ orders from Orion 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 +import Orion from '../Orion'; +import {Wallet} from "ethers"; +import {simpleFetch} from "simple-typed-fetch"; + +(async() => { + const apiKey = '958153f1-b8b9-3ec4-84eb-2147429105d9'; + const secretKey = 'secretKey'; + const yourWalletPrivateKey = '0x...'; + + const orion = new Orion('testing'); // Leave empty for test 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 simpleFetch(bsc.blockchainService.getInfo)(); + + // 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); + + const rfqOrder = await bsc.aggregator.RFQOrder( + assetToAddress.ORN, // Spending asset + assetToAddress.USDT, // Receiving asset + '10000000000', // 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): + +```json + { + 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) \ No newline at end of file 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 55fdb08..f7f2955 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@orionprotocol/sdk", - "version": "0.20.64", + "version": "0.20.66-rc", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@orionprotocol/sdk", - "version": "0.20.64", + "version": "0.20.66-rc", "hasInstallScript": true, "license": "ISC", "dependencies": { @@ -18,6 +18,7 @@ "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", @@ -39,6 +40,7 @@ "@babel/plugin-syntax-import-assertions": "^7.20.0", "@tsconfig/esm": "^1.0.4", "@tsconfig/strictest": "^2.0.1", + "@types/crypto-js": "^4.2.2", "@types/express": "^4.17.17", "@types/jest": "^29.5.1", "@types/node": "^20.5.1", @@ -2545,6 +2547,12 @@ "@types/node": "*" } }, + "node_modules/@types/crypto-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.2.2.tgz", + "integrity": "sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==", + "dev": true + }, "node_modules/@types/eslint": { "version": "8.44.2", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.2.tgz", @@ -2638,9 +2646,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 +4405,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", @@ -13604,6 +13617,12 @@ "@types/node": "*" } }, + "@types/crypto-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.2.2.tgz", + "integrity": "sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==", + "dev": true + }, "@types/eslint": { "version": "8.44.2", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.2.tgz", @@ -13697,9 +13716,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 +15038,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 4c77189..5788d98 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@orionprotocol/sdk", - "version": "0.20.65", + "version": "0.20.66-rc", "description": "Orion Protocol SDK", "main": "./lib/index.cjs", "module": "./lib/index.js", @@ -57,6 +57,7 @@ "@babel/plugin-syntax-import-assertions": "^7.20.0", "@tsconfig/esm": "^1.0.4", "@tsconfig/strictest": "^2.0.1", + "@types/crypto-js": "^4.2.2", "@types/express": "^4.17.17", "@types/jest": "^29.5.1", "@types/node": "^20.5.1", @@ -93,6 +94,7 @@ "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/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..aa35962 --- /dev/null +++ b/src/Unit/Pmm/index.ts @@ -0,0 +1,78 @@ +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 = ''; + } + + 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 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.quotation, 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..2651660 --- /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({ + quotation: 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..85d7052 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; @@ -122,5 +125,6 @@ export default class Unit { this.config.basicAuth ); this.exchange = new Exchange(this); + this.pmm = new Pmm(this); } } diff --git a/src/services/Aggregator/index.ts b/src/services/Aggregator/index.ts index 64cdc45..f04e4c5 100644 --- a/src/services/Aggregator/index.ts +++ b/src/services/Aggregator/index.ts @@ -19,6 +19,9 @@ import httpToWS from '../../utils/httpToWS.js'; import { ethers } from 'ethers'; import orderSchema from './schemas/orderSchema.js'; import { fetchWithValidation } from 'simple-typed-fetch'; +import hmacSHA256 from "crypto-js/hmac-sha256"; +import Hex from "crypto-js/enc-hex"; +import {pmmOrderSchema} from "../../Unit/Pmm/schemas/order"; class Aggregator { private readonly apiUrl: string; @@ -369,6 +372,106 @@ 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)); + } + + private sign(message : string, key: string) { + return hmacSHA256( + this.encode_utf8(message), + this.encode_utf8(key) + ).toString(Hex); + } + + 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' + , url = `${this.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 = this.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 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.quotation = parseResult.data.quotation; + 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/BlockchainService/index.ts b/src/services/BlockchainService/index.ts index a64a358..16d5e59 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); @@ -176,6 +178,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, 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/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,