From aa61ed24a5f9285902f8d694b198a1ec1aa5bd00 Mon Sep 17 00:00:00 2001 From: shoom3301 Date: Tue, 8 Jun 2021 15:41:20 +0400 Subject: [PATCH] feat(limit-order-rfq): util for create,fill,cancel limit order RFQ npm run rfq-utils --- package.json | 5 +- src/model/limit-order-protocol.model.ts | 1 + src/utils/limit-order-rfq.const.ts | 133 +++++++++++++ src/utils/limit-order-rfq.model.ts | 31 +++ src/utils/limit-order-rfq.utils.ts | 240 ++++++++++++++++++++++++ test/fake-provider.connector.ts | 4 +- tsconfig.scripts.json | 14 ++ yarn.lock | 9 +- 8 files changed, 432 insertions(+), 5 deletions(-) create mode 100644 src/utils/limit-order-rfq.const.ts create mode 100644 src/utils/limit-order-rfq.model.ts create mode 100644 src/utils/limit-order-rfq.utils.ts create mode 100644 tsconfig.scripts.json diff --git a/package.json b/package.json index 7a2796e..53bd9a1 100644 --- a/package.json +++ b/package.json @@ -19,11 +19,13 @@ "prettier": "prettier --write .", "make-badges": "istanbul-badges-readme", "ci-pipeline": "yarn run lint && yarn run test && yarn run typecheck", - "set-npm-auth": "echo \"//npm.pkg.github.com/:_authToken=${NPM_AUTH_TOKEN}\" >> .npmrc" + "set-npm-auth": "echo \"//npm.pkg.github.com/:_authToken=${NPM_AUTH_TOKEN}\" >> .npmrc", + "rfq-utils": "ts-node --project tsconfig.scripts.json ./src/utils/limit-order-rfq.utils.ts" }, "dependencies": { "@ethersproject/bignumber": "^5.1.1", "eth-sig-util": "^3.0.1", + "prompts": "^2.4.1", "web3": "^2.0.0-alpha.1" }, "devDependencies": { @@ -31,6 +33,7 @@ "@babel/preset-env": "^7.13.15", "@babel/preset-typescript": "^7.13.0", "@types/jest": "^26.0.22", + "@types/prompts": "^2.0.12", "@typescript-eslint/eslint-plugin": "4", "babel-jest": "^26.6.3", "eslint": "7", diff --git a/src/model/limit-order-protocol.model.ts b/src/model/limit-order-protocol.model.ts index 96e598b..8f97770 100644 --- a/src/model/limit-order-protocol.model.ts +++ b/src/model/limit-order-protocol.model.ts @@ -4,6 +4,7 @@ import {LimitOrderPredicateCallData} from '../limit-order-predicate.builder'; export enum ChainId { etherumMainnet = 1, binanceMainnet = 56, + polygonMainnet = 137, } export type LimitOrderSignature = string; diff --git a/src/utils/limit-order-rfq.const.ts b/src/utils/limit-order-rfq.const.ts new file mode 100644 index 0000000..54fb587 --- /dev/null +++ b/src/utils/limit-order-rfq.const.ts @@ -0,0 +1,133 @@ +import {PromptObject} from 'prompts'; +import {ChainId} from '../model/limit-order-protocol.model'; + +const commonProperties: PromptObject[] = [ + { + type: 'select', + name: 'chainId', + message: 'Select network', + choices: [ + {title: 'Ethereum', value: ChainId.etherumMainnet}, + {title: 'BSC', value: ChainId.binanceMainnet}, + {title: 'Polygon', value: ChainId.polygonMainnet}, + ], + }, + { + type: 'password', + name: 'privateKey', + message: 'Enter your private key', + }, +]; + +export const operationSchema: PromptObject[] = [ + { + type: 'select', + name: 'operation', + choices: [ + {title: 'create', value: 'create'}, + {title: 'fill', value: 'fill'}, + {title: 'cancel', value: 'cancel'}, + ], + message: 'Choose operation for limit order RFQ: create, fill, cancel', + }, +]; + +export const createOrderSchema: PromptObject[] = [ + ...commonProperties, + { + type: 'number', + name: 'orderId', + message: 'Limit order RFQ id', + }, + { + type: 'number', + name: 'expiresIn', + message: + 'Expires in (seconds, for example: 300 - order will expired in 5 mins)', + initial: 300, + }, + { + type: 'text', + name: 'makerAssetAddress', + message: 'Maker asset address', + }, + { + type: 'text', + name: 'takerAssetAddress', + message: 'Taker asset address', + }, + { + type: 'text', + name: 'makerAmount', + message: 'Maker asset amount', + }, + { + type: 'text', + name: 'takerAmount', + message: 'Taker asset amount', + }, + { + type: 'text', + name: 'takerAddress', + message: 'Taker address (optional)', + }, +]; + +export const fillOrderSchema: PromptObject[] = [ + ...commonProperties, + { + type: 'number', + name: 'gasPrice', + message: 'Gas price (GWEI)', + initial: 10, + }, + { + type: 'text', + name: 'order', + message: 'Limit order RFQ json', + }, + { + type: 'text', + name: 'makerAmount', + message: + 'Maker asset fill amount (set 0 if you will use taker asset amount)', + }, + { + type: 'text', + name: 'takerAmount', + message: 'Taker asset amount (set 0 if has set maker asset amount)', + }, +]; + +export const cancelOrderSchema: PromptObject[] = [ + ...commonProperties, + { + type: 'number', + name: 'gasPrice', + message: 'Gas price (GWEI)', + initial: 10, + }, + { + type: 'text', + name: 'orderInfo', + message: 'Order info', + }, +]; + +export const rpcUrls: {[key: number]: string} = { + [ChainId.etherumMainnet]: 'https://web3-node.1inch.exchange', + [ChainId.binanceMainnet]: 'https://bsc-dataseed.binance.org', + [ChainId.polygonMainnet]: 'https://bor-nodes.1inch.exchange', +}; + +export const contractAddresses: {[key: number]: string} = { + [ChainId.etherumMainnet]: '0x3ef51736315f52d568d6d2cf289419b9cfffe782', + [ChainId.binanceMainnet]: '0xe3456f4ee65e745a44ec3bcb83d0f2529d1b84eb', + [ChainId.polygonMainnet]: '0xb707d89d29c189421163515c59e42147371d6857', +}; + +export const explorersUrls: {[key: number]: string} = { + [ChainId.etherumMainnet]: 'https://etherscan.io', + [ChainId.binanceMainnet]: 'https://bscscan.com', + [ChainId.polygonMainnet]: 'https://explorer-mainnet.maticvigil.com', +}; diff --git a/src/utils/limit-order-rfq.model.ts b/src/utils/limit-order-rfq.model.ts new file mode 100644 index 0000000..610154e --- /dev/null +++ b/src/utils/limit-order-rfq.model.ts @@ -0,0 +1,31 @@ +export interface CreatingParams { + chainId: number; + privateKey: string; + orderId: number; + expiresIn: number; + makerAssetAddress: string; + takerAssetAddress: string; + makerAmount: string; + takerAmount: string; + takerAddress: string; +} + +export interface FillingParams { + chainId: number; + privateKey: string; + gasPrice: number; + order: string; + makerAmount: string; + takerAmount: string; +} + +export interface CancelingParams { + chainId: number; + privateKey: string; + gasPrice: number; + orderInfo: string; +} + +export interface OperationParams { + operation: string; +} diff --git a/src/utils/limit-order-rfq.utils.ts b/src/utils/limit-order-rfq.utils.ts new file mode 100644 index 0000000..b83cbf4 --- /dev/null +++ b/src/utils/limit-order-rfq.utils.ts @@ -0,0 +1,240 @@ +import prompts from 'prompts'; +import kleur from 'kleur'; +import Web3 from 'web3'; +import {FakeProviderConnector} from '../../test/fake-provider.connector'; +import {LimitOrderBuilder} from '../limit-order.builder'; +import {LimitOrderRFQ} from '../model/limit-order-protocol.model'; +import {LimitOrderProtocolFacade} from '../limit-order-protocol.facade'; +import { + cancelOrderSchema, + contractAddresses, + createOrderSchema, + explorersUrls, + fillOrderSchema, + operationSchema, + rpcUrls, +} from './limit-order-rfq.const'; +import { + CancelingParams, + CreatingParams, + FillingParams, + OperationParams, +} from './limit-order-rfq.model'; +import {TransactionConfig} from 'web3-core'; + +(async () => { + const operationResult = (await prompts(operationSchema)) as OperationParams; + + switch (operationResult.operation) { + case 'create': + await createOrderOperation(); + break; + case 'fill': + await fillOrderOperation(); + break; + case 'cancel': + await cancelOrderOperation(); + break; + default: + console.log('Unknown operation: ', operationResult.operation); + break; + } +})(); + +async function createOrderOperation() { + const creatingParams = (await prompts(createOrderSchema)) as CreatingParams; + + const newOrder = createOrder(creatingParams); + + console.log(kleur.green().bold('New limit order RFQ: ')); + console.log(kleur.white().underline(JSON.stringify(newOrder, null, 4))); +} + +async function fillOrderOperation() { + const fillingParams = (await prompts(fillOrderSchema)) as FillingParams; + const orderForFill: LimitOrderRFQ = JSON.parse(fillingParams.order); + + console.log(kleur.green().bold('Order for filling: ')); + console.log(kleur.white().underline(JSON.stringify(orderForFill, null, 4))); + + const callDataForFill = await fillOrder(fillingParams, orderForFill); + + console.log(kleur.green().bold('Order filling transaction: ')); + printTransactionLink( + explorerTxLink(fillingParams.chainId, callDataForFill) + ); +} + +async function cancelOrderOperation() { + const cancelingParams = (await prompts( + cancelOrderSchema + )) as CancelingParams; + + const cancelingTxHash = await cancelOrder(cancelingParams); + + console.log(kleur.green().bold('Order canceling transaction: ')); + printTransactionLink( + explorerTxLink(cancelingParams.chainId, cancelingTxHash) + ); +} + +/* eslint-disable max-lines-per-function */ +function createOrder(params: CreatingParams): LimitOrderRFQ { + const contractAddress = contractAddresses[params.chainId]; + const web3 = new Web3(rpcUrls[params.chainId]); + const providerConnector = new FakeProviderConnector( + params.privateKey, + web3 + ); + const walletAddress = web3.eth.accounts.privateKeyToAccount( + params.privateKey + ).address; + + const limitOrderBuilder = new LimitOrderBuilder( + contractAddress, + params.chainId, + providerConnector + ); + + return limitOrderBuilder.buildOrderRFQ({ + id: params.orderId, + expiresInTimestampMs: Date.now() + params.expiresIn * 1000, + makerAddress: walletAddress, + makerAssetAddress: params.makerAssetAddress, + takerAssetAddress: params.takerAssetAddress, + makerAmount: params.makerAmount, + takerAmount: params.takerAmount, + takerAddress: params.takerAddress || undefined, + }); +} +/* eslint-enable max-lines-per-function */ + +/* eslint-disable max-lines-per-function */ +async function fillOrder( + params: FillingParams, + order: LimitOrderRFQ +): Promise { + const contractAddress = contractAddresses[params.chainId]; + const web3 = new Web3(rpcUrls[params.chainId]); + const providerConnector = new FakeProviderConnector( + params.privateKey, + web3 + ); + const walletAddress = web3.eth.accounts.privateKeyToAccount( + params.privateKey + ).address; + + const limitOrderBuilder = new LimitOrderBuilder( + contractAddress, + params.chainId, + providerConnector + ); + const limitOrderProtocolFacade = new LimitOrderProtocolFacade( + contractAddress, + providerConnector + ); + + const typedData = limitOrderBuilder.buildOrderRFQTypedData(order); + const signature = await limitOrderBuilder.buildOrderSignature( + walletAddress, + typedData + ); + + const callData = limitOrderProtocolFacade.fillOrderRFQ( + order, + signature, + params.makerAmount, + params.takerAmount + ); + + const txConfig: TransactionConfig = { + to: contractAddress, + from: walletAddress, + data: callData, + value: '0', + gas: 120_000, + gasPrice: gweiToWei(params.gasPrice), + nonce: await web3.eth.getTransactionCount(walletAddress), + }; + + return sendSignedTransaction(web3, txConfig, params.privateKey); +} +/* eslint-enable max-lines-per-function */ + +/* eslint-disable max-lines-per-function */ +async function cancelOrder(params: CancelingParams): Promise { + const contractAddress = contractAddresses[params.chainId]; + const web3 = new Web3( + new Web3.providers.HttpProvider(rpcUrls[params.chainId]) + ); + const providerConnector = new FakeProviderConnector( + params.privateKey, + web3 + ); + const walletAddress = web3.eth.accounts.privateKeyToAccount( + params.privateKey + ).address; + + const limitOrderProtocolFacade = new LimitOrderProtocolFacade( + contractAddress, + providerConnector + ); + + const callData = limitOrderProtocolFacade.cancelOrderRFQ(params.orderInfo); + const txConfig: TransactionConfig = { + to: contractAddress, + from: walletAddress, + data: callData, + value: '0', + gas: 50_000, + gasPrice: gweiToWei(params.gasPrice), + nonce: await web3.eth.getTransactionCount(walletAddress), + }; + + return sendSignedTransaction(web3, txConfig, params.privateKey); +} +/* eslint-enable max-lines-per-function */ + +async function sendSignedTransaction( + web3: Web3, + txConfig: TransactionConfig, + privateKey: string +): Promise { + const sign = await web3.eth.accounts.signTransaction(txConfig, privateKey); + + return await new Promise((resolve, reject) => { + web3.eth.sendSignedTransaction( + sign.rawTransaction as string, + (error, hash) => { + if (error) { + reject(error); + return; + } + + resolve(hash); + } + ); + }); +} + +function explorerTxLink(chainId: number, txHash: string): string { + const explorerUrl = explorersUrls[chainId]; + + return `${explorerUrl}/tx/${txHash}`; +} + +function gweiToWei(value: number): string { + return value + '000000000'; +} + +function printTransactionLink(text: string): void { + console.log( + kleur.white('************************************************') + ); + console.log(kleur.white(' ')); + console.log(kleur.white().underline(text)); + console.log(kleur.white(' ')); + console.log( + kleur.white('************************************************') + ); +} diff --git a/test/fake-provider.connector.ts b/test/fake-provider.connector.ts index 9f3d855..31f7abd 100644 --- a/test/fake-provider.connector.ts +++ b/test/fake-provider.connector.ts @@ -8,12 +8,10 @@ export class FakeProviderConnector extends Web3ProviderConnector { } signTypedData( - walletAddress: string, + _walletAddress: string, typedData: EIP712TypedData, _typedDataHash: string ): Promise { - console.log(walletAddress); - const result = signTypedData_v4(Buffer.from(this.privateKey, 'hex'), { data: typedData, } as any); diff --git a/tsconfig.scripts.json b/tsconfig.scripts.json new file mode 100644 index 0000000..e557407 --- /dev/null +++ b/tsconfig.scripts.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "experimentalDecorators": true, + "moduleResolution": "node", + "importHelpers": true, + "module": "commonjs", + "target": "ES2017", + "resolveJsonModule": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "lib": ["es2018", "dom", "es2019", "esnext"], + "types": ["node"] + } +} diff --git a/yarn.lock b/yarn.lock index e0a619a..1e1fded 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1273,6 +1273,13 @@ resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.2.3.tgz#ef65165aea2924c9359205bf748865b8881753c0" integrity sha512-PijRCG/K3s3w1We6ynUKdxEc5AcuuH3NBmMDP8uvKVp6X43UY7NQlTzczakXP3DJR0F4dfNQIGjU2cUeRYs2AA== +"@types/prompts@^2.0.12": + version "2.0.12" + resolved "https://registry.yarnpkg.com/@types/prompts/-/prompts-2.0.12.tgz#5cc1557f88e4d69dad93230fff97a583006f858b" + integrity sha512-Hr6osqfNg3IcQT3pJDXCsSnb0KnldY/hXeJCKJriwbZLnedN9n1e8kcZwLc25GIWULDb6h5aEyOBbf33XpZBXQ== + dependencies: + "@types/node" "*" + "@types/secp256k1@^4.0.1": version "4.0.2" resolved "https://registry.yarnpkg.com/@types/secp256k1/-/secp256k1-4.0.2.tgz#20c29a87149d980f64464e56539bf4810fdb5d1d" @@ -5650,7 +5657,7 @@ progress@^2.0.0: resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== -prompts@^2.0.1: +prompts@^2.0.1, prompts@^2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.1.tgz#befd3b1195ba052f9fd2fde8a486c4e82ee77f61" integrity sha512-EQyfIuO2hPDsX1L/blblV+H7I0knhgAd82cVneCwcdND9B8AuCDuRcBH6yIcG4dFzlOUqbazQqwGjx5xmsNLuQ==