From 55d8fc140f68a7deef737c28faa411332fc1de26 Mon Sep 17 00:00:00 2001 From: Christopher Turner Date: Wed, 25 Aug 2021 20:18:03 +1000 Subject: [PATCH 1/7] three commas api service --- config.json | 7 ++ package-lock.json | 19 ++++++ package.json | 2 + src/services/three-commas.js | 122 +++++++++++++++++++++++++++++++++++ src/utils.js | 21 ++++++ 5 files changed, 171 insertions(+) create mode 100644 config.json create mode 100644 src/services/three-commas.js create mode 100644 src/utils.js diff --git a/config.json b/config.json new file mode 100644 index 0000000..68aac5b --- /dev/null +++ b/config.json @@ -0,0 +1,7 @@ +{ + "threeCommas": { + "baseURL": "https://api.3commas.io/public/api", + "apiKey": "INSERT API KEY", + "secretKey": "INSERT SECRET KEY" + } +} diff --git a/package-lock.json b/package-lock.json index e8416bb..2ddd916 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2327,6 +2327,11 @@ } } }, + "dedupe": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/dedupe/-/dedupe-3.0.2.tgz", + "integrity": "sha512-1BmvzpTdYjyvzTMgd8+A7R9jNJh8GfvVZJqREZpqq7OpePtVFNdw56FjsHFdC35ROSng6y+h4qFYO/VkiKEztg==" + }, "deep-eql": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", @@ -4660,6 +4665,15 @@ "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" }, + "json-templates": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/json-templates/-/json-templates-4.1.0.tgz", + "integrity": "sha512-Xjcnphott7Kj09zTSvszDVMXNa6utrQjXR25oxFMGKRNuX9wYQsJgKeQWVAxHE26I8lPUriIJ9nba1fCCBiI4Q==", + "requires": { + "dedupe": "^3.0.2", + "object-path": "^0.11.4" + } + }, "jsonfile": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", @@ -5598,6 +5612,11 @@ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" }, + "object-path": { + "version": "0.11.5", + "resolved": "https://registry.npmjs.org/object-path/-/object-path-0.11.5.tgz", + "integrity": "sha512-jgSbThcoR/s+XumvGMTMf81QVBmah+/Q7K7YduKeKVWL7N111unR2d6pZZarSk6kY/caeNxUDyxOvMWyzoU2eg==" + }, "object.assign": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", diff --git a/package.json b/package.json index cad4cb2..59105c7 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,9 @@ "express": "^4.17.1", "googleapis": "^39.2.0", "gs": "0.0.2", + "json-templates": "^4.1.0", "lodash": "^4.17.21", + "node-fetch": "^2.6.1", "qs": "^6.10.1", "readline": "^1.3.0", "request": "^2.88.2", diff --git a/src/services/three-commas.js b/src/services/three-commas.js new file mode 100644 index 0000000..acf9322 --- /dev/null +++ b/src/services/three-commas.js @@ -0,0 +1,122 @@ +const { join } = require("path"); +const fetch = require("node-fetch"); +const { threeCommas: config } = require("../../config.json"); +const { sign } = require("../utils"); + +// make the api object +module.exports = factory({ + getDeals: { + method: "GET", + path: "/ver1/deals", + signed: true, + }, +}); + +/** + * Factory function for build an object for the 3Commas API service. + * + * @example + * // example of the API definition object + * const definitions = { + * methodName: { + * method:"GET|POST|PUT|...", + * path: "/api/path", + * signed: true, // use a signed request + * } + * }; + * + * @param {Object} definitions + * @returns {Object} + */ +function factory(definitions) { + const api = {}; + + for (const [name, define] of Object.entries(definitions)) { + const { method, path, signed } = define; + const func = signed ? signedRequest : request; + + api[name] = func.bind(null, method, path); + } + + return api; +} + +/** + * Makes a generic request to 3Commas API service. + * + * @param {string} method + * @param {string} apiPath + * @param {Object} [params] + * @param {Object} [headers] + * @returns {*} + */ +async function request(method, apiPath, params = {}, headers = {}) { + let body = method !== "GET" ? params : undefined; + const url = toURL(apiPath, body ? undefined : params); + + // use the params as the body for non-GET requests + if (body) { + const searchParams = new URLSearchParams(params); + body = searchParams.toString(); + } + + const response = await fetch(url.toString(), { + method, + headers, + body, + }); + + return response.json(); +} + +/** + * Makes a signed request to 3Commas API service. + * + * @param {string} method + * @param {string} apiPath + * @param {Object} [params] + * @param {Object} [headers] + * @returns {*} + */ +function signedRequest(method, apiPath, params = {}, headers = {}) { + const { apiKey, secretKey } = config; + const fullURL = toURL(apiPath, params); + const pathToSign = toPathWithQueryString(fullURL); + const signature = sign(pathToSign, secretKey); + + return request(method, apiPath, params, { + APIKEY: apiKey, + Signature: signature, + ...headers, + }); +} + +/** + * Constructs a full 3Commas URL with a given API path. + * + * @param {string} apiPath + * @param {Object} [params] + * @returns {URL} + */ +function toURL(apiPath, params = {}) { + const baseURL = new URL(config.baseURL); + const path = join(baseURL.pathname, apiPath); + const url = new URL(path, baseURL.origin); + + for (const [name, value] of Object.entries(params)) { + url.searchParams.set(name, value); + } + + return url; +} + +/** + * Converts a URL to path with query string appended. + * + * @param {URL} url + * @returns {string} + */ +function toPathWithQueryString(url) { + const urlString = url.toString(); + return urlString.substr(url.origin.length); +} diff --git a/src/utils.js b/src/utils.js new file mode 100644 index 0000000..bfd6bdb --- /dev/null +++ b/src/utils.js @@ -0,0 +1,21 @@ +const { createHmac } = require("crypto"); +const { threeCommas: config } = require("../config.json"); + +module.exports = { + sign, +}; + +/** + * Signs data with a secret key. If no key given it will use the + * `threeCommas.secretKey` set in `config.json`. + * + * @param {import("crypto").BinaryLike} data + * @param {import("crypto").BinaryLike} [secretKey] + * @returns {string} + */ +function sign(data, secretKey = config.secretKey) { + const hash = createHmac("SHA256", secretKey); + const sig = hash.update(data).digest("hex"); + + return sig; +} From 7a5a4be00299c907d797bb35185ef01ce9408fe4 Mon Sep 17 00:00:00 2001 From: Christopher Turner Date: Wed, 25 Aug 2021 23:14:21 +1000 Subject: [PATCH 2/7] adds auto trail function --- config.json | 12 +++ package-lock.json | 75 +++++++++++++++++- package.json | 2 + src/functions/auto-trail.js | 118 +++++++++++++++++++++++++++++ src/index.js | 5 ++ src/{services => }/three-commas.js | 45 +++++++++-- src/utils.js | 8 +- 7 files changed, 250 insertions(+), 15 deletions(-) create mode 100644 src/functions/auto-trail.js create mode 100644 src/index.js rename src/{services => }/three-commas.js (71%) diff --git a/config.json b/config.json index 68aac5b..3380653 100644 --- a/config.json +++ b/config.json @@ -3,5 +3,17 @@ "baseURL": "https://api.3commas.io/public/api", "apiKey": "INSERT API KEY", "secretKey": "INSERT SECRET KEY" + }, + "functions": { + "autoTrail": [ + { + "enabled": true, + "accountId": "INSERT ACCOUNT", + "ignore": ["/BOT NAME REGEX/", 123456], + "minSafetyOrders": 3, + "takeProfit": 2, + "trailing": 0.5 + } + ] } } diff --git a/package-lock.json b/package-lock.json index 2ddd916..e85e948 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2163,11 +2163,18 @@ "integrity": "sha512-RI/Hh4kqRc1UKLOAf/T5zdMMX5DQIlDxwUe3wSyMMnEbGunnpENCdbUgM+dW7kXidZqCttBrmw7BhN4TMddkCw==" }, "debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", "requires": { - "ms": "^2.1.1" + "ms": "2.1.2" + }, + "dependencies": { + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } } }, "decamelize": { @@ -4141,6 +4148,16 @@ "requires": { "agent-base": "^4.3.0", "debug": "^3.1.0" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "requires": { + "ms": "^2.1.1" + } + } } }, "human-signals": { @@ -4355,6 +4372,14 @@ "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==" }, + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "requires": { + "is-plain-object": "^2.0.4" + } + }, "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -4439,6 +4464,14 @@ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=" }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "requires": { + "isobject": "^3.0.1" + } + }, "is-promise": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", @@ -4509,6 +4542,11 @@ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + }, "isomorphic-ws": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz", @@ -5649,6 +5687,19 @@ "es-abstract": "^1.18.0-next.2" } }, + "object.omit": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-3.0.0.tgz", + "integrity": "sha512-EO+BCv6LJfu+gBIF3ggLicFebFLN5zqzz/WWJlMFfkMyGth+oBkhxzDl0wx2W4GkLzuQs/FsSkXZb2IMWQqmBQ==", + "requires": { + "is-extendable": "^1.0.0" + } + }, + "omit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/omit/-/omit-1.0.1.tgz", + "integrity": "sha1-BYoBtLbbEoezbstY36jhh3iVgBc=" + }, "on-finished": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", @@ -6003,6 +6054,14 @@ "requires": { "lodash": "^4.17.14" } + }, + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "requires": { + "ms": "^2.1.1" + } } } }, @@ -7357,6 +7416,14 @@ "readable-stream": "^2.3.5" }, "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "requires": { + "ms": "^2.1.1" + } + }, "mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", diff --git a/package.json b/package.json index 59105c7..feb5de5 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "axios": "^0.21.1", "crypto-js": "^4.0.0", "date-fns": "^2.23.0", + "debug": "^4.3.2", "ejs": "^3.1.6", "express": "^4.17.1", "googleapis": "^39.2.0", @@ -12,6 +13,7 @@ "json-templates": "^4.1.0", "lodash": "^4.17.21", "node-fetch": "^2.6.1", + "object.omit": "^3.0.0", "qs": "^6.10.1", "readline": "^1.3.0", "request": "^2.88.2", diff --git a/src/functions/auto-trail.js b/src/functions/auto-trail.js new file mode 100644 index 0000000..574e64d --- /dev/null +++ b/src/functions/auto-trail.js @@ -0,0 +1,118 @@ +const debug = require("debug")("3commas-control:auto-trailing"); +const { functions } = require("../../config.json"); +const { getDeals, updateDeal } = require("../three-commas"); + +const autoTrails = functions.autoTrail; + +/** + * Auto trail function. Queries active deals looking for potential trailing take + * profit opportunities defined in `config.json`. + * + * @returns {Promise} + */ +module.exports = async function autoTrail() { + const updates = []; + + for (let i = 0, l = autoTrails.length; i < l; ++i) { + const { + enabled, + accountId, + ignore, + minSafetyOrders, + takeProfit, + trailing, + } = autoTrails[i]; + + if (!enabled) { + continue; + } + + const params = { + scope: "active", + account_id: accountId, + }; + + for await (const deal of iterate(params)) { + if (!shouldTrail(deal, minSafetyOrders, ignore)) { + debug("%s skipping", deal.bot_name); + continue; + } + + const update = updateDeal({ + deal_id: deal.id, + trailing_enabled: true, + take_profit: takeProfit, + trailing_deviation: trailing, + }); + + updates.push(update); + + debug( + "%s trailing enabled (TTP %d% / %d%)", + deal.bot_name, + takeProfit, + trailing + ); + } + } + + return Promise.all(updates); +}; + +/** + * Utility iterator for looping over deals. + * + * @param {Object} [params] + * @returns {AsyncIterator} + */ +async function* iterate({ limit = 1000, offset = 0, ...params }) { + const deals = await getDeals({ + ...params, + limit, + offset, + }); + + for (let i = 0; i < deals.length; ++i) { + yield deals[i]; + } + + if (deals.length === limit) { + yield* iterateDeals({ + ...params, + offset: offset + limit, + limit, + }); + } +} + +/** + * Returns if a given deal should enable trailing take profits. + * + * @param {Object} deal + * @param {number} minSafetyOrders + * @param {Array} [ignore] + * @returns {boolean} + */ +function shouldTrail(deal, minSafetyOrders, ignore = []) { + // already trailing, don't touch it + if (deal.trailing_enabled) { + return false; + } + + // doesn't have minimum safet orders + if (deal.completed_safety_orders_count < minSafetyOrders) { + return false; + } + + // should bot be ignored + return !ignore.some((predicate) => { + // match bot_ids + if (typeof predicate === "number") { + return deal.bot_id === predicate; + } + + // match bot_names + const regexp = new RegExp(predicate); + return regexp.test(deal.bot_name); + }); +} diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..b2981fe --- /dev/null +++ b/src/index.js @@ -0,0 +1,5 @@ +const autoTrail = require("./functions/auto-trail"); + +module.exports = { + autoTrail, +}; diff --git a/src/services/three-commas.js b/src/three-commas.js similarity index 71% rename from src/services/three-commas.js rename to src/three-commas.js index acf9322..0876c50 100644 --- a/src/services/three-commas.js +++ b/src/three-commas.js @@ -1,14 +1,25 @@ const { join } = require("path"); const fetch = require("node-fetch"); -const { threeCommas: config } = require("../../config.json"); -const { sign } = require("../utils"); +const parse = require("json-templates"); +const omit = require("object.omit"); +const debug = require("debug")("3commas-control:api"); +const { threeCommas } = require("../config.json"); +const { sign } = require("./utils"); -// make the api object +/** + * Builds the 3Commas API object. + * + */ module.exports = factory({ getDeals: { + signed: true, method: "GET", path: "/ver1/deals", + }, + updateDeal: { signed: true, + method: "PATCH", + path: "/ver1/deals/{{deal_id}}/update_deal", }, }); @@ -51,6 +62,8 @@ function factory(definitions) { * @returns {*} */ async function request(method, apiPath, params = {}, headers = {}) { + [apiPath, params] = replacePathParams(apiPath, params); + let body = method !== "GET" ? params : undefined; const url = toURL(apiPath, body ? undefined : params); @@ -60,10 +73,15 @@ async function request(method, apiPath, params = {}, headers = {}) { body = searchParams.toString(); } + debug("%s %s...", method, url.toString()); + const response = await fetch(url.toString(), { method, - headers, body, + headers: { + "Content-Type": "application/x-www-form-urlencoded", + ...headers, + }, }); return response.json(); @@ -79,7 +97,9 @@ async function request(method, apiPath, params = {}, headers = {}) { * @returns {*} */ function signedRequest(method, apiPath, params = {}, headers = {}) { - const { apiKey, secretKey } = config; + [apiPath, params] = replacePathParams(apiPath, params); + + const { apiKey, secretKey } = threeCommas; const fullURL = toURL(apiPath, params); const pathToSign = toPathWithQueryString(fullURL); const signature = sign(pathToSign, secretKey); @@ -99,7 +119,7 @@ function signedRequest(method, apiPath, params = {}, headers = {}) { * @returns {URL} */ function toURL(apiPath, params = {}) { - const baseURL = new URL(config.baseURL); + const baseURL = new URL(threeCommas.baseURL); const path = join(baseURL.pathname, apiPath); const url = new URL(path, baseURL.origin); @@ -120,3 +140,16 @@ function toPathWithQueryString(url) { const urlString = url.toString(); return urlString.substr(url.origin.length); } + +function replacePathParams(path, params = {}) { + const template = parse(path); + + if (template.parameters.length) { + const omitKeys = template.parameters.map(({ key }) => key); + + path = template(params); + params = omit(params, omitKeys); + } + + return [path, params]; +} diff --git a/src/utils.js b/src/utils.js index bfd6bdb..ae541a8 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,19 +1,17 @@ const { createHmac } = require("crypto"); -const { threeCommas: config } = require("../config.json"); module.exports = { sign, }; /** - * Signs data with a secret key. If no key given it will use the - * `threeCommas.secretKey` set in `config.json`. + * Signs data with a secret key. * * @param {import("crypto").BinaryLike} data - * @param {import("crypto").BinaryLike} [secretKey] + * @param {import("crypto").BinaryLike} secretKey * @returns {string} */ -function sign(data, secretKey = config.secretKey) { +function sign(data, secretKey) { const hash = createHmac("SHA256", secretKey); const sig = hash.update(data).digest("hex"); From 56bc4b82276eaf296127814a2b9247ecf8470b81 Mon Sep 17 00:00:00 2001 From: Christopher Turner Date: Thu, 26 Aug 2021 18:29:44 +1000 Subject: [PATCH 3/7] converted auto-trail function to google pub/sub event --- .env.sample | 3 ++ .gitignore | 1 + config.json | 19 ---------- package-lock.json | 62 +++++++++++++++++++++++++++---- package.json | 3 ++ src/functions/auto-trail.js | 73 +++++++++++++++---------------------- src/index.js | 4 +- src/three-commas.js | 25 ++++++++++--- src/utils.js | 5 +++ 9 files changed, 118 insertions(+), 77 deletions(-) create mode 100644 .env.sample delete mode 100644 config.json diff --git a/.env.sample b/.env.sample new file mode 100644 index 0000000..693c20f --- /dev/null +++ b/.env.sample @@ -0,0 +1,3 @@ +DEBUG="3commas-control:*" +THREE_COMMAS_API_KEY= +THREE_COMMAS_SECRET_KEY= \ No newline at end of file diff --git a/.gitignore b/.gitignore index 433db2e..4cd21c3 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ /serverless.yml /credentials.json /node_modules/ +/.env \ No newline at end of file diff --git a/config.json b/config.json deleted file mode 100644 index 3380653..0000000 --- a/config.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "threeCommas": { - "baseURL": "https://api.3commas.io/public/api", - "apiKey": "INSERT API KEY", - "secretKey": "INSERT SECRET KEY" - }, - "functions": { - "autoTrail": [ - { - "enabled": true, - "accountId": "INSERT ACCOUNT", - "ignore": ["/BOT NAME REGEX/", 123456], - "minSafetyOrders": 3, - "takeProfit": 2, - "trailing": 0.5 - } - ] - } -} diff --git a/package-lock.json b/package-lock.json index e85e948..79952c4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -59,6 +59,42 @@ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.13.15.tgz", "integrity": "sha512-b9COtcAlVEQljy/9fbcMHpG+UIW9ReF+gpaxDHTlZd0c6/UU9ng8zdySAW9sRTzpvcdCHn6bUcbuYUgGzLAWVQ==" }, + "@google-cloud/functions-framework": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@google-cloud/functions-framework/-/functions-framework-1.9.0.tgz", + "integrity": "sha512-5MsxLiBdRU6EaUzhTClm97XqNIiQWXrz93w5KzgBjPyvQya6Nuz98IZbd/A8AKZZBOv9AelzAZ+Atqu076adkg==", + "requires": { + "body-parser": "^1.18.3", + "express": "^4.16.4", + "minimist": "^1.2.5", + "on-finished": "^2.3.0", + "read-pkg-up": "^7.0.1", + "semver": "^7.3.5" + }, + "dependencies": { + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + } + }, + "semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "requires": { + "lru-cache": "^6.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } + }, "@hapi/accept": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/@hapi/accept/-/accept-3.2.4.tgz", @@ -527,6 +563,11 @@ "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } + }, + "dotenv": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz", + "integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==" } } }, @@ -748,6 +789,11 @@ "ms": "2.1.2" } }, + "dotenv": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz", + "integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==" + }, "https-proxy-agent": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", @@ -2601,9 +2647,9 @@ "integrity": "sha1-02UX/iS3zaYfznpQJqACSvr1pDk=" }, "dotenv": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz", - "integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==" + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", + "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==" }, "download": { "version": "8.0.0", @@ -5695,11 +5741,6 @@ "is-extendable": "^1.0.0" } }, - "omit": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/omit/-/omit-1.0.1.tgz", - "integrity": "sha1-BYoBtLbbEoezbstY36jhh3iVgBc=" - }, "on-finished": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", @@ -6839,6 +6880,11 @@ "ms": "2.1.2" } }, + "dotenv": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz", + "integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==" + }, "https-proxy-agent": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", diff --git a/package.json b/package.json index feb5de5..98cc058 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,14 @@ { + "main": "src/index.js", "dependencies": { "3commas-api-node": "^1.0.9", + "@google-cloud/functions-framework": "^1.9.0", "aws-sdk": "^2.889.0", "axios": "^0.21.1", "crypto-js": "^4.0.0", "date-fns": "^2.23.0", "debug": "^4.3.2", + "dotenv": "^10.0.0", "ejs": "^3.1.6", "express": "^4.17.1", "googleapis": "^39.2.0", diff --git a/src/functions/auto-trail.js b/src/functions/auto-trail.js index 574e64d..8e4b197 100644 --- a/src/functions/auto-trail.js +++ b/src/functions/auto-trail.js @@ -1,62 +1,49 @@ const debug = require("debug")("3commas-control:auto-trailing"); -const { functions } = require("../../config.json"); const { getDeals, updateDeal } = require("../three-commas"); - -const autoTrails = functions.autoTrail; +const { parseEventJSON } = require("../utils"); /** - * Auto trail function. Queries active deals looking for potential trailing take - * profit opportunities defined in `config.json`. + * Auto trail function. + * + * Queries active deals looking for potential trailing take profit opportunities. * * @returns {Promise} */ -module.exports = async function autoTrail() { - const updates = []; +module.exports = async function autoTrail(event) { + const { accountId, ignore, minSafetyOrders, takeProfit, trailing } = + parseEventJSON(event); - for (let i = 0, l = autoTrails.length; i < l; ++i) { - const { - enabled, - accountId, - ignore, - minSafetyOrders, - takeProfit, - trailing, - } = autoTrails[i]; + const params = { + scope: "active", + account_id: accountId, + }; + + const updates = []; - if (!enabled) { + for await (const deal of iterate(params)) { + if (!shouldTrail(deal, minSafetyOrders, ignore)) { + debug("%s skipping", deal.bot_name); continue; } - const params = { - scope: "active", - account_id: accountId, - }; - - for await (const deal of iterate(params)) { - if (!shouldTrail(deal, minSafetyOrders, ignore)) { - debug("%s skipping", deal.bot_name); - continue; - } - - const update = updateDeal({ - deal_id: deal.id, - trailing_enabled: true, - take_profit: takeProfit, - trailing_deviation: trailing, - }); + const update = updateDeal({ + deal_id: deal.id, + trailing_enabled: true, + take_profit: takeProfit, + trailing_deviation: trailing, + }); - updates.push(update); + updates.push(update); - debug( - "%s trailing enabled (TTP %d% / %d%)", - deal.bot_name, - takeProfit, - trailing - ); - } + debug( + "%s trailing enabled (TTP %d% / %d%)", + deal.bot_name, + takeProfit, + trailing + ); } - return Promise.all(updates); + await Promise.all(updates); }; /** diff --git a/src/index.js b/src/index.js index b2981fe..7cb2a68 100644 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,5 @@ -const autoTrail = require("./functions/auto-trail"); +require("dotenv/config"); module.exports = { - autoTrail, + autoTrail: require("./functions/auto-trail"), }; diff --git a/src/three-commas.js b/src/three-commas.js index 0876c50..d6a185b 100644 --- a/src/three-commas.js +++ b/src/three-commas.js @@ -3,9 +3,10 @@ const fetch = require("node-fetch"); const parse = require("json-templates"); const omit = require("object.omit"); const debug = require("debug")("3commas-control:api"); -const { threeCommas } = require("../config.json"); const { sign } = require("./utils"); +const baseURL = new URL("https://api.3commas.io/public/api"); + /** * Builds the 3Commas API object. * @@ -73,7 +74,7 @@ async function request(method, apiPath, params = {}, headers = {}) { body = searchParams.toString(); } - debug("%s %s...", method, url.toString()); + debug("[%s] %s", method, url.toString()); const response = await fetch(url.toString(), { method, @@ -96,10 +97,11 @@ async function request(method, apiPath, params = {}, headers = {}) { * @param {Object} [headers] * @returns {*} */ -function signedRequest(method, apiPath, params = {}, headers = {}) { +async function signedRequest(method, apiPath, params = {}, headers = {}) { [apiPath, params] = replacePathParams(apiPath, params); - const { apiKey, secretKey } = threeCommas; + const { apiKey, secretKey } = await getAPIKeys(); + const fullURL = toURL(apiPath, params); const pathToSign = toPathWithQueryString(fullURL); const signature = sign(pathToSign, secretKey); @@ -119,7 +121,6 @@ function signedRequest(method, apiPath, params = {}, headers = {}) { * @returns {URL} */ function toURL(apiPath, params = {}) { - const baseURL = new URL(threeCommas.baseURL); const path = join(baseURL.pathname, apiPath); const url = new URL(path, baseURL.origin); @@ -153,3 +154,17 @@ function replacePathParams(path, params = {}) { return [path, params]; } + +/** + * Returns the API and Secret keys. + * + * @returns {Promise<{ apiKey: string, secretKey: string}>} + */ +async function getAPIKeys() { + return { + apiKey: process.env.THREE_COMMAS_API_KEY, + secretKey: process.env.THREE_COMMAS_SECRET_KEY, + }; + + // todo: google secret manager +} diff --git a/src/utils.js b/src/utils.js index ae541a8..8d5fd58 100644 --- a/src/utils.js +++ b/src/utils.js @@ -2,6 +2,7 @@ const { createHmac } = require("crypto"); module.exports = { sign, + parseEventJSON, }; /** @@ -17,3 +18,7 @@ function sign(data, secretKey) { return sig; } + +function parseEventJSON({ data }) { + return JSON.parse(Buffer.from(data, "base64").toString()); +} From fc544d43a3e905027ac2a1dd6de4a5880fd04239 Mon Sep 17 00:00:00 2001 From: Christopher Turner Date: Thu, 26 Aug 2021 22:07:03 +1000 Subject: [PATCH 4/7] superbots functions --- src/functions/auto-trail.js | 28 +----- src/functions/superbot.js | 190 ++++++++++++++++++++++++++++++++++++ src/index.js | 1 + src/three-commas.js | 41 +++++++- 4 files changed, 231 insertions(+), 29 deletions(-) create mode 100644 src/functions/superbot.js diff --git a/src/functions/auto-trail.js b/src/functions/auto-trail.js index 8e4b197..4e37d46 100644 --- a/src/functions/auto-trail.js +++ b/src/functions/auto-trail.js @@ -20,7 +20,7 @@ module.exports = async function autoTrail(event) { const updates = []; - for await (const deal of iterate(params)) { + for await (const deal of getDeals.iterate(params)) { if (!shouldTrail(deal, minSafetyOrders, ignore)) { debug("%s skipping", deal.bot_name); continue; @@ -46,32 +46,6 @@ module.exports = async function autoTrail(event) { await Promise.all(updates); }; -/** - * Utility iterator for looping over deals. - * - * @param {Object} [params] - * @returns {AsyncIterator} - */ -async function* iterate({ limit = 1000, offset = 0, ...params }) { - const deals = await getDeals({ - ...params, - limit, - offset, - }); - - for (let i = 0; i < deals.length; ++i) { - yield deals[i]; - } - - if (deals.length === limit) { - yield* iterateDeals({ - ...params, - offset: offset + limit, - limit, - }); - } -} - /** * Returns if a given deal should enable trailing take profits. * diff --git a/src/functions/superbot.js b/src/functions/superbot.js new file mode 100644 index 0000000..f1a3c22 --- /dev/null +++ b/src/functions/superbot.js @@ -0,0 +1,190 @@ +const debug = require("debug")("3commas-control:superbot"); +const date = require("date-fns"); +const { getDeals, getBots, updateBot } = require("../three-commas"); +const { parseEventJSON } = require("../utils"); + +const superbotPrefix = "SUPERBOT:"; + +/** + * Superbot function. + * + * Enables/disables superbot mode for running bots. + * + * @returns {Promise} + */ +module.exports = async function superbot(event) { + const { + accountId, + minDealsClosed, + intervalInMins, + maxSuperbots, + maxDurationInMins, + baseOrderAmount, + safetyOrderAmount, + } = parseEventJSON(event); + + const botParams = { + limit: 100, + account_id: accountId, + scope: "enabled", + }; + + // fetch enabled bots + const bots = {}; + for await (const bot of getBots.iterate(botParams)) bots[bot.id] = bot; + + let superbotsCount = Object.keys(bots).reduce( + (count, id) => count + isSuperbot(bots[id]), + 0 + ); + + const now = new Date(); + const minStartDate = date.sub(now, { minutes: intervalInMins }); + // increase the from date a little as 3commas searches from the created date + const searchFrom = date.sub(now, { + minutes: intervalInMins * minDealsClosed, + }); + + const counters = {}; + const botIdsEnabled = new Set(); + const updates = []; + + const dealParams = { + scope: "completed", + account_id: accountId, + from: searchFrom.toISOString(), + }; + + // enable superbots + for await (const deal of getDeals.iterate(dealParams)) { + // check if we haven't reached max superbots + if (superbotsCount >= maxSuperbots) { + break; + } + + // check the min start date + const created = new Date(deal.created_at); + if (created < minStartDate) { + // deal was in the buffer, skip + continue; + } + + const botId = deal.bot_id; + + // init bot deals counter + if (counters[botId] === undefined) { + counters[botId] = 0; + } + + const count = ++counters[botId]; + const bot = bots[botId]; + + if ( + !bot || // no bot ??? + isSuperbot(bot) || // no super-duperbots XD + count < minDealsClosed || // doesn't meet the min deals + botIdsEnabled.has(botId) // already enabled + ) { + continue; + } + + const update = updateBot({ + bot_id: botId, + name: superbotName(bot), + base_order_volume: baseOrderAmount, + safety_order_volume: safetyOrderAmount, + + // 3commas required fields :S + pairs: JSON.stringify(bot.pairs), + take_profit: bot.take_profit, + martingale_volume_coefficient: bot.martingale_volume_coefficient, + martingale_step_coefficient: bot.martingale_step_coefficient, + max_safety_orders: bot.max_safety_orders, + active_safety_orders_count: bot.active_safety_orders_count, + safety_order_step_percentage: bot.safety_order_step_percentage, + take_profit_type: bot.take_profit_type, + strategy_list: JSON.stringify(bot.strategy_list), + }); + + debug("%s SUPERBOT enabled", bot.name); + + ++superbotsCount; + botIdsEnabled.add(botId); + updates.push(update); + } + + // turn off superbots + const superbotIds = Object.keys(bots).filter((id) => isSuperbot(bots[id])); + for (let i = 0, l = superbotIds.length; i < l; ++i) { + const id = superbotIds[i]; + const bot = bots[id]; + + const deals = await getDeals({ + account_id: accountId, + bot_id: id, + scope: "active", + limit: 1000, // highly unlikely to have a 1000 deals open + }); + + const minDurationDate = date.sub(new Date(), { + minutes: maxDurationInMins, + }); + const disableSuperbot = deals.some((deal) => { + const created = new Date(deal.created_at); + return created < minDurationDate; + }); + + if (disableSuperbot) { + const [originalName, originalBaseOrderAmount, originalSafetyORderAmount] = + extractNameAndOrders(bot); + + const update = updateBot({ + bot_id: id, + name: originalName, + base_order_volume: originalBaseOrderAmount, + safety_order_volume: originalSafetyORderAmount, + + // 3commas required fields :S + pairs: JSON.stringify(bot.pairs), + take_profit: bot.take_profit, + martingale_volume_coefficient: bot.martingale_volume_coefficient, + martingale_step_coefficient: bot.martingale_step_coefficient, + max_safety_orders: bot.max_safety_orders, + active_safety_orders_count: bot.active_safety_orders_count, + safety_order_step_percentage: bot.safety_order_step_percentage, + take_profit_type: bot.take_profit_type, + strategy_list: JSON.stringify(bot.strategy_list), + }); + + updates.push(update); + + debug("%s SUPERBOT disabled", originalName); + } + } + + await Promise.all(updates); +}; + +function isSuperbot({ name, bot_name: botName }) { + return (botName || name).startsWith(superbotPrefix); +} + +function superbotName({ + name, + base_order_volume: oldBaseOrderAMount, + safety_order_volume: oldSafetyOrderAmount, +}) { + return [ + superbotPrefix, + name, + `[${oldBaseOrderAMount}/${oldSafetyOrderAmount}]`, + ].join(" "); +} + +function extractNameAndOrders({ name }) { + const regexp = new RegExp( + `^${superbotPrefix}\\s+(.+)\\s+\\[(\\d+\\.\\d+)\\/(\\d+\\.\\d+)\\]$` + ); + + return name.match(regexp).slice(1); +} diff --git a/src/index.js b/src/index.js index 7cb2a68..04c2513 100644 --- a/src/index.js +++ b/src/index.js @@ -2,4 +2,5 @@ require("dotenv/config"); module.exports = { autoTrail: require("./functions/auto-trail"), + superbot: require("./functions/superbot"), }; diff --git a/src/three-commas.js b/src/three-commas.js index d6a185b..a43285f 100644 --- a/src/three-commas.js +++ b/src/three-commas.js @@ -14,6 +14,7 @@ const baseURL = new URL("https://api.3commas.io/public/api"); module.exports = factory({ getDeals: { signed: true, + iterator: true, method: "GET", path: "/ver1/deals", }, @@ -22,6 +23,17 @@ module.exports = factory({ method: "PATCH", path: "/ver1/deals/{{deal_id}}/update_deal", }, + getBots: { + signed: true, + iterator: true, + method: "GET", + path: "/ver1/bots", + }, + updateBot: { + signed: true, + method: "PATCH", + path: "/ver1/bots/{{bot_id}}/update", + }, }); /** @@ -44,15 +56,40 @@ function factory(definitions) { const api = {}; for (const [name, define] of Object.entries(definitions)) { - const { method, path, signed } = define; + const { method, path, signed, iterator } = define; const func = signed ? signedRequest : request; + const boundFunc = func.bind(null, method, path); - api[name] = func.bind(null, method, path); + api[name] = iterator ? assignIterate(boundFunc) : boundFunc; } return api; } +function assignIterate(func) { + return Object.assign(func, { + async *iterate({ limit = 1000, offset = 0, ...params }) { + const deals = await func({ + ...params, + limit, + offset, + }); + + for (let i = 0; i < deals.length; ++i) { + yield deals[i]; + } + + if (deals.length === limit) { + yield* func.iterate({ + ...params, + offset: offset + limit, + limit, + }); + } + }, + }); +} + /** * Makes a generic request to 3Commas API service. * From 60b97460ecf8e6ffeadd63eff2f8c216e39ca0ea Mon Sep 17 00:00:00 2001 From: Christopher Turner Date: Thu, 26 Aug 2021 22:20:41 +1000 Subject: [PATCH 5/7] google secrets --- package-lock.json | 498 ++++++++++++++++++++++++++++++++++++++++++++ package.json | 2 + src/three-commas.js | 39 +++- 3 files changed, 532 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 79952c4..9858b74 100644 --- a/package-lock.json +++ b/package-lock.json @@ -95,6 +95,278 @@ } } }, + "@google-cloud/paginator": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-3.0.5.tgz", + "integrity": "sha512-N4Uk4BT1YuskfRhKXBs0n9Lg2YTROZc6IMpkO/8DIHODtm5s3xY8K5vVBo23v/2XulY3azwITQlYWgT4GdLsUw==", + "requires": { + "arrify": "^2.0.0", + "extend": "^3.0.2" + } + }, + "@google-cloud/precise-date": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@google-cloud/precise-date/-/precise-date-2.0.3.tgz", + "integrity": "sha512-+SDJ3ZvGkF7hzo6BGa8ZqeK3F6Z4+S+KviC9oOK+XCs3tfMyJCh/4j93XIWINgMMDIh9BgEvlw4306VxlXIlYA==" + }, + "@google-cloud/projectify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-2.1.0.tgz", + "integrity": "sha512-qbpidP/fOvQNz3nyabaVnZqcED1NNzf7qfeOlgtAZd9knTwY+KtsGRkYpiQzcATABy4gnGP2lousM3S0nuWVzA==" + }, + "@google-cloud/promisify": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-2.0.3.tgz", + "integrity": "sha512-d4VSA86eL/AFTe5xtyZX+ePUjE8dIFu2T8zmdeNBSa5/kNgXPCx/o/wbFNHAGLJdGnk1vddRuMESD9HbOC8irw==" + }, + "@google-cloud/pubsub": { + "version": "2.17.0", + "resolved": "https://registry.npmjs.org/@google-cloud/pubsub/-/pubsub-2.17.0.tgz", + "integrity": "sha512-9Xya69A5VAYVEGf651jy071RuBIjv+jpyozSc3j8V21LIiKRr9x+KyplHcLTYWdj+uXbP9cry8Ck8JEFc7GiqQ==", + "requires": { + "@google-cloud/paginator": "^3.0.0", + "@google-cloud/precise-date": "^2.0.0", + "@google-cloud/projectify": "^2.0.0", + "@google-cloud/promisify": "^2.0.0", + "@opentelemetry/api": "^1.0.0", + "@opentelemetry/semantic-conventions": "^0.24.0", + "@types/duplexify": "^3.6.0", + "@types/long": "^4.0.0", + "arrify": "^2.0.0", + "extend": "^3.0.2", + "google-auth-library": "^7.0.0", + "google-gax": "^2.24.1", + "is-stream-ended": "^0.1.4", + "lodash.snakecase": "^4.1.1", + "p-defer": "^3.0.0" + }, + "dependencies": { + "agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "requires": { + "debug": "4" + } + }, + "gaxios": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-4.3.0.tgz", + "integrity": "sha512-pHplNbslpwCLMyII/lHPWFQbJWOX0B3R1hwBEOvzYi1GmdKZruuEHK4N9V6f7tf1EaPYyF80mui1+344p6SmLg==", + "requires": { + "abort-controller": "^3.0.0", + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.3.0" + } + }, + "gcp-metadata": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-4.3.0.tgz", + "integrity": "sha512-L9XQUpvKJCM76YRSmcxrR4mFPzPGsgZUH+GgHMxAET8qc6+BhRJq63RLhWakgEO2KKVgeSDVfyiNjkGSADwNTA==", + "requires": { + "gaxios": "^4.0.0", + "json-bigint": "^1.0.0" + } + }, + "google-auth-library": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-7.6.2.tgz", + "integrity": "sha512-yvEnwVsvgH8RXTtpf6e84e7dqIdUEKJhmQvTJwzYP+RDdHjLrDp9sk2u2ZNDJPLKZ7DJicx/+AStcQspJiq+Qw==", + "requires": { + "arrify": "^2.0.0", + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "fast-text-encoding": "^1.0.0", + "gaxios": "^4.0.0", + "gcp-metadata": "^4.2.0", + "gtoken": "^5.0.4", + "jws": "^4.0.0", + "lru-cache": "^6.0.0" + } + }, + "google-p12-pem": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-3.1.2.tgz", + "integrity": "sha512-tjf3IQIt7tWCDsa0ofDQ1qqSCNzahXDxdAGJDbruWqu3eCg5CKLYKN+hi0s6lfvzYZ1GDVr+oDF9OOWlDSdf0A==", + "requires": { + "node-forge": "^0.10.0" + } + }, + "gtoken": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-5.3.1.tgz", + "integrity": "sha512-yqOREjzLHcbzz1UrQoxhBtpk8KjrVhuqPE7od1K2uhyxG2BHjKZetlbLw/SPZak/QqTIQW+addS+EcjqQsZbwQ==", + "requires": { + "gaxios": "^4.0.0", + "google-p12-pem": "^3.0.3", + "jws": "^4.0.0" + } + }, + "https-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", + "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", + "requires": { + "agent-base": "6", + "debug": "4" + } + }, + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==" + }, + "json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "requires": { + "bignumber.js": "^9.0.0" + } + }, + "jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "requires": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + } + }, + "node-forge": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz", + "integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==" + }, + "p-defer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-3.0.0.tgz", + "integrity": "sha512-ugZxsxmtTln604yeYd29EGrNhazN2lywetzpKhfmQjW/VJmhpDmWbiX+h0zL8V91R0UXkhb3KtPmyq9PZw3aYw==" + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } + }, + "@google-cloud/secret-manager": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/@google-cloud/secret-manager/-/secret-manager-3.10.0.tgz", + "integrity": "sha512-zpZzq5zBU49s8wIvgdVpOLhQ1pDDa84VWK57NPesN56q1OeBMU3g+ykW5pwARJx4svTOzPxsbv8x4kHqe8+GUQ==", + "requires": { + "google-gax": "^2.24.1" + } + }, + "@grpc/grpc-js": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.3.7.tgz", + "integrity": "sha512-CKQVuwuSPh40tgOkR7c0ZisxYRiN05PcKPW72mQL5y++qd7CwBRoaJZvU5xfXnCJDFBmS3qZGQ71Frx6Ofo2XA==", + "requires": { + "@types/node": ">=12.12.47" + } + }, + "@grpc/proto-loader": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.6.4.tgz", + "integrity": "sha512-7xvDvW/vJEcmLUltCUGOgWRPM8Oofv0eCFSVMuKqaqWJaXSzmB+m9hiyqe34QofAl4WAzIKUZZlinIF9FOHyTQ==", + "requires": { + "@types/long": "^4.0.1", + "lodash.camelcase": "^4.3.0", + "long": "^4.0.0", + "protobufjs": "^6.10.0", + "yargs": "^16.1.1" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" + }, + "cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, + "long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" + }, + "string-width": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", + "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" + }, + "yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "requires": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + } + }, + "yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==" + } + } + }, "@hapi/accept": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/@hapi/accept/-/accept-3.2.4.tgz", @@ -462,6 +734,16 @@ "fastq": "^1.6.0" } }, + "@opentelemetry/api": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.0.2.tgz", + "integrity": "sha512-DCF9oC89ao8/EJUqrp/beBlDR8Bp2R43jqtzayqCoomIvkwTuPfLcHdVhIGRR69GFlkykFjcDW+V92t0AS7Tww==" + }, + "@opentelemetry/semantic-conventions": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-0.24.0.tgz", + "integrity": "sha512-a/szuMQV0Quy0/M7kKdglcbRSoorleyyOwbTNNJ32O+RBN766wbQlMTvdimImTmwYWGr+NJOni1EcC242WlRcA==" + }, "@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -960,6 +1242,14 @@ "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.2.tgz", "integrity": "sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w==" }, + "@types/duplexify": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@types/duplexify/-/duplexify-3.6.0.tgz", + "integrity": "sha512-5zOA53RUlzN74bvrSGwjudssD9F3a797sDZQkiYpUOxW+WHaXTCPz4/d5Dgi6FKnOqZ2CpaTo0DhgIfsXAOE/A==", + "requires": { + "@types/node": "*" + } + }, "@types/http-cache-semantics": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.0.tgz", @@ -1325,6 +1615,11 @@ "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz", "integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==" }, + "arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==" + }, "asn1": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", @@ -3104,6 +3399,11 @@ "es6-symbol": "^3.1.1" } }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" + }, "escape-goat": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz", @@ -3869,6 +4169,175 @@ "semver": "^5.5.0" } }, + "google-gax": { + "version": "2.24.2", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-2.24.2.tgz", + "integrity": "sha512-4OtyEIt/KAXRX5o2W/6DGf8MnMs1lMXwcGoPHR4PwXfTUVKjK7ywRe2/yRIMkYEDzAwu/kppPgfpX+kCG2rWfw==", + "requires": { + "@grpc/grpc-js": "~1.3.0", + "@grpc/proto-loader": "^0.6.1", + "@types/long": "^4.0.0", + "abort-controller": "^3.0.0", + "duplexify": "^4.0.0", + "fast-text-encoding": "^1.0.3", + "google-auth-library": "^7.6.1", + "is-stream-ended": "^0.1.4", + "node-fetch": "^2.6.1", + "object-hash": "^2.1.1", + "proto3-json-serializer": "^0.1.1", + "protobufjs": "6.11.2", + "retry-request": "^4.0.0" + }, + "dependencies": { + "agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "requires": { + "debug": "4" + } + }, + "gaxios": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-4.3.0.tgz", + "integrity": "sha512-pHplNbslpwCLMyII/lHPWFQbJWOX0B3R1hwBEOvzYi1GmdKZruuEHK4N9V6f7tf1EaPYyF80mui1+344p6SmLg==", + "requires": { + "abort-controller": "^3.0.0", + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.3.0" + } + }, + "gcp-metadata": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-4.3.0.tgz", + "integrity": "sha512-L9XQUpvKJCM76YRSmcxrR4mFPzPGsgZUH+GgHMxAET8qc6+BhRJq63RLhWakgEO2KKVgeSDVfyiNjkGSADwNTA==", + "requires": { + "gaxios": "^4.0.0", + "json-bigint": "^1.0.0" + } + }, + "google-auth-library": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-7.6.2.tgz", + "integrity": "sha512-yvEnwVsvgH8RXTtpf6e84e7dqIdUEKJhmQvTJwzYP+RDdHjLrDp9sk2u2ZNDJPLKZ7DJicx/+AStcQspJiq+Qw==", + "requires": { + "arrify": "^2.0.0", + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "fast-text-encoding": "^1.0.0", + "gaxios": "^4.0.0", + "gcp-metadata": "^4.2.0", + "gtoken": "^5.0.4", + "jws": "^4.0.0", + "lru-cache": "^6.0.0" + } + }, + "google-p12-pem": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-3.1.2.tgz", + "integrity": "sha512-tjf3IQIt7tWCDsa0ofDQ1qqSCNzahXDxdAGJDbruWqu3eCg5CKLYKN+hi0s6lfvzYZ1GDVr+oDF9OOWlDSdf0A==", + "requires": { + "node-forge": "^0.10.0" + } + }, + "gtoken": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-5.3.1.tgz", + "integrity": "sha512-yqOREjzLHcbzz1UrQoxhBtpk8KjrVhuqPE7od1K2uhyxG2BHjKZetlbLw/SPZak/QqTIQW+addS+EcjqQsZbwQ==", + "requires": { + "gaxios": "^4.0.0", + "google-p12-pem": "^3.0.3", + "jws": "^4.0.0" + } + }, + "https-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", + "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", + "requires": { + "agent-base": "6", + "debug": "4" + } + }, + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==" + }, + "json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "requires": { + "bignumber.js": "^9.0.0" + } + }, + "jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "requires": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + } + }, + "node-forge": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz", + "integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==" + }, + "protobufjs": { + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.2.tgz", + "integrity": "sha512-4BQJoPooKJl2G9j3XftkIXjoC9C0Av2NOrWmbLWT1vH32GcSUHjM0Arra6UfTsVyfMAuFzaLucXn1sadxJydAw==", + "requires": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/long": "^4.0.1", + "@types/node": ">=13.7.0", + "long": "^4.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } + }, "google-p12-pem": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-1.0.4.tgz", @@ -4542,6 +5011,11 @@ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" }, + "is-stream-ended": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-stream-ended/-/is-stream-ended-0.1.4.tgz", + "integrity": "sha512-xj0XPvmr7bQFTvirqnFr50o0hQIh6ZItDqloxt5aJrR4NQsYeSsyFQERYGCAzfindAcnKjINnwEEgLx4IqVzQw==" + }, "is-string": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.5.tgz", @@ -5027,6 +5501,11 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=" + }, "lodash.defaults": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", @@ -5077,6 +5556,11 @@ "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" }, + "lodash.snakecase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", + "integrity": "sha1-OdcUo1NXFHg3rv1ktdy7Fr7Nj40=" + }, "lodash.union": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", @@ -6272,6 +6756,11 @@ "resolved": "https://registry.npmjs.org/promise-queue/-/promise-queue-2.2.5.tgz", "integrity": "sha1-L29ffA9tCBCelnZZx5uIqe1ek7Q=" }, + "proto3-json-serializer": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-0.1.3.tgz", + "integrity": "sha512-X0DAtxCBsy1NDn84huVFGOFgBslT2gBmM+85nY6/5SOAaCon1jzVNdvi74foIyFvs5CjtSbQsepsM5TsyNhqQw==" + }, "protobufjs": { "version": "6.10.2", "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.10.2.tgz", @@ -6615,6 +7104,15 @@ "resolved": "https://registry.npmjs.org/retry/-/retry-0.10.1.tgz", "integrity": "sha1-52OI0heZLCUnUCQdPTlW/tmNj/Q=" }, + "retry-request": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-4.2.2.tgz", + "integrity": "sha512-xA93uxUD/rogV7BV59agW/JHPGXeREMWiZc9jhcwY4YdZ7QOtC7qbomYg0n4wyk2lJhggjvKvhNX8wln/Aldhg==", + "requires": { + "debug": "^4.1.1", + "extend": "^3.0.2" + } + }, "reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", diff --git a/package.json b/package.json index 98cc058..efc3051 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,8 @@ "dependencies": { "3commas-api-node": "^1.0.9", "@google-cloud/functions-framework": "^1.9.0", + "@google-cloud/pubsub": "^2.17.0", + "@google-cloud/secret-manager": "^3.10.0", "aws-sdk": "^2.889.0", "axios": "^0.21.1", "crypto-js": "^4.0.0", diff --git a/src/three-commas.js b/src/three-commas.js index a43285f..662a9b6 100644 --- a/src/three-commas.js +++ b/src/three-commas.js @@ -3,10 +3,13 @@ const fetch = require("node-fetch"); const parse = require("json-templates"); const omit = require("object.omit"); const debug = require("debug")("3commas-control:api"); +const { SecretManagerServiceClient } = require("@google-cloud/secret-manager"); const { sign } = require("./utils"); const baseURL = new URL("https://api.3commas.io/public/api"); +const secretManagerClient = new SecretManagerServiceClient(); + /** * Builds the 3Commas API object. * @@ -137,7 +140,7 @@ async function request(method, apiPath, params = {}, headers = {}) { async function signedRequest(method, apiPath, params = {}, headers = {}) { [apiPath, params] = replacePathParams(apiPath, params); - const { apiKey, secretKey } = await getAPIKeys(); + const [apiKey, secretKey] = await getAPIKeys(); const fullURL = toURL(apiPath, params); const pathToSign = toPathWithQueryString(fullURL); @@ -195,13 +198,35 @@ function replacePathParams(path, params = {}) { /** * Returns the API and Secret keys. * - * @returns {Promise<{ apiKey: string, secretKey: string}>} + * @returns {Promise<[string, string]>} */ async function getAPIKeys() { - return { - apiKey: process.env.THREE_COMMAS_API_KEY, - secretKey: process.env.THREE_COMMAS_SECRET_KEY, - }; + const projectId = process.env.GCP_PROJECT; + + // fallback to envs + if (!projectId) { + return [ + process.env.THREE_COMMAS_API_KEY, + process.env.THREE_COMMAS_SECRET_KEY, + ]; + } + + const apiKeyName = `projects/${projectId}/secrets/3commas-api-key/versions/latest`; + const secretKeyName = `projects/${projectId}/secrets/3commas-secret-key/versions/latest`; + + return Promise.all([apiKeyName, secretKeyName].map(getSecretValue)); +} + +/** + * Gets a secret value. + * + * @param {string} name + * @returns {Promise} + */ +async function getSecretValue(name) { + const [version] = await secretManagerClient.accessSecretVersion({ + name, + }); - // todo: google secret manager + return version.payload.data.toString(); } From d65feff35729ea990bdd83bc87aea6cbedbf9cc6 Mon Sep 17 00:00:00 2001 From: Christopher Turner Date: Sun, 29 Aug 2021 09:37:57 +1000 Subject: [PATCH 6/7] current superbot implementation --- src/functions/superbot.js | 246 +++++++++++++++++++++----------------- src/three-commas.js | 124 +++++++++++++------ src/utils.js | 5 + 3 files changed, 229 insertions(+), 146 deletions(-) diff --git a/src/functions/superbot.js b/src/functions/superbot.js index f1a3c22..0875539 100644 --- a/src/functions/superbot.js +++ b/src/functions/superbot.js @@ -1,9 +1,14 @@ const debug = require("debug")("3commas-control:superbot"); const date = require("date-fns"); const { getDeals, getBots, updateBot } = require("../three-commas"); -const { parseEventJSON } = require("../utils"); +const { parseEventJSON, delay } = require("../utils"); const superbotPrefix = "SUPERBOT:"; +const superbotRegExp = new RegExp( + `^${superbotPrefix}\\s+(.+)\\s+\\[(\\d+\\.\\d+)\\/(\\d+\\.\\d+)\\]$` +); + +const botCache = new Map(); /** * Superbot function. @@ -13,6 +18,7 @@ const superbotPrefix = "SUPERBOT:"; * @returns {Promise} */ module.exports = async function superbot(event) { + const options = parseEventJSON(event); const { accountId, minDealsClosed, @@ -21,7 +27,18 @@ module.exports = async function superbot(event) { maxDurationInMins, baseOrderAmount, safetyOrderAmount, - } = parseEventJSON(event); + } = options; + + const now = new Date(); + const minStartDate = date.sub(now, { minutes: intervalInMins }); + const minDurationDate = date.sub(now, { + minutes: maxDurationInMins, + }); + + // -------------------------------------------------------------------------- + + // Part 1: Pre-fetch bots + // Warm up a cache of all active bots associated with account. const botParams = { limit: 100, @@ -29,142 +46,116 @@ module.exports = async function superbot(event) { scope: "enabled", }; - // fetch enabled bots - const bots = {}; - for await (const bot of getBots.iterate(botParams)) bots[bot.id] = bot; + for await (const bot of getBots.iterate(botParams)) botCache.set(bot.id, bot); - let superbotsCount = Object.keys(bots).reduce( - (count, id) => count + isSuperbot(bots[id]), - 0 - ); + // -------------------------------------------------------------------------- - const now = new Date(); - const minStartDate = date.sub(now, { minutes: intervalInMins }); - // increase the from date a little as 3commas searches from the created date - const searchFrom = date.sub(now, { - minutes: intervalInMins * minDealsClosed, + // Part 2: Superbot suppression + // Firstly, work out from the deals which bots are superbots and of which, + // disable any that have went past their max deal duration. + + const activeDeals = await getDeals({ + account_id: accountId, + order_direction: "desc", + scope: "active", }); - const counters = {}; - const botIdsEnabled = new Set(); - const updates = []; + const activeSuperbotDeals = activeDeals.filter( + filterSuperbotDeals(baseOrderAmount, safetyOrderAmount) + ); + const activeEnabledSuperbotDeals = activeSuperbotDeals.filter(isSuperbot); + const activeExpiredSuperbotDeals = activeEnabledSuperbotDeals.filter( + filterExpiredSuperbotDeals(minDurationDate) + ); - const dealParams = { - scope: "completed", - account_id: accountId, - from: searchFrom.toISOString(), - }; + const botIdsToDisable = activeExpiredSuperbotDeals.map((deal) => deal.bot_id); + await Promise.all(botIdsToDisable.map(disableSuperbot)); - // enable superbots - for await (const deal of getDeals.iterate(dealParams)) { - // check if we haven't reached max superbots - if (superbotsCount >= maxSuperbots) { - break; - } + // -------------------------------------------------------------------------- - // check the min start date - const created = new Date(deal.created_at); - if (created < minStartDate) { - // deal was in the buffer, skip - continue; - } + // Part 3: Superbot detection + // Next let's see if we can detect if a bot has been pumping to turn + // superbot mode on. - const botId = deal.bot_id; + let superbotsCount = activeSuperbotDeals.length; + const activeSuperbotIds = activeSuperbotDeals.map((deal) => deal.bot_id); - // init bot deals counter - if (counters[botId] === undefined) { - counters[botId] = 0; + for (const [id, bot] of botCache) { + // superbot limit reached + if (superbotsCount >= maxSuperbots) { + break; } - const count = ++counters[botId]; - const bot = bots[botId]; - - if ( - !bot || // no bot ??? - isSuperbot(bot) || // no super-duperbots XD - count < minDealsClosed || // doesn't meet the min deals - botIdsEnabled.has(botId) // already enabled - ) { + // already a superbot + const superbot = activeSuperbotIds.includes(id); + if (superbot) { continue; } - const update = updateBot({ - bot_id: botId, - name: superbotName(bot), - base_order_volume: baseOrderAmount, - safety_order_volume: safetyOrderAmount, - - // 3commas required fields :S - pairs: JSON.stringify(bot.pairs), - take_profit: bot.take_profit, - martingale_volume_coefficient: bot.martingale_volume_coefficient, - martingale_step_coefficient: bot.martingale_step_coefficient, - max_safety_orders: bot.max_safety_orders, - active_safety_orders_count: bot.active_safety_orders_count, - safety_order_step_percentage: bot.safety_order_step_percentage, - take_profit_type: bot.take_profit_type, - strategy_list: JSON.stringify(bot.strategy_list), - }); - - debug("%s SUPERBOT enabled", bot.name); - - ++superbotsCount; - botIdsEnabled.add(botId); - updates.push(update); - } + // 3Commas `from` parameter goes from the created date so we need to fetch + // the latest 3 closed deals per bot. However 3Commas weights this request + // heavily so we need to backoff so our IP doesn't get blocked. A simple + // 1000ms delay seems to do the trick. + await delay(1000); - // turn off superbots - const superbotIds = Object.keys(bots).filter((id) => isSuperbot(bots[id])); - for (let i = 0, l = superbotIds.length; i < l; ++i) { - const id = superbotIds[i]; - const bot = bots[id]; + debug("checking %s deals", bot.name); + // recent deals const deals = await getDeals({ account_id: accountId, bot_id: id, - scope: "active", - limit: 1000, // highly unlikely to have a 1000 deals open + order_direction: "desc", + scope: "completed", + limit: minDealsClosed, }); - const minDurationDate = date.sub(new Date(), { - minutes: maxDurationInMins, - }); - const disableSuperbot = deals.some((deal) => { - const created = new Date(deal.created_at); - return created < minDurationDate; + // count how many closed within the interval + const innerDealsClosed = deals.filter(({ closed_at: closedAt }) => { + const closed = new Date(closedAt); + return minStartDate < closed; }); - if (disableSuperbot) { - const [originalName, originalBaseOrderAmount, originalSafetyORderAmount] = - extractNameAndOrders(bot); - - const update = updateBot({ + if (innerDealsClosed.length === minDealsClosed) { + await updateBot({ + ...requiredFields(bot), bot_id: id, - name: originalName, - base_order_volume: originalBaseOrderAmount, - safety_order_volume: originalSafetyORderAmount, - - // 3commas required fields :S - pairs: JSON.stringify(bot.pairs), - take_profit: bot.take_profit, - martingale_volume_coefficient: bot.martingale_volume_coefficient, - martingale_step_coefficient: bot.martingale_step_coefficient, - max_safety_orders: bot.max_safety_orders, - active_safety_orders_count: bot.active_safety_orders_count, - safety_order_step_percentage: bot.safety_order_step_percentage, - take_profit_type: bot.take_profit_type, - strategy_list: JSON.stringify(bot.strategy_list), + name: superbotName(bot), + base_order_volume: baseOrderAmount, + safety_order_volume: safetyOrderAmount, }); - updates.push(update); - - debug("%s SUPERBOT disabled", originalName); + superbotsCount += 1; + debug("%s SUPERBOT enabled", bot.name); } } - - await Promise.all(updates); }; +function filterSuperbotDeals(baseOrderAmount, safetyOrderAmount) { + return (deal) => { + // detect by the bot name + if (isSuperbot(deal)) { + return true; + } + + const dealBaseOrderAmount = parseFloat(deal.base_order_volume); + const dealSafetyOrderAmount = parseFloat(deal.safety_order_volume); + + // Is the active deal the same order amounts. This is a fallback detection + // for when a superbot has been disabled however it still has an open deal. + return ( + dealSafetyOrderAmount === safetyOrderAmount && + dealBaseOrderAmount === baseOrderAmount + ); + }; +} + +function filterExpiredSuperbotDeals(minDate) { + return (deal) => { + const created = new Date(deal.created_at); + return created < minDate; + }; +} + function isSuperbot({ name, bot_name: botName }) { return (botName || name).startsWith(superbotPrefix); } @@ -182,9 +173,40 @@ function superbotName({ } function extractNameAndOrders({ name }) { - const regexp = new RegExp( - `^${superbotPrefix}\\s+(.+)\\s+\\[(\\d+\\.\\d+)\\/(\\d+\\.\\d+)\\]$` - ); + return name.match(superbotRegExp).slice(1); +} + +async function disableSuperbot(id) { + const bot = botCache.get(id); - return name.match(regexp).slice(1); + const [originalName, originalBaseOrderAmount, originalSafetyORderAmount] = + extractNameAndOrders(bot); + + await updateBot({ + ...requiredFields(bot), + bot_id: id, + name: originalName, + base_order_volume: originalBaseOrderAmount, + safety_order_volume: originalSafetyORderAmount, + }); + + debug("%s SUPERBOT disabled", originalName); +} + +function requiredFields(bot) { + // stupid 3Commas required fields + return { + name: bot.name, + base_order_volume: bot.base_order_volume, + safety_order_volume: bot.safety_order_volume, + pairs: JSON.stringify(bot.pairs), + take_profit: bot.take_profit, + martingale_volume_coefficient: bot.martingale_volume_coefficient, + martingale_step_coefficient: bot.martingale_step_coefficient, + max_safety_orders: bot.max_safety_orders, + active_safety_orders_count: bot.active_safety_orders_count, + safety_order_step_percentage: bot.safety_order_step_percentage, + take_profit_type: bot.take_profit_type, + strategy_list: JSON.stringify(bot.strategy_list), + }; } diff --git a/src/three-commas.js b/src/three-commas.js index 662a9b6..1edd012 100644 --- a/src/three-commas.js +++ b/src/three-commas.js @@ -5,6 +5,7 @@ const omit = require("object.omit"); const debug = require("debug")("3commas-control:api"); const { SecretManagerServiceClient } = require("@google-cloud/secret-manager"); const { sign } = require("./utils"); +const WebSocket = require("ws"); const baseURL = new URL("https://api.3commas.io/public/api"); @@ -14,30 +15,33 @@ const secretManagerClient = new SecretManagerServiceClient(); * Builds the 3Commas API object. * */ -module.exports = factory({ - getDeals: { - signed: true, - iterator: true, - method: "GET", - path: "/ver1/deals", - }, - updateDeal: { - signed: true, - method: "PATCH", - path: "/ver1/deals/{{deal_id}}/update_deal", - }, - getBots: { - signed: true, - iterator: true, - method: "GET", - path: "/ver1/bots", - }, - updateBot: { - signed: true, - method: "PATCH", - path: "/ver1/bots/{{bot_id}}/update", - }, -}); +Object.assign( + exports, + factory({ + getDeals: { + signed: true, + iterator: true, + method: "GET", + path: "/ver1/deals", + }, + updateDeal: { + signed: true, + method: "PATCH", + path: "/ver1/deals/{{deal_id}}/update_deal", + }, + getBots: { + signed: true, + iterator: true, + method: "GET", + path: "/ver1/bots", + }, + updateBot: { + signed: true, + method: "PATCH", + path: "/ver1/bots/{{bot_id}}/update", + }, + }) +); /** * Factory function for build an object for the 3Commas API service. @@ -201,18 +205,17 @@ function replacePathParams(path, params = {}) { * @returns {Promise<[string, string]>} */ async function getAPIKeys() { - const projectId = process.env.GCP_PROJECT; - - // fallback to envs - if (!projectId) { - return [ - process.env.THREE_COMMAS_API_KEY, - process.env.THREE_COMMAS_SECRET_KEY, - ]; + const { THREE_COMMAS_API_KEY, THREE_COMMAS_SECRET_KEY } = process.env; + + // envs + if (THREE_COMMAS_SECRET_KEY && THREE_COMMAS_SECRET_KEY) { + return [THREE_COMMAS_API_KEY, THREE_COMMAS_SECRET_KEY]; } - const apiKeyName = `projects/${projectId}/secrets/3commas-api-key/versions/latest`; - const secretKeyName = `projects/${projectId}/secrets/3commas-secret-key/versions/latest`; + const apiKeyName = + "projects/1018578123164/secrets/3commas-api-key/versions/latest"; + const secretKeyName = + "projects/1018578123164/secrets/3commas-secret-key/versions/latest"; return Promise.all([apiKeyName, secretKeyName].map(getSecretValue)); } @@ -230,3 +233,56 @@ async function getSecretValue(name) { return version.payload.data.toString(); } + +/** + * Stream deal updates. + * + * @param {Function} callback + * @returns {void} + */ +exports.stream = async function stream(callback) { + const [apiKey, secretKey] = await getAPIKeys(); + const signature = sign("/deals", secretKey); + + const streamIdentifier = JSON.stringify({ + channel: "DealsChannel", + users: [ + { + api_key: apiKey, + signature, + }, + ], + }); + + const ws = new WebSocket("wss://ws.3commas.io/websocket"); + + ws.on("open", () => { + debug("socket opened, trying to subscribe"); + + ws.send( + JSON.stringify({ + command: "subscribe", + identifier: streamIdentifier, + }) + ); + }); + + ws.on("error", () => debug("socket error")); + ws.on("close", () => debug("socket closed")); + + ws.on("message", (event) => { + const data = JSON.parse(event.toString()); + const { identifier, message, type } = data; + + if ( + type === "ping" || + type === "confirm_subscription" || + identifier !== streamIdentifier + ) { + type === "confirm_subscription" && debug("subscribed"); + return; + } + + callback(message); + }); +}; diff --git a/src/utils.js b/src/utils.js index 8d5fd58..2483769 100644 --- a/src/utils.js +++ b/src/utils.js @@ -3,6 +3,7 @@ const { createHmac } = require("crypto"); module.exports = { sign, parseEventJSON, + delay, }; /** @@ -22,3 +23,7 @@ function sign(data, secretKey) { function parseEventJSON({ data }) { return JSON.parse(Buffer.from(data, "base64").toString()); } + +async function delay(ms) { + return new Promise((res) => setTimeout(res, ms)); +} From 0d109bd66a9bb275621cb1816a593785f5d3e32b Mon Sep 17 00:00:00 2001 From: Tom Somerville Date: Tue, 5 Apr 2022 07:09:08 +0800 Subject: [PATCH 7/7] Updated bots --- botHandler.js | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/botHandler.js b/botHandler.js index 3d24d14..200bc5f 100644 --- a/botHandler.js +++ b/botHandler.js @@ -4,7 +4,9 @@ const subMinutes = require('date-fns/subMinutes'); const ACCOUNT_ID_TOM = process.env.THREE_COMMAS_ACCOUNT_ID; const ACCOUNT_ID_CHRIS = process.env.THREE_COMMAS_ACCOUNT_ID_CHRIS; +const ACCOUNT_ID_KUCOIN_TOM = process.env.THREE_COMMAS_ACCOUNT_ID_KUCOIN_TOM; const SHOULD_RUN_BOTS = true; +const SHOULD_RUN_SUPER_BOTS = false; const paramsToEnableSuperBot = { timePeriod: 45, @@ -53,7 +55,7 @@ module.exports.handleBots = async () => { module.exports.toggleSuperBots = async () => { console.log("STARTING review of bots to enabled a superbot"); - if (!SHOULD_RUN_BOTS) { + if (!SHOULD_RUN_SUPER_BOTS) { return { statusCode: 200, body: JSON.stringify({ message: 'NOT UPDATING BOTS', success: true }), @@ -105,8 +107,12 @@ module.exports.toggleSuperBots = async () => { }; module.exports.handleChrisBots = async () => { + console.log("NOT RUNNING"); + return false; + console.log("STARTING update of all Chris bots"); + const filters = []; filters.push((deal) => deal.completed_safety_orders_count >= 3); @@ -126,6 +132,28 @@ module.exports.handleChrisBots = async () => { }; }; +module.exports.handleKucoinTomBots = async () => { + console.log("STARTING update of all ACCOUNT_ID_KUCOIN_TOM bots"); + + const filters = []; + + filters.push((deal) => deal.completed_safety_orders_count >= 9); + filters.push({trailing_enabled: false}); + filters.push((deal) => !deal.bot_name.includes("(SKIP)")); + + const deals = await updateAllDeals(ACCOUNT_ID_KUCOIN_TOM, filters, { + trailing_enabled: true, + take_profit: 2.40, + trailing_deviation: 0.4 + }); + + console.log("FINISHED update of all ACCOUNT_ID_KUCOIN_TOM bots"); + return { + statusCode: 200, + body: JSON.stringify({ message: 'Updated all bots, see logs', success: true, deals }), + }; +}; + module.exports.updateAllDeals = async () => { const filters = [];