From 8f929b921b592815f0ae65a1ba486098ab84ef89 Mon Sep 17 00:00:00 2001 From: Kincaid O'Neil Date: Wed, 27 Feb 2019 10:42:05 -0500 Subject: [PATCH 01/21] chore: update deps, add argon2, lint/format tweaks - prettier should also format js files - tslint shouldn't auto-fix --- package-lock.json | 342 +++++++++++++++++++++++++++++----------------- package.json | 24 ++-- tslint.json | 25 +++- 3 files changed, 252 insertions(+), 139 deletions(-) diff --git a/package-lock.json b/package-lock.json index 77a4b97..9706b9d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -56,44 +56,44 @@ } }, "@babel/core": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.2.2.tgz", - "integrity": "sha512-59vB0RWt09cAct5EIe58+NzGP4TFSD3Bz//2/ELy3ZeTeKF6VTD1AXlH8BGGbCX0PuobZBsIzO7IAI9PH67eKw==", + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.3.4.tgz", + "integrity": "sha512-jRsuseXBo9pN197KnDwhhaaBzyZr2oIcLHHTt2oDdQrej5Qp57dCCJafWx5ivU8/alEYDpssYqv1MUqcxwQlrA==", "dev": true, "requires": { "@babel/code-frame": "^7.0.0", - "@babel/generator": "^7.2.2", + "@babel/generator": "^7.3.4", "@babel/helpers": "^7.2.0", - "@babel/parser": "^7.2.2", + "@babel/parser": "^7.3.4", "@babel/template": "^7.2.2", - "@babel/traverse": "^7.2.2", - "@babel/types": "^7.2.2", + "@babel/traverse": "^7.3.4", + "@babel/types": "^7.3.4", "convert-source-map": "^1.1.0", "debug": "^4.1.0", "json5": "^2.1.0", - "lodash": "^4.17.10", + "lodash": "^4.17.11", "resolve": "^1.3.2", "semver": "^5.4.1", "source-map": "^0.5.0" }, "dependencies": { "@babel/generator": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.3.0.tgz", - "integrity": "sha512-dZTwMvTgWfhmibq4V9X+LMf6Bgl7zAodRn9PvcPdhlzFMbvUutx74dbEv7Atz3ToeEpevYEJtAwfxq/bDCzHWg==", + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.3.4.tgz", + "integrity": "sha512-8EXhHRFqlVVWXPezBW5keTiQi/rJMQTg/Y9uVCEZ0CAF3PKtCCaVRnp64Ii1ujhkoDhhF1fVsImoN4yJ2uz4Wg==", "dev": true, "requires": { - "@babel/types": "^7.3.0", + "@babel/types": "^7.3.4", "jsesc": "^2.5.1", - "lodash": "^4.17.10", + "lodash": "^4.17.11", "source-map": "^0.5.0", "trim-right": "^1.0.1" } }, "@babel/parser": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.3.1.tgz", - "integrity": "sha512-ATz6yX/L8LEnC3dtLQnIx4ydcPxhLcoy9Vl6re00zb2w5lG6itY6Vhnr1KFRPq/FHNsgl/gh2mjNN20f9iJTTA==", + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.3.4.tgz", + "integrity": "sha512-tXZCqWtlOOP4wgCp6RjRvLmfuhnqTLy9VHwRochJBCP2nDm27JnnuFEnXFASVyQNHk36jD1tAammsCEEqgscIQ==", "dev": true }, "@babel/template": { @@ -108,30 +108,30 @@ } }, "@babel/traverse": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.2.3.tgz", - "integrity": "sha512-Z31oUD/fJvEWVR0lNZtfgvVt512ForCTNKYcJBGbPb1QZfve4WGH8Wsy7+Mev33/45fhP/hwQtvgusNdcCMgSw==", + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.3.4.tgz", + "integrity": "sha512-TvTHKp6471OYEcE/91uWmhR6PrrYywQntCHSaZ8CM8Vmp+pjAusal4nGB2WCCQd0rvI7nOMKn9GnbcvTUz3/ZQ==", "dev": true, "requires": { "@babel/code-frame": "^7.0.0", - "@babel/generator": "^7.2.2", + "@babel/generator": "^7.3.4", "@babel/helper-function-name": "^7.1.0", "@babel/helper-split-export-declaration": "^7.0.0", - "@babel/parser": "^7.2.3", - "@babel/types": "^7.2.2", + "@babel/parser": "^7.3.4", + "@babel/types": "^7.3.4", "debug": "^4.1.0", "globals": "^11.1.0", - "lodash": "^4.17.10" + "lodash": "^4.17.11" } }, "@babel/types": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.3.0.tgz", - "integrity": "sha512-QkFPw68QqWU1/RVPyBe8SO7lXbPfjtqAxRYQKpFpaB8yMq7X2qAqfwK5LKoQufEkSmO5NQ70O6Kc3Afk03RwXw==", + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.3.4.tgz", + "integrity": "sha512-WEkp8MsLftM7O/ty580wAmZzN1nDmCACc5+jFzUt+GUFNNIi3LdRlueYz0YIlmJhlZx1QYDMZL5vdWCL0fNjFQ==", "dev": true, "requires": { "esutils": "^2.0.2", - "lodash": "^4.17.10", + "lodash": "^4.17.11", "to-fast-properties": "^2.0.0" } }, @@ -252,9 +252,9 @@ }, "dependencies": { "@babel/parser": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.3.1.tgz", - "integrity": "sha512-ATz6yX/L8LEnC3dtLQnIx4ydcPxhLcoy9Vl6re00zb2w5lG6itY6Vhnr1KFRPq/FHNsgl/gh2mjNN20f9iJTTA==", + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.3.4.tgz", + "integrity": "sha512-tXZCqWtlOOP4wgCp6RjRvLmfuhnqTLy9VHwRochJBCP2nDm27JnnuFEnXFASVyQNHk36jD1tAammsCEEqgscIQ==", "dev": true }, "@babel/template": { @@ -269,13 +269,13 @@ } }, "@babel/types": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.3.0.tgz", - "integrity": "sha512-QkFPw68QqWU1/RVPyBe8SO7lXbPfjtqAxRYQKpFpaB8yMq7X2qAqfwK5LKoQufEkSmO5NQ70O6Kc3Afk03RwXw==", + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.3.4.tgz", + "integrity": "sha512-WEkp8MsLftM7O/ty580wAmZzN1nDmCACc5+jFzUt+GUFNNIi3LdRlueYz0YIlmJhlZx1QYDMZL5vdWCL0fNjFQ==", "dev": true, "requires": { "esutils": "^2.0.2", - "lodash": "^4.17.10", + "lodash": "^4.17.11", "to-fast-properties": "^2.0.0" } } @@ -341,13 +341,13 @@ }, "dependencies": { "@babel/types": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.3.0.tgz", - "integrity": "sha512-QkFPw68QqWU1/RVPyBe8SO7lXbPfjtqAxRYQKpFpaB8yMq7X2qAqfwK5LKoQufEkSmO5NQ70O6Kc3Afk03RwXw==", + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.3.4.tgz", + "integrity": "sha512-WEkp8MsLftM7O/ty580wAmZzN1nDmCACc5+jFzUt+GUFNNIi3LdRlueYz0YIlmJhlZx1QYDMZL5vdWCL0fNjFQ==", "dev": true, "requires": { "esutils": "^2.0.2", - "lodash": "^4.17.10", + "lodash": "^4.17.11", "to-fast-properties": "^2.0.0" } } @@ -365,13 +365,13 @@ }, "dependencies": { "@babel/types": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.3.0.tgz", - "integrity": "sha512-QkFPw68QqWU1/RVPyBe8SO7lXbPfjtqAxRYQKpFpaB8yMq7X2qAqfwK5LKoQufEkSmO5NQ70O6Kc3Afk03RwXw==", + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.3.4.tgz", + "integrity": "sha512-WEkp8MsLftM7O/ty580wAmZzN1nDmCACc5+jFzUt+GUFNNIi3LdRlueYz0YIlmJhlZx1QYDMZL5vdWCL0fNjFQ==", "dev": true, "requires": { "esutils": "^2.0.2", - "lodash": "^4.17.10", + "lodash": "^4.17.11", "to-fast-properties": "^2.0.0" } } @@ -406,9 +406,9 @@ } }, "@babel/plugin-proposal-object-rest-spread": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.3.1.tgz", - "integrity": "sha512-Nmmv1+3LqxJu/V5jU9vJmxR/KIRWFk2qLHmbB56yRRRFhlaSuOVXscX3gUmhaKgUhzA3otOHVubbIEVYsZ0eZg==", + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.3.4.tgz", + "integrity": "sha512-j7VQmbbkA+qrzNqbKHrBsW3ddFnOeva6wzSe/zB7T+xaxGc+RCpwo44wCmRixAIGRoIpmVgvzFzNJqQcO3/9RA==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0", @@ -453,9 +453,9 @@ } }, "@babel/plugin-transform-async-to-generator": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.2.0.tgz", - "integrity": "sha512-CEHzg4g5UraReozI9D4fblBYABs7IM6UerAVG7EJVrTLC5keh00aEuLUT+O40+mJCEzaXkYfTCUKIyeDfMOFFQ==", + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.3.4.tgz", + "integrity": "sha512-Y7nCzv2fw/jEZ9f678MuKdMo99MFDJMT/PvD9LisrR5JDFcJH6vYeH6RnjVt3p5tceyGRvTtEN0VOlU+rgHZjA==", "dev": true, "requires": { "@babel/helper-module-imports": "^7.0.0", @@ -692,6 +692,14 @@ } } }, + "@phc/format": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@phc/format/-/format-0.5.0.tgz", + "integrity": "sha512-JWtZ5P1bfXU0bAtTzCpOLYHDXuxSVdtL/oqz4+xa97h8w9E5IlVN333wugXVFv8vZ1hbXObKQf1ptXmFFcMByg==", + "requires": { + "safe-buffer": "^5.1.2" + } + }, "@types/bn.js": { "version": "4.11.4", "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-4.11.4.tgz", @@ -1095,6 +1103,16 @@ "integrity": "sha512-ZWc51jO3qegGkVh8Hwpv636EkbesNV5ZNQPCtRa+0qytRYPEs9IYT9qITY9buezqUH5uqyzlWLcufrzU2rffdg==", "dev": true }, + "argon2": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/argon2/-/argon2-0.20.1.tgz", + "integrity": "sha512-ds6SU6YAXoJQGgc9tMOfb55Dyls+b3oaY9bSED0/O83aqlBOEEKR+mbmrR37MmlGaDqKrGDfWoTlHUqeZw8sHQ==", + "requires": { + "@phc/format": "^0.5.0", + "bindings": "^1.3.0", + "node-addon-api": "^1.6.0" + } + }, "argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -1265,9 +1283,9 @@ "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==" }, "ava": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/ava/-/ava-1.2.0.tgz", - "integrity": "sha512-lLfHKB2CRTKdo6OJ2HQ92w5c7EE+Fa1q3XfVbxiCQCHsraypbwAJS9X1zKHtY7yr+uqjnJba4l+VbtU5/9I7yA==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ava/-/ava-1.2.1.tgz", + "integrity": "sha512-EHqbPGdd8aNvlvRNL7liD1J9Auf9kByHj5Zi7zF7Z5ukn2ZStZgVBf7LSqirKIOWScB3XZzFQbO59SnTvzD5kA==", "dev": true, "requires": { "@ava/babel-preset-stage-4": "^2.0.0", @@ -1354,26 +1372,26 @@ }, "dependencies": { "@babel/generator": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.3.0.tgz", - "integrity": "sha512-dZTwMvTgWfhmibq4V9X+LMf6Bgl7zAodRn9PvcPdhlzFMbvUutx74dbEv7Atz3ToeEpevYEJtAwfxq/bDCzHWg==", + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.3.4.tgz", + "integrity": "sha512-8EXhHRFqlVVWXPezBW5keTiQi/rJMQTg/Y9uVCEZ0CAF3PKtCCaVRnp64Ii1ujhkoDhhF1fVsImoN4yJ2uz4Wg==", "dev": true, "requires": { - "@babel/types": "^7.3.0", + "@babel/types": "^7.3.4", "jsesc": "^2.5.1", - "lodash": "^4.17.10", + "lodash": "^4.17.11", "source-map": "^0.5.0", "trim-right": "^1.0.1" } }, "@babel/types": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.3.0.tgz", - "integrity": "sha512-QkFPw68QqWU1/RVPyBe8SO7lXbPfjtqAxRYQKpFpaB8yMq7X2qAqfwK5LKoQufEkSmO5NQ70O6Kc3Afk03RwXw==", + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.3.4.tgz", + "integrity": "sha512-WEkp8MsLftM7O/ty580wAmZzN1nDmCACc5+jFzUt+GUFNNIi3LdRlueYz0YIlmJhlZx1QYDMZL5vdWCL0fNjFQ==", "dev": true, "requires": { "esutils": "^2.0.2", - "lodash": "^4.17.10", + "lodash": "^4.17.11", "to-fast-properties": "^2.0.0" } }, @@ -1452,15 +1470,6 @@ "requires": { "ansi-regex": "^4.0.0" } - }, - "supports-color": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", - "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } } } }, @@ -1502,7 +1511,7 @@ }, "chalk": { "version": "1.1.3", - "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", "dev": true, "requires": { @@ -2245,6 +2254,16 @@ "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } + } } }, "check-types": { @@ -2440,15 +2459,15 @@ "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" }, "codecov": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/codecov/-/codecov-3.1.0.tgz", - "integrity": "sha512-aWQc/rtHbcWEQLka6WmBAOpV58J2TwyXqlpAQGhQaSiEUoigTTUk6lLd2vB3kXkhnDyzyH74RXfmV4dq2txmdA==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/codecov/-/codecov-3.2.0.tgz", + "integrity": "sha512-3NJvNARXxilqnqVfgzDHyVrF4oeVgaYW1c1O6Oi5mn93exE7HTSSFNiYdwojWW6IwrCZABJ8crpNbKoo9aUHQw==", "dev": true, "requires": { "argv": "^0.0.2", "ignore-walk": "^3.0.1", "js-yaml": "^3.12.0", - "request": "^2.87.0", + "teeny-request": "^3.7.0", "urlgrey": "^0.4.4" } }, @@ -3011,7 +3030,7 @@ "dependencies": { "globby": { "version": "6.1.0", - "resolved": "http://registry.npmjs.org/globby/-/globby-6.1.0.tgz", + "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=", "dev": true, "requires": { @@ -3024,7 +3043,7 @@ "dependencies": { "pify": { "version": "2.3.0", - "resolved": "http://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", "dev": true } @@ -3132,7 +3151,7 @@ }, "dotenv": { "version": "4.0.0", - "resolved": "http://registry.npmjs.org/dotenv/-/dotenv-4.0.0.tgz", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-4.0.0.tgz", "integrity": "sha1-hk7xN5rO1Vzm+V3r7NzhefegzR0=", "dev": true }, @@ -3316,9 +3335,9 @@ } }, "envkey": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/envkey/-/envkey-1.2.4.tgz", - "integrity": "sha512-0Eay6CbG41C3mehHvNRQmgZovZqexcitg5BIDHJ2Q+f8ENflz0Aq6ecwZvM+iA4A/cN5RHWC0XQdoSgmBRS6dA==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/envkey/-/envkey-1.2.5.tgz", + "integrity": "sha512-Yi0YMDlNYfCABY6dXgy+9iNPC0P/WsOHF6HAZC1A33WWvsjuiJAFszKDuu0JK/ljkXMFoMu7/aSl3KLBgEp/Nw==", "dev": true, "requires": { "dotenv": "^4.0.0" @@ -3410,9 +3429,9 @@ } }, "esm": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/esm/-/esm-3.1.4.tgz", - "integrity": "sha512-GScwIz0110RTNzBmAQEdqaAYkD9zVhj2Jo+jeizjIcdyTw+C6S0Zv/dlPYgfF41hRTu2f1vQYliubzIkusx2gA==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.7.tgz", + "integrity": "sha512-zsyD5gO8CY9dpK3IrdC4WHtvtHGXEFOpYA4zB+6p+Kygf3vv/6kF3YMEQLOArwKPPNvKt8gjI8UYhQW8bXM/YQ==", "dev": true }, "espower-location-detector": { @@ -4143,9 +4162,9 @@ "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" }, "fp-ts": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/fp-ts/-/fp-ts-1.13.0.tgz", - "integrity": "sha512-QDPtCkuLay+dX3zinnvt0ER+j8mdWZZSpM//DIY3GycgwUTDPBKFD7CxX5DU/mIDstcWqGuuUSrMNtB6MWluCQ==" + "version": "1.14.2", + "resolved": "https://registry.npmjs.org/fp-ts/-/fp-ts-1.14.2.tgz", + "integrity": "sha512-QatIz/9KZsVsf/VXpzAXC9K2wjVJuT3fHSyIFhSLa8ApmqzcRDxKe0oNv75cY2cObywN5ekzfa3dLFqTJYnf+Q==" }, "fragment-cache": { "version": "0.2.1", @@ -5637,6 +5656,14 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } } } }, @@ -5795,9 +5822,9 @@ } }, "ilp-plugin-xrp-paychan-shared": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ilp-plugin-xrp-paychan-shared/-/ilp-plugin-xrp-paychan-shared-4.1.1.tgz", - "integrity": "sha512-IoH2cmmJGvQNjB7RIpXt0Tli7/Q7X98ygIsKWHyOS23TEGLnm4Cs69Hn/6WPLIDWGG/6ILNN4PF4hmhTP4LrjA==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ilp-plugin-xrp-paychan-shared/-/ilp-plugin-xrp-paychan-shared-4.1.3.tgz", + "integrity": "sha512-gvce1ZlyP2s23+7IUdrxcUTM8ncEZ2qZCvwNi6C/5PDAkoQEFCEnEHp+kTF1SwUT8Wb6xymQKRVRKzn08a0IDg==", "requires": { "bignumber.js": "^5.0.0", "debug": "^3.1.0", @@ -6233,7 +6260,7 @@ }, "is-obj": { "version": "1.0.1", - "resolved": "http://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", "dev": true }, @@ -6469,7 +6496,7 @@ "dependencies": { "minimist": { "version": "1.2.0", - "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "dev": true } @@ -7099,6 +7126,17 @@ "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" }, + "node-addon-api": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.6.2.tgz", + "integrity": "sha512-479Bjw9nTE5DdBSZZWprFryHGjUaQC31y1wHo19We/k0BZlrmhqQitWoUL0cD8+scljCbIUL+E58oRDEakdGGA==" + }, + "node-fetch": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.3.0.tgz", + "integrity": "sha512-MOd8pV3fxENbryESLgVIeaGKrdl+uaYhCSSVkjeOb/31/njTpcis5aWfdqgNlHIrKOLRbMnfPINPOML2CIFeXA==", + "dev": true + }, "node-gyp-build": { "version": "3.8.0", "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-3.8.0.tgz", @@ -8431,32 +8469,52 @@ "integrity": "sha1-aabOicRCpEQDFBrS+bNwvVu29O4=" }, "ora": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ora/-/ora-3.0.0.tgz", - "integrity": "sha512-LBS97LFe2RV6GJmXBi6OKcETKyklHNMV0xw7BtsVn2MlsgsydyZetSCbCANr+PFLmDyv4KV88nn0eCKza665Mg==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-3.1.0.tgz", + "integrity": "sha512-vRBPaNCclUi8pUxRF/G8+5qEQkc6EgzKK1G2ZNJUIGu088Un5qIxFXeDgymvPRM9nmrcUOGzQgS1Vmtz+NtlMw==", "dev": true, "requires": { - "chalk": "^2.3.1", + "chalk": "^2.4.2", "cli-cursor": "^2.1.0", - "cli-spinners": "^1.1.0", + "cli-spinners": "^1.3.1", "log-symbols": "^2.2.0", - "strip-ansi": "^4.0.0", + "strip-ansi": "^5.0.0", "wcwidth": "^1.0.1" }, "dependencies": { "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.0.0.tgz", + "integrity": "sha512-iB5Dda8t/UqpPI/IjsejXu5jOGDrzn41wJyljwPH65VCIbk6+1BzFIMJGFwTNrYXT1CrD+B4l19U7awiQ8rk7w==", "dev": true }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.0.0.tgz", + "integrity": "sha512-Uu7gQyZI7J7gn5qLn1Np3G9vcYGTVqB+lFTytnDJv83dd8T22aGH451P3jueT2/QemInJDfxHB5Tde5OzgG1Ow==", "dev": true, "requires": { - "ansi-regex": "^3.0.0" + "ansi-regex": "^4.0.0" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" } } } @@ -8476,7 +8534,7 @@ }, "os-tmpdir": { "version": "1.0.2", - "resolved": "http://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", "dev": true }, @@ -8568,7 +8626,7 @@ "dependencies": { "got": { "version": "6.7.1", - "resolved": "http://registry.npmjs.org/got/-/got-6.7.1.tgz", + "resolved": "https://registry.npmjs.org/got/-/got-6.7.1.tgz", "integrity": "sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA=", "dev": true, "requires": { @@ -8863,9 +8921,9 @@ "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=" }, "prettier": { - "version": "1.16.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.16.3.tgz", - "integrity": "sha512-kn/GU6SMRYPxUakNXhpP0EedT/KmaPzr0H5lIsDogrykbaxOpOfAFfk5XA7DZrJyMAv1wlMV3CPcZruGXVVUZw==", + "version": "1.16.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.16.4.tgz", + "integrity": "sha512-ZzWuos7TI5CKUeQAtFd6Zhm2s6EpAD/ZLApIhsF9pRvRtM1RFo61dM/4MSRUA0SuLugA/zgrZD8m0BaY46Og7g==", "dev": true }, "pretty-ms": { @@ -9067,7 +9125,7 @@ "dependencies": { "minimist": { "version": "1.2.0", - "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "dev": true } @@ -9259,7 +9317,7 @@ "dependencies": { "jsesc": { "version": "0.5.0", - "resolved": "http://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", "dev": true } @@ -9333,9 +9391,9 @@ "dev": true }, "resolve": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.9.0.tgz", - "integrity": "sha512-TZNye00tI67lwYvzxCxHGjwTNlUV70io54/Ed4j6PscB8xVfuBJpRenI/o6dVk0cY0PYTY27AgCoGGxRnYuItQ==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.10.0.tgz", + "integrity": "sha512-3sUr9aq5OfSg2S9pNtPA9hL1FVEAjvfOC4leW0SNf/mpnaakz2a9femSd6LqAww2RaFctwyf1lCqnTHuF1rxDg==", "dev": true, "requires": { "path-parse": "^1.0.6" @@ -9767,7 +9825,7 @@ }, "serialize-error": { "version": "2.1.0", - "resolved": "http://registry.npmjs.org/serialize-error/-/serialize-error-2.1.0.tgz", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-2.1.0.tgz", "integrity": "sha1-ULZ51WNc34Rme9yOWa9OW4HV9go=", "dev": true }, @@ -10405,9 +10463,10 @@ } }, "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, "requires": { "has-flag": "^3.0.0" } @@ -10486,6 +10545,17 @@ } } }, + "teeny-request": { + "version": "3.11.3", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-3.11.3.tgz", + "integrity": "sha512-CKncqSF7sH6p4rzCgkb/z/Pcos5efl0DmolzvlqRQUNcpRIruOhY9+T1FsIlyEbfWd7MsFpodROOwHYh2BaXzw==", + "dev": true, + "requires": { + "https-proxy-agent": "^2.2.1", + "node-fetch": "^2.2.0", + "uuid": "^3.3.2" + } + }, "term-size": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/term-size/-/term-size-1.2.0.tgz", @@ -10710,9 +10780,9 @@ "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==" }, "tslint": { - "version": "5.12.1", - "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.12.1.tgz", - "integrity": "sha512-sfodBHOucFg6egff8d1BvuofoOQ/nOeYNfbp7LDlKBcLNrL3lmS5zoiDGyOMdT7YsEXAwWpTdAHwOGOc8eRZAw==", + "version": "5.13.0", + "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.13.0.tgz", + "integrity": "sha512-ECOOQRxXCYnUUePG5h/+Z1Zouobk3KFpIHA9aKBB/nnMxs97S1JJPDGt5J4cGm1y9U9VmVlfboOxA8n1kSNzGw==", "dev": true, "requires": { "babel-code-frame": "^6.22.0", @@ -10723,6 +10793,7 @@ "glob": "^7.1.1", "js-yaml": "^3.7.0", "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", "resolve": "^1.3.2", "semver": "^5.3.0", "tslib": "^1.8.0", @@ -10738,9 +10809,9 @@ } }, "tslint-config-prettier": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/tslint-config-prettier/-/tslint-config-prettier-1.17.0.tgz", - "integrity": "sha512-NKWNkThwqE4Snn4Cm6SZB7lV5RMDDFsBwz6fWUkTxOKGjMx8ycOHnjIbhn7dZd5XmssW3CwqUjlANR6EhP9YQw==", + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/tslint-config-prettier/-/tslint-config-prettier-1.18.0.tgz", + "integrity": "sha512-xPw9PgNPLG3iKRxmK7DWr+Ea/SzrvfHtjFt5LBl61gk2UBG/DB9kCXRjv+xyIU1rUtnayLeMUVJBcMX8Z17nDg==", "dev": true }, "tslint-config-standard": { @@ -10780,6 +10851,15 @@ } } }, + "tslint-immutable": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/tslint-immutable/-/tslint-immutable-5.3.0.tgz", + "integrity": "sha512-BvQqkrnSAw6hu29TzX6+nrI01gSqZFpvOEQD7HPma7Wc2srYE4eWsfTTRnIlL0oUTzMiNDD0FqaANnI0ju3RLA==", + "dev": true, + "requires": { + "tsutils": "^2.28.0 || ^3.0.0" + } + }, "tsutils": { "version": "2.29.0", "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz", @@ -10835,9 +10915,9 @@ "integrity": "sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g==" }, "typescript": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.3.1.tgz", - "integrity": "sha512-cTmIDFW7O0IHbn1DPYjkiebHxwtCMU+eTy30ZtJNBPF9j2O1ITu5XH2YnBeVRKWHqF+3JQwWJv0Q0aUgX8W7IA==", + "version": "3.3.3333", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.3.3333.tgz", + "integrity": "sha512-JjSKsAfuHBE/fB2oZ8NxtRTk5iGcg6hkYXMnZ3Wc+b2RSqejEqTaem11mHASMnFilHrax3sLK0GDzcJrekZYLw==", "dev": true }, "uid2": { @@ -11679,6 +11759,14 @@ "ansi-regex": "^3.0.0" } }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } + }, "yargs": { "version": "12.0.5", "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz", diff --git a/package.json b/package.json index b12ba5c..5966595 100644 --- a/package.json +++ b/package.json @@ -14,8 +14,8 @@ "scripts": { "build": "tsc", "fix": "run-s format lint", - "format": "prettier --write 'src/**/*.ts' ava.config.js package.json tsconfig.json tslint.json", - "lint": "tslint --fix --project .", + "format": "prettier --write 'src/**/*.{ts,js}' ava.config.js package.json tsconfig.json tslint.json", + "lint": "tslint --project .", "test": "nyc ava", "test-inspect": "node --inspect-brk=9231 node_modules/ava/profile.js --serial" }, @@ -48,14 +48,15 @@ "dependencies": { "@kava-labs/crypto-rate-utils": "^2.0.2", "@kava-labs/ilp-plugin-xrp-asym-client": "^1.7.0", + "argon2": "^0.20.1", "axios": "^0.18.0", "bignumber.js": "^7.2.1", "ethereumjs-util": "^6.1.0", - "fp-ts": "^1.13.0", + "fp-ts": "^1.14.2", "ilp-packet": "^3.0.8", "ilp-plugin-ethereum": "^3.0.0-beta.12", "ilp-plugin-lightning": "^1.0.0-beta.21", - "ilp-plugin-xrp-paychan-shared": "^4.1.1", + "ilp-plugin-xrp-paychan-shared": "^4.1.3", "ilp-protocol-ildcp": "^2.0.1", "ilp-protocol-stream": "github:interledgerjs/ilp-protocol-stream#3fe1f02273c8ac60927d902c7a88c45111c5699b", "oer-utils": "^4.0.0", @@ -73,19 +74,20 @@ "@types/node": "^10.12.21", "@types/protobufjs": "^5.0.31", "@types/web3": "^1.0.18", - "ava": "^1.2.0", - "codecov": "^3.1.0", - "envkey": "^1.2.2", + "ava": "^1.2.1", + "codecov": "^3.2.0", + "envkey": "^1.2.5", "husky": "^1.3.1", "npm-run-all": "^4.1.3", "nyc": "^13.3.0", - "prettier": "^1.16.3", + "prettier": "^1.16.4", "ts-node": "^8.0.2", - "tslint": "^5.12.1", - "tslint-config-prettier": "^1.15.0", + "tslint": "^5.13.0", + "tslint-config-prettier": "^1.18.0", "tslint-config-standard": "^8.0.1", "tslint-eslint-rules": "^5.4.0", - "typescript": "^3.3.1" + "tslint-immutable": "^5.3.0", + "typescript": "^3.3.3333" }, "husky": { "hooks": { diff --git a/tslint.json b/tslint.json index 08af001..26cc7bf 100644 --- a/tslint.json +++ b/tslint.json @@ -2,6 +2,29 @@ "extends": [ "tslint-config-standard", "tslint-eslint-rules", + "tslint-immutable", "tslint-config-prettier" - ] + ], + "rules": { + // Recommended built-in rules + "no-var-keyword": true, + "no-parameter-reassignment": true, + "typedef": [true, "call-signature"], + + // Immutability rules + "readonly-keyword": true, + // "readonly-array": true, + "no-let": true + // "no-array-mutation": true, + // "no-object-mutation": true, + // "no-delete": true, + // "no-method-signature": true, + + // Functional style rules + // "no-this": true, + // "no-class": true, + // "no-mixed-interface": true, + // "no-expression-statement": true, + // "no-if-statement": true + } } From ddb63d3b732c14e5d950bbaea5efb12ec282f156 Mon Sep 17 00:00:00 2001 From: Kincaid O'Neil Date: Wed, 27 Feb 2019 11:03:00 -0500 Subject: [PATCH 02/21] chore: precompile for nyc/ava - pro: error messages/stack traces are much clearer - con: code coverage wasn't working right, but hopefully this fixes it --- .nycrc.json | 8 +++----- ava.config.js | 7 ++----- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/.nycrc.json b/.nycrc.json index 59f342f..dfc9bdb 100644 --- a/.nycrc.json +++ b/.nycrc.json @@ -1,7 +1,5 @@ { - "include": ["src/**/*.ts"], - "exclude": ["**/__tests__/**/*.ts", "**/*.d.ts"], - "extension": [".ts"], - "require": ["ts-node/register"], - "reporter": ["text", "html", "lcov"] + "include": ["build/**/*.js"], + "exclude": ["**/__tests__/**/*.js", "**/*.ts"], + "extension": [".js"] } diff --git a/ava.config.js b/ava.config.js index 84b677f..2b42b7e 100644 --- a/ava.config.js +++ b/ava.config.js @@ -1,10 +1,7 @@ export default { - files: ['src/__tests__/**/*.ts'], + files: ['build/__tests__/**/*.js'], failFast: true, verbose: true, serial: true, - timeout: '3m', - compileEnhancements: false, - extensions: ['ts'], - require: ['ts-node/register'] + timeout: '3m' } From b5575dad5a09e2ec23b396fabcd1991fae4490f3 Mon Sep 17 00:00:00 2001 From: Kincaid O'Neil Date: Wed, 27 Feb 2019 11:46:25 -0500 Subject: [PATCH 03/21] feat: serialize and persist config, simplify internals - fix linting errors/add readonly properties - remove "settlement module" interface - add wip commented out encryption code --- src/config.ts | 98 +++++++++++++++ src/credential.ts | 34 ++++-- src/engine.ts | 46 +++---- src/index.ts | 220 ++++++++++++++-------------------- src/settlement/lnd.ts | 72 ++++++----- src/settlement/machinomy.ts | 92 ++++++-------- src/settlement/xrp-paychan.ts | 85 ++++++------- src/uplink.ts | 73 ++++++----- 8 files changed, 385 insertions(+), 335 deletions(-) create mode 100644 src/config.ts diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..e35398c --- /dev/null +++ b/src/config.ts @@ -0,0 +1,98 @@ +import { LedgerEnv, State } from '.' +import { CredentialConfigs, credentialToConfig } from './credential' +import { BaseUplinkConfig } from './uplink' +import { open, readFile, writeFile, mkdir } from 'fs' +import { promisify } from 'util' +import { homedir } from 'os' +import { hash, verify, argon2id } from 'argon2' +import { randomBytes, createCipheriv } from 'crypto' + +// TODO Basic versioning? +// TODO Use ConfigSchema[] to enable testnet + mainnet configs? + +export interface ConfigSchema { + readonly ledgerEnv: LedgerEnv + readonly credentials: CredentialConfigs[] + readonly uplinks: BaseUplinkConfig[] +} + +const CONFIG_DIR = `${homedir()}/.switch` +export const CONFIG_PATH = `${CONFIG_DIR}/config.json` + +export const persistConfig = async (fd: number, state: State) => + promisify(writeFile)( + fd, + JSON.stringify({ + ledgerEnv: state.ledgerEnv, + uplinks: state.uplinks.map(uplink => uplink.config), + credentials: state.credentials.map(credentialToConfig) + }) + ) + +export const loadConfig = async (): Promise< + [number, ConfigSchema | undefined] +> => { + await promisify(mkdir)(CONFIG_DIR).catch(err => { + if (err.code === 'EEXIST') return + else throw err + }) + + const fd = await promisify(open)(CONFIG_PATH, 'a+') + + const content = await promisify(readFile)(fd, { + encoding: 'utf8' + }) + + // TODO Add *robust* schema validation + return [fd, content.length === 0 ? undefined : JSON.parse(content)] +} + +/** + * ------------------------------------ + * ENCRYPTION + * ------------------------------------ + */ + +/* +const ENCRYPTION_ALGORITHM = 'aes-256-gcm' +const ENCRYPTION_ENCODING = 'utf-8' + +const IV_LENGTH = 12 +const AUTH_TAG_LENGTH = 16 + +const createOutput = async ( + plaintext: string, + salt: string, + iv: string +) => ({ + // TODO Should this also include algorithm-related info? argon2id, iteration count, etc? + + salt, + iv: (await generateSalt()).toString(), + ciphertext: (await encryptConfig(plaintext)).toString() +}) + +// TODO the salt should be passed in here +const encryptConfig = async (config: string): Promise => { + const encryptionKey = await deriveEncryptionKey(password, salt) + + const iv = await randomBytes(IV_LENGTH) + const cipher = createCipheriv(ENCRYPTION_ALGORITHM, encryptionKey, iv) + const ciphertext = cipher.update(Buffer.from(config)) + return Buffer.concat([ciphertext, cipher.final()]) +} + +const decryptConfig = async (ciperText: string) => {} + +// TODO Salt should be created separately +const deriveEncryptionKey = async ( + password: string, + salt?: string +): Promise => + hash(password, { + type: argon2id, + salt: salt ? Buffer.from(salt, 'hex') : await generateSalt() + }) + +const generateSalt = (): Promise => promisify(randomBytes)(16) +*/ diff --git a/src/credential.ts b/src/credential.ts index 08b9bcd..be3a0d3 100644 --- a/src/credential.ts +++ b/src/credential.ts @@ -2,32 +2,35 @@ import { SettlementEngineType } from './engine' import { ValidatedLndCredential, ReadyLndCredential, - Lnd + Lnd, + configFromLndCredential } from './settlement/lnd' import { UnvalidatedXrpSecret, ValidatedXrpSecret, - XrpPaychan + XrpPaychan, + configFromXrpCredential } from './settlement/xrp-paychan' import { State } from '.' import { Machinomy, ReadyEthereumCredential, - ValidatedEthereumPrivateKey + ValidatedEthereumPrivateKey, + configFromEthereumCredential } from './settlement/machinomy' export type CredentialConfigs = ( | ValidatedLndCredential | ValidatedEthereumPrivateKey | UnvalidatedXrpSecret) & { - settlerType: SettlementEngineType + readonly settlerType: SettlementEngineType } export type ReadyCredentials = ( | ReadyLndCredential | ReadyEthereumCredential | ValidatedXrpSecret) & { - settlerType: SettlementEngineType + readonly settlerType: SettlementEngineType } export const setupCredential = (credential: CredentialConfigs) => { @@ -41,7 +44,7 @@ export const setupCredential = (credential: CredentialConfigs) => { } } -// TODO Should this also check the settlerType of the credential? Or could there be a hash/unique id? +// TODO Should this also check the settlerType of the credential? Or could there be a hash/uniqueId? export const getCredential = (state: State) => < TReadyCredential extends ReadyCredentials >( @@ -81,14 +84,14 @@ export const getCredentialId = (credential: ReadyCredentials) => { } } -export const closeCredential = (credential: ReadyCredentials) => { +export const closeCredential = async (credential: ReadyCredentials) => { switch (credential.settlerType) { case SettlementEngineType.Lnd: return Lnd.closeCredential(credential) case SettlementEngineType.Machinomy: - return Machinomy.closeCredential(credential) + return case SettlementEngineType.XrpPaychan: - return XrpPaychan.closeCredential(credential) + return } } @@ -104,3 +107,16 @@ export const isThatCredential = ( ) => (someCredential: ReadyCredentials): someCredential is TReadyCredential => someCredential.settlerType === credential.settlerType && getCredentialId(someCredential) === getCredentialId(credential) + +export const credentialToConfig = ( + credential: ReadyCredentials +): CredentialConfigs => { + switch (credential.settlerType) { + case SettlementEngineType.Lnd: + return configFromLndCredential(credential) + case SettlementEngineType.Machinomy: + return configFromEthereumCredential(credential) + case SettlementEngineType.XrpPaychan: + return configFromXrpCredential(credential) + } +} diff --git a/src/engine.ts b/src/engine.ts index 378186f..14eaf2d 100644 --- a/src/engine.ts +++ b/src/engine.ts @@ -1,12 +1,11 @@ import { AssetUnit } from '@kava-labs/crypto-rate-utils' import BigNumber from 'bignumber.js' -import { LndSettlementEngine, Lnd } from './settlement/lnd' +import { LndSettlementEngine } from './settlement/lnd' import { XrpPaychanSettlementEngine, - XrpPaychan + closeXrpPaychanEngine } from './settlement/xrp-paychan' -import { State, LedgerEnv } from '.' -import { Machinomy } from './settlement/machinomy' +import { MachinomySettlementEngine } from './settlement/machinomy' export enum SettlementEngineType { /** Lightning daeman */ @@ -18,42 +17,29 @@ export enum SettlementEngineType { } export interface SettlementEngine { - settlerType: SettlementEngineType // TODO - - assetCode: string - assetScale: number - baseUnit: (amount?: BigNumber.Value) => AssetUnit - exchangeUnit: (amount?: BigNumber.Value) => AssetUnit + readonly settlerType: SettlementEngineType + readonly assetCode: string + readonly assetScale: number + readonly baseUnit: (amount?: BigNumber.Value) => AssetUnit + readonly exchangeUnit: (amount?: BigNumber.Value) => AssetUnit /** * Mapping of BTP websocket URIs for remote connectors, * specific to the ledger env of the settlement engine */ - remoteConnectors: { + readonly remoteConnectors: { readonly [name: string]: (token: string) => string } } -export type SettlementEngines = LndSettlementEngine | XrpPaychanSettlementEngine +export type SettlementEngines = ( + | LndSettlementEngine + | MachinomySettlementEngine + | XrpPaychanSettlementEngine) & + SettlementEngine -// TODO Add "closeEngine" to specific settlement modules -export const closeEngine = (settler: SettlementEngines) => { +export const closeEngine = async (settler: SettlementEngines) => { switch (settler.settlerType) { - case SettlementEngineType.Lnd: - return - case SettlementEngineType.XrpPaychan: - return - } -} - -export const createEngine = (ledgerEnv: LedgerEnv) => async ( - settlerType: SettlementEngineType -): Promise => { - switch (settlerType) { - case SettlementEngineType.Lnd: - return Lnd.setupEngine(ledgerEnv) - case SettlementEngineType.Machinomy: - return Machinomy.setupEngine(ledgerEnv) case SettlementEngineType.XrpPaychan: - return XrpPaychan.setupEngine(ledgerEnv) + return closeXrpPaychanEngine(settler) } } diff --git a/src/index.ts b/src/index.ts index 8abb58b..5cdd4d8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,108 +1,107 @@ import { AssetUnit, - RateApi, connectCoinCap, + RateApi, usd } from '@kava-labs/crypto-rate-utils' +import BigNumber from 'bignumber.js' +import { + closeCredential, + setupCredential, + CredentialConfigs, + getOrCreateCredential, + isThatCredentialId, + ReadyCredentials, + getCredential +} from './credential' +import { closeEngine, SettlementEngineType } from './engine' +import { streamMoney } from './services/switch' +import { Lnd, LndSettlementEngine } from './settlement/lnd' +import { Machinomy, MachinomySettlementEngine } from './settlement/machinomy' +import { + XrpPaychan, + XrpPaychanSettlementEngine +} from './settlement/xrp-paychan' import { - BaseUplinkConfig, AuthorizeDeposit, AuthorizeWithdrawal, - BaseUplinks, - ReadyUplinks, closeUplink, + createUplink, depositToUplink, isThatUplink, + ReadyUplinks, withdrawFromUplink, - createUplink + connectUplink } from './uplink' -import { - closeEngine, - SettlementEngineType, - SettlementEngine, - SettlementEngines, - createEngine -} from './engine' -import { LndSettlementModule, LndSettlementEngine } from './settlement/lnd' -import { - XrpPaychanSettlementModule, - XrpPaychanSettlementEngine -} from './settlement/xrp-paychan' -import { streamMoney } from './services/switch' -import BigNumber from 'bignumber.js' -import { - ReadyCredentials, - getOrCreateCredential, - closeCredential, - isThatCredentialId, - CredentialConfigs -} from './credential' -import { MachinomySettlementEngine } from 'settlement/machinomy' - -export type SettlementModules = LndSettlementModule | XrpPaychanSettlementModule - -// TODO Is this really necessarily, or could I rename them all to, e.g., "setupXrpCredential" ? -export type SettlementModule< - TSettlerType extends SettlementEngineType, - /** Settlements engines */ - TSettlementEngine extends SettlementEngine, - /** Credentials */ - TValidatedCredential extends CredentialConfigs, - TReadyCredential extends ReadyCredentials, - /** Uplinks */ - // TODO Do the specific types for uplinks themselves actually matter, or is it really just the credential types? - TBaseUplink extends BaseUplinks, - TReadyUplink extends ReadyUplinks -> = { - // settlerType: TSettlerType - /** Settlement engine */ - readonly setupEngine: (ledgerEnv: LedgerEnv) => Promise - /** Credentials */ - readonly setupCredential: ( - opts: TValidatedCredential - ) => (state: State) => Promise - readonly uniqueId: (cred: TReadyCredential) => string - readonly closeCredential: (cred: TReadyCredential) => Promise - /** Uplinks */ - readonly connectUplink: ( - cred: TReadyCredential - ) => (state: State) => (config: BaseUplinkConfig) => Promise - readonly deposit?: ( - uplink: TReadyUplink - ) => ( - state: State - ) => (opts: { - amount: BigNumber - authorize: AuthorizeDeposit - }) => Promise - readonly withdraw?: ( - uplink: TReadyUplink - ) => (state: State) => (authorize: AuthorizeWithdrawal) => Promise +import { loadConfig, persistConfig } from './config' +import { close } from 'fs' +import { promisify } from 'util' + +type ThenArg = T extends Promise ? U : T +export type SwitchApi = ThenArg> + +export enum LedgerEnv { + Mainnet = 'mainnet', + Testnet = 'testnet', + Local = 'local' +} + +export { SettlementEngineType, ReadyUplinks } + +export interface State { + readonly ledgerEnv: LedgerEnv + readonly rateBackend: RateApi + readonly maxInFlightUsd: AssetUnit + // TODO Is this simpler as an array and filter? Hard to get the types right + readonly settlers: { + // [settlerType: keyof typeof SettlementEngineType]: SettlementEngines + readonly [SettlementEngineType.Lnd]: LndSettlementEngine + readonly [SettlementEngineType.Machinomy]: MachinomySettlementEngine + readonly [SettlementEngineType.XrpPaychan]: XrpPaychanSettlementEngine + } + /* tslint:disable-next-line:readonly-keyword TODO */ + credentials: ReadyCredentials[] + /* tslint:disable-next-line:readonly-keyword TODO */ + uplinks: ReadyUplinks[] } export const connect = async (ledgerEnv: LedgerEnv = LedgerEnv.Testnet) => { - let state: State = { + const [fd, config] = await loadConfig() + + // TODO Make sure the config has the right ledgerEnv to support multiple? Idk + + const state: State = { ledgerEnv, rateBackend: await connectCoinCap(), maxInFlightUsd: usd(0.1), settlers: { - // TODO Fix the settlement engine creation ... this is bad - [SettlementEngineType.Lnd]: await createEngine(ledgerEnv)( - SettlementEngineType.Lnd - ), - [SettlementEngineType.Machinomy]: (await createEngine(ledgerEnv)( - SettlementEngineType.Machinomy - )) as MachinomySettlementEngine, - [SettlementEngineType.XrpPaychan]: (await createEngine(ledgerEnv)( - SettlementEngineType.XrpPaychan - )) as XrpPaychanSettlementEngine + [SettlementEngineType.Lnd]: await Lnd.setupEngine(ledgerEnv), + [SettlementEngineType.Machinomy]: await Machinomy.setupEngine(ledgerEnv), + [SettlementEngineType.XrpPaychan]: await XrpPaychan.setupEngine(ledgerEnv) }, credentials: [], uplinks: [] } - // TODO Add functionality to connect existing uplinks based on config - // (unnecessary/backburner until persistence is added) + // TODO Handle error cases if the uplinks fail to connect + + state.credentials = config + ? await Promise.all( + config.credentials.map(cred => setupCredential(cred)(state)) + ) + : [] + + state.uplinks = config + ? await Promise.all( + config.uplinks.map(uplinkConfig => { + // TODO What if, for some reason, the credential doesn't exist? + const cred = getCredential(state)(uplinkConfig.credentialId) + return connectUplink(state)(cred!)(uplinkConfig) + }) + ) + : [] + + const saveInterval = setInterval(() => persistConfig(fd, state), 10000) const add = async ( credentialConfig: CredentialConfigs @@ -117,9 +116,9 @@ export const connect = async (ledgerEnv: LedgerEnv = LedgerEnv.Testnet) => { uplink, ...opts }: { - uplink: ReadyUplinks - amount: BigNumber - authorize: AuthorizeDeposit + readonly uplink: ReadyUplinks + readonly amount: BigNumber + readonly authorize: AuthorizeDeposit }): Promise => { const internalUplink = state.uplinks.filter(isThatUplink(uplink))[0] const internalDeposit = depositToUplink(internalUplink) @@ -130,8 +129,8 @@ export const connect = async (ledgerEnv: LedgerEnv = LedgerEnv.Testnet) => { uplink, authorize }: { - uplink: ReadyUplinks - authorize: AuthorizeWithdrawal + readonly uplink: ReadyUplinks + readonly authorize: AuthorizeWithdrawal }) => { const internalUplink = state.uplinks.filter(isThatUplink(uplink))[0] const internalWithdraw = withdrawFromUplink(internalUplink) @@ -147,10 +146,7 @@ export const connect = async (ledgerEnv: LedgerEnv = LedgerEnv.Testnet) => { return } await closeUplink(internalUplink) - state = { - ...state, - uplinks: state.uplinks.filter(el => !isThatUplink(uplink)(el)) - } + state.uplinks = state.uplinks.filter(el => !isThatUplink(uplink)(el)) // Remove the credential const credentialsToClose = state.credentials.filter( @@ -158,24 +154,18 @@ export const connect = async (ledgerEnv: LedgerEnv = LedgerEnv.Testnet) => { ) await Promise.all(credentialsToClose.map(closeCredential)) - state = { - ...state, - credentials: state.credentials.filter( - someCredential => !credentialsToClose.includes(someCredential) - ) - } - - // TODO Close engine, if there aren't any other credentials that rely on it? + state.credentials = state.credentials.filter( + someCredential => !credentialsToClose.includes(someCredential) + ) } const disconnect = async () => { + clearInterval(saveInterval) + await persistConfig(fd, state) await Promise.all(state.uplinks.map(closeUplink)) await Promise.all(state.credentials.map(closeCredential)) - await Promise.all( - Object.values(state.settlers) - .filter((a): a is SettlementEngines => !!a) // TODO ! - .map(closeEngine) - ) + await Promise.all(Object.values(state.settlers).map(closeEngine)) + await promisify(close)(fd) } // TODO Should disconnecting the API prevent other operations from occuring? (they may not work anyways) @@ -190,29 +180,3 @@ export const connect = async (ledgerEnv: LedgerEnv = LedgerEnv.Testnet) => { disconnect } } - -type ThenArg = T extends Promise ? U : T -export type SwitchApi = ThenArg> - -export enum LedgerEnv { - Mainnet = 'mainnet', - Testnet = 'testnet', - Local = 'local' -} - -export { SettlementEngineType, ReadyUplinks } - -export interface State { - readonly ledgerEnv: LedgerEnv - readonly rateBackend: RateApi - readonly maxInFlightUsd: AssetUnit - // TODO Is this simpler as an array and filter? Hard to get the types right - settlers: { - // [settlerType: keyof typeof SettlementEngineType]: SettlementEngines - [SettlementEngineType.Lnd]: LndSettlementEngine - [SettlementEngineType.Machinomy]: MachinomySettlementEngine - [SettlementEngineType.XrpPaychan]: XrpPaychanSettlementEngine - } - credentials: ReadyCredentials[] - uplinks: ReadyUplinks[] -} diff --git a/src/settlement/lnd.ts b/src/settlement/lnd.ts index 9e6839f..2b76783 100644 --- a/src/settlement/lnd.ts +++ b/src/settlement/lnd.ts @@ -17,7 +17,7 @@ import LightningPlugin, { import { BehaviorSubject, from, fromEvent, interval, merge } from 'rxjs' import { filter, mergeMap, throttleTime } from 'rxjs/operators' import { URL } from 'url' -import { LedgerEnv, SettlementModule, State } from '..' +import { LedgerEnv, State } from '..' import { SettlementEngine, SettlementEngineType } from '../engine' import { Flavor } from '../types/util' import { @@ -35,12 +35,14 @@ import { MemoryStore } from '../utils/store' * ------------------------------------ */ -export type LndSettlementEngine = Flavor +export interface LndSettlementEngine extends SettlementEngine { + readonly settlerType: SettlementEngineType.Lnd +} + const setupEngine = async ( ledgerEnv: LedgerEnv ): Promise => ({ - settlerType: SettlementEngineType.Lnd, // TODO - + settlerType: SettlementEngineType.Lnd, assetCode: 'BTC', assetScale: 8, baseUnit: satoshi, @@ -73,40 +75,42 @@ const splitHost = (host: string): Option => })) export type ValidHost = { - hostname: string - port: number + readonly hostname: string + readonly port: number } // TODO Add method to validate credentials using `setupCredential` then `closeCredential` export interface ValidatedLndCredential { - settlerType: SettlementEngineType.Lnd // TODO! - - /** Hostname that exposes peering and gRPC server (on different ports) */ - hostname: string + /** TODO */ + readonly settlerType: SettlementEngineType.Lnd + /** LND node hostname that exposes peering and gRPC server on different ports */ + readonly hostname: string /** Port for gRPC connections */ - grpcPort?: number + readonly grpcPort?: number /** TLS cert as a Base64-encoded string */ - tlsCert: string + readonly tlsCert: string /** LND macaroon as Base64-encoded string */ - macaroon: string + readonly macaroon: string } export type LndIdentityPublicKey = Flavor export interface ReadyLndCredential { - settlerType: SettlementEngineType.Lnd - + /** TODO */ + readonly settlerType: SettlementEngineType.Lnd /** gRPC client connected to Lighnting node for performing requests */ - service: LndService + readonly service: LndService /** Bidirectional streaming RPC to send outgoing payments and receive attestations */ - paymentStream: PaymentStream + readonly paymentStream: PaymentStream /** Streaming RPC of newly added or settled invoices */ - invoiceStream: InvoiceStream + readonly invoiceStream: InvoiceStream /** Lightning secp256k1 public key */ - identityPublicKey: LndIdentityPublicKey + readonly identityPublicKey: LndIdentityPublicKey /** Streaming updates of balance in channel */ - channelBalance$: BehaviorSubject + readonly channelBalance$: BehaviorSubject + /** TODO */ + readonly config: ValidatedLndCredential } const fetchChannelBalance = async (lightning: LndService) => { @@ -153,7 +157,8 @@ const setupCredential = (opts: ValidatedLndCredential) => async (): Promise< paymentStream, invoiceStream, identityPublicKey, - channelBalance$ + channelBalance$, + config: opts } } @@ -161,6 +166,10 @@ const setupCredential = (opts: ValidatedLndCredential) => async (): Promise< export const closeCredential = async ({ service }: ReadyLndCredential) => service.close() +export const configFromLndCredential = ( + cred: ReadyLndCredential +): ValidatedLndCredential => cred.config + /* * ------------------------------------ * UPLINK @@ -168,13 +177,13 @@ export const closeCredential = async ({ service }: ReadyLndCredential) => */ export interface LndUplinkConfig extends BaseUplinkConfig { - settlerType: SettlementEngineType.Lnd - credentialId: LndIdentityPublicKey + readonly settlerType: SettlementEngineType.Lnd + readonly credentialId: LndIdentityPublicKey } export interface LndBaseUplink extends BaseUplink { - settlerType: SettlementEngineType.Lnd - credentialId: LndIdentityPublicKey + readonly settlerType: SettlementEngineType.Lnd + readonly credentialId: LndIdentityPublicKey } export type ReadyLndUplink = LndBaseUplink & ReadyUplink // TODO 'ReadyUplink' doesn't exist! @@ -240,18 +249,7 @@ const connectUplink = (credential: ReadyLndCredential) => ( * ------------------------------------ */ -export type LndSettlementModule = SettlementModule< - SettlementEngineType.Lnd, - LndSettlementEngine, - ValidatedLndCredential, - ReadyLndCredential, - // LndUplinkConfig, - LndBaseUplink, - ReadyLndUplink -> - -export const Lnd: LndSettlementModule = { - // settlerType: SettlementEngineType.Lnd, +export const Lnd = { setupEngine, setupCredential, uniqueId, diff --git a/src/settlement/machinomy.ts b/src/settlement/machinomy.ts index 9b17664..319fd0a 100644 --- a/src/settlement/machinomy.ts +++ b/src/settlement/machinomy.ts @@ -21,7 +21,7 @@ import { HttpProvider } from 'web3/providers' import createLogger from '../utils/log' import { SettlementEngine, SettlementEngineType } from '../engine' import { fetchGasPrice } from './shared/eth' -import { LedgerEnv, State, SettlementModule } from '..' +import { LedgerEnv, State } from '..' import { BehaviorSubject, fromEvent } from 'rxjs' import { map, timeout, first } from 'rxjs/operators' @@ -32,7 +32,8 @@ import { map, timeout, first } from 'rxjs/operators' */ export interface MachinomySettlementEngine extends SettlementEngine { - ethereumProvider: HttpProvider + readonly settlerType: SettlementEngineType.Machinomy + readonly ethereumProvider: HttpProvider } export const setupEngine = async ( @@ -43,13 +44,8 @@ export const setupEngine = async ( `https://${network}.infura.io/v3/92e263da65ac4703bf99df7828c6beca` ) - // TODO Does the Web3 provider even perform any block polling/are multiple instances bad? - - // TODO Download and run a Parity light client here? - return { settlerType: SettlementEngineType.Machinomy, - assetCode: 'ETH', assetScale: 9, baseUnit: gwei, @@ -75,36 +71,44 @@ export const setupEngine = async ( */ export interface ValidatedEthereumPrivateKey { - settlerType: SettlementEngineType.Machinomy - privateKey: string + readonly settlerType: SettlementEngineType.Machinomy + readonly privateKey: string } export type ReadyEthereumCredential = { - settlerType: SettlementEngineType.Machinomy - privateKey: string - address: string + readonly settlerType: SettlementEngineType.Machinomy + readonly privateKey: string + readonly address: string } +/** + * Ensure that the given string is begins with given prefix + * - Prefix the string if it doesn't already + */ +const prefixWith = (prefix: string, str: string) => + str.startsWith(prefix) ? str : prefix + str + +const addressFromPrivate = (privateKey: string) => + privateToAddress(toBuffer(privateKey)).toString('hex') + +// TODO If the private key is invalid, this should return a specific error rather than throwing export const setupCredential = ({ privateKey, settlerType }: ValidatedEthereumPrivateKey) => async (): Promise< ReadyEthereumCredential -> => { - if (!privateKey.startsWith('0x')) { - privateKey = '0x' + privateKey - } - - return { - settlerType, - privateKey, - address: '0x' + privateToAddress(toBuffer(privateKey)).toString('hex') - } -} +> => ({ + settlerType, + privateKey: prefixWith('0x', privateKey), + address: prefixWith('0x', addressFromPrivate(prefixWith('0x', privateKey))) +}) export const uniqueId = (cred: ReadyEthereumCredential) => cred.address -export const closeCredential = () => Promise.resolve() +export const configFromEthereumCredential = ({ + address, + ...config +}: ReadyEthereumCredential): ValidatedEthereumPrivateKey => config /** * ------------------------------------ @@ -113,14 +117,14 @@ export const closeCredential = () => Promise.resolve() */ export interface MachinomyUplinkConfig extends BaseUplinkConfig { - settlerType: SettlementEngineType.Machinomy - credential: ValidatedEthereumPrivateKey + readonly settlerType: SettlementEngineType.Machinomy + readonly credential: ValidatedEthereumPrivateKey } export interface MachinomyBaseUplink extends BaseUplink { - plugin: EthereumPlugin - settlerType: SettlementEngineType.Machinomy - pluginAccount: EthereumAccount + readonly plugin: EthereumPlugin + readonly settlerType: SettlementEngineType.Machinomy + readonly pluginAccount: EthereumAccount } export type ReadyMachinomyUplink = MachinomyBaseUplink & ReadyUplink @@ -222,8 +226,8 @@ export const deposit = (uplink: ReadyMachinomyUplink) => () => async ({ amount, authorize }: { - amount: BigNumber - authorize: AuthorizeDeposit + readonly amount: BigNumber + readonly authorize: AuthorizeDeposit }) => { const fundAmountWei = convert(eth(amount), wei()) await uplink.pluginAccount.fundOutgoingChannel(fundAmountWei, async fee => { @@ -259,6 +263,7 @@ const withdraw = (uplink: ReadyMachinomyUplink) => (state: State) => async ( } ) + // TODO This won't reject if the withdraw fails! const requestClose = uplink.pluginAccount.requestClose() // Simultaneously withdraw and request incoming capacity to be removed @@ -273,33 +278,10 @@ const withdraw = (uplink: ReadyMachinomyUplink) => (state: State) => async ( * ------------------------------------ */ -export interface MachinomySettlementModule - extends SettlementModule< - SettlementEngineType.Machinomy, - MachinomySettlementEngine, - ValidatedEthereumPrivateKey, - ReadyEthereumCredential, - MachinomyBaseUplink, - ReadyMachinomyUplink - > { - readonly deposit: ( - uplink: ReadyMachinomyUplink - ) => ( - state: State - ) => (opts: { - amount: BigNumber - authorize: AuthorizeDeposit - }) => Promise - readonly withdraw: ( - uplink: ReadyMachinomyUplink - ) => (state: State) => (authorize: AuthorizeDeposit) => Promise -} - -export const Machinomy: MachinomySettlementModule = { +export const Machinomy = { setupEngine, setupCredential, uniqueId, - closeCredential, connectUplink, deposit, withdraw diff --git a/src/settlement/xrp-paychan.ts b/src/settlement/xrp-paychan.ts index e5ca1f0..5762d19 100644 --- a/src/settlement/xrp-paychan.ts +++ b/src/settlement/xrp-paychan.ts @@ -9,7 +9,7 @@ import { FormattedPaymentChannel, RippleAPI } from 'ripple-lib' import { BehaviorSubject, combineLatest, Observable, Subject } from 'rxjs' import { filter, map } from 'rxjs/operators' import { Flavor } from 'types/util' -import { LedgerEnv, SettlementModule, State } from '..' +import { LedgerEnv, State } from '..' import { isThatCredentialId } from '../credential' import { SettlementEngine, SettlementEngineType } from '../engine' import { @@ -31,7 +31,8 @@ const log = createLogger('switch-api:xrp-paychan') */ export interface XrpPaychanSettlementEngine extends SettlementEngine { - api: RippleAPI + readonly settlerType: SettlementEngineType.XrpPaychan + readonly api: RippleAPI } const getXrpServerWebsocketUri = (ledgerEnv: LedgerEnv): string => @@ -67,6 +68,10 @@ const setupEngine = async ( } } +export const closeXrpPaychanEngine = ({ + api +}: XrpPaychanSettlementEngine): Promise => api.disconnect() + /** * ------------------------------------ * CREDENTIAL @@ -74,15 +79,15 @@ const setupEngine = async ( */ export type UnvalidatedXrpSecret = { - settlerType: SettlementEngineType.XrpPaychan - secret: string + readonly settlerType: SettlementEngineType.XrpPaychan + readonly secret: string } export type ValidatedXrpSecret = Flavor< { - settlerType: SettlementEngineType.XrpPaychan - secret: string - address: string + readonly settlerType: SettlementEngineType.XrpPaychan + readonly secret: string + readonly address: string }, 'ValidatedXrpSecret' > @@ -105,8 +110,10 @@ const setupCredential = (cred: UnvalidatedXrpSecret) => async ( const uniqueId = (cred: ValidatedXrpSecret): string => cred.address -// TODO Can I eliminate this? -const closeCredential = () => Promise.resolve() +export const configFromXrpCredential = ({ + address, + ...cred +}: ValidatedXrpSecret): UnvalidatedXrpSecret => cred /** * ------------------------------------ @@ -114,17 +121,12 @@ const closeCredential = () => Promise.resolve() * ------------------------------------ */ -export interface XrpPaychanUplinkConfig extends BaseUplinkConfig { - settlerType: SettlementEngineType.XrpPaychan - credentialId: string -} - export interface XrpPaychanBaseUplink extends BaseUplink { - settlerType: SettlementEngineType.XrpPaychan - credentialId: string - plugin: XrpAsymClient - incomingChannelAmount$: BehaviorSubject - outgoingChannelAmount$: BehaviorSubject + readonly settlerType: SettlementEngineType.XrpPaychan + readonly credentialId: string + readonly plugin: XrpAsymClient + readonly incomingChannelAmount$: BehaviorSubject + readonly outgoingChannelAmount$: BehaviorSubject } export type ReadyXrpPaychanUplink = XrpPaychanBaseUplink & ReadyUplink @@ -157,8 +159,8 @@ const connectUplink = (credential: ValidatedXrpSecret) => ( /** Stream of updated properties on the underlying plugin */ const plugin$ = new Subject<{ - key: any - val: any + readonly key: any + readonly val: any }>() /** Trap all property updates on the plugin to emit them on observable */ @@ -302,8 +304,8 @@ const deposit = (uplink: ReadyXrpPaychanUplink) => (state: State) => async ({ amount, authorize }: { - amount: BigNumber - authorize: AuthorizeDeposit + readonly amount: BigNumber + readonly authorize: AuthorizeDeposit }) => { const { api } = state.settlers[uplink.settlerType] const { address } = getCredential(state)(uplink.credentialId) @@ -355,6 +357,7 @@ const deposit = (uplink: ReadyXrpPaychanUplink) => (state: State) => async ({ // TODO Since the channel is now open, perform the rest of the connect handshake + // TODO Basically, perform this handshake whenever there's no incoming capacity if (requiresNewChannel) { await uplink.plugin._performConnectHandshake() @@ -379,6 +382,16 @@ const withdraw = (uplink: ReadyXrpPaychanUplink) => (state: State) => async ( } const { address, secret } = readyCredential + /** + * TODO This also throws an error if there's no incoming claim -- + * I should perform checks and submit a single tx in that case + * + * (TODO add close=true option to asym-client? a lot of the checks are already there) + * (also, a LOT of race conditions in asym-client in general... yikes!) + * + * (At some point, it'd just be easier to port it over from eth!) + */ + // Submit a claim to the ledger, if it's profitable // TODO Combine this and channel close into a single tx? await uplink.plugin._autoClaim() @@ -434,34 +447,10 @@ const withdraw = (uplink: ReadyXrpPaychanUplink) => (state: State) => async ( * ------------------------------------ */ -export interface XrpPaychanSettlementModule - extends SettlementModule< - SettlementEngineType.XrpPaychan, - XrpPaychanSettlementEngine, - UnvalidatedXrpSecret, - ValidatedXrpSecret, - XrpPaychanBaseUplink, - ReadyXrpPaychanUplink - > { - readonly deposit: ( - uplink: ReadyXrpPaychanUplink - ) => ( - state: State - ) => (opts: { - amount: BigNumber - authorize: AuthorizeDeposit - }) => Promise - - readonly withdraw: ( - uplink: ReadyXrpPaychanUplink - ) => (state: State) => (authorize: AuthorizeWithdrawal) => Promise -} - -export const XrpPaychan: XrpPaychanSettlementModule = { +export const XrpPaychan = { setupEngine, setupCredential, uniqueId, - closeCredential, connectUplink, deposit, withdraw diff --git a/src/uplink.ts b/src/uplink.ts index fbf69c9..6562e51 100644 --- a/src/uplink.ts +++ b/src/uplink.ts @@ -16,12 +16,8 @@ import { distinctUntilChanged, map } from 'rxjs/operators' import { State } from '.' import { startStreamServer, stopStreamServer } from './services/stream-server' import { SettlementEngine, SettlementEngineType } from './engine' -import { LndBaseUplink, LndUplinkConfig, Lnd } from './settlement/lnd' -import { - XrpPaychanBaseUplink, - XrpPaychanUplinkConfig, - XrpPaychan -} from './settlement/xrp-paychan' +import { LndBaseUplink, Lnd } from './settlement/lnd' +import { XrpPaychanBaseUplink, XrpPaychan } from './settlement/xrp-paychan' import { DataHandler, IlpPrepareHandler, Plugin } from './types/plugin' import { defaultDataHandler, defaultIlpPrepareHandler } from './utils/packet' import { SimpleStore, MemoryStore } from './utils/store' @@ -32,24 +28,29 @@ import { Machinomy, MachinomyBaseUplink } from './settlement/machinomy' const log = createLogger('switch-api:uplink') +/** TODO The config to export should be *re-generated* each time by an uplink */ + export interface BaseUplinkConfig { - settlerType: SettlementEngineType - stream: { - /** Enables deterministic generation of previous shared secrets so we can accept payments */ - serverSecret: Buffer + readonly settlerType: SettlementEngineType + readonly credentialId: string + readonly stream: { + /** + * Deterministic generation of previous shared secrets so we can accept payments + * - Encoded as a hex string + */ + readonly serverSecret: string } - plugin: { - btp: { - serverUri: string - authToken: string + readonly plugin: { + readonly btp: { + readonly serverUri: string + readonly authToken: string } - store: SimpleStore + // TODO Should the wrapper & plugin have separate stores? (for security) (with new connector, it probs won't) + // TODO Should the store be versioned to the version of the plugin? (would make migrations easier) + readonly store: SimpleStore } } -export type UplinkConfig = (LndUplinkConfig | XrpPaychanUplinkConfig) & - BaseUplinkConfig - export interface BaseUplink { readonly plugin: Plugin readonly settlerType: SettlementEngineType @@ -85,8 +86,10 @@ export interface ReadyUplink { /** Wrapper plugin with balance logic to and perform accounting and limit the packets we fulfill */ readonly pluginWrapper: PluginWrapper /** Handle incoming packets from the endpoint sending money or trading */ + /* tslint:disable-next-line:readonly-keyword TODO */ streamClientHandler: IlpPrepareHandler /** Handle incoming packets from the endpoint receiving money from other parties */ + /* tslint:disable-next-line:readonly-keyword TODO */ streamServerHandler: DataHandler /** ILP address assigned from upstream connector */ readonly clientAddress: string @@ -98,8 +101,10 @@ export interface ReadyUplink { readonly availableToSend$: BehaviorSubject /** Total amount that we could receive immediately over Interledger */ readonly availableToReceive$: BehaviorSubject - /** STREAM server to accept incoming payments from any Interledger user */ + /** STREAM server to accept incoming payments from any Interledger client */ readonly streamServer: StreamServer + /** TODO */ + readonly config: BaseUplinkConfig } export type ReadyUplinks = ReadyUplink & BaseUplinks @@ -145,8 +150,9 @@ export const createUplink = (state: State) => async ( const config: BaseUplinkConfig = { settlerType: readyCredential.settlerType, + credentialId, stream: { - serverSecret: await generateSecret() + serverSecret: (await generateSecret()).toString('hex') }, plugin: { btp: { @@ -205,6 +211,7 @@ export const connectUplink = (state: State) => ( totalReceived$.pipe(distinctBigNum) ) .pipe(sumAll) + // TODO Try subscribing directly...? .subscribe( amount => { balance$.next(amount) @@ -230,7 +237,9 @@ export const connectUplink = (state: State) => ( // the peer needs the capacity to send us the settlement for that -- it should be subtracted! const handlers: { + /* tslint:disable-next-line:readonly-keyword TODO */ streamServerHandler: DataHandler + /* tslint:disable-next-line:readonly-keyword TODO */ streamClientHandler: IlpPrepareHandler } = { streamServerHandler: defaultDataHandler, @@ -239,20 +248,21 @@ export const connectUplink = (state: State) => ( // Setup internal packet handlers and routing setupHandlers( - pluginWrapper, + plugin, clientAddress, (data: Buffer) => handlers.streamServerHandler(data), (prepare: IlpPrepare) => handlers.streamClientHandler(prepare) ) // Accept incoming payments + // TODO For now, this won't work, because there's no balance logic the stream plugin being used const registerServerHandler = (handler: DataHandler) => { handlers.streamServerHandler = handler } const streamServer = await startStreamServer( plugin, registerServerHandler, - config.stream.serverSecret + Buffer.from(config.stream.serverSecret, 'hex') ) return Object.assign(handlers, { @@ -263,7 +273,8 @@ export const connectUplink = (state: State) => ( pluginWrapper, balance$, availableToSend$, - availableToReceive$ + availableToReceive$, + config }) } @@ -355,7 +366,7 @@ export const sendPacket = async ( * Registers a handler for incoming packets not addressed to a * specific Stream connection, such as packets sent from another uplink * - * EFFECT: changes data handler on internal plugin + * EFFECT: mutates data handler mapped to the internal plugin */ export const registerPacketHandler = (handler: IlpPrepareHandler) => ( uplink: ReadyUplinks @@ -363,6 +374,12 @@ export const registerPacketHandler = (handler: IlpPrepareHandler) => ( uplink.streamClientHandler = handler } +/** + * Removes an existing handler for incoming packets not + * addressed to a specific Stream connection + * + * EFFECT: mutates data handler mapped to the internal plugin + */ export const deregisterPacketHandler = registerPacketHandler( defaultIlpPrepareHandler ) @@ -388,16 +405,16 @@ export const getNativeMaxInFlight = async ( export type AuthorizeDeposit = (params: { /** Total amount that will move from layer 1 to layer 2, in units of exchange */ - value: BigNumber + readonly value: BigNumber /** Amount burned/lost as fee as a result of the transaction, in units of exchange */ - fee: BigNumber + readonly fee: BigNumber }) => Promise export type AuthorizeWithdrawal = (params: { /** Total amount that will move from layer 2 to layer 1, in units of exchange */ - value: BigNumber + readonly value: BigNumber /** Amount burned/lost as fee as a result of the transaction, in units of exchange */ - fee: BigNumber + readonly fee: BigNumber }) => Promise export const depositToUplink = (uplink: ReadyUplinks) => { From 6b8260dc1f3ad2c9ad9d35efc049399bb69bb217 Mon Sep 17 00:00:00 2001 From: Kincaid O'Neil Date: Wed, 27 Feb 2019 11:58:14 -0500 Subject: [PATCH 04/21] fix: more stringent linting - middlewares no longer implements full plugin interface since it's not necesary for now --- src/__tests__/stream.test.ts | 20 ++++++++++++ src/services/stream-server.ts | 12 ++++---- src/services/switch.ts | 11 ++++--- src/types/util.ts | 4 +-- src/utils/middlewares.ts | 57 ++++++++++++----------------------- src/utils/store.ts | 11 ++++--- 6 files changed, 60 insertions(+), 55 deletions(-) diff --git a/src/__tests__/stream.test.ts b/src/__tests__/stream.test.ts index 4972667..de30541 100644 --- a/src/__tests__/stream.test.ts +++ b/src/__tests__/stream.test.ts @@ -10,6 +10,9 @@ import { } from '..' import BigNumber from 'bignumber.js' import { performance } from 'perf_hooks' +import { promisify } from 'util' +import { unlink } from 'fs' +import { CONFIG_PATH } from '../config' const test = anyTest as TestInterface @@ -37,6 +40,8 @@ export const addXrp = ({ add }: SwitchApi): Promise => // Before & after each test, construct and disconnect the API test.beforeEach(async t => { + // Delete any existing config + await promisify(unlink)(CONFIG_PATH) t.context = await connect(process.env.LEDGER_ENV! as LedgerEnv) }) @@ -107,6 +112,8 @@ const testFunding = ( 'uplink can stream money to itself' ) + // TODO In the case of eth, this won't actually get the final claim before it withdraws + await t.notThrowsAsync( withdraw({ uplink, authorize: () => Promise.resolve() }), 'withdraws from channel without throwing an error' @@ -196,3 +203,16 @@ test('btc -> eth', testExchange(addBtc, addEth)) test('btc -> xrp', testExchange(addBtc, addXrp)) test('eth -> btc', testExchange(addEth, addBtc)) test('eth -> xrp', testExchange(addEth, addXrp)) + +// test.only('persistence', async t => { +// const api = t.context +// const { disconnect } = api + +// await Promise.all([addBtc(api), addEth(api)]) +// await disconnect() + +// t.context = await connect(process.env.LEDGER_ENV! as LedgerEnv) +// const newApi = t.context + +// t.true(newApi.state.uplinks.length === 2) +// }) diff --git a/src/services/stream-server.ts b/src/services/stream-server.ts index 551d344..7833cbd 100644 --- a/src/services/stream-server.ts +++ b/src/services/stream-server.ts @@ -37,23 +37,23 @@ export const wrapStreamPlugin = ( plugin: Plugin, registerDataHandler: (handler: DataHandler) => void ): IlpStreamPlugin => ({ - connect() { + connect(): Promise { return plugin.connect() }, - disconnect() { + disconnect(): Promise { // Don't let Stream disconnect the plugin return Promise.resolve() }, - isConnected() { + isConnected(): boolean { return plugin.isConnected() }, - sendData(data) { + sendData(data): Promise { return plugin.sendData(data) }, - registerDataHandler(handler) { + registerDataHandler(handler): void { registerDataHandler(handler) }, - deregisterDataHandler() { + deregisterDataHandler(): void { registerDataHandler(defaultDataHandler) } }) diff --git a/src/services/switch.ts b/src/services/switch.ts index bf1bd9a..ef6bfdc 100644 --- a/src/services/switch.ts +++ b/src/services/switch.ts @@ -17,6 +17,9 @@ const log = createLogger('switch-api:stream') BigNumber.config({ EXPONENTIAL_AT: 1e9 }) // Almost never use exponential notation +// TODO Remove this rule... fix this eventually! +/* tslint:disable:no-let */ + /** End stream if no packets are successfully fulfilled within this interval */ const IDLE_TIMEOUT = 10000 @@ -39,17 +42,17 @@ export const streamMoney = (state: State) => async ({ slippage = 0.01 }: { /** Amount of money to be sent over stream, in units of exchange */ - amount: BigNumber + readonly amount: BigNumber /** Send assets via the given source ledger/plugin */ - source: ReadyUplinks + readonly source: ReadyUplinks /** Receive assets via the given destination ledger/plugin */ - dest: ReadyUplinks + readonly dest: ReadyUplinks /** * Maximum percentage of slippage allowed. If the per-packet exchange rate * drops below the price oracle's rate minus this slippage, * the packet will be rejected */ - slippage?: BigNumber.Value + readonly slippage?: BigNumber.Value }): Promise => { const sourceSettler = state.settlers[source.settlerType] const destSettler = state.settlers[dest.settlerType] diff --git a/src/types/util.ts b/src/types/util.ts index a86a271..d9eb7fa 100644 --- a/src/types/util.ts +++ b/src/types/util.ts @@ -1,7 +1,7 @@ interface Flavoring { - _type?: FlavorT + readonly _type?: FlavorT } export type Flavor = T & Flavoring -export type Brand = K & { __brand: T } +export type Brand = K & { readonly __brand: T } diff --git a/src/utils/middlewares.ts b/src/utils/middlewares.ts index e3ae9e0..5598513 100644 --- a/src/utils/middlewares.ts +++ b/src/utils/middlewares.ts @@ -15,20 +15,25 @@ import { BehaviorSubject } from 'rxjs' BigNumber.config({ EXPONENTIAL_AT: 1e9 }) export interface PluginWrapperOpts { - plugin: Plugin - maxBalance?: BigNumber.Value - maxPacketAmount?: BigNumber.Value - log: Logger - assetCode: string - assetScale: number - store?: MemoryStore + readonly plugin: Plugin + readonly maxBalance?: BigNumber.Value + readonly maxPacketAmount?: BigNumber.Value + readonly log: Logger + readonly assetCode: string + readonly assetScale: number + readonly store: MemoryStore } -export class PluginWrapper implements Plugin { +// TODO Since this isn't really used as a class anymore, could I just use these as standalone functions +//   existing around a plugin? +// (How do I ensure that stream only calls these functions, though?) + +export class PluginWrapper { static readonly version = 2 // Internal plugin private readonly plugin: Plugin + /* tslint:disable-next-line:readonly-keyword TODO */ private dataHandler: DataHandler = defaultDataHandler /** @@ -89,7 +94,7 @@ export class PluginWrapper implements Plugin { this.plugin.registerDataHandler(data => this.handleData(data)) this.plugin.registerMoneyHandler(amount => this.handleMoney(amount)) - this.store = store || new MemoryStore() + this.store = store this.log = log this.assetCode = assetCode this.assetScale = assetScale @@ -144,7 +149,7 @@ export class PluginWrapper implements Plugin { return response } - async sendMoney(amount: string) { + async sendMoney(amount: string): Promise { if (parseInt(amount, 10) <= 0) { return } @@ -161,7 +166,7 @@ export class PluginWrapper implements Plugin { * Incoming packets/settlements (receivable balance) */ - private async handleMoney(amount: string) { + private async handleMoney(amount: string): Promise { if (new BigNumber(amount).isZero()) { return } @@ -238,39 +243,15 @@ export class PluginWrapper implements Plugin { * Plugin wrapper */ - async connect(opts?: object) { - return this.plugin.connect(opts) - } - - disconnect() { - return this.plugin.disconnect() - } - - isConnected() { - return this.plugin.isConnected() - } - - registerDataHandler(handler: DataHandler) { - if (this.dataHandler !== defaultDataHandler) { - throw new Error('request handler is already registered') - } - + registerDataHandler(handler: DataHandler): void { this.dataHandler = handler } - deregisterDataHandler() { + deregisterDataHandler(): void { this.dataHandler = defaultDataHandler } - registerMoneyHandler() { - return - } - - deregisterMoneyHandler() { - return - } - - private format(amount: BigNumber.Value) { + private format(amount: BigNumber.Value): string { return `${new BigNumber(amount).shiftedBy( -this.assetScale )} ${this.assetCode.toLowerCase()}` diff --git a/src/utils/store.ts b/src/utils/store.ts index 4b9e103..a4f18cc 100644 --- a/src/utils/store.ts +++ b/src/utils/store.ts @@ -1,16 +1,17 @@ export interface PluginStore { - get: (key: string) => Promise - put: (key: string, value: string) => Promise - del: (key: string) => Promise + readonly get: (key: string) => Promise + readonly put: (key: string, value: string) => Promise + readonly del: (key: string) => Promise } export interface SimpleStore { + /* tslint:disable-next-line:readonly-keyword TODO */ [key: string]: string } export class MemoryStore implements PluginStore { - private store: SimpleStore - private prefix: string + private readonly store: SimpleStore + private readonly prefix: string constructor(store: SimpleStore = {}, prefix = '') { this.store = store From aa7f1ff5bee37f6825c110a59039e2de4df04eca Mon Sep 17 00:00:00 2001 From: Kincaid O'Neil Date: Thu, 28 Feb 2019 16:19:52 -0500 Subject: [PATCH 05/21] fix: update eth plugin --- package-lock.json | 46 +++++++++++++++++++++++----------------------- package.json | 4 ++-- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9706b9d..a3d8a8a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1511,7 +1511,7 @@ }, "chalk": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", "dev": true, "requires": { @@ -3030,7 +3030,7 @@ "dependencies": { "globby": { "version": "6.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", + "resolved": "http://registry.npmjs.org/globby/-/globby-6.1.0.tgz", "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=", "dev": true, "requires": { @@ -3043,7 +3043,7 @@ "dependencies": { "pify": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "resolved": "http://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", "dev": true } @@ -3151,7 +3151,7 @@ }, "dotenv": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-4.0.0.tgz", + "resolved": "http://registry.npmjs.org/dotenv/-/dotenv-4.0.0.tgz", "integrity": "sha1-hk7xN5rO1Vzm+V3r7NzhefegzR0=", "dev": true }, @@ -4162,9 +4162,9 @@ "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" }, "fp-ts": { - "version": "1.14.2", - "resolved": "https://registry.npmjs.org/fp-ts/-/fp-ts-1.14.2.tgz", - "integrity": "sha512-QatIz/9KZsVsf/VXpzAXC9K2wjVJuT3fHSyIFhSLa8ApmqzcRDxKe0oNv75cY2cObywN5ekzfa3dLFqTJYnf+Q==" + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/fp-ts/-/fp-ts-1.14.3.tgz", + "integrity": "sha512-tAnefu8QCuPaUaEywmhzpBWbRq2NRcailU5gtV2O09+xXVnJMBIyJi4qXy2fMlhMFsebDqUPpgoHpSErs94HTg==" }, "fragment-cache": { "version": "0.2.1", @@ -5700,9 +5700,9 @@ } }, "ilp-plugin-ethereum": { - "version": "3.0.0-beta.12", - "resolved": "https://registry.npmjs.org/ilp-plugin-ethereum/-/ilp-plugin-ethereum-3.0.0-beta.12.tgz", - "integrity": "sha512-Jpp613KiRFs/3NPoNj81DxLAufw+FSOJQHBkr4he2sciVCBNFJFWYZJfh26OgSNGLHkdMDPqXZjPvPqJd/TcyA==", + "version": "3.0.0-beta.13", + "resolved": "https://registry.npmjs.org/ilp-plugin-ethereum/-/ilp-plugin-ethereum-3.0.0-beta.13.tgz", + "integrity": "sha512-aBc7r6kkFCPJgDhuVPoSm8Tkl492YZX6V8LbhAJktz/B9is0nRKgNVyCdeTsj0KHWvkxUvyj0J04hVXrneBVWg==", "requires": { "@kava-labs/crypto-rate-utils": "^2.0.2", "@types/ethereumjs-util": "^5.2.0", @@ -6260,7 +6260,7 @@ }, "is-obj": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "resolved": "http://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", "dev": true }, @@ -6496,7 +6496,7 @@ "dependencies": { "minimist": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "dev": true } @@ -8534,7 +8534,7 @@ }, "os-tmpdir": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "resolved": "http://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", "dev": true }, @@ -8626,7 +8626,7 @@ "dependencies": { "got": { "version": "6.7.1", - "resolved": "https://registry.npmjs.org/got/-/got-6.7.1.tgz", + "resolved": "http://registry.npmjs.org/got/-/got-6.7.1.tgz", "integrity": "sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA=", "dev": true, "requires": { @@ -8646,9 +8646,9 @@ } }, "pako": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.8.tgz", - "integrity": "sha512-6i0HVbUfcKaTv+EG8ZTr75az7GFXcLYk9UyLEg7Notv/Ma+z/UG3TCoz6GiNeOrn1E/e63I0X/Hpw18jHOTUnA==" + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.9.tgz", + "integrity": "sha512-tPjtE6dq+dOSg8NMkqRmFjUYH9fect1zmYgB0g6ztQMaVNI7N1CEvLZud2bPHhg7PRgfKEeTshSPiqXb1F7A+A==" }, "parallel-transform": { "version": "1.1.0", @@ -9125,7 +9125,7 @@ "dependencies": { "minimist": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "dev": true } @@ -9317,7 +9317,7 @@ "dependencies": { "jsesc": { "version": "0.5.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "resolved": "http://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", "dev": true } @@ -9825,7 +9825,7 @@ }, "serialize-error": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-2.1.0.tgz", + "resolved": "http://registry.npmjs.org/serialize-error/-/serialize-error-2.1.0.tgz", "integrity": "sha1-ULZ51WNc34Rme9yOWa9OW4HV9go=", "dev": true }, @@ -10583,9 +10583,9 @@ } }, "terser-webpack-plugin": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.2.2.tgz", - "integrity": "sha512-1DMkTk286BzmfylAvLXwpJrI7dWa5BnFmscV/2dCr8+c56egFcbaeFAl7+sujAjdmpLam21XRdhA4oifLyiWWg==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.2.3.tgz", + "integrity": "sha512-GOK7q85oAb/5kE12fMuLdn2btOS9OBZn4VsecpHDywoUC/jLhSAKOiYo0ezx7ss2EXPMzyEWFoE0s1WLE+4+oA==", "requires": { "cacache": "^11.0.2", "find-cache-dir": "^2.0.0", diff --git a/package.json b/package.json index 5966595..d04245f 100644 --- a/package.json +++ b/package.json @@ -52,9 +52,9 @@ "axios": "^0.18.0", "bignumber.js": "^7.2.1", "ethereumjs-util": "^6.1.0", - "fp-ts": "^1.14.2", + "fp-ts": "^1.14.3", "ilp-packet": "^3.0.8", - "ilp-plugin-ethereum": "^3.0.0-beta.12", + "ilp-plugin-ethereum": "^3.0.0-beta.13", "ilp-plugin-lightning": "^1.0.0-beta.21", "ilp-plugin-xrp-paychan-shared": "^4.1.3", "ilp-protocol-ildcp": "^2.0.1", From 5081bcefd0bbafad56e5a60c21ab98dbe65910f0 Mon Sep 17 00:00:00 2001 From: Kincaid O'Neil Date: Thu, 28 Feb 2019 16:52:43 -0500 Subject: [PATCH 06/21] fix: error deleting config --- src/__tests__/stream.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/__tests__/stream.test.ts b/src/__tests__/stream.test.ts index de30541..68f5e6c 100644 --- a/src/__tests__/stream.test.ts +++ b/src/__tests__/stream.test.ts @@ -41,7 +41,7 @@ export const addXrp = ({ add }: SwitchApi): Promise => test.beforeEach(async t => { // Delete any existing config - await promisify(unlink)(CONFIG_PATH) + await promisify(unlink)(CONFIG_PATH).catch(() => Promise.resolve()) t.context = await connect(process.env.LEDGER_ENV! as LedgerEnv) }) From b9eec0945f6050910c1c441a2eeff13f73427549 Mon Sep 17 00:00:00 2001 From: Kincaid O'Neil Date: Thu, 28 Feb 2019 17:26:19 -0500 Subject: [PATCH 07/21] fix: codecov reporting with sourcemaps --- ava.config.js | 3 ++- package-lock.json | 6 +++--- package.json | 1 + 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/ava.config.js b/ava.config.js index 2b42b7e..debf03f 100644 --- a/ava.config.js +++ b/ava.config.js @@ -3,5 +3,6 @@ export default { failFast: true, verbose: true, serial: true, - timeout: '3m' + timeout: '3m', + require: ['source-map-support/register'] } diff --git a/package-lock.json b/package-lock.json index a3d8a8a..b2678f6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10194,9 +10194,9 @@ } }, "source-map-support": { - "version": "0.5.9", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.9.tgz", - "integrity": "sha512-gR6Rw4MvUlYy83vP0vxoVNzM6t8MUXqNuRsuBmBHQDu1Fh6X015FrLdgoDKcNdkwGubozq0P4N0Q37UyFVr1EA==", + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.10.tgz", + "integrity": "sha512-YfQ3tQFTK/yzlGJuX8pTwa4tifQj4QS2Mj7UegOu8jAz59MqIiMGPXxQhVQiIMNzayuUSF/jEuVnfFF5JqybmQ==", "requires": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" diff --git a/package.json b/package.json index d04245f..026aff7 100644 --- a/package.json +++ b/package.json @@ -81,6 +81,7 @@ "npm-run-all": "^4.1.3", "nyc": "^13.3.0", "prettier": "^1.16.4", + "source-map-support": "^0.5.10", "ts-node": "^8.0.2", "tslint": "^5.13.0", "tslint-config-prettier": "^1.18.0", From 5f46e38f02dcf698dc49d420f3002ee7acf353b7 Mon Sep 17 00:00:00 2001 From: Kincaid O'Neil Date: Fri, 1 Mar 2019 14:01:16 -0500 Subject: [PATCH 08/21] fix(eth): error loading persisted channel state --- package-lock.json | 12 ++++++------ package.json | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index b2678f6..0f4cf69 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5700,9 +5700,9 @@ } }, "ilp-plugin-ethereum": { - "version": "3.0.0-beta.13", - "resolved": "https://registry.npmjs.org/ilp-plugin-ethereum/-/ilp-plugin-ethereum-3.0.0-beta.13.tgz", - "integrity": "sha512-aBc7r6kkFCPJgDhuVPoSm8Tkl492YZX6V8LbhAJktz/B9is0nRKgNVyCdeTsj0KHWvkxUvyj0J04hVXrneBVWg==", + "version": "3.0.0-beta.14", + "resolved": "https://registry.npmjs.org/ilp-plugin-ethereum/-/ilp-plugin-ethereum-3.0.0-beta.14.tgz", + "integrity": "sha512-VcoyHWa2xpghMN39uXoCkIfhCgbBIBSvYpJBFtOFXLKDpCuvbB3hwZpnLaSkB6oMLFpCvjTTTp2tkFhfmQAe+A==", "requires": { "@kava-labs/crypto-rate-utils": "^2.0.2", "@types/ethereumjs-util": "^5.2.0", @@ -8646,9 +8646,9 @@ } }, "pako": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.9.tgz", - "integrity": "sha512-tPjtE6dq+dOSg8NMkqRmFjUYH9fect1zmYgB0g6ztQMaVNI7N1CEvLZud2bPHhg7PRgfKEeTshSPiqXb1F7A+A==" + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.10.tgz", + "integrity": "sha512-0DTvPVU3ed8+HNXOu5Bs+o//Mbdj9VNQMUOe9oKCwh8l0GNwpTDMKCWbRjgtD291AWnkAgkqA/LOnQS8AmS1tw==" }, "parallel-transform": { "version": "1.1.0", diff --git a/package.json b/package.json index 026aff7..c583ce5 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "ethereumjs-util": "^6.1.0", "fp-ts": "^1.14.3", "ilp-packet": "^3.0.8", - "ilp-plugin-ethereum": "^3.0.0-beta.13", + "ilp-plugin-ethereum": "^3.0.0-beta.14", "ilp-plugin-lightning": "^1.0.0-beta.21", "ilp-plugin-xrp-paychan-shared": "^4.1.3", "ilp-protocol-ildcp": "^2.0.1", From 4345eda7f4eda3840230c6387ee9f02012af5cb2 Mon Sep 17 00:00:00 2001 From: Kincaid O'Neil Date: Fri, 1 Mar 2019 14:02:25 -0500 Subject: [PATCH 09/21] test: try to fix codecov, again --- .nycrc.json | 3 ++- ava.config.js | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.nycrc.json b/.nycrc.json index dfc9bdb..751ddda 100644 --- a/.nycrc.json +++ b/.nycrc.json @@ -1,5 +1,6 @@ { "include": ["build/**/*.js"], "exclude": ["**/__tests__/**/*.js", "**/*.ts"], - "extension": [".js"] + "extension": [".js"], + "reporter": ["text", "html", "lcov"] } diff --git a/ava.config.js b/ava.config.js index debf03f..4e24590 100644 --- a/ava.config.js +++ b/ava.config.js @@ -1,8 +1,8 @@ export default { - files: ['build/__tests__/**/*.js'], + files: ['build/__tests__/**/*.test.js'], failFast: true, verbose: true, serial: true, - timeout: '3m', + timeout: '30s', require: ['source-map-support/register'] } From bc8fb989baa467a775e79b7e2e30da5dd067940d Mon Sep 17 00:00:00 2001 From: Kincaid O'Neil Date: Fri, 1 Mar 2019 14:11:22 -0500 Subject: [PATCH 10/21] test: reorganize test logic - more concise helpers - acknowledge failing tests (temporarily) - delete config after each test --- src/__tests__/_helpers.ts | 191 ------------------------ src/__tests__/add-remove.test.ts | 55 ++++--- src/__tests__/disconnect.test.ts | 21 +-- src/__tests__/exchange.test.ts | 94 ++++++++++++ src/__tests__/funding.test.ts | 99 ++++++++++++ src/__tests__/helpers.ts | 41 +++++ src/__tests__/stream-same-asset.test.ts | 18 --- src/__tests__/stream.test.ts | 28 ---- 8 files changed, 282 insertions(+), 265 deletions(-) delete mode 100644 src/__tests__/_helpers.ts create mode 100644 src/__tests__/exchange.test.ts create mode 100644 src/__tests__/funding.test.ts create mode 100644 src/__tests__/helpers.ts delete mode 100644 src/__tests__/stream-same-asset.test.ts delete mode 100644 src/__tests__/stream.test.ts diff --git a/src/__tests__/_helpers.ts b/src/__tests__/_helpers.ts deleted file mode 100644 index 54a1593..0000000 --- a/src/__tests__/_helpers.ts +++ /dev/null @@ -1,191 +0,0 @@ -import { AssetUnit, convert, usd } from '@kava-labs/crypto-rate-utils' -import 'envkey' -import { ExecutionContext } from 'ava' -import BigNumber from 'bignumber.js' -import { performance } from 'perf_hooks' -import { SwitchApi, SettlementEngineType, ReadyUplinks } from '..' -import { CredentialConfigs } from '../credential' - -// Return configs for connecting to accounts set up in env vars. -const ethConfig = (n: number): CredentialConfigs => { - return { - settlerType: SettlementEngineType.Machinomy, - privateKey: process.env[`ETH_PRIVATE_KEY_CLIENT_${n}`]! - } -} -const btcConfig = (n: number): CredentialConfigs => { - return { - settlerType: SettlementEngineType.Lnd, - hostname: process.env[`LIGHTNING_LND_HOST_CLIENT_${n}`]!, - tlsCert: process.env[`LIGHTNING_TLS_CERT_PATH_CLIENT_${n}`]!, - macaroon: process.env[`LIGHTNING_MACAROON_PATH_CLIENT_${n}`]!, - grpcPort: parseInt(process.env[`LIGHTNING_LND_GRPCPORT_CLIENT_${n}`]!, 10) - } -} -const xrpConfig = (n: number): CredentialConfigs => { - return { - settlerType: SettlementEngineType.XrpPaychan, - secret: process.env[`XRP_SECRET_CLIENT_${n}`]! - } -} - -export const addEth = (n: number) => ({ - add -}: SwitchApi): Promise => add(ethConfig(n)) -export const addBtc = (n: number) => ({ - add -}: SwitchApi): Promise => add(btcConfig(n)) -export const addXrp = (n: number) => ({ - add -}: SwitchApi): Promise => add(xrpConfig(n)) - -// Helper to test deposit and withdraw on uplinks -export const testFunding = ( - createUplink: (api: SwitchApi) => Promise -) => async (t: ExecutionContext) => { - const { state, deposit, withdraw, streamMoney } = t.context - const uplink = await createUplink(t.context) - - const settler = state.settlers[uplink.settlerType] - - // Instead down to the base unit of the ledger if there's more precision than that - const toUplinkUnit = (unit: AssetUnit) => - convert(unit, settler.exchangeUnit(), state.rateBackend).decimalPlaces( - settler.exchangeUnit().exchangeUnit, - BigNumber.ROUND_DOWN - ) - - t.true(uplink.balance$.value.isZero(), 'initial layer 2 balance is 0') - - // TODO Check base layer balances to make sure fees are also correctly reported! - // TODO Check that incoming capacity is opened! - - /** - * TODO Issue with xrp: openAmount has 9 digits of precision, but balance$ only has 6! - * e.g. openAmount === "2.959676012", uplink.balance$ === "2.959676" - */ - - const openAmount = toUplinkUnit(usd(1)) - await t.notThrowsAsync( - deposit({ - uplink, - amount: openAmount, - authorize: () => Promise.resolve() - }), - 'opens channel without throwing an error' - ) - - t.true( - uplink.balance$.value.isEqualTo(openAmount), - 'balance$ correctly reflects the initial channel open' - ) - - const depositAmount = toUplinkUnit(usd(2)) - await t.notThrowsAsync( - deposit({ - uplink, - amount: depositAmount, - authorize: () => Promise.resolve() - }), - 'deposits to channel without throwing an error' - ) - - t.true( - uplink.balance$.value.isEqualTo(openAmount.plus(depositAmount)), - 'balance$ correctly reflects the deposit to the channel' - ) - - // Rebalance so there's some money in both the incoming & outgoing channels - await t.notThrowsAsync( - streamMoney({ - amount: toUplinkUnit(usd(1.1)), - source: uplink, - dest: uplink - }), - 'uplink can stream money to itself' - ) - - await t.notThrowsAsync( - withdraw({ uplink, authorize: () => Promise.resolve() }), - 'withdraws from channel without throwing an error' - ) - - t.true( - uplink.balance$.value.isZero(), - 'balance$ of uplink goes back to zero following a withdraw' - ) -} - -// Helper to test streaming between different uplinks -export const testExchange = ( - createSource: (api: SwitchApi) => Promise, - createDest: (api: SwitchApi) => Promise -) => async (t: ExecutionContext) => { - const { state, deposit, streamMoney } = t.context - - const createFundedUplink = async ( - createUplink: (api: SwitchApi) => Promise - ) => { - const uplink = await createUplink(t.context) - await deposit({ - uplink, - amount: convert( - usd(3), - state.settlers[uplink.settlerType].exchangeUnit(), - state.rateBackend - ), - authorize: () => Promise.resolve() - }) - return uplink - } - - const [sourceUplink, destUplink] = await Promise.all([ - createFundedUplink(createSource), - createFundedUplink(createDest) - ]) - - // Without this pause after creating the uplinks, a stream from lnd to lnd fails. - // TODO fix - await new Promise(r => setTimeout(r, 500)) - - const initialSourceBalance = sourceUplink.balance$.value - const initialDestBalance = destUplink.balance$.value - - const sourceUnit = state.settlers[sourceUplink.settlerType].exchangeUnit - const destUnit = state.settlers[destUplink.settlerType].exchangeUnit - - const amountToSend = convert(usd(2), sourceUnit(), state.rateBackend) - const start = performance.now() - await t.notThrowsAsync( - streamMoney({ - amount: amountToSend, - source: sourceUplink, - dest: destUplink - }) - ) - t.log(`time: ${performance.now() - start} ms`) - - // Wait up to 2 seconds for the final settlements to come in (sigh) - await new Promise(r => setTimeout(r, 2000)) - - const finalSourceBalance = sourceUplink.balance$.value - t.true( - initialSourceBalance.minus(amountToSend).isEqualTo(finalSourceBalance), - 'source balance accurately represents the amount that was sent' - ) - - const estimatedReceiveAmount = convert( - sourceUnit(amountToSend), - destUnit(), - state.rateBackend - ) - const estimatedDestFinalBalance = initialDestBalance.plus( - estimatedReceiveAmount - ) - const finalDestBalance = destUplink.balance$.value - t.true( - finalDestBalance.isGreaterThan(estimatedDestFinalBalance.times(0.99)) && - finalDestBalance.isLessThan(estimatedDestFinalBalance.times(1.01)), - 'destination balance accounts for the amount that was sent, with margin for exchange rate fluctuations' - ) -} diff --git a/src/__tests__/add-remove.test.ts b/src/__tests__/add-remove.test.ts index 26d4ff2..c49aad1 100644 --- a/src/__tests__/add-remove.test.ts +++ b/src/__tests__/add-remove.test.ts @@ -1,21 +1,29 @@ -import anyTest, { TestInterface, ExecutionContext } from 'ava' -import 'envkey' +import anyTest, { ExecutionContext, TestInterface } from 'ava' import { - SwitchApi, connect, LedgerEnv, + ReadyUplinks, SettlementEngineType, - ReadyUplinks + SwitchApi } from '..' -import { addXrp, addEth, addBtc } from './_helpers' +import { addBtc, addEth, addXrp } from './helpers' +import { promisify } from 'util' +import { unlink } from 'fs' +import { CONFIG_PATH } from '../config' +require('envkey') const test = anyTest as TestInterface // Before & after each test, construct and disconnect the API + test.beforeEach(async t => { + // Delete any existing config + await promisify(unlink)(CONFIG_PATH).catch(() => Promise.resolve()) t.context = await connect(process.env.LEDGER_ENV! as LedgerEnv) }) -test.afterEach(async t => t.context.disconnect()) + +// TODO REMOVE THE CATCH ! +test.afterEach(async t => t.context.disconnect().catch(() => Promise.resolve())) // Test adding and removing uplinks const testAddRemove = ( @@ -27,22 +35,28 @@ const testAddRemove = ( await t.context.remove(uplink) t.false(t.context.state.uplinks.includes(uplink)) } -test('add then remove btc', testAddRemove(addBtc(1))) -test('add then remove eth without deposit', testAddRemove(addEth(1))) -test('add then remove xrp without deposit', testAddRemove(addXrp(1))) + +test('btc: add then remove', testAddRemove(addBtc())) +test('eth: add then remove', testAddRemove(addEth())) + +// TODO Uncomment this! +test.failing('xrp: add then remove', testAddRemove(addXrp())) // Test that uplinks with the same credentials cannot be added -test('cannot add duplicate eth uplink', async t => { - await addEth(1)(t.context) - await t.throwsAsync(addEth(1)(t.context)) + +test('eth: cannot add duplicate uplink', async t => { + await addEth()(t.context) + await t.throwsAsync(addEth()(t.context)) }) -test('cannot add duplicate xrp uplink', async t => { - await addXrp(1)(t.context) - await t.throwsAsync(addXrp(1)(t.context)) + +test('xrp: cannot add duplicate uplink', async t => { + await addXrp()(t.context) + await t.throwsAsync(addXrp()(t.context)) }) -test('cannot add duplicate btc uplink', async t => { - await addBtc(1)(t.context) - await t.throwsAsync(addBtc(1)(t.context)) + +test('btc: cannot add duplicate uplink', async t => { + await addBtc()(t.context) + await t.throwsAsync(addBtc()(t.context)) }) // Test credential config input validation @@ -57,6 +71,7 @@ test('add with invalid xrp secret throws', async t => { 'Non-base58 character' ) }) + test('add with un-activated xrp secret throws', async t => { await t.throwsAsync( t.context.add({ @@ -66,6 +81,7 @@ test('add with un-activated xrp secret throws', async t => { 'actNotFound' ) }) + // Test eth private keys. As long as they contain correct characters and are the right length they are a valid key. test('add with invalid eth secret throws', async t => { await t.throwsAsync( @@ -75,8 +91,10 @@ test('add with invalid eth secret throws', async t => { 'this is not a valid eth secret despite being the correct leength' }) ) + // TODO Fix that -> // Note: if the secret is correct length but contains invalid characters, an invalid length error is thrown ('private key length is invalid'). }) + // Test valid lnd uri, but invalid credentials. test('add with invalid lnd credentials throws', async t => { await t.throwsAsync( @@ -92,6 +110,7 @@ test('add with invalid lnd credentials throws', async t => { 'Failed to connect before the deadline' ) }) + test('add with invalid lnd uri throws', async t => { await t.throwsAsync( t.context.add({ diff --git a/src/__tests__/disconnect.test.ts b/src/__tests__/disconnect.test.ts index 5c68e3d..95dde6e 100644 --- a/src/__tests__/disconnect.test.ts +++ b/src/__tests__/disconnect.test.ts @@ -1,28 +1,29 @@ -import anyTest, { TestInterface, ExecutionContext } from 'ava' +import anyTest, { TestInterface } from 'ava' import 'envkey' -import { SwitchApi, connect, LedgerEnv, ReadyUplinks } from '..' -import { addXrp, addEth, addBtc, testExchange } from './_helpers' +import { SwitchApi, connect, LedgerEnv } from '..' +import { addEth } from './helpers' import BigNumber from 'bignumber.js' +import { promisify } from 'util' +import { unlink } from 'fs' +import { CONFIG_PATH } from '../config' const test = anyTest as TestInterface // Before & after each test, construct and disconnect the API test.beforeEach(async t => { + // Delete any existing config + await promisify(unlink)(CONFIG_PATH).catch(() => Promise.resolve()) t.context = await connect(process.env.LEDGER_ENV! as LedgerEnv) }) -test('after connect', async t => { - await t.notThrowsAsync(t.context.disconnect()) -}) - test('after add eth', async t => { - const uplink = await addEth(1)(t.context) + const uplink = await addEth()(t.context) await t.notThrowsAsync(t.context.disconnect()) }) test('after deposit eth', async t => { - const uplink = await addEth(1)(t.context) + const uplink = await addEth()(t.context) const openAmount = new BigNumber(0.01) await t.context.deposit({ uplink, @@ -33,7 +34,7 @@ test('after deposit eth', async t => { }) test('after withdraw eth', async t => { - const uplink = await addEth(1)(t.context) + const uplink = await addEth()(t.context) const openAmount = new BigNumber(0.01) await t.context.deposit({ uplink, diff --git a/src/__tests__/exchange.test.ts b/src/__tests__/exchange.test.ts new file mode 100644 index 0000000..420b72f --- /dev/null +++ b/src/__tests__/exchange.test.ts @@ -0,0 +1,94 @@ +import anyTest, { TestInterface, ExecutionContext } from 'ava' +import { SwitchApi, connect, LedgerEnv, ReadyUplinks } from '..' +import { addEth, addXrp, addBtc, createFundedUplink } from './helpers' +import { promisify } from 'util' +import { unlink } from 'fs' +import { CONFIG_PATH } from '../config' +import { convert, usd } from '@kava-labs/crypto-rate-utils' +import { performance } from 'perf_hooks' +require('envkey') + +const test = anyTest as TestInterface + +// Before & after each test, construct and disconnect the API + +// TODO Turn this into a generic helper +test.beforeEach(async t => { + // Delete any existing config + await promisify(unlink)(CONFIG_PATH).catch(() => Promise.resolve()) + t.context = await connect(process.env.LEDGER_ENV! as LedgerEnv) + + // TODO Partially apply addEth, addXrp, addBtc here? + // TODO Partially apply createFunded uplink? +}) + +// TODO Turn this into a generic helper +test.afterEach(async t => t.context.disconnect()) + +export const testExchange = ( + createSource: (api: SwitchApi) => Promise, + createDest: (api: SwitchApi) => Promise +) => async (t: ExecutionContext) => { + const api = t.context + const { state, streamMoney } = api + + const [sourceUplink, destUplink] = await Promise.all([ + createFundedUplink(api)(createSource), + createFundedUplink(api)(createDest) + ]) + + // TODO Without this pause, Lnd -> Lnd will fail + await new Promise(r => setTimeout(r, 500)) + + const initialSourceBalance = sourceUplink.balance$.value + const initialDestBalance = destUplink.balance$.value + + const sourceUnit = state.settlers[sourceUplink.settlerType].exchangeUnit + const destUnit = state.settlers[destUplink.settlerType].exchangeUnit + + const amountToSend = convert(usd(2), sourceUnit(), state.rateBackend) + const start = performance.now() + await t.notThrowsAsync( + streamMoney({ + amount: amountToSend, + source: sourceUplink, + dest: destUplink + }) + ) + t.log(`time: ${performance.now() - start} ms`) + + // Wait up to 2 seconds for the final settlements to come in + // TODO Fix this + await new Promise(r => setTimeout(r, 2000)) + + const finalSourceBalance = sourceUplink.balance$.value + t.true( + initialSourceBalance.minus(amountToSend).isEqualTo(finalSourceBalance), + 'source balance accurately represents the amount that was sent' + ) + + const estimatedReceiveAmount = convert( + sourceUnit(amountToSend), + destUnit(), + state.rateBackend + ) + const estimatedDestFinalBalance = initialDestBalance.plus( + estimatedReceiveAmount + ) + const finalDestBalance = destUplink.balance$.value + t.true( + finalDestBalance.isGreaterThan(estimatedDestFinalBalance.times(0.99)) && + finalDestBalance.isLessThan(estimatedDestFinalBalance.times(1.01)), + 'destination balance accounts for the amount that was sent, with margin for exchange rate fluctuations' + ) +} + +test('xrp -> eth', testExchange(addXrp(), addEth())) +test('xrp -> btc', testExchange(addXrp(), addBtc())) +test('btc -> eth', testExchange(addBtc(), addEth())) +test('btc -> xrp', testExchange(addBtc(), addXrp())) +test('eth -> btc', testExchange(addEth(), addBtc())) +test('eth -> xrp', testExchange(addEth(), addXrp())) +test('xrp -> xrp', testExchange(addXrp(), addXrp(2))) +test('eth -> eth', testExchange(addEth(), addEth(2))) +test('btc -> btc', testExchange(addBtc(), addBtc(2))) diff --git a/src/__tests__/funding.test.ts b/src/__tests__/funding.test.ts new file mode 100644 index 0000000..b2b47b4 --- /dev/null +++ b/src/__tests__/funding.test.ts @@ -0,0 +1,99 @@ +import { AssetUnit, convert, usd } from '@kava-labs/crypto-rate-utils' +import anyTest, { ExecutionContext, TestInterface } from 'ava' +import BigNumber from 'bignumber.js' +import { unlink } from 'fs' +import { promisify } from 'util' +import { connect, LedgerEnv, ReadyUplinks, SwitchApi } from '..' +import { CONFIG_PATH } from '../config' +import { addEth, addXrp } from './helpers' +require('envkey') + +const test = anyTest as TestInterface + +// Before & after each test, construct and disconnect the API + +test.beforeEach(async t => { + // Delete any existing config + await promisify(unlink)(CONFIG_PATH).catch(() => Promise.resolve()) + t.context = await connect(process.env.LEDGER_ENV! as LedgerEnv) +}) + +test.afterEach(async t => t.context.disconnect()) + +// Helper to test deposit and withdraw on uplinks +export const testFunding = ( + createUplink: (api: SwitchApi) => Promise +) => async (t: ExecutionContext) => { + const { state, deposit, withdraw, streamMoney } = t.context + const uplink = await createUplink(t.context) + + const settler = state.settlers[uplink.settlerType] + + // Instead down to the base unit of the ledger if there's more precision than that + const toUplinkUnit = (unit: AssetUnit) => + convert(unit, settler.exchangeUnit(), state.rateBackend).decimalPlaces( + settler.exchangeUnit().exchangeUnit, + BigNumber.ROUND_DOWN + ) + + t.true(uplink.balance$.value.isZero(), 'initial layer 2 balance is 0') + + // TODO Check base layer balances to make sure fees are also correctly reported! + // TODO Check that incoming capacity is opened! + + // TODO Issue with xrp: openAmount has 9 digits of precision, but balance$ only has 6! + // e.g. openAmount === "2.959676012", uplink.balance$ === "2.959676" + + const openAmount = toUplinkUnit(usd(1)) + await t.notThrowsAsync( + deposit({ + uplink, + amount: openAmount, + authorize: () => Promise.resolve() + }), + 'opens channel without throwing an error' + ) + + t.true( + uplink.balance$.value.isEqualTo(openAmount), + 'balance$ correctly reflects the initial channel open' + ) + + const depositAmount = toUplinkUnit(usd(2)) + await t.notThrowsAsync( + deposit({ + uplink, + amount: depositAmount, + authorize: () => Promise.resolve() + }), + 'deposits to channel without throwing an error' + ) + + t.true( + uplink.balance$.value.isEqualTo(openAmount.plus(depositAmount)), + 'balance$ correctly reflects the deposit to the channel' + ) + + // Rebalance so there's some money in both the incoming & outgoing channels + await t.notThrowsAsync( + streamMoney({ + amount: toUplinkUnit(usd(1.1)), + source: uplink, + dest: uplink + }), + 'uplink can stream money to itself' + ) + + await t.notThrowsAsync( + withdraw({ uplink, authorize: () => Promise.resolve() }), + 'withdraws from channel without throwing an error' + ) + + t.true( + uplink.balance$.value.isZero(), + 'balance$ of uplink goes back to zero following a withdraw' + ) +} + +test('eth: deposit & withdraw', testFunding(addEth())) +test('xrp: deposit & withdraw', testFunding(addXrp())) diff --git a/src/__tests__/helpers.ts b/src/__tests__/helpers.ts new file mode 100644 index 0000000..d56c8f9 --- /dev/null +++ b/src/__tests__/helpers.ts @@ -0,0 +1,41 @@ +import { SwitchApi, SettlementEngineType, ReadyUplinks } from '..' +import { convert, usd } from '@kava-labs/crypto-rate-utils' + +export const addEth = (n = 1) => ({ add }: SwitchApi): Promise => + add({ + settlerType: SettlementEngineType.Machinomy, + privateKey: process.env[`ETH_PRIVATE_KEY_CLIENT_${n}`]! + }) + +export const addBtc = (n = 1) => ({ add }: SwitchApi): Promise => + add({ + settlerType: SettlementEngineType.Lnd, + hostname: process.env[`LIGHTNING_LND_HOST_CLIENT_${n}`]!, + tlsCert: process.env[`LIGHTNING_TLS_CERT_PATH_CLIENT_${n}`]!, + macaroon: process.env[`LIGHTNING_MACAROON_PATH_CLIENT_${n}`]!, + grpcPort: parseInt(process.env[`LIGHTNING_LND_GRPCPORT_CLIENT_${n}`]!, 10) + }) + +export const addXrp = (n = 1) => ({ add }: SwitchApi): Promise => + add({ + settlerType: SettlementEngineType.XrpPaychan, + secret: process.env[`XRP_SECRET_CLIENT_${n}`]! + }) + +export const createFundedUplink = (api: SwitchApi) => async ( + createUplink: (api: SwitchApi) => Promise +) => { + const uplink = await createUplink(api) + + await api.deposit({ + uplink, + amount: convert( + usd(3), + api.state.settlers[uplink.settlerType].exchangeUnit(), + api.state.rateBackend + ), + authorize: () => Promise.resolve() + }) + + return uplink +} diff --git a/src/__tests__/stream-same-asset.test.ts b/src/__tests__/stream-same-asset.test.ts deleted file mode 100644 index 23ef54a..0000000 --- a/src/__tests__/stream-same-asset.test.ts +++ /dev/null @@ -1,18 +0,0 @@ -import anyTest, { TestInterface } from 'ava' -import 'envkey' -import { SwitchApi, connect, LedgerEnv, ReadyUplinks } from '..' -import { addXrp, addEth, addBtc, testExchange } from './_helpers' - -const test = anyTest as TestInterface - -// Before & after each test, construct and disconnect the API - -test.beforeEach(async t => { - t.context = await connect(process.env.LEDGER_ENV! as LedgerEnv) -}) - -test.afterEach(async t => t.context.disconnect()) - -test('xrp -> xrp different credentials', testExchange(addXrp(1), addXrp(2))) -test('eth -> eth different credentials', testExchange(addEth(1), addEth(2))) -test('btc -> btc different credentials', testExchange(addBtc(2), addBtc(1))) diff --git a/src/__tests__/stream.test.ts b/src/__tests__/stream.test.ts deleted file mode 100644 index 7dc63fe..0000000 --- a/src/__tests__/stream.test.ts +++ /dev/null @@ -1,28 +0,0 @@ -import anyTest, { TestInterface } from 'ava' -import { SwitchApi, connect, LedgerEnv } from '..' -import { addEth, addXrp, addBtc, testFunding, testExchange } from './_helpers' -import { promisify } from 'util' -import { unlink } from 'fs' -import { CONFIG_PATH } from '../config' - -const test = anyTest as TestInterface - -// Before & after each test, construct and disconnect the API - -test.beforeEach(async t => { - // Delete any existing config - await promisify(unlink)(CONFIG_PATH).catch(() => Promise.resolve()) - t.context = await connect(process.env.LEDGER_ENV! as LedgerEnv) -}) - -test.afterEach(async t => t.context.disconnect()) - -test('deposit & withdraw eth', testFunding(addEth(1))) -test('deposit & withdraw xrp', testFunding(addXrp(1))) - -test('xrp -> eth', testExchange(addXrp(1), addEth(1))) -test('xrp -> btc', testExchange(addXrp(1), addBtc(1))) -test('btc -> eth', testExchange(addBtc(1), addEth(1))) -test('btc -> xrp', testExchange(addBtc(1), addXrp(1))) -test('eth -> btc', testExchange(addEth(1), addBtc(1))) -test('eth -> xrp', testExchange(addEth(1), addXrp(1))) From 656bc649364404aa1849a17305a3c1fc988edd5f Mon Sep 17 00:00:00 2001 From: Kincaid O'Neil Date: Fri, 1 Mar 2019 15:24:33 -0500 Subject: [PATCH 11/21] fix: xrp withdrawal issues (#16 and #9) --- package-lock.json | 6 +-- package.json | 2 +- src/__tests__/add-remove.test.ts | 7 +-- src/settlement/xrp-paychan.ts | 47 +++++-------------- .../ilp-plugin-xrp-asym-client/index.d.ts | 6 +-- 5 files changed, 20 insertions(+), 48 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0f4cf69..f18431e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -572,9 +572,9 @@ } }, "@kava-labs/ilp-plugin-xrp-asym-client": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@kava-labs/ilp-plugin-xrp-asym-client/-/ilp-plugin-xrp-asym-client-1.7.0.tgz", - "integrity": "sha512-ByTkCWGYaAiCKwjgcYPFYWO4pHmj7EicAcKzZzzSVJcEOYCH7NZOpt6O5ptEpP23BSKTa7UIiVNAhRtZPbia+w==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@kava-labs/ilp-plugin-xrp-asym-client/-/ilp-plugin-xrp-asym-client-1.7.1.tgz", + "integrity": "sha512-bVRyRK6bCNXMjDS7t/gZ+W6RNTyjWcCtxQDVaYOpPaXZ8Ef+R9VrMw0MrnRkiEDXptMDuTLi+6myrBn0t8HWoA==", "requires": { "assert": "^1.4.1", "base64url": "^3.0.1", diff --git a/package.json b/package.json index c583ce5..1c54600 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "homepage": "https://github.com/Kava-Labs/switch-api#readme", "dependencies": { "@kava-labs/crypto-rate-utils": "^2.0.2", - "@kava-labs/ilp-plugin-xrp-asym-client": "^1.7.0", + "@kava-labs/ilp-plugin-xrp-asym-client": "^1.7.1", "argon2": "^0.20.1", "axios": "^0.18.0", "bignumber.js": "^7.2.1", diff --git a/src/__tests__/add-remove.test.ts b/src/__tests__/add-remove.test.ts index c49aad1..470ae44 100644 --- a/src/__tests__/add-remove.test.ts +++ b/src/__tests__/add-remove.test.ts @@ -22,8 +22,7 @@ test.beforeEach(async t => { t.context = await connect(process.env.LEDGER_ENV! as LedgerEnv) }) -// TODO REMOVE THE CATCH ! -test.afterEach(async t => t.context.disconnect().catch(() => Promise.resolve())) +test.afterEach(async t => t.context.disconnect()) // Test adding and removing uplinks const testAddRemove = ( @@ -38,9 +37,7 @@ const testAddRemove = ( test('btc: add then remove', testAddRemove(addBtc())) test('eth: add then remove', testAddRemove(addEth())) - -// TODO Uncomment this! -test.failing('xrp: add then remove', testAddRemove(addXrp())) +test('xrp: add then remove', testAddRemove(addXrp())) // Test that uplinks with the same credentials cannot be added diff --git a/src/settlement/xrp-paychan.ts b/src/settlement/xrp-paychan.ts index 5762d19..1ffc207 100644 --- a/src/settlement/xrp-paychan.ts +++ b/src/settlement/xrp-paychan.ts @@ -382,19 +382,14 @@ const withdraw = (uplink: ReadyXrpPaychanUplink) => (state: State) => async ( } const { address, secret } = readyCredential - /** - * TODO This also throws an error if there's no incoming claim -- - * I should perform checks and submit a single tx in that case - * - * (TODO add close=true option to asym-client? a lot of the checks are already there) - * (also, a LOT of race conditions in asym-client in general... yikes!) - * - * (At some point, it'd just be easier to port it over from eth!) - */ + // Prompt the user to authorize the withdrawal + // TODO Add actual fee calculation to the XRP plugins + const fee = convert(drop(10), xrp()) + const value = uplink.outgoingCapacity$.value.plus(uplink.totalReceived$.value) + await authorize({ fee, value }) - // Submit a claim to the ledger, if it's profitable - // TODO Combine this and channel close into a single tx? - await uplink.plugin._autoClaim() + // Submit latest claim and close the incoming channel + await uplink.plugin._autoClaim(true) /* * Per https://github.com/interledgerjs/ilp-plugin-xrp-paychan-shared/pull/23, @@ -402,37 +397,17 @@ const withdraw = (uplink: ReadyXrpPaychanUplink) => (state: State) => async ( */ const submitter = createSubmitter(api, address, secret) - // TODO xrp-asym-server uses api and not tx-submitter. Why don't I? (Could resolve issue) - const closeChannel = (channelId: string) => - submitter + const outgoingChannelId = uplink.plugin._channel + if (outgoingChannelId) { + await submitter .submit('preparePaymentChannelClaim', { - channel: channelId, + channel: outgoingChannelId, close: true }) .catch((err: Error) => log.error('Failed to close channel: ', err)) - - // Prompt the user to authorize the withdrawal - const fee = convert(drop(10), xrp()) - const value = uplink.outgoingCapacity$.value.plus(uplink.totalReceived$.value) - await authorize({ fee, value }) - - const outgoingChannelId = uplink.plugin._channel - if (outgoingChannelId) { - await closeChannel(outgoingChannelId) - } - - // xrp-paychan-shared occasionally throws an error when it tries to remove a pending - // transaction from its queue after it got confirmed (something with maxLedgerVersion?) - // TODO This *possibly* fixes that bug - await new Promise(r => setTimeout(r, 2000)) - - const incomingChannelId = uplink.plugin._clientChannel - if (incomingChannelId) { - await closeChannel(incomingChannelId) } // Ensure that the balances are updated to reflect the closed channels - uplink.incomingChannelAmount$.next( await refreshIncomingChannel(state)(uplink.plugin) ) diff --git a/src/types/ilp-plugin-xrp-asym-client/index.d.ts b/src/types/ilp-plugin-xrp-asym-client/index.d.ts index 9644149..6bd4af6 100644 --- a/src/types/ilp-plugin-xrp-asym-client/index.d.ts +++ b/src/types/ilp-plugin-xrp-asym-client/index.d.ts @@ -63,10 +63,10 @@ declare module '@kava-labs/ilp-plugin-xrp-asym-client' { _performConnectHandshake(): Promise /** - * Submit the incoming claim to the ledger as a checkpoint, - * if it's profitable + * Submit the incoming claim to the ledger as a checkpoint, if it's profitable + * @param closeChannel Should the transaction also close the channel (true), or not (false, by default)? */ - _autoClaim(): Promise + _autoClaim(closeChannel: boolean): Promise /** Outgoing payment channel claim signature and amount in base units (-9) */ _lastClaim?: PaymentChannelClaim From 45c9d36376e3dfb99fb1124d4a5074961a9eceed Mon Sep 17 00:00:00 2001 From: Kincaid O'Neil Date: Fri, 1 Mar 2019 15:30:44 -0500 Subject: [PATCH 12/21] fix: bump ava timeout to 2m - no logs in ci triggers timeout errors when multiple funding txs are required --- ava.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ava.config.js b/ava.config.js index 4e24590..ff66f8c 100644 --- a/ava.config.js +++ b/ava.config.js @@ -3,6 +3,6 @@ export default { failFast: true, verbose: true, serial: true, - timeout: '30s', + timeout: '2m', require: ['source-map-support/register'] } From e2f8d046069fa9966fdfe9fc4e2ccc926b8f0e8a Mon Sep 17 00:00:00 2001 From: Kincaid O'Neil Date: Fri, 1 Mar 2019 15:43:19 -0500 Subject: [PATCH 13/21] fix(xrp): insufficient outgoing amount - temporarily addresses #15, at least for the Kava connector's config - after deposit, refresh outgoing channel amount slightly later to ensure the channel exists --- src/settlement/xrp-paychan.ts | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/src/settlement/xrp-paychan.ts b/src/settlement/xrp-paychan.ts index 1ffc207..ef67acd 100644 --- a/src/settlement/xrp-paychan.ts +++ b/src/settlement/xrp-paychan.ts @@ -1,4 +1,4 @@ -import { convert, drop, xrp, xrpBase } from '@kava-labs/crypto-rate-utils' +import { convert, drop, xrp, xrpBase, usd } from '@kava-labs/crypto-rate-utils' import XrpAsymClient, { PaymentChannelClaim } from '@kava-labs/ilp-plugin-xrp-asym-client' @@ -310,7 +310,11 @@ const deposit = (uplink: ReadyXrpPaychanUplink) => (state: State) => async ({ const { api } = state.settlers[uplink.settlerType] const { address } = getCredential(state)(uplink.credentialId) - // TODO Check that the total amount deposited > 2 XRP (per connector-config)! + // TODO Temporarily solve issue with deadlock due to insufficient outgoing + // (server won't fund incoming, even if more is deposited later) + if (convert(xrp(amount), usd(), state.rateBackend).isLessThan(0.6)) { + throw new Error('insufficient deposit amount') + } // Confirm that the account has sufficient funds to cover the reserve const { ownerCount, xrpBalance } = await api.getAccountInfo(address) // TODO May throw if the account isn't found @@ -351,20 +355,18 @@ const deposit = (uplink: ReadyXrpPaychanUplink) => (state: State) => async ({ ? await uplink.plugin._createOutgoingChannel(amount.toString()) : await uplink.plugin._fundOutgoingChannel(amount.toString()) + // TODO Change this to perform the handshake whenever there's no incoming capacity + if (requiresNewChannel) { + await uplink.plugin._performConnectHandshake() + } + uplink.outgoingChannelAmount$.next( await refreshOutgoingChannel(state)(uplink.plugin) ) - // TODO Since the channel is now open, perform the rest of the connect handshake - - // TODO Basically, perform this handshake whenever there's no incoming capacity - if (requiresNewChannel) { - await uplink.plugin._performConnectHandshake() - - uplink.incomingChannelAmount$.next( - await refreshIncomingChannel(state)(uplink.plugin) - ) - } + uplink.incomingChannelAmount$.next( + await refreshIncomingChannel(state)(uplink.plugin) + ) } const withdraw = (uplink: ReadyXrpPaychanUplink) => (state: State) => async ( From b2f8ec1826e81d7d8db0efae5a222b9855057230 Mon Sep 17 00:00:00 2001 From: Kincaid O'Neil Date: Mon, 4 Mar 2019 14:31:39 -0500 Subject: [PATCH 14/21] feat: authorize by default --- src/index.ts | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/index.ts b/src/index.ts index 5cdd4d8..a6febc9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -114,23 +114,30 @@ export const connect = async (ledgerEnv: LedgerEnv = LedgerEnv.Testnet) => { const deposit = async ({ uplink, - ...opts + amount, + authorize = () => Promise.resolve() }: { readonly uplink: ReadyUplinks readonly amount: BigNumber - readonly authorize: AuthorizeDeposit + readonly authorize?: AuthorizeDeposit }): Promise => { const internalUplink = state.uplinks.filter(isThatUplink(uplink))[0] const internalDeposit = depositToUplink(internalUplink) - return internalDeposit && internalDeposit(state)(opts) + return ( + internalDeposit && + internalDeposit(state)({ + amount, + authorize + }) + ) } const withdraw = async ({ uplink, - authorize + authorize = () => Promise.resolve() }: { readonly uplink: ReadyUplinks - readonly authorize: AuthorizeWithdrawal + readonly authorize?: AuthorizeWithdrawal }) => { const internalUplink = state.uplinks.filter(isThatUplink(uplink))[0] const internalWithdraw = withdrawFromUplink(internalUplink) From 3165bf957757bd86f68f1920d61a6a5aa679314b Mon Sep 17 00:00:00 2001 From: Kincaid O'Neil Date: Mon, 4 Mar 2019 15:37:54 -0500 Subject: [PATCH 15/21] fix: incoming packets not piped through balance middleware - remove unused function - export util from lnd --- src/settlement/lnd.ts | 5 +---- src/uplink.ts | 4 ++-- src/utils/crypto.ts | 7 +------ src/utils/middlewares.ts | 10 +++++----- 4 files changed, 9 insertions(+), 17 deletions(-) diff --git a/src/settlement/lnd.ts b/src/settlement/lnd.ts index 2b76783..4e3dbc7 100644 --- a/src/settlement/lnd.ts +++ b/src/settlement/lnd.ts @@ -68,7 +68,7 @@ const setupEngine = async ( * Confirm a host is semantically valid (e.g. "localhost:8080") * and split into component hostname and port */ -const splitHost = (host: string): Option => +export const splitHost = (host: string): Option => tryCatch(() => new URL('https://' + host)).map(({ hostname, port }) => ({ hostname, port: parseInt(port, 10) @@ -79,8 +79,6 @@ export type ValidHost = { readonly port: number } -// TODO Add method to validate credentials using `setupCredential` then `closeCredential` - export interface ValidatedLndCredential { /** TODO */ readonly settlerType: SettlementEngineType.Lnd @@ -97,7 +95,6 @@ export interface ValidatedLndCredential { export type LndIdentityPublicKey = Flavor export interface ReadyLndCredential { - /** TODO */ readonly settlerType: SettlementEngineType.Lnd /** gRPC client connected to Lighnting node for performing requests */ readonly service: LndService diff --git a/src/uplink.ts b/src/uplink.ts index 3bb9327..df9b77e 100644 --- a/src/uplink.ts +++ b/src/uplink.ts @@ -248,7 +248,7 @@ export const connectUplink = (state: State) => ( // Setup internal packet handlers and routing setupHandlers( - plugin, + pluginWrapper, clientAddress, (data: Buffer) => handlers.streamServerHandler(data), (prepare: IlpPrepare) => handlers.streamClientHandler(prepare) @@ -290,7 +290,7 @@ export const connectUplink = (state: State) => ( * EFFECT: registers handlers on the plugin */ export const setupHandlers = ( - plugin: Plugin, + plugin: PluginWrapper, clientAddress: string, streamServerHandler: DataHandler, streamClientHandler: IlpPrepareHandler diff --git a/src/utils/crypto.ts b/src/utils/crypto.ts index 145a0f0..4318be6 100644 --- a/src/utils/crypto.ts +++ b/src/utils/crypto.ts @@ -1,11 +1,6 @@ -import { createHash, createHmac, randomBytes } from 'crypto' +import { createHash, randomBytes } from 'crypto' import { promisify } from 'util' -export const hmac = (key: string | Buffer, message: string | Buffer) => - createHmac('sha256', key) - .update(message) - .digest() - export const sha256 = (preimage: string | Buffer) => createHash('sha256') .update(preimage) diff --git a/src/utils/middlewares.ts b/src/utils/middlewares.ts index 5598513..793be2d 100644 --- a/src/utils/middlewares.ts +++ b/src/utils/middlewares.ts @@ -16,8 +16,8 @@ BigNumber.config({ EXPONENTIAL_AT: 1e9 }) export interface PluginWrapperOpts { readonly plugin: Plugin - readonly maxBalance?: BigNumber.Value - readonly maxPacketAmount?: BigNumber.Value + readonly maxBalance: BigNumber.Value + readonly maxPacketAmount: BigNumber.Value readonly log: Logger readonly assetCode: string readonly assetScale: number @@ -83,8 +83,8 @@ export class PluginWrapper { constructor({ plugin, - maxBalance = Infinity, - maxPacketAmount = Infinity, + maxBalance, + maxPacketAmount, log, store, assetCode, @@ -167,7 +167,7 @@ export class PluginWrapper { */ private async handleMoney(amount: string): Promise { - if (new BigNumber(amount).isZero()) { + if (parseInt(amount, 10) <= 0) { return } From 7091e4c52fd0ca12c252f22198e0153d06a2604b Mon Sep 17 00:00:00 2001 From: Kincaid O'Neil Date: Mon, 4 Mar 2019 16:40:57 -0500 Subject: [PATCH 16/21] test: basic persistence/serialization test --- src/__tests__/persistence.test.ts | 42 +++++++++++++++++++++++++++++++ src/config.ts | 16 ++++++------ 2 files changed, 50 insertions(+), 8 deletions(-) create mode 100644 src/__tests__/persistence.test.ts diff --git a/src/__tests__/persistence.test.ts b/src/__tests__/persistence.test.ts new file mode 100644 index 0000000..3a5a3ba --- /dev/null +++ b/src/__tests__/persistence.test.ts @@ -0,0 +1,42 @@ +import test from 'ava' +import { connect, LedgerEnv } from '..' +import { addBtc, addEth, addXrp } from './helpers' +import { promisify } from 'util' +import { unlink, readFile } from 'fs' +import { CONFIG_PATH } from '../config' +require('envkey') + +const readConfig = () => + promisify(readFile)(CONFIG_PATH, { + encoding: 'utf8' + }) + +test('persists state locally', async t => { + // Delete any existing config + await promisify(unlink)(CONFIG_PATH).catch(() => Promise.resolve()) + + // Connect API + const api = await connect(process.env.LEDGER_ENV! as LedgerEnv) + await Promise.all([addBtc, addEth, addXrp].map(create => create()(api))) + await api.disconnect() // Should persist the state + + const initialSerializedConfig = await readConfig() + + // Reconnect the API + await t.notThrowsAsync(async () => { + const newApi = await connect(process.env.LEDGER_ENV! as LedgerEnv) + + t.is(newApi.state.credentials.length, 3, 'same number of credentials') + t.is(newApi.state.uplinks.length, 3, 'same number of uplink') + + await newApi.disconnect() + }, 'connects api using existing config') + + const rebuiltSerializedConfig = await readConfig() + + t.is( + rebuiltSerializedConfig, + initialSerializedConfig, + 'config is persisted and can be rebuilt from the persisted version' + ) +}) diff --git a/src/config.ts b/src/config.ts index e35398c..826692e 100644 --- a/src/config.ts +++ b/src/config.ts @@ -19,15 +19,15 @@ export interface ConfigSchema { const CONFIG_DIR = `${homedir()}/.switch` export const CONFIG_PATH = `${CONFIG_DIR}/config.json` +export const serializeConfig = (state: State) => + JSON.stringify({ + ledgerEnv: state.ledgerEnv, + uplinks: state.uplinks.map(uplink => uplink.config), + credentials: state.credentials.map(credentialToConfig) + }) + export const persistConfig = async (fd: number, state: State) => - promisify(writeFile)( - fd, - JSON.stringify({ - ledgerEnv: state.ledgerEnv, - uplinks: state.uplinks.map(uplink => uplink.config), - credentials: state.credentials.map(credentialToConfig) - }) - ) + promisify(writeFile)(fd, serializeConfig(state)) export const loadConfig = async (): Promise< [number, ConfigSchema | undefined] From a958750b12cccb057e05d41ea0d2a4a5c5ed145d Mon Sep 17 00:00:00 2001 From: Kincaid O'Neil Date: Mon, 4 Mar 2019 17:54:09 -0500 Subject: [PATCH 17/21] fix: simplify persist test --- src/__tests__/persistence.test.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/__tests__/persistence.test.ts b/src/__tests__/persistence.test.ts index 3a5a3ba..1630a8c 100644 --- a/src/__tests__/persistence.test.ts +++ b/src/__tests__/persistence.test.ts @@ -23,14 +23,10 @@ test('persists state locally', async t => { const initialSerializedConfig = await readConfig() // Reconnect the API - await t.notThrowsAsync(async () => { - const newApi = await connect(process.env.LEDGER_ENV! as LedgerEnv) - - t.is(newApi.state.credentials.length, 3, 'same number of credentials') - t.is(newApi.state.uplinks.length, 3, 'same number of uplink') - - await newApi.disconnect() - }, 'connects api using existing config') + const newApi = await connect(process.env.LEDGER_ENV! as LedgerEnv) + t.is(newApi.state.credentials.length, 3, 'same number of credentials') + t.is(newApi.state.uplinks.length, 3, 'same number of uplink') + await newApi.disconnect() const rebuiltSerializedConfig = await readConfig() From c96aa87b4438056e51757baec8131a1f5d3cb4fe Mon Sep 17 00:00:00 2001 From: Kincaid O'Neil Date: Tue, 5 Mar 2019 10:20:51 -0500 Subject: [PATCH 18/21] fix(persist): add w flag when writing file --- src/config.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/config.ts b/src/config.ts index 826692e..aa861a5 100644 --- a/src/config.ts +++ b/src/config.ts @@ -27,7 +27,9 @@ export const serializeConfig = (state: State) => }) export const persistConfig = async (fd: number, state: State) => - promisify(writeFile)(fd, serializeConfig(state)) + promisify(writeFile)(fd, serializeConfig(state), { + flag: 'w' + }) export const loadConfig = async (): Promise< [number, ConfigSchema | undefined] From 8be1b985c18600db53262a46dcf85bccca33f796 Mon Sep 17 00:00:00 2001 From: Kincaid O'Neil Date: Tue, 5 Mar 2019 12:19:42 -0500 Subject: [PATCH 19/21] fix: use r+ mode, truncate file before writing - on Linux, positional writes aren't supported in append mode - r+ mode doesn't overwrite file content by default --- src/config.ts | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/src/config.ts b/src/config.ts index aa861a5..c244ed2 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,7 +1,7 @@ import { LedgerEnv, State } from '.' import { CredentialConfigs, credentialToConfig } from './credential' import { BaseUplinkConfig } from './uplink' -import { open, readFile, writeFile, mkdir } from 'fs' +import { open, readFile, writeFile, mkdir, ftruncate } from 'fs' import { promisify } from 'util' import { homedir } from 'os' import { hash, verify, argon2id } from 'argon2' @@ -26,11 +26,18 @@ export const serializeConfig = (state: State) => credentials: state.credentials.map(credentialToConfig) }) -export const persistConfig = async (fd: number, state: State) => - promisify(writeFile)(fd, serializeConfig(state), { - flag: 'w' - }) +export const persistConfig = async (fd: number, state: State) => { + await promisify(ftruncate)(fd) // r+ mode doesn't overwrite, so first delete file contents + await promisify(writeFile)(fd, serializeConfig(state)) +} +/** + * - Opening the file in "w+" mode truncates/deletes the existing content + * - Opening the file in "a+" mode doesn't work on Linux, since positional writes are ignored + * when the file is opened in append mode, and won't replace the existing content + * - Opening the file in "r+" mode allows reading and writing, but fails if the file doesn't + * exist (race condition). More importantly, it doesn't allow truncating/deleting the file + */ export const loadConfig = async (): Promise< [number, ConfigSchema | undefined] > => { @@ -39,7 +46,13 @@ export const loadConfig = async (): Promise< else throw err }) - const fd = await promisify(open)(CONFIG_PATH, 'a+') + const fd = await promisify(open)(CONFIG_PATH, 'r+').catch(err => { + if (err.code === 'ENOENT') { + return promisify(open)(CONFIG_PATH, 'w+') + } else { + throw err + } + }) const content = await promisify(readFile)(fd, { encoding: 'utf8' From ff3586fa9ce6860e147e3bd9ede5a98d8602a631 Mon Sep 17 00:00:00 2001 From: Kincaid O'Neil Date: Tue, 5 Mar 2019 12:42:04 -0500 Subject: [PATCH 20/21] docs(readme): add persistence note --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ca98ced..ef5d50b 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,7 @@ Create an instance of the API, which automatically connects to the underlying le import { connect, LedgerEnv, SettlementEngineType } from '@kava-labs/switch-api' // Connect to testnet +// (State is loaded and persisted to ~/.switch/config.json automatically) const api = await connect() // Alternatively, run a local connector using Kava's connector-config @@ -229,7 +230,7 @@ await api.disconnect() ## Known Issues -- Currently, no state is persisted, meaning payment channels claims and credentials are not saved after the session ends. We've architected the API to enable this, and it's a top priority within the next week or two. +- Persisted private keys, secrets and other data is currently stored **unencrypted**. - By design, clients do not currently pay for incoming capacity on ETH nor XRP. However, that's not a sustainable solution. In order to scale and prevent liquidity denial of service attacks, clients should pay a fee to "buy" incoming capacity/bandwidth for a period of time. However, this negotiation and accounting adds a great deal of complexity. - Uplinks don't operate an internal `ilp-connector`, which may introduce some minor security risks. We intend to update this after the internal plugin architecture is refactored. - Machinomy payment channels don't currently support watchtowers, which can become a security risk if a client is offline for an extended period of time and the connector disputes the channel. (In XRP, this is less of an issue, since the on-chain fees are low enough that regular checkpoints of the latest claim can be submitted to the ledger). @@ -237,7 +238,7 @@ await api.disconnect() ## Roadmap -- [ ] Persistence and encryption of the configuration, so the uplinks and payment channel claims are restored between sessions (very soon) +- [ ] Encryption of stored credentials - [ ] Internal refactoring/improving code quality - [ ] Support for user-defined connectors - [ ] Additional assets, including ERC-20 tokens such as DAI From 537691d49513695b08c2aa70e2041766fe82b765 Mon Sep 17 00:00:00 2001 From: Kincaid O'Neil Date: Fri, 8 Mar 2019 17:52:32 -0500 Subject: [PATCH 21/21] fix: address comments --- src/__tests__/disconnect.test.ts | 6 +++++- src/config.ts | 15 +++++++++------ src/index.ts | 11 +++++++---- src/settlement/machinomy.ts | 11 +++++++---- 4 files changed, 28 insertions(+), 15 deletions(-) diff --git a/src/__tests__/disconnect.test.ts b/src/__tests__/disconnect.test.ts index 95dde6e..c843f18 100644 --- a/src/__tests__/disconnect.test.ts +++ b/src/__tests__/disconnect.test.ts @@ -17,8 +17,12 @@ test.beforeEach(async t => { t.context = await connect(process.env.LEDGER_ENV! as LedgerEnv) }) +test('after connect', async t => { + await t.notThrowsAsync(t.context.disconnect()) +}) + test('after add eth', async t => { - const uplink = await addEth()(t.context) + await addEth()(t.context) await t.notThrowsAsync(t.context.disconnect()) }) diff --git a/src/config.ts b/src/config.ts index c244ed2..95ffa74 100644 --- a/src/config.ts +++ b/src/config.ts @@ -26,9 +26,9 @@ export const serializeConfig = (state: State) => credentials: state.credentials.map(credentialToConfig) }) -export const persistConfig = async (fd: number, state: State) => { - await promisify(ftruncate)(fd) // r+ mode doesn't overwrite, so first delete file contents - await promisify(writeFile)(fd, serializeConfig(state)) +export const persistConfig = async (fileDescriptor: number, state: State) => { + await promisify(ftruncate)(fileDescriptor) // r+ mode doesn't overwrite, so first delete file contents + await promisify(writeFile)(fileDescriptor, serializeConfig(state)) } /** @@ -46,7 +46,7 @@ export const loadConfig = async (): Promise< else throw err }) - const fd = await promisify(open)(CONFIG_PATH, 'r+').catch(err => { + const fileDescriptor = await promisify(open)(CONFIG_PATH, 'r+').catch(err => { if (err.code === 'ENOENT') { return promisify(open)(CONFIG_PATH, 'w+') } else { @@ -54,12 +54,15 @@ export const loadConfig = async (): Promise< } }) - const content = await promisify(readFile)(fd, { + const content = await promisify(readFile)(fileDescriptor, { encoding: 'utf8' }) // TODO Add *robust* schema validation - return [fd, content.length === 0 ? undefined : JSON.parse(content)] + return [ + fileDescriptor, + content.length === 0 ? undefined : JSON.parse(content) + ] } /** diff --git a/src/index.ts b/src/index.ts index a6febc9..e1e7b97 100644 --- a/src/index.ts +++ b/src/index.ts @@ -66,7 +66,7 @@ export interface State { } export const connect = async (ledgerEnv: LedgerEnv = LedgerEnv.Testnet) => { - const [fd, config] = await loadConfig() + const [fileDescriptor, config] = await loadConfig() // TODO Make sure the config has the right ledgerEnv to support multiple? Idk @@ -101,7 +101,10 @@ export const connect = async (ledgerEnv: LedgerEnv = LedgerEnv.Testnet) => { ) : [] - const saveInterval = setInterval(() => persistConfig(fd, state), 10000) + const saveInterval = setInterval( + () => persistConfig(fileDescriptor, state), + 10000 + ) const add = async ( credentialConfig: CredentialConfigs @@ -168,11 +171,11 @@ export const connect = async (ledgerEnv: LedgerEnv = LedgerEnv.Testnet) => { const disconnect = async () => { clearInterval(saveInterval) - await persistConfig(fd, state) + await persistConfig(fileDescriptor, state) await Promise.all(state.uplinks.map(closeUplink)) await Promise.all(state.credentials.map(closeCredential)) await Promise.all(Object.values(state.settlers).map(closeEngine)) - await promisify(close)(fd) + await promisify(close)(fileDescriptor) } // TODO Should disconnecting the API prevent other operations from occuring? (they may not work anyways) diff --git a/src/settlement/machinomy.ts b/src/settlement/machinomy.ts index 319fd0a..763c835 100644 --- a/src/settlement/machinomy.ts +++ b/src/settlement/machinomy.ts @@ -82,10 +82,10 @@ export type ReadyEthereumCredential = { } /** - * Ensure that the given string is begins with given prefix + * Ensure that the given string begins with given prefix * - Prefix the string if it doesn't already */ -const prefixWith = (prefix: string, str: string) => +const ensurePrefixedWith = (prefix: string, str: string) => str.startsWith(prefix) ? str : prefix + str const addressFromPrivate = (privateKey: string) => @@ -99,8 +99,11 @@ export const setupCredential = ({ ReadyEthereumCredential > => ({ settlerType, - privateKey: prefixWith('0x', privateKey), - address: prefixWith('0x', addressFromPrivate(prefixWith('0x', privateKey))) + privateKey: ensurePrefixedWith('0x', privateKey), + address: ensurePrefixedWith( + '0x', + addressFromPrivate(ensurePrefixedWith('0x', privateKey)) + ) }) export const uniqueId = (cred: ReadyEthereumCredential) => cred.address