From 17a70b41558ceacc6bc8cd3e107ec4e18b0014df Mon Sep 17 00:00:00 2001 From: microwavedcola1 Date: Fri, 24 Sep 2021 16:10:08 +0200 Subject: [PATCH] withdraw Signed-off-by: microwavedcola1 --- README.md | 1 - .../service-v3.postman_collection.json | 91 ++++++++++++++++++- mango-service-v3/service-v3.yml | 54 +++++++++++ mango-service-v3/src/mango.simple.client.ts | 66 ++++++++++---- mango-service-v3/src/utils.ts | 11 +++ mango-service-v3/src/wallet.controller.ts | 53 ++++++++++- 6 files changed, 252 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 48056e9..3837393 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,6 @@ losely sorted in order of importance/priority - stop loss, - market orders - modify order - - funding rates - withdraw - funding payments - advanced order types e.g. split diff --git a/mango-service-v3/service-v3.postman_collection.json b/mango-service-v3/service-v3.postman_collection.json index 10fe12b..63c1280 100644 --- a/mango-service-v3/service-v3.postman_collection.json +++ b/mango-service-v3/service-v3.postman_collection.json @@ -277,6 +277,95 @@ } ] }, + { + "name": "wallet - withdraw", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"coin\": \"USDC\",\n \"size\": 1000\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/wallet/withdrawals", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "wallet", + "withdrawals" + ] + } + }, + "response": [ + { + "name": "wallet - withdraw", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"coin\": \"USDC\",\n \"size\": 1000\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/wallet/withdrawals", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "wallet", + "withdrawals" + ] + } + }, + "status": "Bad Request", + "code": 400, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "X-Powered-By", + "value": "Express" + }, + { + "key": "Content-Type", + "value": "application/json; charset=utf-8" + }, + { + "key": "Content-Length", + "value": "98" + }, + { + "key": "ETag", + "value": "W/\"62-a5tgkKEcwWc3BLd/jTRTDN5QwDI\"" + }, + { + "key": "Date", + "value": "Fri, 24 Sep 2021 14:00:15 GMT" + }, + { + "key": "Connection", + "value": "keep-alive" + }, + { + "key": "Keep-Alive", + "value": "timeout=5" + } + ], + "cookie": [], + "body": "{\n \"errors\": [\n {\n \"msg\": \"Transaction failed: MangoErrorCode::InsufficientFunds; src/processor.rs:831\"\n }\n ]\n}" + } + ] + }, { "name": "markets - get all", "request": { @@ -875,7 +964,7 @@ "header": [], "body": { "mode": "raw", - "raw": "{\n \"market\": \"BTC-PERP\",\n \"side\": \"buy\",\n \"price\": 20000,\n \"type\": \"limit\",\n \"size\": 0.0001,\n \"reduceOnly\": false,\n \"ioc\": false,\n \"postOnly\": false,\n \"clientId\": 123\n}\n", + "raw": "{\n \"market\": \"BTC-PERP\",\n \"side\": \"buy\",\n \"price\": 20000,\n \"type\": \"limit\",\n \"size\": 0.0001,\n \"reduceOnly\": false,\n \"ioc\": false,\n \"postOnly\": false,\n \"clientId\": \"{{$randomInt}}\"\n}\n", "options": { "raw": { "language": "json" diff --git a/mango-service-v3/service-v3.yml b/mango-service-v3/service-v3.yml index f618d34..e635439 100644 --- a/mango-service-v3/service-v3.yml +++ b/mango-service-v3/service-v3.yml @@ -205,6 +205,60 @@ paths: total: 50.000004999999994 usdValue: 50.000004999999994 availableWithoutBorrow: 50.000004999999994 + /wallet/withdrawals: + post: + tags: + - default + summary: wallet - withdraw + requestBody: + content: + application/json: + schema: + type: object + example: + coin: USDC + size: 1000 + responses: + '400': + description: Bad Request + headers: + X-Powered-By: + schema: + type: string + example: Express + Content-Type: + schema: + type: string + example: application/json; charset=utf-8 + Content-Length: + schema: + type: integer + example: '98' + ETag: + schema: + type: string + example: W/"62-a5tgkKEcwWc3BLd/jTRTDN5QwDI" + Date: + schema: + type: string + example: Fri, 24 Sep 2021 14:00:15 GMT + Connection: + schema: + type: string + example: keep-alive + Keep-Alive: + schema: + type: string + example: timeout=5 + content: + application/json: + schema: + type: object + example: + errors: + - msg: >- + Transaction failed: MangoErrorCode::InsufficientFunds; + src/processor.rs:831 /markets: get: tags: diff --git a/mango-service-v3/src/mango.simple.client.ts b/mango-service-v3/src/mango.simple.client.ts index b13757e..e72f28d 100644 --- a/mango-service-v3/src/mango.simple.client.ts +++ b/mango-service-v3/src/mango.simple.client.ts @@ -6,6 +6,7 @@ import { getMarketByBaseSymbolAndKind, getMarketByPublicKey, getMultipleAccounts, + getTokenBySymbol, GroupConfig, MangoAccount, MangoClient, @@ -23,12 +24,14 @@ import { Commitment, Connection, PublicKey, + TransactionSignature, } from "@solana/web3.js"; import fs from "fs"; import fetch from "node-fetch"; import os from "os"; import { OrderInfo } from "types"; import { logger, zipDict } from "./utils"; +import BN from "bn.js"; class MangoSimpleClient { constructor( @@ -42,25 +45,6 @@ class MangoSimpleClient { setInterval(this.roundRobinClusterUrl, 20_000); } - private roundRobinClusterUrl() { - if (process.env.CLUSTER_URL) { - return; - } - - let possibleClustersUrls = [ - "https://api.mainnet-beta.solana.com", - "https://lokidfxnwlabdq.main.genesysgo.net:8899/", - "https://solana-api.projectserum.com/", - ]; - const clusterUrl = - possibleClustersUrls[ - Math.floor(Math.random() * possibleClustersUrls.length) - ]; - - logger.info(`switching to rpc node - ${clusterUrl}...`); - this.connection = new Connection(clusterUrl, "processed" as Commitment); - } - static async create() { const groupName = "mainnet.1"; const clusterUrl = @@ -429,7 +413,8 @@ class MangoSimpleClient { side, price, quantity, - orderType + orderType, + new BN(clientOrderId) ); } } @@ -510,6 +495,28 @@ class MangoSimpleClient { return orderInfos; } + public async withdraw( + tokenSymbol: string, + amount: number + ): Promise { + const tokenToWithdraw = getTokenBySymbol( + this.mangoGroupConfig, + tokenSymbol + ); + const tokenIndex = this.mangoGroup.getTokenIndex(tokenToWithdraw.mintKey); + return this.client.withdraw( + this.mangoGroup, + this.mangoAccount, + this.owner, + this.mangoGroup.tokens[tokenIndex].rootBank, + this.mangoGroup.rootBankAccounts[tokenIndex].nodeBankAccounts[0] + .publicKey, + this.mangoGroup.rootBankAccounts[tokenIndex].nodeBankAccounts[0].vault, + Number(amount), + false + ); + } + /// private private parseSpotOrders( @@ -572,6 +579,25 @@ class MangoSimpleClient { market: { account: market, config }, })); } + + private roundRobinClusterUrl() { + if (process.env.CLUSTER_URL) { + return; + } + + let possibleClustersUrls = [ + "https://api.mainnet-beta.solana.com", + "https://lokidfxnwlabdq.main.genesysgo.net:8899/", + "https://solana-api.projectserum.com/", + ]; + const clusterUrl = + possibleClustersUrls[ + Math.floor(Math.random() * possibleClustersUrls.length) + ]; + + logger.info(`switching to rpc node - ${clusterUrl}...`); + this.connection = new Connection(clusterUrl, "processed" as Commitment); + } } export default MangoSimpleClient; diff --git a/mango-service-v3/src/utils.ts b/mango-service-v3/src/utils.ts index 7f714d7..173c3c5 100644 --- a/mango-service-v3/src/utils.ts +++ b/mango-service-v3/src/utils.ts @@ -22,6 +22,10 @@ const allMarketNames = mangoGroupConfig.spotMarkets ) ); +const allCoins = mangoGroupConfig.tokens.map( + (tokenConfig) => tokenConfig.symbol +); + /// general export function zipDict( @@ -47,3 +51,10 @@ export const isValidMarket: CustomValidator = (marketName) => { } return Promise.resolve(); }; + +export const isValidCoin: CustomValidator = (coin) => { + if (allCoins.indexOf(coin) === -1) { + return Promise.reject(`Coin ${coin} not supported!`); + } + return Promise.resolve(); +}; diff --git a/mango-service-v3/src/wallet.controller.ts b/mango-service-v3/src/wallet.controller.ts index 76eabf7..7b7114b 100644 --- a/mango-service-v3/src/wallet.controller.ts +++ b/mango-service-v3/src/wallet.controller.ts @@ -7,11 +7,13 @@ import { } from "@blockworks-foundation/mango-client"; import { OpenOrders } from "@project-serum/serum"; import Controller from "controller.interface"; -import { NextFunction, Request, Response, Router } from "express"; +import { BadRequestErrorCustom } from "dtos"; +import e, { NextFunction, Request, Response, Router } from "express"; +import { body } from "express-validator"; import { sumBy } from "lodash"; import MangoSimpleClient from "mango.simple.client"; import { Balances } from "./types"; -import { i80f48ToPercent } from "./utils"; +import { i80f48ToPercent, isValidCoin } from "./utils"; class WalletController implements Controller { public path = "/api/wallet"; @@ -22,7 +24,16 @@ class WalletController implements Controller { } private initializeRoutes() { + // POST /wallet/balances this.router.get(`${this.path}/balances`, this.fetchBalances); + + // POST /wallet/withdrawals + this.router.post( + `${this.path}/withdrawals`, + body("coin").not().isEmpty().custom(isValidCoin), + body("size").isNumeric(), + this.withdraw + ); } private fetchBalances = async ( @@ -214,6 +225,24 @@ class WalletController implements Controller { response.send({ success: true, result: balanceDtos } as BalancesDto); }; + + private withdraw = async ( + request: Request, + response: Response, + next: NextFunction + ) => { + const withdrawDto = request.body as WithdrawDto; + this.mangoSimpleClient + .withdraw(withdrawDto.coin, withdrawDto.size) + .then(() => { + response.status(200); + }) + .catch((error) => { + return response.status(400).send({ + errors: [{ msg: error.message } as BadRequestErrorCustom], + }); + }); + }; } export default WalletController; @@ -248,3 +277,23 @@ interface BalanceDto { usdValue: number; availableWithoutBorrow: number; } + +// e.g. +// { +// "coin": "USDTBEAR", +// "size": 20.2, +// "address": "0x83a127952d266A6eA306c40Ac62A4a70668FE3BE", +// "tag": null, +// "password": "my_withdrawal_password", +// "code": 152823 +// } + +interface WithdrawDto { + coin: string; + size: number; + // unused + address: undefined; + tag: undefined; + password: undefined; + code: undefined; +}