added PMM

This commit is contained in:
KS
2024-03-10 20:28:00 +03:00
parent 184f66bf17
commit 6f5b537c47
13 changed files with 408 additions and 10 deletions

View File

@@ -60,6 +60,7 @@ Orions 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)

View File

@@ -13,7 +13,7 @@ export default {
'ts-jest',
{
isolatedModules: true,
// tsconfig: 'tsconfig.json',
// tsconfig: 'tsconfig.json',
useESM: true,
},
],

40
package-lock.json generated
View File

@@ -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",

View File

@@ -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",

View File

@@ -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"
}
];

78
src/Unit/Pmm/index.ts Normal file
View File

@@ -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<typeof pmmOrderSchema>, 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));
}
}

View File

@@ -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(''),
});

View File

@@ -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);
}
}

View File

@@ -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<z.infer<typeof pmmOrderSchema>> {
// 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';

View File

@@ -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,

View File

@@ -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';

View File

@@ -0,0 +1,7 @@
import { z } from 'zod';
const pmmSchema = z.object({
orionPMMRouterContractAddress: z.string()
});
export default pmmSchema

View File

@@ -11,6 +11,7 @@
"lib"
],
"compilerOptions": {
"moduleResolution": "node",
"target": "esnext",
"module": "ESNext",
"esModuleInterop": true,