From 5ea7c5c03ca956831c5a062d2fbe0904d6dfe1e7 Mon Sep 17 00:00:00 2001 From: Teo Gebhard Date: Tue, 16 Feb 2021 14:41:18 +0200 Subject: [PATCH 01/12] Basic TypeScript support --- .babel.config.js | 3 +- .babel.node.config.js | 3 +- .eslintrc.js | 8 ++ package-lock.json | 214 ++++++++++++++++++++++++++++++++++++- package.json | 6 +- src/{index.js => index.ts} | 2 +- tsconfig.json | 15 +++ webpack.config.js | 12 +-- 8 files changed, 250 insertions(+), 13 deletions(-) rename src/{index.js => index.ts} (93%) create mode 100644 tsconfig.json diff --git a/.babel.config.js b/.babel.config.js index de6aae5ea..422df09ab 100644 --- a/.babel.config.js +++ b/.babel.config.js @@ -18,7 +18,8 @@ module.exports = { ] }, exclude: ['transform-regenerator', '@babel/plugin-transform-regenerator'] - }] + }], + ['@babel/preset-typescript'] ], plugins: [ "add-module-exports", diff --git a/.babel.node.config.js b/.babel.node.config.js index 6af312e3b..245b492cd 100644 --- a/.babel.node.config.js +++ b/.babel.node.config.js @@ -10,7 +10,8 @@ module.exports = { targets: { node: true } - }] + }], + ['@babel/preset-typescript'] ], plugins: [ 'add-module-exports', diff --git a/.eslintrc.js b/.eslintrc.js index fdfe91128..a69d14f9b 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -31,5 +31,13 @@ module.exports = { 'no-restricted-syntax': [ 'error', 'ForInStatement', 'LabeledStatement', 'WithStatement' ], + 'import/extensions': ['error', 'never', { json: 'always' }] + }, + settings: { + 'import/resolver': { + node: { + extensions: ['.js', '.ts'] + } + } } } diff --git a/package-lock.json b/package-lock.json index ce22166ce..d7828847a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -585,6 +585,23 @@ "@babel/helper-plugin-utils": "^7.10.4" } }, + "@babel/plugin-syntax-typescript": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.12.13.tgz", + "integrity": "sha512-cHP3u1JiUiG2LFDKbXnwVad81GvfyIOmCD6HIEId6ojrY0Drfy2q1jw7BwN7dE84+kTnBjLkXoL3IEy/3JPu2w==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "dependencies": { + "@babel/helper-plugin-utils": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.12.13.tgz", + "integrity": "sha512-C+10MXCXJLiR6IeG9+Wiejt9jmtFpxUc3MQqCmPY8hfCjyUGl9kT+B2okzEZrtykiwrc4dbCPdDoz0A/HQbDaA==", + "dev": true + } + } + }, "@babel/plugin-transform-arrow-functions": { "version": "7.12.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.12.1.tgz", @@ -890,6 +907,173 @@ "@babel/helper-plugin-utils": "^7.10.4" } }, + "@babel/plugin-transform-typescript": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.12.13.tgz", + "integrity": "sha512-z1VWskPJxK9tfxoYvePWvzSJC+4pxXr8ArmRm5ofqgi+mwpKg6lvtomkIngBYMJVnKhsFYVysCQLDn//v2RHcg==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.12.13", + "@babel/helper-plugin-utils": "^7.12.13", + "@babel/plugin-syntax-typescript": "^7.12.13" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz", + "integrity": "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==", + "dev": true, + "requires": { + "@babel/highlight": "^7.12.13" + } + }, + "@babel/generator": { + "version": "7.12.15", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.12.15.tgz", + "integrity": "sha512-6F2xHxBiFXWNSGb7vyCUTBF8RCLY66rS0zEPcP8t/nQyXjha5EuK4z7H5o7fWG8B4M7y6mqVWq1J+1PuwRhecQ==", + "dev": true, + "requires": { + "@babel/types": "^7.12.13", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + } + }, + "@babel/helper-create-class-features-plugin": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.12.13.tgz", + "integrity": "sha512-Vs/e9wv7rakKYeywsmEBSRC9KtmE7Px+YBlESekLeJOF0zbGUicGfXSNi3o+tfXSNS48U/7K9mIOOCR79Cl3+Q==", + "dev": true, + "requires": { + "@babel/helper-function-name": "^7.12.13", + "@babel/helper-member-expression-to-functions": "^7.12.13", + "@babel/helper-optimise-call-expression": "^7.12.13", + "@babel/helper-replace-supers": "^7.12.13", + "@babel/helper-split-export-declaration": "^7.12.13" + } + }, + "@babel/helper-function-name": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.12.13.tgz", + "integrity": "sha512-TZvmPn0UOqmvi5G4vvw0qZTpVptGkB1GL61R6lKvrSdIxGm5Pky7Q3fpKiIkQCAtRCBUwB0PaThlx9vebCDSwA==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.12.13", + "@babel/template": "^7.12.13", + "@babel/types": "^7.12.13" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.13.tgz", + "integrity": "sha512-DjEVzQNz5LICkzN0REdpD5prGoidvbdYk1BVgRUOINaWJP2t6avB27X1guXK1kXNrX0WMfsrm1A/ZBthYuIMQg==", + "dev": true, + "requires": { + "@babel/types": "^7.12.13" + } + }, + "@babel/helper-member-expression-to-functions": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.12.13.tgz", + "integrity": "sha512-B+7nN0gIL8FZ8SvMcF+EPyB21KnCcZHQZFczCxbiNGV/O0rsrSBlWGLzmtBJ3GMjSVMIm4lpFhR+VdVBuIsUcQ==", + "dev": true, + "requires": { + "@babel/types": "^7.12.13" + } + }, + "@babel/helper-optimise-call-expression": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.13.tgz", + "integrity": "sha512-BdWQhoVJkp6nVjB7nkFWcn43dkprYauqtk++Py2eaf/GRDFm5BxRqEIZCiHlZUGAVmtwKcsVL1dC68WmzeFmiA==", + "dev": true, + "requires": { + "@babel/types": "^7.12.13" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.12.13.tgz", + "integrity": "sha512-C+10MXCXJLiR6IeG9+Wiejt9jmtFpxUc3MQqCmPY8hfCjyUGl9kT+B2okzEZrtykiwrc4dbCPdDoz0A/HQbDaA==", + "dev": true + }, + "@babel/helper-replace-supers": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.12.13.tgz", + "integrity": "sha512-pctAOIAMVStI2TMLhozPKbf5yTEXc0OJa0eENheb4w09SrgOWEs+P4nTOZYJQCqs8JlErGLDPDJTiGIp3ygbLg==", + "dev": true, + "requires": { + "@babel/helper-member-expression-to-functions": "^7.12.13", + "@babel/helper-optimise-call-expression": "^7.12.13", + "@babel/traverse": "^7.12.13", + "@babel/types": "^7.12.13" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.12.13.tgz", + "integrity": "sha512-tCJDltF83htUtXx5NLcaDqRmknv652ZWCHyoTETf1CXYJdPC7nohZohjUgieXhv0hTJdRf2FjDueFehdNucpzg==", + "dev": true, + "requires": { + "@babel/types": "^7.12.13" + } + }, + "@babel/highlight": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.12.13.tgz", + "integrity": "sha512-kocDQvIbgMKlWxXe9fof3TQ+gkIPOUSEYhJjqUjvKMez3krV7vbzYCDq39Oj11UAVK7JqPVGQPlgE85dPNlQww==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.12.11", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.12.15", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.15.tgz", + "integrity": "sha512-AQBOU2Z9kWwSZMd6lNjCX0GUgFonL1wAM1db8L8PMk9UDaGsRCArBkU4Sc+UCM3AE4hjbXx+h58Lb3QT4oRmrA==", + "dev": true + }, + "@babel/template": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.12.13.tgz", + "integrity": "sha512-/7xxiGA57xMo/P2GVvdEumr8ONhFOhfgq2ihK3h1e6THqzTAkHbkXgB0xI9yeTfIUoH3+oAeHhqm/I43OTbbjA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.12.13", + "@babel/parser": "^7.12.13", + "@babel/types": "^7.12.13" + } + }, + "@babel/traverse": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.13.tgz", + "integrity": "sha512-3Zb4w7eE/OslI0fTp8c7b286/cQps3+vdLW3UcwC8VSJC6GbKn55aeVVu2QJNuCDoeKyptLOFrPq8WqZZBodyA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.12.13", + "@babel/generator": "^7.12.13", + "@babel/helper-function-name": "^7.12.13", + "@babel/helper-split-export-declaration": "^7.12.13", + "@babel/parser": "^7.12.13", + "@babel/types": "^7.12.13", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.19" + } + }, + "@babel/types": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.13.tgz", + "integrity": "sha512-oKrdZTld2im1z8bDwTOQvUbxKwE+854zc16qWZQlcTqMN00pWxHQ4ZeOq0yDMnisOpRykH2/5Qqcrk/OlbAjiQ==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.12.11", + "lodash": "^4.17.19", + "to-fast-properties": "^2.0.0" + } + } + } + }, "@babel/plugin-transform-unicode-escapes": { "version": "7.12.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.12.1.tgz", @@ -996,6 +1180,25 @@ "esutils": "^2.0.2" } }, + "@babel/preset-typescript": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.12.13.tgz", + "integrity": "sha512-gYry7CeXwD2wtw5qHzrtzKaShEhOfTmKb4i0ZxeYBcBosN5VuAudsNbjX7Oj5EAfQ3K4s4HsVMQRRcqGsPvs2A==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.12.13", + "@babel/helper-validator-option": "^7.12.11", + "@babel/plugin-transform-typescript": "^7.12.13" + }, + "dependencies": { + "@babel/helper-plugin-utils": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.12.13.tgz", + "integrity": "sha512-C+10MXCXJLiR6IeG9+Wiejt9jmtFpxUc3MQqCmPY8hfCjyUGl9kT+B2okzEZrtykiwrc4dbCPdDoz0A/HQbDaA==", + "dev": true + } + } + }, "@babel/runtime": { "version": "7.12.5", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.12.5.tgz", @@ -1042,9 +1245,9 @@ } }, "@babel/types": { - "version": "7.12.12", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.12.tgz", - "integrity": "sha512-lnIX7piTxOH22xE7fDXDbSHg9MM1/6ORnafpJmov5rs0kX5g4BZxeXNJLXsMRiO0U5Rb8/FvMS6xlTnTHvxonQ==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.13.tgz", + "integrity": "sha512-oKrdZTld2im1z8bDwTOQvUbxKwE+854zc16qWZQlcTqMN00pWxHQ4ZeOq0yDMnisOpRykH2/5Qqcrk/OlbAjiQ==", "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.12.11", @@ -13464,6 +13667,11 @@ "is-typedarray": "^1.0.0" } }, + "typescript": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.4.tgz", + "integrity": "sha512-+Uru0t8qIRgjuCpiSPpfGuhHecMllk5Zsazj5LZvVsEStEjmIRRBZe+jHjGQvsgS7M1wONy2PQXd67EMyV6acg==" + }, "unicode-canonical-property-names-ecmascript": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz", diff --git a/package.json b/package.json index 97499c96b..53e63e7cd 100644 --- a/package.json +++ b/package.json @@ -8,12 +8,14 @@ }, "main": "dist/streamr-client.nodejs.js", "browser": "dist/streamr-client.web.min.js", + "types": "dist/types/src/index.d.ts", "directories": { "example": "examples", "test": "test" }, "scripts": { - "build": "rm dist/*; NODE_ENV=production webpack --mode=production --progress", + "build": "rm -rf dist; NODE_ENV=production webpack --mode=production --progress && npm run build:types", + "build:types": "tsc --emitDeclarationOnly", "benchmarks": "node test/benchmarks/publish.js && node test/benchmarks/subscribe.js", "prebuild-benchmark": "npm run build -- --config-name=node-lib", "build-benchmark": "npm run benchmarks", @@ -49,6 +51,7 @@ "@babel/plugin-transform-modules-commonjs": "^7.12.1", "@babel/plugin-transform-runtime": "^7.12.10", "@babel/preset-env": "^7.12.11", + "@babel/preset-typescript": "^7.12.13", "async-mutex": "^0.2.6", "babel-eslint": "^10.1.0", "babel-loader": "^8.2.2", @@ -107,6 +110,7 @@ "quick-lru": "^5.1.1", "readable-stream": "^3.6.0", "streamr-client-protocol": "^7.1.2", + "typescript": "^4.1.4", "uuid": "^8.3.2", "webpack-node-externals": "^2.5.2", "ws": "^7.4.2" diff --git a/src/index.js b/src/index.ts similarity index 93% rename from src/index.js rename to src/index.ts index a70115695..d200639c0 100644 --- a/src/index.js +++ b/src/index.ts @@ -7,7 +7,7 @@ import * as DataUnionEndpoints from './rest/DataUnionEndpoints' Object.assign(StreamrClient.prototype, { ...StreamEndpoints, ...LoginEndpoints, - ...DataUnionEndpoints, + ...DataUnionEndpoints }) export default StreamrClient diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 000000000..277e2659b --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "es6", + "module": "commonjs", + "allowJs": true, + "declaration": true, + "declarationDir": "dist/types", + "outDir": "dist", + "strict": true, + "esModuleInterop": true, + "resolveJsonModule": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} \ No newline at end of file diff --git a/webpack.config.js b/webpack.config.js index 6cfa15224..256000d2d 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -25,7 +25,7 @@ module.exports = (env, argv) => { const commonConfig = { mode: isProduction ? 'production' : 'development', - entry: path.join(__dirname, 'src', 'index.js'), + entry: path.join(__dirname, 'src', 'index.ts'), devtool: 'source-map', output: { path: path.join(__dirname, 'dist'), @@ -41,7 +41,7 @@ module.exports = (env, argv) => { module: { rules: [ { - test: /(\.jsx|\.js)$/, + test: /(\.jsx|\.js|\.ts)$/, exclude: /(node_modules|bower_components)/, use: { loader: 'babel-loader', @@ -53,7 +53,7 @@ module.exports = (env, argv) => { } }, { - test: /(\.jsx|\.js)$/, + test: /(\.jsx|\.js|\.ts)$/, loader: 'eslint-loader', exclude: /(node_modules|streamr-client-protocol|dist)/, // excluding streamr-client-protocol makes build work when 'npm link'ed }, @@ -61,7 +61,7 @@ module.exports = (env, argv) => { }, resolve: { modules: [path.resolve('./node_modules'), path.resolve('./src')], - extensions: ['.json', '.js'], + extensions: ['.json', '.js', '.ts'], }, plugins: [ gitRevisionPlugin, @@ -87,7 +87,7 @@ module.exports = (env, argv) => { serverConfig.module.rules = [ { - test: /(\.jsx|\.js)$/, + test: /(\.jsx|\.js|\.ts)$/, exclude: /(node_modules|bower_components)/, use: { loader: 'babel-loader', @@ -99,7 +99,7 @@ module.exports = (env, argv) => { } }, { - test: /(\.jsx|\.js)$/, + test: /(\.jsx|\.js|\.ts)$/, loader: 'eslint-loader', exclude: /(node_modules|streamr-client-protocol|dist)/, // excluding streamr-client-protocol makes build work when 'npm link'ed }, From 8609f2fb31d67d8c26fff01d468705173096d810 Mon Sep 17 00:00:00 2001 From: Teo Gebhard Date: Tue, 16 Feb 2021 14:41:24 +0200 Subject: [PATCH 02/12] Fix test file --- test/legacy/HistoricalSubscription.test.js | 1 - 1 file changed, 1 deletion(-) diff --git a/test/legacy/HistoricalSubscription.test.js b/test/legacy/HistoricalSubscription.test.js index 939d5c364..582dc880d 100644 --- a/test/legacy/HistoricalSubscription.test.js +++ b/test/legacy/HistoricalSubscription.test.js @@ -1,4 +1,3 @@ -/ import sinon from 'sinon' import { ControlLayer, MessageLayer, Errors } from 'streamr-client-protocol' import { wait } from 'streamr-test-utils' From 488747f81bb5920eba7b9b8d7477865ab744c988 Mon Sep 17 00:00:00 2001 From: Teo Gebhard Date: Tue, 16 Feb 2021 14:41:28 +0200 Subject: [PATCH 03/12] Export Subscription class --- src/subscribe/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/subscribe/index.js b/src/subscribe/index.js index a41decbe4..456ec49ef 100644 --- a/src/subscribe/index.js +++ b/src/subscribe/index.js @@ -10,7 +10,7 @@ import Validator from './Validator' import messageStream from './messageStream' import resendStream from './resendStream' -class Subscription extends Emitter { +export class Subscription extends Emitter { constructor(client, opts, onFinally = () => {}) { super() this.client = client From e1e7a2b3452baae37bb513be36c89c1d524cb070 Mon Sep 17 00:00:00 2001 From: Teo Gebhard Date: Tue, 16 Feb 2021 14:41:34 +0200 Subject: [PATCH 04/12] Add placeholder type annotations --- .eslintrc.js | 6 +- package-lock.json | 12 +++ package.json | 2 + src/{StreamrClient.js => StreamrClient.ts} | 87 ++++++++++----- ...nionEndpoints.js => DataUnionEndpoints.ts} | 102 +++++++++--------- .../{LoginEndpoints.js => LoginEndpoints.ts} | 13 +-- ...{StreamEndpoints.js => StreamEndpoints.ts} | 38 +++---- src/types.ts | 3 + 8 files changed, 161 insertions(+), 102 deletions(-) rename src/{StreamrClient.js => StreamrClient.ts} (80%) rename src/rest/{DataUnionEndpoints.js => DataUnionEndpoints.ts} (92%) rename src/rest/{LoginEndpoints.js => LoginEndpoints.ts} (86%) rename src/rest/{StreamEndpoints.js => StreamEndpoints.ts} (83%) create mode 100644 src/types.ts diff --git a/.eslintrc.js b/.eslintrc.js index a69d14f9b..18ba0b22b 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -31,7 +31,11 @@ module.exports = { 'no-restricted-syntax': [ 'error', 'ForInStatement', 'LabeledStatement', 'WithStatement' ], - 'import/extensions': ['error', 'never', { json: 'always' }] + 'import/extensions': ['error', 'never', { json: 'always' }], + 'lines-between-class-members': 'off', + 'padded-blocks': 'off', + 'no-use-before-define': 'off', + 'import/order': 'off' }, settings: { 'import/resolver': { diff --git a/package-lock.json b/package-lock.json index d7828847a..afacd097c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2493,6 +2493,12 @@ "@babel/types": "^7.3.0" } }, + "@types/debug": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.5.tgz", + "integrity": "sha512-Q1y515GcOdTHgagaVFhHnIFQ38ygs/kmxdNpvpou+raI9UO3YZcHDngBSYKQklcKlvA7iuQlmIKbzvmxcOE9CQ==", + "dev": true + }, "@types/graceful-fs": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.4.tgz", @@ -2556,6 +2562,12 @@ "integrity": "sha512-6gOkRe7OIioWAXfnO/2lFiv+SJichKVSys1mSsgyrYHSEjk8Ctv4tSR/Odvnu+HWlH2C8j53dahU03XmQdd5fA==", "dev": true }, + "@types/qs": { + "version": "6.9.5", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.5.tgz", + "integrity": "sha512-/JHkVHtx/REVG0VVToGRGH2+23hsYLHdyG+GrvoUGlGAd0ErauXDyvHtRI/7H7mzLm+tBCKA7pfcpkQ1lf58iQ==", + "dev": true + }, "@types/stack-utils": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.0.tgz", diff --git a/package.json b/package.json index 53e63e7cd..ddb2cb616 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,8 @@ "@babel/plugin-transform-runtime": "^7.12.10", "@babel/preset-env": "^7.12.11", "@babel/preset-typescript": "^7.12.13", + "@types/debug": "^4.1.5", + "@types/qs": "^6.9.5", "async-mutex": "^0.2.6", "babel-eslint": "^10.1.0", "babel-loader": "^8.2.2", diff --git a/src/StreamrClient.js b/src/StreamrClient.ts similarity index 80% rename from src/StreamrClient.js rename to src/StreamrClient.ts index ac7a898b1..91cae1d94 100644 --- a/src/StreamrClient.js +++ b/src/StreamrClient.ts @@ -1,4 +1,5 @@ import EventEmitter from 'eventemitter3' +// @ts-expect-error import { ControlLayer } from 'streamr-client-protocol' import Debug from 'debug' @@ -11,23 +12,24 @@ import Connection from './Connection' import Publisher from './publish' import Subscriber from './subscribe' import { getUserId } from './user' +import { Todo } from './types' /** * Wrap connection message events with message parsing. */ class StreamrConnection extends Connection { - constructor(...args) { + constructor(...args: Todo) { super(...args) this.on('message', this.onConnectionMessage) } // eslint-disable-next-line class-methods-use-this - parse(messageEvent) { + parse(messageEvent: Todo) { return ControlLayer.ControlMessage.deserialize(messageEvent.data) } - onConnectionMessage(messageEvent) { + onConnectionMessage(messageEvent: Todo) { let controlMessage try { controlMessage = this.parse(messageEvent) @@ -48,28 +50,39 @@ class StreamrConnection extends Connection { } class StreamrCached { - constructor(client) { + + client: Todo + getStream: Todo + getUserInfo: Todo + isStreamPublisher: Todo + isStreamSubscriber: Todo + getUserId: Todo + + constructor(client: StreamrClient) { this.client = client const cacheOptions = client.options.cache + // @ts-expect-error this.getStream = CacheAsyncFn(client.getStream.bind(client), { ...cacheOptions, - cacheKey([maybeStreamId]) { + cacheKey([maybeStreamId]: Todo) { const { streamId } = validateOptions(maybeStreamId) return streamId } }) this.getUserInfo = CacheAsyncFn(client.getUserInfo.bind(client), cacheOptions) + // @ts-expect-error this.isStreamPublisher = CacheAsyncFn(client.isStreamPublisher.bind(client), { ...cacheOptions, - cacheKey([maybeStreamId, ethAddress]) { + cacheKey([maybeStreamId, ethAddress]: Todo) { const { streamId } = validateOptions(maybeStreamId) return `${streamId}|${ethAddress}` } }) + // @ts-expect-error this.isStreamSubscriber = CacheAsyncFn(client.isStreamSubscriber.bind(client), { ...cacheOptions, - cacheKey([maybeStreamId, ethAddress]) { + cacheKey([maybeStreamId, ethAddress]: Todo) { const { streamId } = validateOptions(maybeStreamId) return `${streamId}|${ethAddress}` } @@ -78,10 +91,10 @@ class StreamrCached { this.getUserId = CacheAsyncFn(client.getUserId.bind(client), cacheOptions) } - clearStream(streamId) { + clearStream(streamId: Todo) { this.getStream.clear() - this.isStreamPublisher.clearMatching((s) => s.startsWith(streamId)) - this.isStreamSubscriber.clearMatching((s) => s.startsWith(streamId)) + this.isStreamPublisher.clearMatching((s: Todo) => s.startsWith(streamId)) + this.isStreamSubscriber.clearMatching((s: Todo) => s.startsWith(streamId)) } clearUser() { @@ -91,6 +104,7 @@ class StreamrCached { clear() { this.clearUser() + // @ts-expect-error this.clearStream() } } @@ -99,7 +113,19 @@ class StreamrCached { const uid = process.pid != null ? process.pid : `${uuid().slice(-4)}${uuid().slice(0, 4)}` export default class StreamrClient extends EventEmitter { - constructor(options = {}, connection) { + + id: string + debug: Debug.Debugger + options: Todo + getUserInfo: Todo + session: Session + connection: StreamrConnection + publisher: Todo + subscriber: Subscriber + cached: StreamrCached + ethereum: StreamrEthereum + + constructor(options: Todo = {}, connection?: StreamrConnection) { super() this.id = counterId(`${this.constructor.name}:${uid}`) this.debug = Debug(this.id) @@ -135,6 +161,7 @@ export default class StreamrClient extends EventEmitter { .on('disconnected', this.onConnectionDisconnected) .on('error', this.onConnectionError) + // @ts-expect-error this.publisher = new Publisher(this) this.subscriber = new Subscriber(this) this.cached = new StreamrCached(this) @@ -151,12 +178,12 @@ export default class StreamrClient extends EventEmitter { this.emit('disconnected') } - onConnectionError(err) { + onConnectionError(err: Todo) { this.emit('error', new Connection.ConnectionError(err)) } - getErrorEmitter(source) { - return (err) => { + getErrorEmitter(source: Todo) { + return (err: Todo) => { if (!(err instanceof Connection.ConnectionError || err.reason instanceof Connection.ConnectionError)) { // emit non-connection errors this.emit('error', err) @@ -166,11 +193,12 @@ export default class StreamrClient extends EventEmitter { } } - _onError(err, ...args) { + _onError(err: Todo, ...args: Todo) { + // @ts-expect-error this.onError(err, ...args) } - async send(request) { + async send(request: Todo) { return this.connection.send(request) } @@ -178,7 +206,7 @@ export default class StreamrClient extends EventEmitter { * Override to control output */ - onError(error) { // eslint-disable-line class-methods-use-this + onError(error: Todo) { // eslint-disable-line class-methods-use-this console.error(error) } @@ -214,11 +242,12 @@ export default class StreamrClient extends EventEmitter { ]) } - getSubscriptions(...args) { + getSubscriptions(...args: Todo) { return this.subscriber.getAll(...args) } - getSubscription(...args) { + getSubscription(...args: Todo) { + // @ts-expect-error return this.subscriber.get(...args) } @@ -234,7 +263,7 @@ export default class StreamrClient extends EventEmitter { return this.session.logout() } - async publish(...args) { + async publish(...args: Todo) { return this.publisher.publish(...args) } @@ -242,17 +271,17 @@ export default class StreamrClient extends EventEmitter { return getUserId(this) } - setNextGroupKey(...args) { + setNextGroupKey(...args: Todo) { return this.publisher.setNextGroupKey(...args) } - rotateGroupKey(...args) { + rotateGroupKey(...args: Todo) { return this.publisher.rotateGroupKey(...args) } - async subscribe(opts, onMessage) { - let subTask - let sub + async subscribe(opts: Todo, onMessage: Todo) { + let subTask: Todo + let sub: Todo const hasResend = !!(opts.resend || opts.from || opts.to || opts.last) const onEnd = () => { if (sub && typeof onMessage === 'function') { @@ -281,11 +310,11 @@ export default class StreamrClient extends EventEmitter { return subTask } - async unsubscribe(opts) { + async unsubscribe(opts: Todo) { await this.subscriber.unsubscribe(opts) } - async resend(opts, onMessage) { + async resend(opts: Todo, onMessage: Todo) { const task = this.subscriber.resend(opts) if (typeof onMessage !== 'function') { return task @@ -304,11 +333,11 @@ export default class StreamrClient extends EventEmitter { return task } - enableAutoConnect(...args) { + enableAutoConnect(...args: Todo) { return this.connection.enableAutoConnect(...args) } - enableAutoDisconnect(...args) { + enableAutoDisconnect(...args: Todo) { return this.connection.enableAutoDisconnect(...args) } diff --git a/src/rest/DataUnionEndpoints.js b/src/rest/DataUnionEndpoints.ts similarity index 92% rename from src/rest/DataUnionEndpoints.js rename to src/rest/DataUnionEndpoints.ts index ea1b28e8f..d9f512a45 100644 --- a/src/rest/DataUnionEndpoints.js +++ b/src/rest/DataUnionEndpoints.ts @@ -17,6 +17,7 @@ import { Contract } from '@ethersproject/contracts' import { keccak256 } from '@ethersproject/keccak256' import { verifyMessage } from '@ethersproject/wallet' import debug from 'debug' +import { Todo } from '../types' import { until, getEndpointUrl } from '../utils' @@ -259,7 +260,7 @@ const sidechainAmbABI = [{ /** @typedef {String} EthereumAddress */ -function throwIfBadAddress(address, variableDescription) { +function throwIfBadAddress(address: Todo, variableDescription: Todo) { try { return getAddress(address) } catch (e) { @@ -273,7 +274,7 @@ function throwIfBadAddress(address, variableDescription) { * @param {EthereumAddress} inputAddress from user (NOT case sensitive) * @returns {EthereumAddress} with checksum case */ -function parseAddress(client, inputAddress) { +function parseAddress(client: Todo, inputAddress: Todo) { if (isAddress(inputAddress)) { return getAddress(inputAddress) } @@ -281,8 +282,8 @@ function parseAddress(client, inputAddress) { } // Find the Asyncronous Message-passing Bridge sidechain ("home") contract -let cachedSidechainAmb -async function getSidechainAmb(client, options) { +let cachedSidechainAmb: Todo +async function getSidechainAmb(client: Todo, options: Todo) { if (!cachedSidechainAmb) { const getAmbPromise = async () => { const mainnetProvider = client.ethereum.getMainnetProvider() @@ -306,7 +307,7 @@ async function getSidechainAmb(client, options) { return cachedSidechainAmb } -async function getMainnetAmb(client, options) { +async function getMainnetAmb(client: Todo, options: Todo) { const mainnetProvider = client.ethereum.getMainnetProvider() const factoryMainnetAddress = options.factoryMainnetAddress || client.options.factoryMainnetAddress const factoryMainnet = new Contract(factoryMainnetAddress, factoryMainnetABI, mainnetProvider) @@ -314,7 +315,7 @@ async function getMainnetAmb(client, options) { return new Contract(mainnetAmbAddress, mainnetAmbABI, mainnetProvider) } -async function requiredSignaturesHaveBeenCollected(client, messageHash, options = {}) { +async function requiredSignaturesHaveBeenCollected(client: Todo, messageHash: Todo, options: Todo = {}) { const sidechainAmb = await getSidechainAmb(client, options) const requiredSignatureCount = await sidechainAmb.requiredSignatures() @@ -329,7 +330,7 @@ async function requiredSignaturesHaveBeenCollected(client, messageHash, options } // move signatures from sidechain to mainnet -async function transportSignatures(client, messageHash, options) { +async function transportSignatures(client: Todo, messageHash: Todo, options: Todo) { const sidechainAmb = await getSidechainAmb(client, options) const message = await sidechainAmb.message(messageHash) const messageId = '0x' + message.substr(2, 64) @@ -339,8 +340,8 @@ async function transportSignatures(client, messageHash, options) { log(`${collectedSignatureCount} signatures reported, getting them from the sidechain AMB...`) const signatures = await Promise.all(Array(collectedSignatureCount).fill(0).map(async (_, i) => sidechainAmb.signature(messageHash, i))) - const [vArray, rArray, sArray] = [[], [], []] - signatures.forEach((signature, i) => { + const [vArray, rArray, sArray]: Todo = [[], [], []] + signatures.forEach((signature: string, i) => { log(` Signature ${i}: ${signature} (len=${signature.length}=${signature.length / 2 - 1} bytes)`) rArray.push(signature.substr(2, 64)) sArray.push(signature.substr(66, 64)) @@ -355,6 +356,7 @@ async function transportSignatures(client, messageHash, options) { let gasLimit try { // magic number suggested by https://github.com/poanetwork/tokenbridge/blob/master/oracle/src/utils/constants.js + // @ts-expect-error gasLimit = await mainnetAmb.estimateGas.executeSignatures(message, packedSignatures) + 200000 log(`Calculated gas limit: ${gasLimit.toString()}`) } catch (e) { @@ -412,7 +414,7 @@ async function transportSignatures(client, messageHash, options) { // template for withdraw functions // client could be replaced with AMB (mainnet and sidechain) -async function untilWithdrawIsComplete(client, getWithdrawTxFunc, getBalanceFunc, options = {}) { +async function untilWithdrawIsComplete(client: Todo, getWithdrawTxFunc: Todo, getBalanceFunc: Todo, options: Todo = {}) { const { pollingIntervalMs = 1000, retryTimeoutMs = 60000, @@ -424,7 +426,7 @@ async function untilWithdrawIsComplete(client, getWithdrawTxFunc, getBalanceFunc if (options.payForSignatureTransport) { log(`Got receipt, filtering UserRequestForSignature from ${tr.events.length} events...`) // event UserRequestForSignature(bytes32 indexed messageId, bytes encodedData); - const sigEventArgsArray = tr.events.filter((e) => e.event === 'UserRequestForSignature').map((e) => e.args) + const sigEventArgsArray = tr.events.filter((e: Todo) => e.event === 'UserRequestForSignature').map((e: Todo) => e.args) if (sigEventArgsArray.length < 1) { throw new Error("No UserRequestForSignature events emitted from withdraw transaction, can't transport withdraw to mainnet") } @@ -464,9 +466,9 @@ async function untilWithdrawIsComplete(client, getWithdrawTxFunc, getBalanceFunc // TODO: calculate addresses in JS instead of asking over RPC, see data-union-solidity/contracts/CloneLib.sol // key the cache with name only, since PROBABLY one StreamrClient will ever use only one private key -const mainnetAddressCache = {} // mapping: "name" -> mainnet address +const mainnetAddressCache: Todo = {} // mapping: "name" -> mainnet address /** @returns {Promise} Mainnet address for Data Union */ -async function getDataUnionMainnetAddress(client, dataUnionName, deployerAddress, options = {}) { +async function getDataUnionMainnetAddress(client: Todo, dataUnionName: Todo, deployerAddress: Todo, options: Todo = {}) { if (!mainnetAddressCache[dataUnionName]) { const provider = client.ethereum.getMainnetProvider() const factoryMainnetAddress = options.factoryMainnetAddress || client.options.factoryMainnetAddress @@ -479,9 +481,9 @@ async function getDataUnionMainnetAddress(client, dataUnionName, deployerAddress } // TODO: calculate addresses in JS -const sidechainAddressCache = {} // mapping: mainnet address -> sidechain address +const sidechainAddressCache: Todo = {} // mapping: mainnet address -> sidechain address /** @returns {Promise} Sidechain address for Data Union */ -async function getDataUnionSidechainAddress(client, duMainnetAddress, options = {}) { +async function getDataUnionSidechainAddress(client: Todo, duMainnetAddress: Todo, options: Todo = {}) { if (!sidechainAddressCache[duMainnetAddress]) { const provider = client.ethereum.getMainnetProvider() const factoryMainnetAddress = options.factoryMainnetAddress || client.options.factoryMainnetAddress @@ -493,7 +495,7 @@ async function getDataUnionSidechainAddress(client, duMainnetAddress, options = return sidechainAddressCache[duMainnetAddress] } -function getMainnetContractReadOnly(client, options = {}) { +function getMainnetContractReadOnly(client: Todo, options: Todo = {}) { let dataUnion = options.dataUnion || options.dataUnionAddress || client.options.dataUnion if (isAddress(dataUnion)) { const provider = client.ethereum.getMainnetProvider() @@ -506,13 +508,13 @@ function getMainnetContractReadOnly(client, options = {}) { return dataUnion } -function getMainnetContract(client, options = {}) { +function getMainnetContract(client: Todo, options: Todo = {}) { const du = getMainnetContractReadOnly(client, options) const signer = client.ethereum.getSigner() return du.connect(signer) } -async function getSidechainContract(client, options = {}) { +async function getSidechainContract(client: Todo, options: Todo = {}) { const signer = await client.ethereum.getSidechainSigner() const duMainnet = getMainnetContractReadOnly(client, options) const duSidechainAddress = await getDataUnionSidechainAddress(client, duMainnet.address, options) @@ -520,7 +522,7 @@ async function getSidechainContract(client, options = {}) { return duSidechain } -async function getSidechainContractReadOnly(client, options = {}) { +async function getSidechainContractReadOnly(client: Todo, options: Todo = {}) { const provider = await client.ethereum.getSidechainProvider() const duMainnet = getMainnetContractReadOnly(client, options) const duSidechainAddress = await getDataUnionSidechainAddress(client, duMainnet.address, options) @@ -532,12 +534,12 @@ async function getSidechainContractReadOnly(client, options = {}) { // admin: DEPLOY AND SETUP DATA UNION // ////////////////////////////////////////////////////////////////// -export async function calculateDataUnionMainnetAddress(dataUnionName, deployerAddress, options) { +export async function calculateDataUnionMainnetAddress(dataUnionName: Todo, deployerAddress: Todo, options: Todo) { const address = getAddress(deployerAddress) // throws if bad address return getDataUnionMainnetAddress(this, dataUnionName, address, options) } -export async function calculateDataUnionSidechainAddress(duMainnetAddress, options) { +export async function calculateDataUnionSidechainAddress(duMainnetAddress: Todo, options: Todo) { const address = getAddress(duMainnetAddress) // throws if bad address return getDataUnionSidechainAddress(this, address, options) } @@ -572,7 +574,7 @@ export async function calculateDataUnionSidechainAddress(duMainnetAddress, optio * @param {DeployOptions} options such as adminFee (default: 0) * @return {Promise} that resolves when the new DU is deployed over the bridge to side-chain */ -export async function deployDataUnion(options = {}) { +export async function deployDataUnion(options: Todo = {}) { const { owner, joinPartAgents, @@ -643,13 +645,16 @@ export async function deployDataUnion(options = {}) { ) const dataUnion = new Contract(duMainnetAddress, dataUnionMainnetABI, mainnetWallet) + // @ts-expect-error dataUnion.deployTxReceipt = tr + // @ts-expect-error dataUnion.sidechain = new Contract(duSidechainAddress, dataUnionSidechainABI, sidechainProvider) return dataUnion } -export async function getDataUnionContract(options = {}) { +export async function getDataUnionContract(options: Todo = {}) { const ret = getMainnetContract(this, options) + // @ts-expect-error ret.sidechain = await getSidechainContract(this, options) return ret } @@ -660,7 +665,7 @@ export async function getDataUnionContract(options = {}) { * @param {String} name describes the secret * @returns {String} the server-generated secret */ -export async function createSecret(dataUnionMainnetAddress, name = 'Untitled Data Union Secret') { +export async function createSecret(dataUnionMainnetAddress: Todo, name: string = 'Untitled Data Union Secret') { const duAddress = getAddress(dataUnionMainnetAddress) // throws if bad address const url = getEndpointUrl(this.options.restUrl, 'dataunions', duAddress, 'secrets') const res = await authFetch( @@ -688,7 +693,7 @@ export async function createSecret(dataUnionMainnetAddress, name = 'Untitled Dat * @param {List} memberAddressList to kick * @returns {Promise} partMembers sidechain transaction */ -export async function kick(memberAddressList, options = {}) { +export async function kick(memberAddressList: Todo, options: Todo = {}) { const members = memberAddressList.map(getAddress) // throws if there are bad addresses const duSidechain = await getSidechainContract(this, options) const tx = await duSidechain.partMembers(members) @@ -701,7 +706,7 @@ export async function kick(memberAddressList, options = {}) { * @param {List} memberAddressList to add * @returns {Promise} addMembers sidechain transaction */ -export async function addMembers(memberAddressList, options = {}) { +export async function addMembers(memberAddressList: Todo, options: Todo = {}) { const members = memberAddressList.map(getAddress) // throws if there are bad addresses const duSidechain = await getSidechainContract(this, options) const tx = await duSidechain.addMembers(members) @@ -716,7 +721,7 @@ export async function addMembers(memberAddressList, options = {}) { * @param {EthereumOptions} options (including e.g. `dataUnion` Contract object or address) * @returns {Promise} get receipt once withdraw transaction is confirmed */ -export async function withdrawMember(memberAddress, options) { +export async function withdrawMember(memberAddress: Todo, options: Todo) { const address = getAddress(memberAddress) // throws if bad address const tr = await untilWithdrawIsComplete( this, @@ -734,7 +739,7 @@ export async function withdrawMember(memberAddress, options) { * @param {EthereumOptions} options * @returns {Promise} await on call .wait to actually send the tx */ -export async function getWithdrawMemberTx(memberAddress, options) { +export async function getWithdrawMemberTx(memberAddress: Todo, options: Todo) { const a = getAddress(memberAddress) // throws if bad address const duSidechain = await getSidechainContract(this, options) return duSidechain.withdrawAll(a, true) // sendToMainnet=true @@ -749,7 +754,7 @@ export async function getWithdrawMemberTx(memberAddress, options) { * @param {EthereumOptions} options * @returns {Promise} get receipt once withdraw transaction is confirmed */ -export async function withdrawToSigned(memberAddress, recipientAddress, signature, options) { +export async function withdrawToSigned(memberAddress: Todo, recipientAddress: Todo, signature: Todo, options: Todo) { const from = getAddress(memberAddress) // throws if bad address const to = getAddress(recipientAddress) const tr = await untilWithdrawIsComplete( @@ -770,7 +775,7 @@ export async function withdrawToSigned(memberAddress, recipientAddress, signatur * @param {EthereumOptions} options * @returns {Promise} await on call .wait to actually send the tx */ -export async function getWithdrawToSignedTx(memberAddress, recipientAddress, signature, options) { +export async function getWithdrawToSignedTx(memberAddress: Todo, recipientAddress: Todo, signature: Todo, options: Todo) { const duSidechain = await getSidechainContract(this, options) return duSidechain.withdrawAllToSigned(memberAddress, recipientAddress, true, signature) // sendToMainnet=true } @@ -780,7 +785,7 @@ export async function getWithdrawToSignedTx(memberAddress, recipientAddress, sig * @param {number} newFeeFraction between 0.0 and 1.0 * @param {EthereumOptions} options */ -export async function setAdminFee(newFeeFraction, options) { +export async function setAdminFee(newFeeFraction: Todo, options: Todo) { if (newFeeFraction < 0 || newFeeFraction > 1) { throw new Error('newFeeFraction argument must be a number between 0...1, got: ' + newFeeFraction) } @@ -794,13 +799,13 @@ export async function setAdminFee(newFeeFraction, options) { * Get data union admin fee fraction that admin gets from each revenue event * @returns {number} between 0.0 and 1.0 */ -export async function getAdminFee(options) { +export async function getAdminFee(options: Todo) { const duMainnet = getMainnetContractReadOnly(this, options) const adminFeeBN = await duMainnet.adminFeeFraction() return +adminFeeBN.toString() / 1e18 } -export async function getAdminAddress(options) { +export async function getAdminAddress(options: Todo) { const duMainnet = getMainnetContractReadOnly(this, options) return duMainnet.owner() } @@ -818,7 +823,7 @@ export async function getAdminAddress(options) { * @property {String} member Ethereum mainnet address of the joining member. If not given, use StreamrClient authentication key * @property {String} secret if given, and correct, join the data union immediately */ -export async function joinDataUnion(options = {}) { +export async function joinDataUnion(options: Todo = {}) { const { member, secret, @@ -828,6 +833,7 @@ export async function joinDataUnion(options = {}) { const body = { memberAddress: parseAddress(this, member) } + // @ts-expect-error if (secret) { body.secret = secret } const url = getEndpointUrl(this.options.restUrl, 'dataunions', dataUnion.address, 'joinRequests') @@ -851,7 +857,7 @@ export async function joinDataUnion(options = {}) { * @param {Number} retryTimeoutMs (optional, default: 60000) give up * @return {Promise} resolves when member is in the data union (or fails with HTTP error) */ -export async function hasJoined(memberAddress, options = {}) { +export async function hasJoined(memberAddress: Todo, options: Todo = {}) { const { pollingIntervalMs = 1000, retryTimeoutMs = 60000, @@ -864,14 +870,14 @@ export async function hasJoined(memberAddress, options = {}) { } // TODO: this needs more thought: probably something like getEvents from sidechain? Heavy on RPC? -export async function getMembers(options) { +export async function getMembers(options: Todo) { const duSidechain = await getSidechainContractReadOnly(this, options) throw new Error(`Not implemented for side-chain data union (at ${duSidechain.address})`) // event MemberJoined(address indexed); // event MemberParted(address indexed); } -export async function getDataUnionStats(options) { +export async function getDataUnionStats(options: Todo) { const duSidechain = await getSidechainContractReadOnly(this, options) const [ totalEarnings, @@ -897,7 +903,7 @@ export async function getDataUnionStats(options) { * @param {EthereumAddress} dataUnion to query * @param {EthereumAddress} memberAddress (optional) if not supplied, get the stats of currently logged in StreamrClient (if auth: privateKey) */ -export async function getMemberStats(memberAddress, options) { +export async function getMemberStats(memberAddress: Todo, options: Todo) { const address = parseAddress(this, memberAddress) // TODO: use duSidechain.getMemberStats(address) once it's implemented, to ensure atomic read // (so that memberData is from same block as getEarnings, otherwise withdrawable will be foobar) @@ -921,7 +927,7 @@ export async function getMemberStats(memberAddress, options) { * @param memberAddress whose balance is returned * @return {Promise} */ -export async function getMemberBalance(memberAddress, options) { +export async function getMemberBalance(memberAddress: Todo, options: Todo) { const address = parseAddress(this, memberAddress) const duSidechain = await getSidechainContractReadOnly(this, options) return duSidechain.getWithdrawableEarnings(address) @@ -935,10 +941,10 @@ export async function getMemberBalance(memberAddress, options) { * was given in StreamrClient constructor. * @returns {Promise} token balance in "wei" (10^-18 parts) */ -export async function getTokenBalance(address, options) { +export async function getTokenBalance(address: Todo, options: Todo) { const a = parseAddress(this, address) const tokenAddressMainnet = options.tokenAddress || ( - await getMainnetContractReadOnly(this, options).then((c) => c.token()).catch(() => null) || this.options.tokenAddress + await getMainnetContractReadOnly(this, options).then((c: Todo) => c.token()).catch(() => null) || this.options.tokenAddress ) if (!tokenAddressMainnet) { throw new Error('tokenAddress option not found') } const provider = this.ethereum.getMainnetProvider() @@ -960,7 +966,7 @@ export async function getTokenBalance(address, options) { * @param {EthereumAddress} contractAddress * @returns {number} 1 for old, 2 for current, zero for "not a data union" */ -export async function getDataUnionVersion(contractAddress) { +export async function getDataUnionVersion(contractAddress: Todo) { const a = getAddress(contractAddress) // throws if bad address const provider = this.ethereum.getMainnetProvider() const du = new Contract(a, [{ @@ -987,7 +993,7 @@ export async function getDataUnionVersion(contractAddress) { * @param {EthereumOptions} options (including e.g. `dataUnion` Contract object or address) * @returns {Promise} get receipt once withdraw is complete (tokens are seen in mainnet) */ -export async function withdraw(options = {}) { +export async function withdraw(options: Todo = {}) { const tr = await untilWithdrawIsComplete( this, this.getWithdrawTx.bind(this), @@ -1002,7 +1008,7 @@ export async function withdraw(options = {}) { * @param {EthereumOptions} options (including e.g. `dataUnion` Contract object or address) * @returns {Promise} await on call .wait to actually send the tx */ -export async function getWithdrawTx(options) { +export async function getWithdrawTx(options: Todo) { const signer = await this.ethereum.getSidechainSigner() const address = await signer.getAddress() const duSidechain = await getSidechainContract(this, options) @@ -1025,7 +1031,7 @@ export async function getWithdrawTx(options) { * @param {EthereumOptions} options (including e.g. `dataUnion` Contract object or address) * @returns {Promise} get receipt once withdraw is complete (tokens are seen in mainnet) */ -export async function withdrawTo(recipientAddress, options = {}) { +export async function withdrawTo(recipientAddress: Todo, options = {}) { const to = getAddress(recipientAddress) // throws if bad address const tr = await untilWithdrawIsComplete( this, @@ -1042,7 +1048,7 @@ export async function withdrawTo(recipientAddress, options = {}) { * @param {EthereumOptions} options (including e.g. `dataUnion` Contract object or address) * @returns {Promise} await on call .wait to actually send the tx */ -export async function getWithdrawTxTo(recipientAddress, options) { +export async function getWithdrawTxTo(recipientAddress: Todo, options: Todo) { const signer = await this.ethereum.getSidechainSigner() const address = await signer.getAddress() const duSidechain = await getSidechainContract(this, options) @@ -1067,7 +1073,7 @@ export async function getWithdrawTxTo(recipientAddress, options) { * @param {EthereumOptions} options (including e.g. `dataUnion` Contract object or address) * @returns {string} signature authorizing withdrawing all earnings to given recipientAddress */ -export async function signWithdrawTo(recipientAddress, options) { +export async function signWithdrawTo(recipientAddress: Todo, options: Todo) { return this.signWithdrawAmountTo(recipientAddress, BigNumber.from(0), options) } @@ -1080,7 +1086,7 @@ export async function signWithdrawTo(recipientAddress, options) { * @param {EthereumOptions} options (including e.g. `dataUnion` Contract object or address) * @returns {string} signature authorizing withdrawing all earnings to given recipientAddress */ -export async function signWithdrawAmountTo(recipientAddress, amountTokenWei, options) { +export async function signWithdrawAmountTo(recipientAddress: Todo, amountTokenWei: Todo, options: Todo) { const to = getAddress(recipientAddress) // throws if bad address const signer = this.ethereum.getSigner() // it shouldn't matter if it's mainnet or sidechain signer since key should be the same const address = await signer.getAddress() diff --git a/src/rest/LoginEndpoints.js b/src/rest/LoginEndpoints.ts similarity index 86% rename from src/rest/LoginEndpoints.js rename to src/rest/LoginEndpoints.ts index 2ab9f967e..24f66ecc6 100644 --- a/src/rest/LoginEndpoints.js +++ b/src/rest/LoginEndpoints.ts @@ -1,8 +1,9 @@ +import { Todo } from '../types' import { getEndpointUrl } from '../utils' import authFetch, { AuthFetchError } from './authFetch' -async function getSessionToken(url, props) { +async function getSessionToken(url: Todo, props: Todo) { return authFetch( url, undefined, @@ -16,7 +17,7 @@ async function getSessionToken(url, props) { ) } -export async function getChallenge(address) { +export async function getChallenge(address: Todo) { this.debug('getChallenge %o', { address, }) @@ -30,7 +31,7 @@ export async function getChallenge(address) { ) } -export async function sendChallengeResponse(challenge, signature, address) { +export async function sendChallengeResponse(challenge: Todo, signature: Todo, address: Todo) { this.debug('sendChallengeResponse %o', { challenge, signature, @@ -45,7 +46,7 @@ export async function sendChallengeResponse(challenge, signature, address) { return getSessionToken(url, props) } -export async function loginWithChallengeResponse(signingFunction, address) { +export async function loginWithChallengeResponse(signingFunction: Todo, address: Todo) { this.debug('loginWithChallengeResponse %o', { address, }) @@ -54,7 +55,7 @@ export async function loginWithChallengeResponse(signingFunction, address) { return this.sendChallengeResponse(challenge, signature, address) } -export async function loginWithApiKey(apiKey) { +export async function loginWithApiKey(apiKey: Todo) { this.debug('loginWithApiKey %o', { apiKey, }) @@ -65,7 +66,7 @@ export async function loginWithApiKey(apiKey) { return getSessionToken(url, props) } -export async function loginWithUsernamePassword(username, password) { +export async function loginWithUsernamePassword(username: Todo, password: Todo) { this.debug('loginWithUsernamePassword %o', { username, }) diff --git a/src/rest/StreamEndpoints.js b/src/rest/StreamEndpoints.ts similarity index 83% rename from src/rest/StreamEndpoints.js rename to src/rest/StreamEndpoints.ts index 923b8e2e3..c71320d8d 100644 --- a/src/rest/StreamEndpoints.js +++ b/src/rest/StreamEndpoints.ts @@ -11,6 +11,7 @@ import StreamPart from '../stream/StreamPart' import { isKeyExchangeStream } from '../stream/KeyExchange' import authFetch from './authFetch' +import { Todo } from '../types' const debug = debugFactory('StreamrClient') @@ -24,7 +25,7 @@ const agentByProtocol = { https: new HttpsAgent(agentSettings), } -function getKeepAliveAgentForUrl(url) { +function getKeepAliveAgentForUrl(url: string) { if (url.startsWith('https')) { return agentByProtocol.https } @@ -38,7 +39,7 @@ function getKeepAliveAgentForUrl(url) { // These function are mixed in to StreamrClient.prototype. // In the below functions, 'this' is intended to be the StreamrClient -export async function getStream(streamId) { +export async function getStream(streamId: Todo) { this.debug('getStream %o', { streamId, }) @@ -62,7 +63,7 @@ export async function getStream(streamId) { } } -export async function listStreams(query = {}) { +export async function listStreams(query: Todo = {}) { this.debug('listStreams %o', { query, }) @@ -71,7 +72,7 @@ export async function listStreams(query = {}) { return json ? json.map((stream) => new Stream(this, stream)) : [] } -export async function getStreamByName(name) { +export async function getStreamByName(name: string) { this.debug('getStreamByName %o', { name, }) @@ -82,7 +83,7 @@ export async function getStreamByName(name) { return json[0] ? new Stream(this, json[0]) : undefined } -export async function createStream(props) { +export async function createStream(props: Todo) { this.debug('createStream %o', { props, }) @@ -98,7 +99,7 @@ export async function createStream(props) { return json ? new Stream(this, json) : undefined } -export async function getOrCreateStream(props) { +export async function getOrCreateStream(props: Todo) { this.debug('getOrCreateStream %o', { props, }) @@ -125,16 +126,16 @@ export async function getOrCreateStream(props) { } } -export async function getStreamPublishers(streamId) { +export async function getStreamPublishers(streamId: Todo) { this.debug('getStreamPublishers %o', { streamId, }) const url = getEndpointUrl(this.options.restUrl, 'streams', streamId, 'publishers') const json = await authFetch(url, this.session) - return json.addresses.map((a) => a.toLowerCase()) + return json.addresses.map((a: string) => a.toLowerCase()) } -export async function isStreamPublisher(streamId, ethAddress) { +export async function isStreamPublisher(streamId: Todo, ethAddress: Todo) { this.debug('isStreamPublisher %o', { streamId, ethAddress, @@ -152,16 +153,16 @@ export async function isStreamPublisher(streamId, ethAddress) { } } -export async function getStreamSubscribers(streamId) { +export async function getStreamSubscribers(streamId: Todo) { this.debug('getStreamSubscribers %o', { streamId, }) const url = getEndpointUrl(this.options.restUrl, 'streams', streamId, 'subscribers') const json = await authFetch(url, this.session) - return json.addresses.map((a) => a.toLowerCase()) + return json.addresses.map((a: Todo) => a.toLowerCase()) } -export async function isStreamSubscriber(streamId, ethAddress) { +export async function isStreamSubscriber(streamId: Todo, ethAddress: Todo) { this.debug('isStreamSubscriber %o', { streamId, ethAddress, @@ -178,7 +179,7 @@ export async function isStreamSubscriber(streamId, ethAddress) { } } -export async function getStreamValidationInfo(streamId) { +export async function getStreamValidationInfo(streamId: Todo) { this.debug('getStreamValidationInfo %o', { streamId, }) @@ -187,7 +188,7 @@ export async function getStreamValidationInfo(streamId) { return json } -export async function getStreamLast(streamObjectOrId) { +export async function getStreamLast(streamObjectOrId: Todo) { const { streamId, streamPartition = 0, count = 1 } = validateOptions(streamObjectOrId) this.debug('getStreamLast %o', { streamId, @@ -203,18 +204,19 @@ export async function getStreamLast(streamObjectOrId) { return json } -export async function getStreamPartsByStorageNode(address) { +export async function getStreamPartsByStorageNode(address: Todo) { const json = await authFetch(getEndpointUrl(this.options.restUrl, 'storageNodes', address, 'streams'), this.session) - let result = [] - json.forEach((stream) => { + let result: Todo = [] + json.forEach((stream: Todo) => { result = result.concat(StreamPart.fromStream(stream)) }) return result } -export async function publishHttp(streamObjectOrId, data, requestOptions = {}, keepAlive = true) { +export async function publishHttp(streamObjectOrId: Todo, data: Todo, requestOptions: Todo = {}, keepAlive: Todo = true) { let streamId if (streamObjectOrId instanceof Stream) { + // @ts-expect-error streamId = streamObjectOrId.id } else { streamId = streamObjectOrId diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 000000000..ff7342d4a --- /dev/null +++ b/src/types.ts @@ -0,0 +1,3 @@ +export type Todo = any + +export type StreamrClientAndEndpoints = any \ No newline at end of file From b53e327da6dcab7294f7171fbce4d0f1689cee26 Mon Sep 17 00:00:00 2001 From: Teo Gebhard Date: Tue, 16 Feb 2021 14:41:43 +0200 Subject: [PATCH 05/12] Tests to support TypeScript --- jest.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jest.config.js b/jest.config.js index 7ec99a872..1086a1106 100644 --- a/jest.config.js +++ b/jest.config.js @@ -163,7 +163,7 @@ module.exports = { // A map from regular expressions to paths to transformers transform: { - '\\.js$': ['babel-jest', { + '\\.(js|ts)$': ['babel-jest', { configFile: path.resolve(__dirname, '.babel.node.config.js'), babelrc: false, }] From bfc7141b176abe9ae88952c3f296a3687ed79586 Mon Sep 17 00:00:00 2001 From: Teo Gebhard Date: Tue, 16 Feb 2021 14:41:52 +0200 Subject: [PATCH 06/12] Encapsulate endpoints --- package.json | 2 +- src/StreamrClient.ts | 209 +++- src/index.ts | 13 - src/rest/DataUnionEndpoints.ts | 1081 +++++++++-------- src/rest/LoginEndpoints.ts | 150 +-- src/rest/StreamEndpoints.ts | 353 +++--- src/stream/{index.js => index.ts} | 29 +- test/flakey/EnvStressTest.test.js | 2 +- .../DataUnionEndpoints.test.js | 2 +- .../adminWithdrawMember.test.js | 2 +- .../adminWithdrawSigned.test.js | 2 +- .../DataUnionEndpoints/calculate.test.js | 2 +- .../DataUnionEndpoints/withdraw.test.js | 2 +- .../DataUnionEndpoints/withdrawTo.test.js | 2 +- test/integration/Encryption.test.js | 2 +- test/integration/GapFill.test.js | 2 +- test/integration/LoginEndpoints.test.js | 2 +- test/integration/MultipleClients.test.js | 2 +- test/integration/ResendReconnect.test.js | 2 +- test/integration/Resends.test.js | 2 +- test/integration/Sequencing.test.js | 2 +- test/integration/Session.test.js | 2 +- .../integration/StreamConnectionState.test.js | 2 +- test/integration/StreamEndpoints.test.js | 2 +- test/integration/StreamrClient.test.js | 2 +- test/integration/Subscriber.test.js | 2 +- test/integration/SubscriberResends.test.js | 2 +- test/integration/Subscription.test.js | 2 +- test/integration/Validation.test.js | 2 +- test/integration/authFetch.test.js | 2 +- test/unit/Session.test.js | 2 +- webpack.config.js | 2 +- 32 files changed, 1062 insertions(+), 825 deletions(-) delete mode 100644 src/index.ts rename src/stream/{index.js => index.ts} (84%) diff --git a/package.json b/package.json index ddb2cb616..417f496b5 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ }, "main": "dist/streamr-client.nodejs.js", "browser": "dist/streamr-client.web.min.js", - "types": "dist/types/src/index.d.ts", + "types": "dist/types/src/StreamrClient.d.ts", "directories": { "example": "examples", "test": "test" diff --git a/src/StreamrClient.ts b/src/StreamrClient.ts index 91cae1d94..05c4d8424 100644 --- a/src/StreamrClient.ts +++ b/src/StreamrClient.ts @@ -13,6 +13,9 @@ import Publisher from './publish' import Subscriber from './subscribe' import { getUserId } from './user' import { Todo } from './types' +import { StreamEndpoints } from './rest/StreamEndpoints' +import { LoginEndpoints } from './rest/LoginEndpoints' +import { DataUnionEndpoints } from './rest/DataUnionEndpoints' /** * Wrap connection message events with message parsing. @@ -61,7 +64,6 @@ class StreamrCached { constructor(client: StreamrClient) { this.client = client const cacheOptions = client.options.cache - // @ts-expect-error this.getStream = CacheAsyncFn(client.getStream.bind(client), { ...cacheOptions, cacheKey([maybeStreamId]: Todo) { @@ -70,7 +72,6 @@ class StreamrCached { } }) this.getUserInfo = CacheAsyncFn(client.getUserInfo.bind(client), cacheOptions) - // @ts-expect-error this.isStreamPublisher = CacheAsyncFn(client.isStreamPublisher.bind(client), { ...cacheOptions, cacheKey([maybeStreamId, ethAddress]: Todo) { @@ -79,7 +80,6 @@ class StreamrCached { } }) - // @ts-expect-error this.isStreamSubscriber = CacheAsyncFn(client.isStreamSubscriber.bind(client), { ...cacheOptions, cacheKey([maybeStreamId, ethAddress]: Todo) { @@ -117,13 +117,15 @@ export default class StreamrClient extends EventEmitter { id: string debug: Debug.Debugger options: Todo - getUserInfo: Todo session: Session connection: StreamrConnection publisher: Todo subscriber: Subscriber cached: StreamrCached ethereum: StreamrEthereum + streamEndpoints: StreamEndpoints + loginEndpoints: LoginEndpoints + dataUnionEndpoints: DataUnionEndpoints constructor(options: Todo = {}, connection?: StreamrConnection) { super() @@ -144,7 +146,6 @@ export default class StreamrClient extends EventEmitter { }) // bind event handlers - this.getUserInfo = this.getUserInfo.bind(this) this.onConnectionConnected = this.onConnectionConnected.bind(this) this.onConnectionDisconnected = this.onConnectionDisconnected.bind(this) this._onError = this._onError.bind(this) @@ -166,6 +167,10 @@ export default class StreamrClient extends EventEmitter { this.subscriber = new Subscriber(this) this.cached = new StreamrCached(this) this.ethereum = new StreamrEthereum(this) + + this.streamEndpoints = new StreamEndpoints(this) + this.loginEndpoints = new LoginEndpoints(this) + this.dataUnionEndpoints = new DataUnionEndpoints(this) } async onConnectionConnected() { @@ -352,4 +357,198 @@ export default class StreamrClient extends EventEmitter { static generateEthereumAccount() { return StreamrEthereum.generateEthereumAccount() } + + // TODO many of these methods that use streamEndpoints/loginEndpoints/dataUnionEndpoints are private: remove those + + async getStream(streamId: Todo) { + return this.streamEndpoints.getStream(streamId) + } + + async listStreams(query: Todo = {}) { + return this.streamEndpoints.listStreams(query) + } + + async getStreamByName(name: string) { + return this.streamEndpoints.getStreamByName(name) + } + + async createStream(props: Todo) { + return this.streamEndpoints.createStream(props) + } + + async getOrCreateStream(props: Todo) { + return this.streamEndpoints.getOrCreateStream(props) + } + + async getStreamPublishers(streamId: Todo) { + return this.streamEndpoints.getStreamPublishers(streamId) + } + + async isStreamPublisher(streamId: Todo, ethAddress: Todo) { + return this.streamEndpoints.isStreamPublisher(streamId, ethAddress) + } + + async getStreamSubscribers(streamId: Todo) { + return this.streamEndpoints.getStreamSubscribers(streamId) + } + + async isStreamSubscriber(streamId: Todo, ethAddress: Todo) { + return this.streamEndpoints.isStreamSubscriber(streamId, ethAddress) + } + + async getStreamValidationInfo(streamId: Todo) { + return this.streamEndpoints.getStreamValidationInfo(streamId) + } + + async getStreamLast(streamObjectOrId: Todo) { + return this.streamEndpoints.getStreamLast(streamObjectOrId) + } + + async getStreamPartsByStorageNode(address: Todo) { + return this.streamEndpoints.getStreamPartsByStorageNode(address) + } + + async publishHttp(streamObjectOrId: Todo, data: Todo, requestOptions: Todo = {}, keepAlive: Todo = true) { + return this.streamEndpoints.publishHttp(streamObjectOrId, data, requestOptions, keepAlive) + } + + async getChallenge(address: Todo) { + return this.loginEndpoints.getChallenge(address) + } + + async sendChallengeResponse(challenge: Todo, signature: Todo, address: Todo) { + return this.loginEndpoints.sendChallengeResponse(challenge, signature, address) + } + + async loginWithChallengeResponse(signingFunction: Todo, address: Todo) { + return this.loginEndpoints.loginWithChallengeResponse(signingFunction, address) + } + + async loginWithApiKey(apiKey: Todo) { + return this.loginEndpoints.loginWithApiKey(apiKey) + } + + async loginWithUsernamePassword(username: Todo, password: Todo) { + return this.loginEndpoints.loginWithUsernamePassword(username, password) + } + + async getUserInfo() { + return this.loginEndpoints.getUserInfo() + } + + async logoutEndpoint() { + return this.loginEndpoints.logoutEndpoint() + } + + async calculateDataUnionMainnetAddress(dataUnionName: Todo, deployerAddress: Todo, options: Todo) { + return this.dataUnionEndpoints.calculateDataUnionMainnetAddress(dataUnionName, deployerAddress, options) + } + + async calculateDataUnionSidechainAddress(duMainnetAddress: Todo, options: Todo) { + return this.dataUnionEndpoints.calculateDataUnionSidechainAddress(duMainnetAddress, options) + } + + async deployDataUnion(options: Todo = {}) { + return this.dataUnionEndpoints.deployDataUnion(options) + } + + async getDataUnionContract(options: Todo = {}) { + return this.dataUnionEndpoints.getDataUnionContract(options) + } + + async createSecret(dataUnionMainnetAddress: Todo, name: string = 'Untitled Data Union Secret') { + return this.dataUnionEndpoints.createSecret(dataUnionMainnetAddress, name) + } + + async kick(memberAddressList: Todo, options: Todo = {}) { + return this.dataUnionEndpoints.kick(memberAddressList, options) + } + + async addMembers(memberAddressList: Todo, options: Todo = {}) { + return this.dataUnionEndpoints.addMembers(memberAddressList, options) + } + + async withdrawMember(memberAddress: Todo, options: Todo) { + return this.dataUnionEndpoints.withdrawMember(memberAddress, options) + } + + async getWithdrawMemberTx(memberAddress: Todo, options: Todo) { + return this.dataUnionEndpoints.getWithdrawMemberTx(memberAddress, options) + } + + async withdrawToSigned(memberAddress: Todo, recipientAddress: Todo, signature: Todo, options: Todo) { + return this.dataUnionEndpoints.withdrawToSigned(memberAddress, recipientAddress, signature, options) + } + + async getWithdrawToSignedTx(memberAddress: Todo, recipientAddress: Todo, signature: Todo, options: Todo) { + return this.dataUnionEndpoints.getWithdrawToSignedTx(memberAddress, recipientAddress, signature, options) + } + + async setAdminFee(newFeeFraction: Todo, options: Todo) { + return this.dataUnionEndpoints.setAdminFee(newFeeFraction, options) + } + + async getAdminFee(options: Todo) { + return this.dataUnionEndpoints.getAdminFee(options) + } + + async getAdminAddress(options: Todo) { + return this.dataUnionEndpoints.getAdminAddress(options) + } + + async joinDataUnion(options: Todo = {}) { + return this.dataUnionEndpoints.joinDataUnion(options) + } + + async hasJoined(memberAddress: Todo, options: Todo = {}) { + return this.dataUnionEndpoints.hasJoined(memberAddress, options) + } + + async getMembers(options: Todo) { + return this.dataUnionEndpoints.getMembers(options) + } + + async getDataUnionStats(options: Todo) { + return this.dataUnionEndpoints.getDataUnionStats(options) + } + + async getMemberStats(memberAddress: Todo, options: Todo) { + return this.dataUnionEndpoints.getMemberStats(memberAddress, options) + } + + async getMemberBalance(memberAddress: Todo, options: Todo) { + return this.dataUnionEndpoints.getMemberBalance(memberAddress, options) + } + + async getTokenBalance(address: Todo, options: Todo) { + return this.dataUnionEndpoints.getTokenBalance(address, options) + } + + async getDataUnionVersion(contractAddress: Todo) { + return this.dataUnionEndpoints.getDataUnionVersion(contractAddress) + } + + async withdraw(options: Todo = {}) { + return this.dataUnionEndpoints.withdraw(options) + } + + async getWithdrawTx(options: Todo) { + return this.dataUnionEndpoints.getWithdrawTx(options) + } + + async withdrawTo(recipientAddress: Todo, options = {}) { + return this.dataUnionEndpoints.withdrawTo(recipientAddress, options) + } + + async getWithdrawTxTo(recipientAddress: Todo, options: Todo) { + return this.dataUnionEndpoints.getWithdrawTxTo(recipientAddress, options) + } + + async signWithdrawTo(recipientAddress: Todo, options: Todo) { + return this.dataUnionEndpoints.signWithdrawTo(recipientAddress, options) + } + + async signWithdrawAmountTo(recipientAddress: Todo, amountTokenWei: Todo, options: Todo) { + return this.dataUnionEndpoints.signWithdrawAmountTo(recipientAddress, amountTokenWei, options) + } } diff --git a/src/index.ts b/src/index.ts deleted file mode 100644 index d200639c0..000000000 --- a/src/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -import StreamrClient from './StreamrClient' -import * as StreamEndpoints from './rest/StreamEndpoints' -import * as LoginEndpoints from './rest/LoginEndpoints' -import * as DataUnionEndpoints from './rest/DataUnionEndpoints' - -// Mixin the rest endpoints to the StreamrClient -Object.assign(StreamrClient.prototype, { - ...StreamEndpoints, - ...LoginEndpoints, - ...DataUnionEndpoints -}) - -export default StreamrClient diff --git a/src/rest/DataUnionEndpoints.ts b/src/rest/DataUnionEndpoints.ts index d9f512a45..7daaf158d 100644 --- a/src/rest/DataUnionEndpoints.ts +++ b/src/rest/DataUnionEndpoints.ts @@ -17,6 +17,7 @@ import { Contract } from '@ethersproject/contracts' import { keccak256 } from '@ethersproject/keccak256' import { verifyMessage } from '@ethersproject/wallet' import debug from 'debug' +import StreamrClient from '../StreamrClient' import { Todo } from '../types' import { until, getEndpointUrl } from '../utils' @@ -274,7 +275,7 @@ function throwIfBadAddress(address: Todo, variableDescription: Todo) { * @param {EthereumAddress} inputAddress from user (NOT case sensitive) * @returns {EthereumAddress} with checksum case */ -function parseAddress(client: Todo, inputAddress: Todo) { +function parseAddress(client: StreamrClient, inputAddress: Todo) { if (isAddress(inputAddress)) { return getAddress(inputAddress) } @@ -283,7 +284,7 @@ function parseAddress(client: Todo, inputAddress: Todo) { // Find the Asyncronous Message-passing Bridge sidechain ("home") contract let cachedSidechainAmb: Todo -async function getSidechainAmb(client: Todo, options: Todo) { +async function getSidechainAmb(client: StreamrClient, options: Todo) { if (!cachedSidechainAmb) { const getAmbPromise = async () => { const mainnetProvider = client.ethereum.getMainnetProvider() @@ -297,8 +298,10 @@ async function getSidechainAmb(client: Todo, options: Todo) { outputs: [{ type: 'address' }], stateMutability: 'view', type: 'function' + // @ts-expect-error }], sidechainProvider) const sidechainAmbAddress = await factorySidechain.amb() + // @ts-expect-error return new Contract(sidechainAmbAddress, sidechainAmbABI, sidechainProvider) } cachedSidechainAmb = getAmbPromise() @@ -307,7 +310,7 @@ async function getSidechainAmb(client: Todo, options: Todo) { return cachedSidechainAmb } -async function getMainnetAmb(client: Todo, options: Todo) { +async function getMainnetAmb(client: StreamrClient, options: Todo) { const mainnetProvider = client.ethereum.getMainnetProvider() const factoryMainnetAddress = options.factoryMainnetAddress || client.options.factoryMainnetAddress const factoryMainnet = new Contract(factoryMainnetAddress, factoryMainnetABI, mainnetProvider) @@ -315,7 +318,7 @@ async function getMainnetAmb(client: Todo, options: Todo) { return new Contract(mainnetAmbAddress, mainnetAmbABI, mainnetProvider) } -async function requiredSignaturesHaveBeenCollected(client: Todo, messageHash: Todo, options: Todo = {}) { +async function requiredSignaturesHaveBeenCollected(client: StreamrClient, messageHash: Todo, options: Todo = {}) { const sidechainAmb = await getSidechainAmb(client, options) const requiredSignatureCount = await sidechainAmb.requiredSignatures() @@ -330,7 +333,7 @@ async function requiredSignaturesHaveBeenCollected(client: Todo, messageHash: To } // move signatures from sidechain to mainnet -async function transportSignatures(client: Todo, messageHash: Todo, options: Todo) { +async function transportSignatures(client: StreamrClient, messageHash: Todo, options: Todo) { const sidechainAmb = await getSidechainAmb(client, options) const message = await sidechainAmb.message(messageHash) const messageId = '0x' + message.substr(2, 64) @@ -406,7 +409,9 @@ async function transportSignatures(client: Todo, messageHash: Todo, options: Tod } const signer = client.ethereum.getSigner() + // @ts-expect-error log(`Sending message from signer=${await signer.getAddress()}`) + // @ts-expect-error const txAMB = await mainnetAmb.connect(signer).executeSignatures(message, packedSignatures) const trAMB = await txAMB.wait() return trAMB @@ -414,7 +419,7 @@ async function transportSignatures(client: Todo, messageHash: Todo, options: Tod // template for withdraw functions // client could be replaced with AMB (mainnet and sidechain) -async function untilWithdrawIsComplete(client: Todo, getWithdrawTxFunc: Todo, getBalanceFunc: Todo, options: Todo = {}) { +async function untilWithdrawIsComplete(client: StreamrClient, getWithdrawTxFunc: Todo, getBalanceFunc: Todo, options: Todo = {}) { const { pollingIntervalMs = 1000, retryTimeoutMs = 60000, @@ -468,7 +473,7 @@ async function untilWithdrawIsComplete(client: Todo, getWithdrawTxFunc: Todo, ge // key the cache with name only, since PROBABLY one StreamrClient will ever use only one private key const mainnetAddressCache: Todo = {} // mapping: "name" -> mainnet address /** @returns {Promise} Mainnet address for Data Union */ -async function getDataUnionMainnetAddress(client: Todo, dataUnionName: Todo, deployerAddress: Todo, options: Todo = {}) { +async function getDataUnionMainnetAddress(client: StreamrClient, dataUnionName: Todo, deployerAddress: Todo, options: Todo = {}) { if (!mainnetAddressCache[dataUnionName]) { const provider = client.ethereum.getMainnetProvider() const factoryMainnetAddress = options.factoryMainnetAddress || client.options.factoryMainnetAddress @@ -483,7 +488,7 @@ async function getDataUnionMainnetAddress(client: Todo, dataUnionName: Todo, dep // TODO: calculate addresses in JS const sidechainAddressCache: Todo = {} // mapping: mainnet address -> sidechain address /** @returns {Promise} Sidechain address for Data Union */ -async function getDataUnionSidechainAddress(client: Todo, duMainnetAddress: Todo, options: Todo = {}) { +async function getDataUnionSidechainAddress(client: StreamrClient, duMainnetAddress: Todo, options: Todo = {}) { if (!sidechainAddressCache[duMainnetAddress]) { const provider = client.ethereum.getMainnetProvider() const factoryMainnetAddress = options.factoryMainnetAddress || client.options.factoryMainnetAddress @@ -495,7 +500,7 @@ async function getDataUnionSidechainAddress(client: Todo, duMainnetAddress: Todo return sidechainAddressCache[duMainnetAddress] } -function getMainnetContractReadOnly(client: Todo, options: Todo = {}) { +function getMainnetContractReadOnly(client: StreamrClient, options: Todo = {}) { let dataUnion = options.dataUnion || options.dataUnionAddress || client.options.dataUnion if (isAddress(dataUnion)) { const provider = client.ethereum.getMainnetProvider() @@ -508,593 +513,613 @@ function getMainnetContractReadOnly(client: Todo, options: Todo = {}) { return dataUnion } -function getMainnetContract(client: Todo, options: Todo = {}) { +function getMainnetContract(client: StreamrClient, options: Todo = {}) { const du = getMainnetContractReadOnly(client, options) const signer = client.ethereum.getSigner() + // @ts-expect-error return du.connect(signer) } -async function getSidechainContract(client: Todo, options: Todo = {}) { +async function getSidechainContract(client: StreamrClient, options: Todo = {}) { const signer = await client.ethereum.getSidechainSigner() const duMainnet = getMainnetContractReadOnly(client, options) const duSidechainAddress = await getDataUnionSidechainAddress(client, duMainnet.address, options) + // @ts-expect-error const duSidechain = new Contract(duSidechainAddress, dataUnionSidechainABI, signer) return duSidechain } -async function getSidechainContractReadOnly(client: Todo, options: Todo = {}) { +async function getSidechainContractReadOnly(client: StreamrClient, options: Todo = {}) { const provider = await client.ethereum.getSidechainProvider() const duMainnet = getMainnetContractReadOnly(client, options) const duSidechainAddress = await getDataUnionSidechainAddress(client, duMainnet.address, options) + // @ts-expect-error const duSidechain = new Contract(duSidechainAddress, dataUnionSidechainABI, provider) return duSidechain } -// ////////////////////////////////////////////////////////////////// -// admin: DEPLOY AND SETUP DATA UNION -// ////////////////////////////////////////////////////////////////// - -export async function calculateDataUnionMainnetAddress(dataUnionName: Todo, deployerAddress: Todo, options: Todo) { - const address = getAddress(deployerAddress) // throws if bad address - return getDataUnionMainnetAddress(this, dataUnionName, address, options) -} - -export async function calculateDataUnionSidechainAddress(duMainnetAddress: Todo, options: Todo) { - const address = getAddress(duMainnetAddress) // throws if bad address - return getDataUnionSidechainAddress(this, address, options) -} - -/** - * TODO: update this comment - * @typedef {object} EthereumOptions all optional, hence "options" - * @property {Wallet | string} wallet or private key, default is currently logged in StreamrClient (if auth: privateKey) - * @property {string} key private key, alias for String wallet - * @property {string} privateKey, alias for String wallet - * @property {providers.Provider} provider to use in case wallet was a String, or omitted - * @property {number} confirmations, default is 1 - * @property {BigNumber} gasPrice in wei (part of ethers overrides), default is whatever the network recommends (ethers.js default) - * @see https://docs.ethers.io/ethers.js/html/api-contract.html#overrides - */ -/** - * @typedef {object} AdditionalDeployOptions for deployDataUnion - * @property {EthereumAddress} owner new data union owner, defaults to StreamrClient authenticated user - * @property {Array} joinPartAgents defaults to just the owner - * @property {number} adminFee fraction (number between 0...1 where 1 means 100%) - * @property {EthereumAddress} factoryMainnetAddress defaults to StreamrClient options - * @property {string} dataUnionName unique (to the DataUnionFactory) identifier of the new data union, must not exist yet - */ -/** - * @typedef {EthereumOptions & AdditionalDeployOptions} DeployOptions - */ -// TODO: gasPrice to overrides (not needed for browser, but would be useful in node.js) +export class DataUnionEndpoints { -/** - * Create a new DataUnionMainnet contract to mainnet with DataUnionFactoryMainnet - * This triggers DataUnionSidechain contract creation in sidechain, over the bridge (AMB) - * @param {DeployOptions} options such as adminFee (default: 0) - * @return {Promise} that resolves when the new DU is deployed over the bridge to side-chain - */ -export async function deployDataUnion(options: Todo = {}) { - const { - owner, - joinPartAgents, - dataUnionName, - adminFee = 0, - sidechainPollingIntervalMs = 1000, - sidechainRetryTimeoutMs = 600000, - } = options + client: StreamrClient - let duName = dataUnionName - if (!duName) { - duName = `DataUnion-${Date.now()}` // TODO: use uuid - log(`dataUnionName generated: ${duName}`) + constructor(client: StreamrClient) { + this.client = client } - if (adminFee < 0 || adminFee > 1) { throw new Error('options.adminFeeFraction must be a number between 0...1, got: ' + adminFee) } - const adminFeeBN = BigNumber.from((adminFee * 1e18).toFixed()) // last 2...3 decimals are going to be gibberish - - const mainnetProvider = this.ethereum.getMainnetProvider() - const mainnetWallet = this.ethereum.getSigner() - const sidechainProvider = this.ethereum.getSidechainProvider() - - // parseAddress defaults to authenticated user (also if "owner" is not an address) - const ownerAddress = parseAddress(this, owner) - - let agentAddressList - if (Array.isArray(joinPartAgents)) { - // getAddress throws if there's an invalid address in the array - agentAddressList = joinPartAgents.map(getAddress) - } else { - // streamrNode needs to be joinPartAgent so that EE join with secret works (and join approvals from Marketplace UI) - agentAddressList = [ownerAddress] - if (this.options.streamrNodeAddress) { - agentAddressList.push(getAddress(this.options.streamrNodeAddress)) - } - } - - const duMainnetAddress = await getDataUnionMainnetAddress(this, duName, ownerAddress, options) - const duSidechainAddress = await getDataUnionSidechainAddress(this, duMainnetAddress, options) + // ////////////////////////////////////////////////////////////////// + // admin: DEPLOY AND SETUP DATA UNION + // ////////////////////////////////////////////////////////////////// - if (await mainnetProvider.getCode(duMainnetAddress) !== '0x') { - throw new Error(`Mainnet data union "${duName}" contract ${duMainnetAddress} already exists!`) + async calculateDataUnionMainnetAddress(dataUnionName: Todo, deployerAddress: Todo, options: Todo) { + const address = getAddress(deployerAddress) // throws if bad address + return getDataUnionMainnetAddress(this.client, dataUnionName, address, options) } - const factoryMainnetAddress = throwIfBadAddress( - options.factoryMainnetAddress || this.options.factoryMainnetAddress, - 'StreamrClient.options.factoryMainnetAddress' - ) - if (await mainnetProvider.getCode(factoryMainnetAddress) === '0x') { - throw new Error(`Data union factory contract not found at ${factoryMainnetAddress}, check StreamrClient.options.factoryMainnetAddress!`) + async calculateDataUnionSidechainAddress(duMainnetAddress: Todo, options: Todo) { + const address = getAddress(duMainnetAddress) // throws if bad address + return getDataUnionSidechainAddress(this.client, address, options) } - // function deployNewDataUnion(address owner, uint256 adminFeeFraction, address[] agents, string duName) - const factoryMainnet = new Contract(factoryMainnetAddress, factoryMainnetABI, mainnetWallet) - const tx = await factoryMainnet.deployNewDataUnion( - ownerAddress, - adminFeeBN, - agentAddressList, - duName, - ) - const tr = await tx.wait() + /** + * TODO: update this comment + * @typedef {object} EthereumOptions all optional, hence "options" + * @property {Wallet | string} wallet or private key, default is currently logged in StreamrClient (if auth: privateKey) + * @property {string} key private key, alias for String wallet + * @property {string} privateKey, alias for String wallet + * @property {providers.Provider} provider to use in case wallet was a String, or omitted + * @property {number} confirmations, default is 1 + * @property {BigNumber} gasPrice in wei (part of ethers overrides), default is whatever the network recommends (ethers.js default) + * @see https://docs.ethers.io/ethers.js/html/api-contract.html#overrides + */ + /** + * @typedef {object} AdditionalDeployOptions for deployDataUnion + * @property {EthereumAddress} owner new data union owner, defaults to StreamrClient authenticated user + * @property {Array} joinPartAgents defaults to just the owner + * @property {number} adminFee fraction (number between 0...1 where 1 means 100%) + * @property {EthereumAddress} factoryMainnetAddress defaults to StreamrClient options + * @property {string} dataUnionName unique (to the DataUnionFactory) identifier of the new data union, must not exist yet + */ + /** + * @typedef {EthereumOptions & AdditionalDeployOptions} DeployOptions + */ + // TODO: gasPrice to overrides (not needed for browser, but would be useful in node.js) + + /** + * Create a new DataUnionMainnet contract to mainnet with DataUnionFactoryMainnet + * This triggers DataUnionSidechain contract creation in sidechain, over the bridge (AMB) + * @param {DeployOptions} options such as adminFee (default: 0) + * @return {Promise} that resolves when the new DU is deployed over the bridge to side-chain + */ + async deployDataUnion(options: Todo = {}) { + const { + owner, + joinPartAgents, + dataUnionName, + adminFee = 0, + sidechainPollingIntervalMs = 1000, + sidechainRetryTimeoutMs = 600000, + } = options + + let duName = dataUnionName + if (!duName) { + duName = `DataUnion-${Date.now()}` // TODO: use uuid + log(`dataUnionName generated: ${duName}`) + } - log(`Data Union "${duName}" (mainnet: ${duMainnetAddress}, sidechain: ${duSidechainAddress}) deployed to mainnet, waiting for side-chain...`) - await until( - async () => await sidechainProvider.getCode(duSidechainAddress) !== '0x', - sidechainRetryTimeoutMs, - sidechainPollingIntervalMs - ) + if (adminFee < 0 || adminFee > 1) { throw new Error('options.adminFeeFraction must be a number between 0...1, got: ' + adminFee) } + const adminFeeBN = BigNumber.from((adminFee * 1e18).toFixed()) // last 2...3 decimals are going to be gibberish + + const mainnetProvider = this.client.ethereum.getMainnetProvider() + const mainnetWallet = this.client.ethereum.getSigner() + const sidechainProvider = this.client.ethereum.getSidechainProvider() + + // parseAddress defaults to authenticated user (also if "owner" is not an address) + const ownerAddress = parseAddress(this.client, owner) + + let agentAddressList + if (Array.isArray(joinPartAgents)) { + // getAddress throws if there's an invalid address in the array + agentAddressList = joinPartAgents.map(getAddress) + } else { + // streamrNode needs to be joinPartAgent so that EE join with secret works (and join approvals from Marketplace UI) + agentAddressList = [ownerAddress] + if (this.client.options.streamrNodeAddress) { + agentAddressList.push(getAddress(this.client.options.streamrNodeAddress)) + } + } - const dataUnion = new Contract(duMainnetAddress, dataUnionMainnetABI, mainnetWallet) - // @ts-expect-error - dataUnion.deployTxReceipt = tr - // @ts-expect-error - dataUnion.sidechain = new Contract(duSidechainAddress, dataUnionSidechainABI, sidechainProvider) - return dataUnion -} + const duMainnetAddress = await getDataUnionMainnetAddress(this.client, duName, ownerAddress, options) + const duSidechainAddress = await getDataUnionSidechainAddress(this.client, duMainnetAddress, options) -export async function getDataUnionContract(options: Todo = {}) { - const ret = getMainnetContract(this, options) - // @ts-expect-error - ret.sidechain = await getSidechainContract(this, options) - return ret -} + if (await mainnetProvider.getCode(duMainnetAddress) !== '0x') { + throw new Error(`Mainnet data union "${duName}" contract ${duMainnetAddress} already exists!`) + } -/** - * Add a new data union secret - * @param {EthereumAddress} dataUnionMainnetAddress - * @param {String} name describes the secret - * @returns {String} the server-generated secret - */ -export async function createSecret(dataUnionMainnetAddress: Todo, name: string = 'Untitled Data Union Secret') { - const duAddress = getAddress(dataUnionMainnetAddress) // throws if bad address - const url = getEndpointUrl(this.options.restUrl, 'dataunions', duAddress, 'secrets') - const res = await authFetch( - url, - this.session, - { - method: 'POST', - body: JSON.stringify({ - name - }), - headers: { - 'Content-Type': 'application/json', - }, - }, - ) - return res.secret -} + const factoryMainnetAddress = throwIfBadAddress( + options.factoryMainnetAddress || this.client.options.factoryMainnetAddress, + 'StreamrClient.options.factoryMainnetAddress' + ) + if (await mainnetProvider.getCode(factoryMainnetAddress) === '0x') { + throw new Error(`Data union factory contract not found at ${factoryMainnetAddress}, check StreamrClient.options.factoryMainnetAddress!`) + } -// ////////////////////////////////////////////////////////////////// -// admin: MANAGE DATA UNION -// ////////////////////////////////////////////////////////////////// + // function deployNewDataUnion(address owner, uint256 adminFeeFraction, address[] agents, string duName) + // @ts-expect-error + const factoryMainnet = new Contract(factoryMainnetAddress, factoryMainnetABI, mainnetWallet) + const tx = await factoryMainnet.deployNewDataUnion( + ownerAddress, + adminFeeBN, + agentAddressList, + duName, + ) + const tr = await tx.wait() + + log(`Data Union "${duName}" (mainnet: ${duMainnetAddress}, sidechain: ${duSidechainAddress}) deployed to mainnet, waiting for side-chain...`) + await until( + // @ts-expect-error + async () => await sidechainProvider.getCode(duSidechainAddress) !== '0x', + sidechainRetryTimeoutMs, + sidechainPollingIntervalMs + ) -/** - * Kick given members from data union - * @param {List} memberAddressList to kick - * @returns {Promise} partMembers sidechain transaction - */ -export async function kick(memberAddressList: Todo, options: Todo = {}) { - const members = memberAddressList.map(getAddress) // throws if there are bad addresses - const duSidechain = await getSidechainContract(this, options) - const tx = await duSidechain.partMembers(members) - // TODO: wrap promise for better error reporting in case tx fails (parse reason, throw proper error) - return tx.wait(options.confirmations || 1) -} + // @ts-expect-error + const dataUnion = new Contract(duMainnetAddress, dataUnionMainnetABI, mainnetWallet) + // @ts-expect-error + dataUnion.deployTxReceipt = tr + // @ts-expect-error + dataUnion.sidechain = new Contract(duSidechainAddress, dataUnionSidechainABI, sidechainProvider) + return dataUnion + } -/** - * Add given Ethereum addresses as data union members - * @param {List} memberAddressList to add - * @returns {Promise} addMembers sidechain transaction - */ -export async function addMembers(memberAddressList: Todo, options: Todo = {}) { - const members = memberAddressList.map(getAddress) // throws if there are bad addresses - const duSidechain = await getSidechainContract(this, options) - const tx = await duSidechain.addMembers(members) - // TODO: wrap promise for better error reporting in case tx fails (parse reason, throw proper error) - return tx.wait(options.confirmations || 1) -} + async getDataUnionContract(options: Todo = {}) { + const ret = getMainnetContract(this.client, options) + // @ts-expect-error + ret.sidechain = await getSidechainContract(this.client, options) + return ret + } -/** - * Admin: withdraw earnings (pay gas) on behalf of a member - * TODO: add test - * @param {EthereumAddress} memberAddress the other member who gets their tokens out of the Data Union - * @param {EthereumOptions} options (including e.g. `dataUnion` Contract object or address) - * @returns {Promise} get receipt once withdraw transaction is confirmed - */ -export async function withdrawMember(memberAddress: Todo, options: Todo) { - const address = getAddress(memberAddress) // throws if bad address - const tr = await untilWithdrawIsComplete( - this, - this.getWithdrawMemberTx.bind(this, address), - this.getTokenBalance.bind(this, address), - { ...this.options, ...options } - ) - return tr -} + /** + * Add a new data union secret + * @param {EthereumAddress} dataUnionMainnetAddress + * @param {String} name describes the secret + * @returns {String} the server-generated secret + */ + async createSecret(dataUnionMainnetAddress: Todo, name: string = 'Untitled Data Union Secret') { + const duAddress = getAddress(dataUnionMainnetAddress) // throws if bad address + const url = getEndpointUrl(this.client.options.restUrl, 'dataunions', duAddress, 'secrets') + const res = await authFetch( + url, + this.client.session, + { + method: 'POST', + body: JSON.stringify({ + name + }), + headers: { + 'Content-Type': 'application/json', + }, + }, + ) + return res.secret + } -/** - * Admin: get the tx promise for withdrawing all earnings on behalf of a member - * @param {EthereumAddress} memberAddress the other member who gets their tokens out of the Data Union - * @param {EthereumAddress} dataUnion to withdraw my earnings from - * @param {EthereumOptions} options - * @returns {Promise} await on call .wait to actually send the tx - */ -export async function getWithdrawMemberTx(memberAddress: Todo, options: Todo) { - const a = getAddress(memberAddress) // throws if bad address - const duSidechain = await getSidechainContract(this, options) - return duSidechain.withdrawAll(a, true) // sendToMainnet=true -} + // ////////////////////////////////////////////////////////////////// + // admin: MANAGE DATA UNION + // ////////////////////////////////////////////////////////////////// + + /** + * Kick given members from data union + * @param {List} memberAddressList to kick + * @returns {Promise} partMembers sidechain transaction + */ + async kick(memberAddressList: Todo, options: Todo = {}) { + const members = memberAddressList.map(getAddress) // throws if there are bad addresses + const duSidechain = await getSidechainContract(this.client, options) + const tx = await duSidechain.partMembers(members) + // TODO: wrap promise for better error reporting in case tx fails (parse reason, throw proper error) + return tx.wait(options.confirmations || 1) + } -/** - * Admin: Withdraw a member's earnings to another address, signed by the member - * @param {EthereumAddress} dataUnion to withdraw my earnings from - * @param {EthereumAddress} memberAddress the member whose earnings are sent out - * @param {EthereumAddress} recipientAddress the address to receive the tokens in mainnet - * @param {string} signature from member, produced using signWithdrawTo - * @param {EthereumOptions} options - * @returns {Promise} get receipt once withdraw transaction is confirmed - */ -export async function withdrawToSigned(memberAddress: Todo, recipientAddress: Todo, signature: Todo, options: Todo) { - const from = getAddress(memberAddress) // throws if bad address - const to = getAddress(recipientAddress) - const tr = await untilWithdrawIsComplete( - this, - this.getWithdrawToSignedTx.bind(this, from, to, signature), - this.getTokenBalance.bind(this, to), - { ...this.options, ...options } - ) - return tr -} + /** + * Add given Ethereum addresses as data union members + * @param {List} memberAddressList to add + * @returns {Promise} addMembers sidechain transaction + */ + async addMembers(memberAddressList: Todo, options: Todo = {}) { + const members = memberAddressList.map(getAddress) // throws if there are bad addresses + const duSidechain = await getSidechainContract(this.client, options) + const tx = await duSidechain.addMembers(members) + // TODO: wrap promise for better error reporting in case tx fails (parse reason, throw proper error) + return tx.wait(options.confirmations || 1) + } -/** - * Admin: Withdraw a member's earnings to another address, signed by the member - * @param {EthereumAddress} dataUnion to withdraw my earnings from - * @param {EthereumAddress} memberAddress the member whose earnings are sent out - * @param {EthereumAddress} recipientAddress the address to receive the tokens in mainnet - * @param {string} signature from member, produced using signWithdrawTo - * @param {EthereumOptions} options - * @returns {Promise} await on call .wait to actually send the tx - */ -export async function getWithdrawToSignedTx(memberAddress: Todo, recipientAddress: Todo, signature: Todo, options: Todo) { - const duSidechain = await getSidechainContract(this, options) - return duSidechain.withdrawAllToSigned(memberAddress, recipientAddress, true, signature) // sendToMainnet=true -} + /** + * Admin: withdraw earnings (pay gas) on behalf of a member + * TODO: add test + * @param {EthereumAddress} memberAddress the other member who gets their tokens out of the Data Union + * @param {EthereumOptions} options (including e.g. `dataUnion` Contract object or address) + * @returns {Promise} get receipt once withdraw transaction is confirmed + */ + async withdrawMember(memberAddress: Todo, options: Todo) { + const address = getAddress(memberAddress) // throws if bad address + const tr = await untilWithdrawIsComplete( + this.client, + this.getWithdrawMemberTx.bind(this, address), + this.getTokenBalance.bind(this, address), + { ...this.client.options, ...options } + ) + return tr + } -/** - * Admin: set admin fee for the data union - * @param {number} newFeeFraction between 0.0 and 1.0 - * @param {EthereumOptions} options - */ -export async function setAdminFee(newFeeFraction: Todo, options: Todo) { - if (newFeeFraction < 0 || newFeeFraction > 1) { - throw new Error('newFeeFraction argument must be a number between 0...1, got: ' + newFeeFraction) + /** + * Admin: get the tx promise for withdrawing all earnings on behalf of a member + * @param {EthereumAddress} memberAddress the other member who gets their tokens out of the Data Union + * @param {EthereumAddress} dataUnion to withdraw my earnings from + * @param {EthereumOptions} options + * @returns {Promise} await on call .wait to actually send the tx + */ + async getWithdrawMemberTx(memberAddress: Todo, options: Todo) { + const a = getAddress(memberAddress) // throws if bad address + const duSidechain = await getSidechainContract(this.client, options) + return duSidechain.withdrawAll(a, true) // sendToMainnet=true } - const adminFeeBN = BigNumber.from((newFeeFraction * 1e18).toFixed()) // last 2...3 decimals are going to be gibberish - const duMainnet = getMainnetContract(this, options) - const tx = await duMainnet.setAdminFee(adminFeeBN) - return tx.wait() -} -/** - * Get data union admin fee fraction that admin gets from each revenue event - * @returns {number} between 0.0 and 1.0 - */ -export async function getAdminFee(options: Todo) { - const duMainnet = getMainnetContractReadOnly(this, options) - const adminFeeBN = await duMainnet.adminFeeFraction() - return +adminFeeBN.toString() / 1e18 -} + /** + * Admin: Withdraw a member's earnings to another address, signed by the member + * @param {EthereumAddress} dataUnion to withdraw my earnings from + * @param {EthereumAddress} memberAddress the member whose earnings are sent out + * @param {EthereumAddress} recipientAddress the address to receive the tokens in mainnet + * @param {string} signature from member, produced using signWithdrawTo + * @param {EthereumOptions} options + * @returns {Promise} get receipt once withdraw transaction is confirmed + */ + async withdrawToSigned(memberAddress: Todo, recipientAddress: Todo, signature: Todo, options: Todo) { + const from = getAddress(memberAddress) // throws if bad address + const to = getAddress(recipientAddress) + const tr = await untilWithdrawIsComplete( + this.client, + this.getWithdrawToSignedTx.bind(this, from, to, signature), + this.getTokenBalance.bind(this, to), + { ...this.client.options, ...options } + ) + return tr + } -export async function getAdminAddress(options: Todo) { - const duMainnet = getMainnetContractReadOnly(this, options) - return duMainnet.owner() -} + /** + * Admin: Withdraw a member's earnings to another address, signed by the member + * @param {EthereumAddress} dataUnion to withdraw my earnings from + * @param {EthereumAddress} memberAddress the member whose earnings are sent out + * @param {EthereumAddress} recipientAddress the address to receive the tokens in mainnet + * @param {string} signature from member, produced using signWithdrawTo + * @param {EthereumOptions} options + * @returns {Promise} await on call .wait to actually send the tx + */ + async getWithdrawToSignedTx(memberAddress: Todo, recipientAddress: Todo, signature: Todo, options: Todo) { + const duSidechain = await getSidechainContract(this.client, options) + return duSidechain.withdrawAllToSigned(memberAddress, recipientAddress, true, signature) // sendToMainnet=true + } -// ////////////////////////////////////////////////////////////////// -// member: JOIN & QUERY DATA UNION -// ////////////////////////////////////////////////////////////////// + /** + * Admin: set admin fee for the data union + * @param {number} newFeeFraction between 0.0 and 1.0 + * @param {EthereumOptions} options + */ + async setAdminFee(newFeeFraction: Todo, options: Todo) { + if (newFeeFraction < 0 || newFeeFraction > 1) { + throw new Error('newFeeFraction argument must be a number between 0...1, got: ' + newFeeFraction) + } + const adminFeeBN = BigNumber.from((newFeeFraction * 1e18).toFixed()) // last 2...3 decimals are going to be gibberish + const duMainnet = getMainnetContract(this.client, options) + const tx = await duMainnet.setAdminFee(adminFeeBN) + return tx.wait() + } -/** - * Send a joinRequest, or get into data union instantly with a data union secret - * @param {JoinOptions} options - * - * @typedef {object} JoinOptions - * @property {String} dataUnion Ethereum mainnet address of the data union. If not given, use one given when creating StreamrClient - * @property {String} member Ethereum mainnet address of the joining member. If not given, use StreamrClient authentication key - * @property {String} secret if given, and correct, join the data union immediately - */ -export async function joinDataUnion(options: Todo = {}) { - const { - member, - secret, - } = options - const dataUnion = getMainnetContractReadOnly(this, options) + /** + * Get data union admin fee fraction that admin gets from each revenue event + * @returns {number} between 0.0 and 1.0 + */ + async getAdminFee(options: Todo) { + const duMainnet = getMainnetContractReadOnly(this.client, options) + const adminFeeBN = await duMainnet.adminFeeFraction() + return +adminFeeBN.toString() / 1e18 + } - const body = { - memberAddress: parseAddress(this, member) + async getAdminAddress(options: Todo) { + const duMainnet = getMainnetContractReadOnly(this.client, options) + return duMainnet.owner() } - // @ts-expect-error - if (secret) { body.secret = secret } - - const url = getEndpointUrl(this.options.restUrl, 'dataunions', dataUnion.address, 'joinRequests') - return authFetch( - url, - this.session, - { - method: 'POST', - body: JSON.stringify(body), - headers: { - 'Content-Type': 'application/json', + + // ////////////////////////////////////////////////////////////////// + // member: JOIN & QUERY DATA UNION + // ////////////////////////////////////////////////////////////////// + + /** + * Send a joinRequest, or get into data union instantly with a data union secret + * @param {JoinOptions} options + * + * @typedef {object} JoinOptions + * @property {String} dataUnion Ethereum mainnet address of the data union. If not given, use one given when creating StreamrClient + * @property {String} member Ethereum mainnet address of the joining member. If not given, use StreamrClient authentication key + * @property {String} secret if given, and correct, join the data union immediately + */ + async joinDataUnion(options: Todo = {}) { + const { + member, + secret, + } = options + const dataUnion = getMainnetContractReadOnly(this.client, options) + + const body = { + memberAddress: parseAddress(this.client, member) + } + // @ts-expect-error + if (secret) { body.secret = secret } + + const url = getEndpointUrl(this.client.options.restUrl, 'dataunions', dataUnion.address, 'joinRequests') + return authFetch( + url, + this.client.session, + { + method: 'POST', + body: JSON.stringify(body), + headers: { + 'Content-Type': 'application/json', + }, }, - }, - ) -} + ) + } -/** - * Await this function when you want to make sure a member is accepted in the data union - * @param {EthereumAddress} memberAddress (optional, default is StreamrClient's auth: privateKey) - * @param {Number} pollingIntervalMs (optional, default: 1000) ask server if member is in - * @param {Number} retryTimeoutMs (optional, default: 60000) give up - * @return {Promise} resolves when member is in the data union (or fails with HTTP error) - */ -export async function hasJoined(memberAddress: Todo, options: Todo = {}) { - const { - pollingIntervalMs = 1000, - retryTimeoutMs = 60000, - } = options - const address = parseAddress(this, memberAddress) - const duSidechain = await getSidechainContractReadOnly(this, options) + /** + * Await this function when you want to make sure a member is accepted in the data union + * @param {EthereumAddress} memberAddress (optional, default is StreamrClient's auth: privateKey) + * @param {Number} pollingIntervalMs (optional, default: 1000) ask server if member is in + * @param {Number} retryTimeoutMs (optional, default: 60000) give up + * @return {Promise} resolves when member is in the data union (or fails with HTTP error) + */ + async hasJoined(memberAddress: Todo, options: Todo = {}) { + const { + pollingIntervalMs = 1000, + retryTimeoutMs = 60000, + } = options + const address = parseAddress(this.client, memberAddress) + const duSidechain = await getSidechainContractReadOnly(this.client, options) + + // memberData[0] is enum ActiveStatus {None, Active, Inactive}, and zero means member has never joined + await until(async () => (await duSidechain.memberData(address))[0] !== 0, retryTimeoutMs, pollingIntervalMs) + } - // memberData[0] is enum ActiveStatus {None, Active, Inactive}, and zero means member has never joined - await until(async () => (await duSidechain.memberData(address))[0] !== 0, retryTimeoutMs, pollingIntervalMs) -} + // TODO: this needs more thought: probably something like getEvents from sidechain? Heavy on RPC? + async getMembers(options: Todo) { + const duSidechain = await getSidechainContractReadOnly(this.client, options) + throw new Error(`Not implemented for side-chain data union (at ${duSidechain.address})`) + // event MemberJoined(address indexed); + // event MemberParted(address indexed); + } -// TODO: this needs more thought: probably something like getEvents from sidechain? Heavy on RPC? -export async function getMembers(options: Todo) { - const duSidechain = await getSidechainContractReadOnly(this, options) - throw new Error(`Not implemented for side-chain data union (at ${duSidechain.address})`) - // event MemberJoined(address indexed); - // event MemberParted(address indexed); -} + async getDataUnionStats(options: Todo) { + const duSidechain = await getSidechainContractReadOnly(this.client, options) + const [ + totalEarnings, + totalEarningsWithdrawn, + activeMemberCount, + inactiveMemberCount, + lifetimeMemberEarnings, + joinPartAgentCount, + ] = await duSidechain.getStats() + const totalWithdrawable = totalEarnings.sub(totalEarningsWithdrawn) + return { + activeMemberCount, + inactiveMemberCount, + joinPartAgentCount, + totalEarnings, + totalWithdrawable, + lifetimeMemberEarnings, + } + } -export async function getDataUnionStats(options: Todo) { - const duSidechain = await getSidechainContractReadOnly(this, options) - const [ - totalEarnings, - totalEarningsWithdrawn, - activeMemberCount, - inactiveMemberCount, - lifetimeMemberEarnings, - joinPartAgentCount, - ] = await duSidechain.getStats() - const totalWithdrawable = totalEarnings.sub(totalEarningsWithdrawn) - return { - activeMemberCount, - inactiveMemberCount, - joinPartAgentCount, - totalEarnings, - totalWithdrawable, - lifetimeMemberEarnings, + /** + * Get stats of a single data union member + * @param {EthereumAddress} dataUnion to query + * @param {EthereumAddress} memberAddress (optional) if not supplied, get the stats of currently logged in StreamrClient (if auth: privateKey) + */ + async getMemberStats(memberAddress: Todo, options: Todo) { + const address = parseAddress(this.client, memberAddress) + // TODO: use duSidechain.getMemberStats(address) once it's implemented, to ensure atomic read + // (so that memberData is from same block as getEarnings, otherwise withdrawable will be foobar) + const duSidechain = await getSidechainContractReadOnly(this.client, options) + const mdata = await duSidechain.memberData(address) + const total = await duSidechain.getEarnings(address).catch(() => 0) + const withdrawnEarnings = mdata[3].toString() + const withdrawable = total ? total.sub(withdrawnEarnings) : 0 + return { + status: ['unknown', 'active', 'inactive', 'blocked'][mdata[0]], + earningsBeforeLastJoin: mdata[1].toString(), + lmeAtJoin: mdata[2].toString(), + totalEarnings: total.toString(), + withdrawableEarnings: withdrawable.toString(), + } } -} -/** - * Get stats of a single data union member - * @param {EthereumAddress} dataUnion to query - * @param {EthereumAddress} memberAddress (optional) if not supplied, get the stats of currently logged in StreamrClient (if auth: privateKey) - */ -export async function getMemberStats(memberAddress: Todo, options: Todo) { - const address = parseAddress(this, memberAddress) - // TODO: use duSidechain.getMemberStats(address) once it's implemented, to ensure atomic read - // (so that memberData is from same block as getEarnings, otherwise withdrawable will be foobar) - const duSidechain = await getSidechainContractReadOnly(this, options) - const mdata = await duSidechain.memberData(address) - const total = await duSidechain.getEarnings(address).catch(() => 0) - const withdrawnEarnings = mdata[3].toString() - const withdrawable = total ? total.sub(withdrawnEarnings) : 0 - return { - status: ['unknown', 'active', 'inactive', 'blocked'][mdata[0]], - earningsBeforeLastJoin: mdata[1].toString(), - lmeAtJoin: mdata[2].toString(), - totalEarnings: total.toString(), - withdrawableEarnings: withdrawable.toString(), + /** + * Get the amount of tokens the member would get from a successful withdraw + * @param dataUnion to query + * @param memberAddress whose balance is returned + * @return {Promise} + */ + async getMemberBalance(memberAddress: Todo, options: Todo) { + const address = parseAddress(this.client, memberAddress) + const duSidechain = await getSidechainContractReadOnly(this.client, options) + return duSidechain.getWithdrawableEarnings(address) } -} -/** - * Get the amount of tokens the member would get from a successful withdraw - * @param dataUnion to query - * @param memberAddress whose balance is returned - * @return {Promise} - */ -export async function getMemberBalance(memberAddress: Todo, options: Todo) { - const address = parseAddress(this, memberAddress) - const duSidechain = await getSidechainContractReadOnly(this, options) - return duSidechain.getWithdrawableEarnings(address) -} + /** + * Get token balance for given address + * @param {EthereumAddress} address + * @param options such as tokenAddress. If not given, then first check if + * dataUnion was given in StreamrClient constructor, then check if tokenAddress + * was given in StreamrClient constructor. + * @returns {Promise} token balance in "wei" (10^-18 parts) + */ + async getTokenBalance(address: Todo, options: Todo) { + const a = parseAddress(this.client, address) + const tokenAddressMainnet = options.tokenAddress || ( + await getMainnetContractReadOnly(this.client, options).then((c: Todo) => c.token()).catch(() => null) || this.client.options.tokenAddress + ) + if (!tokenAddressMainnet) { throw new Error('tokenAddress option not found') } + const provider = this.client.ethereum.getMainnetProvider() + const token = new Contract(tokenAddressMainnet, [{ + name: 'balanceOf', + inputs: [{ type: 'address' }], + outputs: [{ type: 'uint256' }], + constant: true, + payable: false, + stateMutability: 'view', + type: 'function' + }], provider) + return token.balanceOf(a) + } -/** - * Get token balance for given address - * @param {EthereumAddress} address - * @param options such as tokenAddress. If not given, then first check if - * dataUnion was given in StreamrClient constructor, then check if tokenAddress - * was given in StreamrClient constructor. - * @returns {Promise} token balance in "wei" (10^-18 parts) - */ -export async function getTokenBalance(address: Todo, options: Todo) { - const a = parseAddress(this, address) - const tokenAddressMainnet = options.tokenAddress || ( - await getMainnetContractReadOnly(this, options).then((c: Todo) => c.token()).catch(() => null) || this.options.tokenAddress - ) - if (!tokenAddressMainnet) { throw new Error('tokenAddress option not found') } - const provider = this.ethereum.getMainnetProvider() - const token = new Contract(tokenAddressMainnet, [{ - name: 'balanceOf', - inputs: [{ type: 'address' }], - outputs: [{ type: 'uint256' }], - constant: true, - payable: false, - stateMutability: 'view', - type: 'function' - }], provider) - return token.balanceOf(a) -} + /** + * Figure out if given mainnet address is old DataUnion (v 1.0) or current 2.0 + * NOTE: Current version of streamr-client-javascript can only handle current version! + * @param {EthereumAddress} contractAddress + * @returns {number} 1 for old, 2 for current, zero for "not a data union" + */ + async getDataUnionVersion(contractAddress: Todo) { + const a = getAddress(contractAddress) // throws if bad address + const provider = this.client.ethereum.getMainnetProvider() + const du = new Contract(a, [{ + name: 'version', + inputs: [], + outputs: [{ type: 'uint256' }], + stateMutability: 'view', + type: 'function' + }], provider) + try { + const version = await du.version() + return +version + } catch (e) { + return 0 + } + } -/** - * Figure out if given mainnet address is old DataUnion (v 1.0) or current 2.0 - * NOTE: Current version of streamr-client-javascript can only handle current version! - * @param {EthereumAddress} contractAddress - * @returns {number} 1 for old, 2 for current, zero for "not a data union" - */ -export async function getDataUnionVersion(contractAddress: Todo) { - const a = getAddress(contractAddress) // throws if bad address - const provider = this.ethereum.getMainnetProvider() - const du = new Contract(a, [{ - name: 'version', - inputs: [], - outputs: [{ type: 'uint256' }], - stateMutability: 'view', - type: 'function' - }], provider) - try { - const version = await du.version() - return +version - } catch (e) { - return 0 + // ////////////////////////////////////////////////////////////////// + // member: WITHDRAW EARNINGS + // ////////////////////////////////////////////////////////////////// + + /** + * Withdraw all your earnings + * @param {EthereumOptions} options (including e.g. `dataUnion` Contract object or address) + * @returns {Promise} get receipt once withdraw is complete (tokens are seen in mainnet) + */ + async withdraw(options: Todo = {}) { + const tr = await untilWithdrawIsComplete( + this.client, + this.getWithdrawTx.bind(this), + this.getTokenBalance.bind(this, null), // null means this StreamrClient's auth credentials + { ...this.client.options, ...options } + ) + return tr } -} -// ////////////////////////////////////////////////////////////////// -// member: WITHDRAW EARNINGS -// ////////////////////////////////////////////////////////////////// + /** + * Get the tx promise for withdrawing all your earnings + * @param {EthereumOptions} options (including e.g. `dataUnion` Contract object or address) + * @returns {Promise} await on call .wait to actually send the tx + */ + async getWithdrawTx(options: Todo) { + const signer = await this.client.ethereum.getSidechainSigner() + // @ts-expect-error + const address = await signer.getAddress() + const duSidechain = await getSidechainContract(this.client, options) -/** - * Withdraw all your earnings - * @param {EthereumOptions} options (including e.g. `dataUnion` Contract object or address) - * @returns {Promise} get receipt once withdraw is complete (tokens are seen in mainnet) - */ -export async function withdraw(options: Todo = {}) { - const tr = await untilWithdrawIsComplete( - this, - this.getWithdrawTx.bind(this), - this.getTokenBalance.bind(this, null), // null means this StreamrClient's auth credentials - { ...this.options, ...options } - ) - return tr -} + const withdrawable = await duSidechain.getWithdrawableEarnings(address) + if (withdrawable.eq(0)) { + throw new Error(`${address} has nothing to withdraw in (sidechain) data union ${duSidechain.address}`) + } -/** - * Get the tx promise for withdrawing all your earnings - * @param {EthereumOptions} options (including e.g. `dataUnion` Contract object or address) - * @returns {Promise} await on call .wait to actually send the tx - */ -export async function getWithdrawTx(options: Todo) { - const signer = await this.ethereum.getSidechainSigner() - const address = await signer.getAddress() - const duSidechain = await getSidechainContract(this, options) - - const withdrawable = await duSidechain.getWithdrawableEarnings(address) - if (withdrawable.eq(0)) { - throw new Error(`${address} has nothing to withdraw in (sidechain) data union ${duSidechain.address}`) + if (this.client.options.minimumWithdrawTokenWei && withdrawable.lt(this.client.options.minimumWithdrawTokenWei)) { + throw new Error(`${address} has only ${withdrawable} to withdraw in ` + + `(sidechain) data union ${duSidechain.address} (min: ${this.client.options.minimumWithdrawTokenWei})`) + } + return duSidechain.withdrawAll(address, true) // sendToMainnet=true } - if (this.options.minimumWithdrawTokenWei && withdrawable.lt(this.options.minimumWithdrawTokenWei)) { - throw new Error(`${address} has only ${withdrawable} to withdraw in ` - + `(sidechain) data union ${duSidechain.address} (min: ${this.options.minimumWithdrawTokenWei})`) + /** + * Withdraw earnings and "donate" them to the given address + * @param {EthereumAddress} recipientAddress the address to receive the tokens + * @param {EthereumOptions} options (including e.g. `dataUnion` Contract object or address) + * @returns {Promise} get receipt once withdraw is complete (tokens are seen in mainnet) + */ + async withdrawTo(recipientAddress: Todo, options = {}) { + const to = getAddress(recipientAddress) // throws if bad address + const tr = await untilWithdrawIsComplete( + this.client, + this.getWithdrawTxTo.bind(this, to), + this.getTokenBalance.bind(this, to), + { ...this.client.options, ...options } + ) + return tr } - return duSidechain.withdrawAll(address, true) // sendToMainnet=true -} -/** - * Withdraw earnings and "donate" them to the given address - * @param {EthereumAddress} recipientAddress the address to receive the tokens - * @param {EthereumOptions} options (including e.g. `dataUnion` Contract object or address) - * @returns {Promise} get receipt once withdraw is complete (tokens are seen in mainnet) - */ -export async function withdrawTo(recipientAddress: Todo, options = {}) { - const to = getAddress(recipientAddress) // throws if bad address - const tr = await untilWithdrawIsComplete( - this, - this.getWithdrawTxTo.bind(this, to), - this.getTokenBalance.bind(this, to), - { ...this.options, ...options } - ) - return tr -} + /** + * Withdraw earnings and "donate" them to the given address + * @param {EthereumAddress} recipientAddress the address to receive the tokens + * @param {EthereumOptions} options (including e.g. `dataUnion` Contract object or address) + * @returns {Promise} await on call .wait to actually send the tx + */ + async getWithdrawTxTo(recipientAddress: Todo, options: Todo) { + const signer = await this.client.ethereum.getSidechainSigner() + // @ts-expect-error + const address = await signer.getAddress() + const duSidechain = await getSidechainContract(this.client, options) + const withdrawable = await duSidechain.getWithdrawableEarnings(address) + if (withdrawable.eq(0)) { + throw new Error(`${address} has nothing to withdraw in (sidechain) data union ${duSidechain.address}`) + } + return duSidechain.withdrawAllTo(recipientAddress, true) // sendToMainnet=true + } -/** - * Withdraw earnings and "donate" them to the given address - * @param {EthereumAddress} recipientAddress the address to receive the tokens - * @param {EthereumOptions} options (including e.g. `dataUnion` Contract object or address) - * @returns {Promise} await on call .wait to actually send the tx - */ -export async function getWithdrawTxTo(recipientAddress: Todo, options: Todo) { - const signer = await this.ethereum.getSidechainSigner() - const address = await signer.getAddress() - const duSidechain = await getSidechainContract(this, options) - const withdrawable = await duSidechain.getWithdrawableEarnings(address) - if (withdrawable.eq(0)) { - throw new Error(`${address} has nothing to withdraw in (sidechain) data union ${duSidechain.address}`) + /** + * Member can sign off to "donate" all earnings to another address such that someone else + * can submit the transaction (and pay for the gas) + * This signature is only valid until next withdrawal takes place (using this signature or otherwise). + * Note that while it's a "blank cheque" for withdrawing all earnings at the moment it's used, it's + * invalidated by the first withdraw after signing it. In other words, any signature can be invalidated + * by making a "normal" withdraw e.g. `await streamrClient.withdraw()` + * Admin can execute the withdraw using this signature: ``` + * await adminStreamrClient.withdrawToSigned(memberAddress, recipientAddress, signature) + * ``` + * @param {EthereumAddress} recipientAddress the address authorized to receive the tokens + * @param {EthereumOptions} options (including e.g. `dataUnion` Contract object or address) + * @returns {string} signature authorizing withdrawing all earnings to given recipientAddress + */ + async signWithdrawTo(recipientAddress: Todo, options: Todo) { + return this.signWithdrawAmountTo(recipientAddress, BigNumber.from(0), options) } - return duSidechain.withdrawAllTo(recipientAddress, true) // sendToMainnet=true -} -/** - * Member can sign off to "donate" all earnings to another address such that someone else - * can submit the transaction (and pay for the gas) - * This signature is only valid until next withdrawal takes place (using this signature or otherwise). - * Note that while it's a "blank cheque" for withdrawing all earnings at the moment it's used, it's - * invalidated by the first withdraw after signing it. In other words, any signature can be invalidated - * by making a "normal" withdraw e.g. `await streamrClient.withdraw()` - * Admin can execute the withdraw using this signature: ``` - * await adminStreamrClient.withdrawToSigned(memberAddress, recipientAddress, signature) - * ``` - * @param {EthereumAddress} recipientAddress the address authorized to receive the tokens - * @param {EthereumOptions} options (including e.g. `dataUnion` Contract object or address) - * @returns {string} signature authorizing withdrawing all earnings to given recipientAddress - */ -export async function signWithdrawTo(recipientAddress: Todo, options: Todo) { - return this.signWithdrawAmountTo(recipientAddress, BigNumber.from(0), options) + /** + * Member can sign off to "donate" specific amount of earnings to another address such that someone else + * can submit the transaction (and pay for the gas) + * This signature is only valid until next withdrawal takes place (using this signature or otherwise). + * @param {EthereumAddress} recipientAddress the address authorized to receive the tokens + * @param {BigNumber|number|string} amountTokenWei that the signature is for (can't be used for less or for more) + * @param {EthereumOptions} options (including e.g. `dataUnion` Contract object or address) + * @returns {string} signature authorizing withdrawing all earnings to given recipientAddress + */ + async signWithdrawAmountTo(recipientAddress: Todo, amountTokenWei: Todo, options: Todo) { + const to = getAddress(recipientAddress) // throws if bad address + const signer = this.client.ethereum.getSigner() // it shouldn't matter if it's mainnet or sidechain signer since key should be the same + // @ts-expect-error + const address = await signer.getAddress() + const duSidechain = await getSidechainContractReadOnly(this.client, options) + const memberData = await duSidechain.memberData(address) + if (memberData[0] === '0') { throw new Error(`${address} is not a member in Data Union (sidechain address ${duSidechain.address})`) } + const withdrawn = memberData[3] + const message = to + hexZeroPad(amountTokenWei, 32).slice(2) + duSidechain.address.slice(2) + hexZeroPad(withdrawn, 32).slice(2) + // @ts-expect-error + const signature = await signer.signMessage(arrayify(message)) + return signature + } } -/** - * Member can sign off to "donate" specific amount of earnings to another address such that someone else - * can submit the transaction (and pay for the gas) - * This signature is only valid until next withdrawal takes place (using this signature or otherwise). - * @param {EthereumAddress} recipientAddress the address authorized to receive the tokens - * @param {BigNumber|number|string} amountTokenWei that the signature is for (can't be used for less or for more) - * @param {EthereumOptions} options (including e.g. `dataUnion` Contract object or address) - * @returns {string} signature authorizing withdrawing all earnings to given recipientAddress - */ -export async function signWithdrawAmountTo(recipientAddress: Todo, amountTokenWei: Todo, options: Todo) { - const to = getAddress(recipientAddress) // throws if bad address - const signer = this.ethereum.getSigner() // it shouldn't matter if it's mainnet or sidechain signer since key should be the same - const address = await signer.getAddress() - const duSidechain = await getSidechainContractReadOnly(this, options) - const memberData = await duSidechain.memberData(address) - if (memberData[0] === '0') { throw new Error(`${address} is not a member in Data Union (sidechain address ${duSidechain.address})`) } - const withdrawn = memberData[3] - const message = to + hexZeroPad(amountTokenWei, 32).slice(2) + duSidechain.address.slice(2) + hexZeroPad(withdrawn, 32).slice(2) - const signature = await signer.signMessage(arrayify(message)) - return signature -} diff --git a/src/rest/LoginEndpoints.ts b/src/rest/LoginEndpoints.ts index 24f66ecc6..910c3b3d2 100644 --- a/src/rest/LoginEndpoints.ts +++ b/src/rest/LoginEndpoints.ts @@ -1,3 +1,4 @@ +import StreamrClient from '../StreamrClient' import { Todo } from '../types' import { getEndpointUrl } from '../utils' @@ -17,85 +18,94 @@ async function getSessionToken(url: Todo, props: Todo) { ) } -export async function getChallenge(address: Todo) { - this.debug('getChallenge %o', { - address, - }) - const url = getEndpointUrl(this.options.restUrl, 'login', 'challenge', address) - return authFetch( - url, - undefined, - { - method: 'POST', - }, - ) -} +export class LoginEndpoints { + + client: StreamrClient -export async function sendChallengeResponse(challenge: Todo, signature: Todo, address: Todo) { - this.debug('sendChallengeResponse %o', { - challenge, - signature, - address, - }) - const url = getEndpointUrl(this.options.restUrl, 'login', 'response') - const props = { - challenge, - signature, - address, + constructor(client: StreamrClient) { + this.client = client } - return getSessionToken(url, props) -} -export async function loginWithChallengeResponse(signingFunction: Todo, address: Todo) { - this.debug('loginWithChallengeResponse %o', { - address, - }) - const challenge = await this.getChallenge(address) - const signature = await signingFunction(challenge.challenge) - return this.sendChallengeResponse(challenge, signature, address) -} + async getChallenge(address: Todo) { + this.client.debug('getChallenge %o', { + address, + }) + const url = getEndpointUrl(this.client.options.restUrl, 'login', 'challenge', address) + return authFetch( + url, + undefined, + { + method: 'POST', + }, + ) + } -export async function loginWithApiKey(apiKey: Todo) { - this.debug('loginWithApiKey %o', { - apiKey, - }) - const url = getEndpointUrl(this.options.restUrl, 'login', 'apikey') - const props = { - apiKey, + async sendChallengeResponse(challenge: Todo, signature: Todo, address: Todo) { + this.client.debug('sendChallengeResponse %o', { + challenge, + signature, + address, + }) + const url = getEndpointUrl(this.client.options.restUrl, 'login', 'response') + const props = { + challenge, + signature, + address, + } + return getSessionToken(url, props) } - return getSessionToken(url, props) -} -export async function loginWithUsernamePassword(username: Todo, password: Todo) { - this.debug('loginWithUsernamePassword %o', { - username, - }) - const url = getEndpointUrl(this.options.restUrl, 'login', 'password') - const props = { - username, - password, + async loginWithChallengeResponse(signingFunction: Todo, address: Todo) { + this.client.debug('loginWithChallengeResponse %o', { + address, + }) + const challenge = await this.getChallenge(address) + const signature = await signingFunction(challenge.challenge) + return this.sendChallengeResponse(challenge, signature, address) } - try { - return await getSessionToken(url, props) - } catch (err) { - if (err && err.response && err.response.status === 404) { - // this 404s if running against new backend with username/password support removed - // wrap with appropriate error message - const message = 'username/password auth is no longer supported. Please create an ethereum identity.' - throw new AuthFetchError(message, err.response, err.body) + + async loginWithApiKey(apiKey: Todo) { + this.client.debug('loginWithApiKey %o', { + apiKey, + }) + const url = getEndpointUrl(this.client.options.restUrl, 'login', 'apikey') + const props = { + apiKey, } - throw err + return getSessionToken(url, props) } -} -export async function getUserInfo() { - this.debug('getUserInfo') - return authFetch(`${this.options.restUrl}/users/me`, this.session) -} + async loginWithUsernamePassword(username: Todo, password: Todo) { + this.client.debug('loginWithUsernamePassword %o', { + username, + }) + const url = getEndpointUrl(this.client.options.restUrl, 'login', 'password') + const props = { + username, + password, + } + try { + return await getSessionToken(url, props) + } catch (err) { + if (err && err.response && err.response.status === 404) { + // this 404s if running against new backend with username/password support removed + // wrap with appropriate error message + const message = 'username/password auth is no longer supported. Please create an ethereum identity.' + throw new AuthFetchError(message, err.response, err.body) + } + throw err + } + } + + async getUserInfo() { + this.client.debug('getUserInfo') + return authFetch(`${this.client.options.restUrl}/users/me`, this.client.session) + } -export async function logoutEndpoint() { - this.debug('logoutEndpoint') - return authFetch(`${this.options.restUrl}/logout`, this.session, { - method: 'POST', - }) + async logoutEndpoint() { + this.client.debug('logoutEndpoint') + return authFetch(`${this.client.options.restUrl}/logout`, this.client.session, { + method: 'POST', + }) + } } diff --git a/src/rest/StreamEndpoints.ts b/src/rest/StreamEndpoints.ts index c71320d8d..3b58d1f5f 100644 --- a/src/rest/StreamEndpoints.ts +++ b/src/rest/StreamEndpoints.ts @@ -12,6 +12,7 @@ import { isKeyExchangeStream } from '../stream/KeyExchange' import authFetch from './authFetch' import { Todo } from '../types' +import StreamrClient from '../StreamrClient' const debug = debugFactory('StreamrClient') @@ -37,203 +38,209 @@ function getKeepAliveAgentForUrl(url: string) { throw new Error(`Unknown protocol in URL: ${url}`) } -// These function are mixed in to StreamrClient.prototype. -// In the below functions, 'this' is intended to be the StreamrClient -export async function getStream(streamId: Todo) { - this.debug('getStream %o', { - streamId, - }) - - if (isKeyExchangeStream(streamId)) { - return new Stream(this, { - id: streamId, - partitions: 1, - }) - } +export class StreamEndpoints { - const url = getEndpointUrl(this.options.restUrl, 'streams', streamId) - try { - const json = await authFetch(url, this.session) - return new Stream(this, json) - } catch (e) { - if (e.response && e.response.status === 404) { - return undefined - } - throw e + client: StreamrClient + + constructor(client: StreamrClient) { + this.client = client } -} -export async function listStreams(query: Todo = {}) { - this.debug('listStreams %o', { - query, - }) - const url = getEndpointUrl(this.options.restUrl, 'streams') + '?' + qs.stringify(query) - const json = await authFetch(url, this.session) - return json ? json.map((stream) => new Stream(this, stream)) : [] -} + async getStream(streamId: Todo) { + this.client.debug('getStream %o', { + streamId, + }) -export async function getStreamByName(name: string) { - this.debug('getStreamByName %o', { - name, - }) - const json = await this.listStreams({ - name, - public: false, - }) - return json[0] ? new Stream(this, json[0]) : undefined -} + if (isKeyExchangeStream(streamId)) { + return new Stream(this.client, { + id: streamId, + partitions: 1, + }) + } -export async function createStream(props: Todo) { - this.debug('createStream %o', { - props, - }) - - const json = await authFetch( - getEndpointUrl(this.options.restUrl, 'streams'), - this.session, - { - method: 'POST', - body: JSON.stringify(props), - }, - ) - return json ? new Stream(this, json) : undefined -} + const url = getEndpointUrl(this.client.options.restUrl, 'streams', streamId) + try { + const json = await authFetch(url, this.client.session) + return new Stream(this.client, json) + } catch (e) { + if (e.response && e.response.status === 404) { + return undefined + } + throw e + } + } -export async function getOrCreateStream(props: Todo) { - this.debug('getOrCreateStream %o', { - props, - }) - let json + async listStreams(query: Todo = {}) { + this.client.debug('listStreams %o', { + query, + }) + const url = getEndpointUrl(this.client.options.restUrl, 'streams') + '?' + qs.stringify(query) + const json = await authFetch(url, this.client.session) + return json ? json.map((stream: any) => new Stream(this.client, stream)) : [] + } - // Try looking up the stream by id or name, whichever is defined - if (props.id) { - json = await this.getStream(props.id) - } else if (props.name) { - json = await this.getStreamByName(props.name) + async getStreamByName(name: string) { + this.client.debug('getStreamByName %o', { + name, + }) + const json = await this.listStreams({ + name, + public: false, + }) + return json[0] ? new Stream(this.client, json[0]) : undefined } - // If not found, try creating the stream - if (!json) { - json = await this.createStream(props) - debug('Created stream: %s (%s)', props.name, json.id) + async createStream(props: Todo) { + this.client.debug('createStream %o', { + props, + }) + + const json = await authFetch( + getEndpointUrl(this.client.options.restUrl, 'streams'), + this.client.session, + { + method: 'POST', + body: JSON.stringify(props), + }, + ) + return json ? new Stream(this.client, json) : undefined } - // If still nothing, throw - if (!json) { - throw new Error(`Unable to find or create stream: ${props.name || props.id}`) - } else { - return new Stream(this, json) + async getOrCreateStream(props: Todo) { + this.client.debug('getOrCreateStream %o', { + props, + }) + let json: any + + // Try looking up the stream by id or name, whichever is defined + if (props.id) { + json = await this.getStream(props.id) + } else if (props.name) { + json = await this.getStreamByName(props.name) + } + + // If not found, try creating the stream + if (!json) { + json = await this.createStream(props) + debug('Created stream: %s (%s)', props.name, json.id) + } + + // If still nothing, throw + if (!json) { + throw new Error(`Unable to find or create stream: ${props.name || props.id}`) + } else { + return new Stream(this.client, json) + } } -} -export async function getStreamPublishers(streamId: Todo) { - this.debug('getStreamPublishers %o', { - streamId, - }) - const url = getEndpointUrl(this.options.restUrl, 'streams', streamId, 'publishers') - const json = await authFetch(url, this.session) - return json.addresses.map((a: string) => a.toLowerCase()) -} + async getStreamPublishers(streamId: Todo) { + this.client.debug('getStreamPublishers %o', { + streamId, + }) + const url = getEndpointUrl(this.client.options.restUrl, 'streams', streamId, 'publishers') + const json = await authFetch(url, this.client.session) + return json.addresses.map((a: string) => a.toLowerCase()) + } -export async function isStreamPublisher(streamId: Todo, ethAddress: Todo) { - this.debug('isStreamPublisher %o', { - streamId, - ethAddress, - }) - const url = getEndpointUrl(this.options.restUrl, 'streams', streamId, 'publisher', ethAddress) - try { - await authFetch(url, this.session) - return true - } catch (e) { - this.debug(e) - if (e.response && e.response.status === 404) { - return false + async isStreamPublisher(streamId: Todo, ethAddress: Todo) { + this.client.debug('isStreamPublisher %o', { + streamId, + ethAddress, + }) + const url = getEndpointUrl(this.client.options.restUrl, 'streams', streamId, 'publisher', ethAddress) + try { + await authFetch(url, this.client.session) + return true + } catch (e) { + this.client.debug(e) + if (e.response && e.response.status === 404) { + return false + } + throw e } - throw e } -} -export async function getStreamSubscribers(streamId: Todo) { - this.debug('getStreamSubscribers %o', { - streamId, - }) - const url = getEndpointUrl(this.options.restUrl, 'streams', streamId, 'subscribers') - const json = await authFetch(url, this.session) - return json.addresses.map((a: Todo) => a.toLowerCase()) -} + async getStreamSubscribers(streamId: Todo) { + this.client.debug('getStreamSubscribers %o', { + streamId, + }) + const url = getEndpointUrl(this.client.options.restUrl, 'streams', streamId, 'subscribers') + const json = await authFetch(url, this.client.session) + return json.addresses.map((a: Todo) => a.toLowerCase()) + } -export async function isStreamSubscriber(streamId: Todo, ethAddress: Todo) { - this.debug('isStreamSubscriber %o', { - streamId, - ethAddress, - }) - const url = getEndpointUrl(this.options.restUrl, 'streams', streamId, 'subscriber', ethAddress) - try { - await authFetch(url, this.session) - return true - } catch (e) { - if (e.response && e.response.status === 404) { - return false + async isStreamSubscriber(streamId: Todo, ethAddress: Todo) { + this.client.debug('isStreamSubscriber %o', { + streamId, + ethAddress, + }) + const url = getEndpointUrl(this.client.options.restUrl, 'streams', streamId, 'subscriber', ethAddress) + try { + await authFetch(url, this.client.session) + return true + } catch (e) { + if (e.response && e.response.status === 404) { + return false + } + throw e } - throw e } -} -export async function getStreamValidationInfo(streamId: Todo) { - this.debug('getStreamValidationInfo %o', { - streamId, - }) - const url = getEndpointUrl(this.options.restUrl, 'streams', streamId, 'validation') - const json = await authFetch(url, this.session) - return json -} + async getStreamValidationInfo(streamId: Todo) { + this.client.debug('getStreamValidationInfo %o', { + streamId, + }) + const url = getEndpointUrl(this.client.options.restUrl, 'streams', streamId, 'validation') + const json = await authFetch(url, this.client.session) + return json + } + + async getStreamLast(streamObjectOrId: Todo) { + const { streamId, streamPartition = 0, count = 1 } = validateOptions(streamObjectOrId) + this.client.debug('getStreamLast %o', { + streamId, + streamPartition, + count, + }) + const query = { + count, + } -export async function getStreamLast(streamObjectOrId: Todo) { - const { streamId, streamPartition = 0, count = 1 } = validateOptions(streamObjectOrId) - this.debug('getStreamLast %o', { - streamId, - streamPartition, - count, - }) - const query = { - count, + const url = getEndpointUrl(this.client.options.restUrl, 'streams', streamId, 'data', 'partitions', streamPartition, 'last') + `?${qs.stringify(query)}` + const json = await authFetch(url, this.client.session) + return json } - const url = getEndpointUrl(this.options.restUrl, 'streams', streamId, 'data', 'partitions', streamPartition, 'last') + `?${qs.stringify(query)}` - const json = await authFetch(url, this.session) - return json -} + async getStreamPartsByStorageNode(address: Todo) { + const json = await authFetch(getEndpointUrl(this.client.options.restUrl, 'storageNodes', address, 'streams'), this.client.session) + let result: Todo = [] + json.forEach((stream: Todo) => { + result = result.concat(StreamPart.fromStream(stream)) + }) + return result + } -export async function getStreamPartsByStorageNode(address: Todo) { - const json = await authFetch(getEndpointUrl(this.options.restUrl, 'storageNodes', address, 'streams'), this.session) - let result: Todo = [] - json.forEach((stream: Todo) => { - result = result.concat(StreamPart.fromStream(stream)) - }) - return result -} + async publishHttp(streamObjectOrId: Todo, data: Todo, requestOptions: Todo = {}, keepAlive: Todo = true) { + let streamId + if (streamObjectOrId instanceof Stream) { + streamId = streamObjectOrId.id + } else { + streamId = streamObjectOrId + } + this.client.debug('publishHttp %o', { + streamId, data, + }) -export async function publishHttp(streamObjectOrId: Todo, data: Todo, requestOptions: Todo = {}, keepAlive: Todo = true) { - let streamId - if (streamObjectOrId instanceof Stream) { - // @ts-expect-error - streamId = streamObjectOrId.id - } else { - streamId = streamObjectOrId - } - this.debug('publishHttp %o', { - streamId, data, - }) - - // Send data to the stream - return authFetch( - getEndpointUrl(this.options.restUrl, 'streams', streamId, 'data'), - this.session, - { - ...requestOptions, - method: 'POST', - body: JSON.stringify(data), - agent: keepAlive ? getKeepAliveAgentForUrl(this.options.restUrl) : undefined, - }, - ) + // Send data to the stream + return authFetch( + getEndpointUrl(this.client.options.restUrl, 'streams', streamId, 'data'), + this.client.session, + { + ...requestOptions, + method: 'POST', + body: JSON.stringify(data), + agent: keepAlive ? getKeepAliveAgentForUrl(this.client.options.restUrl) : undefined, + }, + ) + } } diff --git a/src/stream/index.js b/src/stream/index.ts similarity index 84% rename from src/stream/index.js rename to src/stream/index.ts index cd1f1d658..37117a808 100644 --- a/src/stream/index.js +++ b/src/stream/index.ts @@ -2,9 +2,17 @@ import { getEndpointUrl } from '../utils' import authFetch from '../rest/authFetch' import StorageNode from './StorageNode' +import StreamrClient from '../StreamrClient' +import { Todo } from '../types' export default class Stream { - constructor(client, props) { + + // TODO add field definitions for all fields + // @ts-expect-error + id: string + _client: StreamrClient + + constructor(client: StreamrClient, props: Todo) { this._client = client Object.assign(this, props) } @@ -25,6 +33,7 @@ export default class Stream { const result = {} Object.keys(this).forEach((key) => { if (!key.startsWith('_')) { + // @ts-expect-error result[key] = this[key] } }) @@ -55,13 +64,13 @@ export default class Stream { ) } - async hasPermission(operation, userId) { + async hasPermission(operation: Todo, userId: Todo) { // eth addresses may be in checksumcase, but userId from server has no case const userIdCaseInsensitive = typeof userId === 'string' ? userId.toLowerCase() : undefined // if not string then undefined const permissions = await this.getPermissions() - return permissions.find((p) => { + return permissions.find((p: Todo) => { if (p.operation !== operation) { return false } if (userIdCaseInsensitive === undefined) { @@ -71,8 +80,8 @@ export default class Stream { }) } - async grantPermission(operation, userId) { - const permissionObject = { + async grantPermission(operation: Todo, userId: Todo) { + const permissionObject: Todo = { operation, } @@ -94,7 +103,7 @@ export default class Stream { ) } - async revokePermission(permissionId) { + async revokePermission(permissionId: Todo) { return authFetch( getEndpointUrl(this._client.options.restUrl, 'streams', this.id, 'permissions', permissionId), this._client.session, @@ -111,7 +120,7 @@ export default class Stream { ) } - async addToStorageNode(address) { + async addToStorageNode(address: Todo) { return authFetch( getEndpointUrl(this._client.options.restUrl, 'streams', this.id, 'storageNodes'), this._client.session, @@ -124,7 +133,7 @@ export default class Stream { ) } - async removeFromStorageNode(address) { + async removeFromStorageNode(address: Todo) { return authFetch( getEndpointUrl(this._client.options.restUrl, 'streams', this.id, 'storageNodes', address), this._client.session, @@ -139,10 +148,10 @@ export default class Stream { getEndpointUrl(this._client.options.restUrl, 'streams', this.id, 'storageNodes'), this._client.session, ) - return json.map((item) => new StorageNode(item.storageNodeAddress)) + return json.map((item: Todo) => new StorageNode(item.storageNodeAddress)) } - async publish(...theArgs) { + async publish(...theArgs: Todo) { return this._client.publish(this.id, ...theArgs) } } diff --git a/test/flakey/EnvStressTest.test.js b/test/flakey/EnvStressTest.test.js index f8e5d54f8..d1da5f4d9 100644 --- a/test/flakey/EnvStressTest.test.js +++ b/test/flakey/EnvStressTest.test.js @@ -1,5 +1,5 @@ import { pTimeout } from '../../src/utils' -import StreamrClient from '../../src' +import StreamrClient from '../../src/StreamrClient' import { fakePrivateKey, uid } from '../utils' import config from '../integration/config' diff --git a/test/integration/DataUnionEndpoints/DataUnionEndpoints.test.js b/test/integration/DataUnionEndpoints/DataUnionEndpoints.test.js index 1a6f7e504..0a5ab0a21 100644 --- a/test/integration/DataUnionEndpoints/DataUnionEndpoints.test.js +++ b/test/integration/DataUnionEndpoints/DataUnionEndpoints.test.js @@ -5,7 +5,7 @@ import debug from 'debug' import { getEndpointUrl } from '../../../src/utils' import authFetch from '../../../src/rest/authFetch' -import StreamrClient from '../../../src' +import StreamrClient from '../../../src/StreamrClient' import * as Token from '../../../contracts/TestToken.json' import config from '../config' diff --git a/test/integration/DataUnionEndpoints/adminWithdrawMember.test.js b/test/integration/DataUnionEndpoints/adminWithdrawMember.test.js index bc29b2967..f220d6f04 100644 --- a/test/integration/DataUnionEndpoints/adminWithdrawMember.test.js +++ b/test/integration/DataUnionEndpoints/adminWithdrawMember.test.js @@ -3,7 +3,7 @@ import { formatEther, parseEther } from 'ethers/lib/utils' import debug from 'debug' import { getEndpointUrl, until } from '../../../src/utils' -import StreamrClient from '../../../src' +import StreamrClient from '../../../src/StreamrClient' import * as Token from '../../../contracts/TestToken.json' import * as DataUnionSidechain from '../../../contracts/DataUnionSidechain.json' import config from '../config' diff --git a/test/integration/DataUnionEndpoints/adminWithdrawSigned.test.js b/test/integration/DataUnionEndpoints/adminWithdrawSigned.test.js index 9d018e4de..dcb58630d 100644 --- a/test/integration/DataUnionEndpoints/adminWithdrawSigned.test.js +++ b/test/integration/DataUnionEndpoints/adminWithdrawSigned.test.js @@ -3,7 +3,7 @@ import { formatEther, parseEther } from 'ethers/lib/utils' import debug from 'debug' import { getEndpointUrl, until } from '../../../src/utils' -import StreamrClient from '../../../src' +import StreamrClient from '../../../src/StreamrClient' import * as Token from '../../../contracts/TestToken.json' import * as DataUnionSidechain from '../../../contracts/DataUnionSidechain.json' import config from '../config' diff --git a/test/integration/DataUnionEndpoints/calculate.test.js b/test/integration/DataUnionEndpoints/calculate.test.js index 0fcc5e96d..e7d92ca4e 100644 --- a/test/integration/DataUnionEndpoints/calculate.test.js +++ b/test/integration/DataUnionEndpoints/calculate.test.js @@ -1,7 +1,7 @@ import { providers, Wallet } from 'ethers' import debug from 'debug' -import StreamrClient from '../../../src' +import StreamrClient from '../../../src/StreamrClient' import config from '../config' const log = debug('StreamrClient::DataUnionEndpoints::integration-test-calculate') diff --git a/test/integration/DataUnionEndpoints/withdraw.test.js b/test/integration/DataUnionEndpoints/withdraw.test.js index cf7b28624..08d4a0e8d 100644 --- a/test/integration/DataUnionEndpoints/withdraw.test.js +++ b/test/integration/DataUnionEndpoints/withdraw.test.js @@ -3,7 +3,7 @@ import { formatEther, parseEther } from 'ethers/lib/utils' import debug from 'debug' import { getEndpointUrl, until } from '../../../src/utils' -import StreamrClient from '../../../src' +import StreamrClient from '../../../src/StreamrClient' import * as Token from '../../../contracts/TestToken.json' import * as DataUnionSidechain from '../../../contracts/DataUnionSidechain.json' import config from '../config' diff --git a/test/integration/DataUnionEndpoints/withdrawTo.test.js b/test/integration/DataUnionEndpoints/withdrawTo.test.js index bfabe7fed..b7624c9e0 100644 --- a/test/integration/DataUnionEndpoints/withdrawTo.test.js +++ b/test/integration/DataUnionEndpoints/withdrawTo.test.js @@ -3,7 +3,7 @@ import { formatEther, parseEther } from 'ethers/lib/utils' import debug from 'debug' import { getEndpointUrl, until } from '../../../src/utils' -import StreamrClient from '../../../src' +import StreamrClient from '../../../src/StreamrClient' import * as Token from '../../../contracts/TestToken.json' import * as DataUnionSidechain from '../../../contracts/DataUnionSidechain.json' import config from '../config' diff --git a/test/integration/Encryption.test.js b/test/integration/Encryption.test.js index bd0d08341..3ac930bd1 100644 --- a/test/integration/Encryption.test.js +++ b/test/integration/Encryption.test.js @@ -3,7 +3,7 @@ import { MessageLayer } from 'streamr-client-protocol' import { fakePrivateKey, uid, Msg, getPublishTestMessages } from '../utils' import { Defer } from '../../src/utils' -import StreamrClient from '../../src' +import StreamrClient from '../../src/StreamrClient' import { GroupKey } from '../../src/stream/Encryption' import Connection from '../../src/Connection' diff --git a/test/integration/GapFill.test.js b/test/integration/GapFill.test.js index c6aee66d4..af2b7e7ec 100644 --- a/test/integration/GapFill.test.js +++ b/test/integration/GapFill.test.js @@ -1,7 +1,7 @@ import { wait } from 'streamr-test-utils' import { uid, fakePrivateKey, describeRepeats, getPublishTestMessages } from '../utils' -import StreamrClient from '../../src' +import StreamrClient from '../../src/StreamrClient' import Connection from '../../src/Connection' import config from './config' diff --git a/test/integration/LoginEndpoints.test.js b/test/integration/LoginEndpoints.test.js index 7ceb8213a..9ba86f428 100644 --- a/test/integration/LoginEndpoints.test.js +++ b/test/integration/LoginEndpoints.test.js @@ -2,7 +2,7 @@ import assert from 'assert' import { ethers } from 'ethers' -import StreamrClient from '../../src' +import StreamrClient from '../../src/StreamrClient' import config from './config' diff --git a/test/integration/MultipleClients.test.js b/test/integration/MultipleClients.test.js index 62436a04a..56bce9cad 100644 --- a/test/integration/MultipleClients.test.js +++ b/test/integration/MultipleClients.test.js @@ -1,7 +1,7 @@ import { wait } from 'streamr-test-utils' import { describeRepeats, uid, fakePrivateKey, getWaitForStorage, getPublishTestMessages, addAfterFn } from '../utils' -import StreamrClient from '../../src' +import StreamrClient from '../../src/StreamrClient' import { counterId } from '../../src/utils' import Connection from '../../src/Connection' diff --git a/test/integration/ResendReconnect.test.js b/test/integration/ResendReconnect.test.js index 22031a99c..dc650c20e 100644 --- a/test/integration/ResendReconnect.test.js +++ b/test/integration/ResendReconnect.test.js @@ -1,7 +1,7 @@ import { wait, waitForCondition } from 'streamr-test-utils' import { uid, fakePrivateKey, getPublishTestMessages } from '../utils' -import StreamrClient from '../../src' +import StreamrClient from '../../src/StreamrClient' import { Defer } from '../../src/utils' import config from './config' diff --git a/test/integration/Resends.test.js b/test/integration/Resends.test.js index 6c9914d5b..611dbcf25 100644 --- a/test/integration/Resends.test.js +++ b/test/integration/Resends.test.js @@ -1,7 +1,7 @@ import { wait, waitForCondition, waitForEvent } from 'streamr-test-utils' import { uid, describeRepeats, fakePrivateKey, getPublishTestMessages } from '../utils' -import StreamrClient from '../../src' +import StreamrClient from '../../src/StreamrClient' import { Defer, pTimeout } from '../../src/utils' import Connection from '../../src/Connection' diff --git a/test/integration/Sequencing.test.js b/test/integration/Sequencing.test.js index 4c5800d16..b75720d34 100644 --- a/test/integration/Sequencing.test.js +++ b/test/integration/Sequencing.test.js @@ -1,7 +1,7 @@ import { wait, waitForCondition, waitForEvent } from 'streamr-test-utils' import { uid, fakePrivateKey, getWaitForStorage } from '../utils' -import StreamrClient from '../../src' +import StreamrClient from '../../src/StreamrClient' import Connection from '../../src/Connection' import config from './config' diff --git a/test/integration/Session.test.js b/test/integration/Session.test.js index d20264505..90c9b0bf7 100644 --- a/test/integration/Session.test.js +++ b/test/integration/Session.test.js @@ -1,4 +1,4 @@ -import StreamrClient from '../../src' +import StreamrClient from '../../src/StreamrClient' import { fakePrivateKey } from '../utils' import config from './config' diff --git a/test/integration/StreamConnectionState.test.js b/test/integration/StreamConnectionState.test.js index 48c324a65..4c137da74 100644 --- a/test/integration/StreamConnectionState.test.js +++ b/test/integration/StreamConnectionState.test.js @@ -2,7 +2,7 @@ import { wait } from 'streamr-test-utils' import { ControlLayer } from 'streamr-client-protocol' import { uid, fakePrivateKey, describeRepeats, getPublishTestMessages } from '../utils' -import StreamrClient from '../../src' +import StreamrClient from '../../src/StreamrClient' import { Defer, pLimitFn } from '../../src/utils' import Connection from '../../src/Connection' diff --git a/test/integration/StreamEndpoints.test.js b/test/integration/StreamEndpoints.test.js index e1be6c464..e72aae728 100644 --- a/test/integration/StreamEndpoints.test.js +++ b/test/integration/StreamEndpoints.test.js @@ -1,7 +1,7 @@ import { ethers } from 'ethers' import { wait } from 'streamr-test-utils' -import StreamrClient from '../../src' +import StreamrClient from '../../src/StreamrClient' import { uid } from '../utils' import config from './config' diff --git a/test/integration/StreamrClient.test.js b/test/integration/StreamrClient.test.js index edd1b81b4..407977ad7 100644 --- a/test/integration/StreamrClient.test.js +++ b/test/integration/StreamrClient.test.js @@ -6,7 +6,7 @@ import { ControlLayer, MessageLayer } from 'streamr-client-protocol' import { wait, waitForEvent } from 'streamr-test-utils' import { describeRepeats, uid, fakePrivateKey, getWaitForStorage, getPublishTestMessages, Msg } from '../utils' -import StreamrClient from '../../src' +import StreamrClient from '../../src/StreamrClient' import { Defer, pLimitFn } from '../../src/utils' import Connection from '../../src/Connection' diff --git a/test/integration/Subscriber.test.js b/test/integration/Subscriber.test.js index fdc7ac1ea..379390e51 100644 --- a/test/integration/Subscriber.test.js +++ b/test/integration/Subscriber.test.js @@ -2,7 +2,7 @@ import { ControlLayer } from 'streamr-client-protocol' import { wait } from 'streamr-test-utils' import { uid, fakePrivateKey, describeRepeats, getPublishTestMessages, collect } from '../utils' -import StreamrClient from '../../src' +import StreamrClient from '../../src/StreamrClient' import { Defer } from '../../src/utils' import Connection from '../../src/Connection' diff --git a/test/integration/SubscriberResends.test.js b/test/integration/SubscriberResends.test.js index acda54340..806ba8058 100644 --- a/test/integration/SubscriberResends.test.js +++ b/test/integration/SubscriberResends.test.js @@ -2,7 +2,7 @@ import { ControlLayer } from 'streamr-client-protocol' import { wait } from 'streamr-test-utils' import { Msg, uid, collect, describeRepeats, fakePrivateKey, getWaitForStorage, getPublishTestMessages } from '../utils' -import StreamrClient from '../../src' +import StreamrClient from '../../src/StreamrClient' import Connection from '../../src/Connection' import { Defer } from '../../src/utils' diff --git a/test/integration/Subscription.test.js b/test/integration/Subscription.test.js index 7f1ee0312..b15dfadca 100644 --- a/test/integration/Subscription.test.js +++ b/test/integration/Subscription.test.js @@ -1,7 +1,7 @@ import { wait, waitForEvent } from 'streamr-test-utils' import { uid, fakePrivateKey } from '../utils' -import StreamrClient from '../../src' +import StreamrClient from '../../src/StreamrClient' import config from './config' diff --git a/test/integration/Validation.test.js b/test/integration/Validation.test.js index 6196ee235..54e072497 100644 --- a/test/integration/Validation.test.js +++ b/test/integration/Validation.test.js @@ -1,7 +1,7 @@ import { wait } from 'streamr-test-utils' import { uid, fakePrivateKey, describeRepeats, getPublishTestMessages } from '../utils' -import StreamrClient from '../../src' +import StreamrClient from '../../src/StreamrClient' import Connection from '../../src/Connection' import config from './config' diff --git a/test/integration/authFetch.test.js b/test/integration/authFetch.test.js index 1c8749049..bfef4d431 100644 --- a/test/integration/authFetch.test.js +++ b/test/integration/authFetch.test.js @@ -2,7 +2,7 @@ jest.mock('node-fetch') import fetch from 'node-fetch' -import StreamrClient from '../../src' +import StreamrClient from '../../src/StreamrClient' import { fakePrivateKey } from '../utils' import config from './config' diff --git a/test/unit/Session.test.js b/test/unit/Session.test.js index 9610f5797..52309eba4 100644 --- a/test/unit/Session.test.js +++ b/test/unit/Session.test.js @@ -1,6 +1,6 @@ import sinon from 'sinon' -import StreamrClient from '../../src' +import StreamrClient from '../../src/StreamrClient' import { Defer } from '../../src/utils' import Session from '../../src/Session' import config from '../integration/config' diff --git a/webpack.config.js b/webpack.config.js index 256000d2d..52b22578e 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -25,7 +25,7 @@ module.exports = (env, argv) => { const commonConfig = { mode: isProduction ? 'production' : 'development', - entry: path.join(__dirname, 'src', 'index.ts'), + entry: path.join(__dirname, 'src', 'StreamrClient.ts'), devtool: 'source-map', output: { path: path.join(__dirname, 'dist'), From 04ef6e8cc906f24652b5b4b03325cdaf972160f6 Mon Sep 17 00:00:00 2001 From: Teo Gebhard Date: Tue, 16 Feb 2021 14:42:01 +0200 Subject: [PATCH 07/12] Use explicit parameters in untilWithdrawIsComplete --- src/rest/DataUnionEndpoints.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/rest/DataUnionEndpoints.ts b/src/rest/DataUnionEndpoints.ts index 7daaf158d..e31978c09 100644 --- a/src/rest/DataUnionEndpoints.ts +++ b/src/rest/DataUnionEndpoints.ts @@ -419,7 +419,7 @@ async function transportSignatures(client: StreamrClient, messageHash: Todo, opt // template for withdraw functions // client could be replaced with AMB (mainnet and sidechain) -async function untilWithdrawIsComplete(client: StreamrClient, getWithdrawTxFunc: Todo, getBalanceFunc: Todo, options: Todo = {}) { +async function untilWithdrawIsComplete(client: StreamrClient, getWithdrawTxFunc: (options: Todo) => Todo, getBalanceFunc: (options: Todo) => Todo, options: Todo = {}) { const { pollingIntervalMs = 1000, retryTimeoutMs = 60000, @@ -744,8 +744,8 @@ export class DataUnionEndpoints { const address = getAddress(memberAddress) // throws if bad address const tr = await untilWithdrawIsComplete( this.client, - this.getWithdrawMemberTx.bind(this, address), - this.getTokenBalance.bind(this, address), + (opts) => this.getWithdrawMemberTx(address, opts), + (opts) => this.getTokenBalance(address, opts), { ...this.client.options, ...options } ) return tr @@ -778,8 +778,8 @@ export class DataUnionEndpoints { const to = getAddress(recipientAddress) const tr = await untilWithdrawIsComplete( this.client, - this.getWithdrawToSignedTx.bind(this, from, to, signature), - this.getTokenBalance.bind(this, to), + (opts) => this.getWithdrawToSignedTx(from, to, signature, opts), + (opts) => this.getTokenBalance(to, opts), { ...this.client.options, ...options } ) return tr @@ -1015,8 +1015,8 @@ export class DataUnionEndpoints { async withdraw(options: Todo = {}) { const tr = await untilWithdrawIsComplete( this.client, - this.getWithdrawTx.bind(this), - this.getTokenBalance.bind(this, null), // null means this StreamrClient's auth credentials + (opts) => this.getWithdrawTx(opts), + (opts) => this.getTokenBalance(null, opts), // null means this StreamrClient's auth credentials { ...this.client.options, ...options } ) return tr @@ -1055,8 +1055,8 @@ export class DataUnionEndpoints { const to = getAddress(recipientAddress) // throws if bad address const tr = await untilWithdrawIsComplete( this.client, - this.getWithdrawTxTo.bind(this, to), - this.getTokenBalance.bind(this, to), + (opts) => this.getWithdrawTxTo(to, opts), + (opts) => this.getTokenBalance(to, opts), { ...this.client.options, ...options } ) return tr From 4f9cdf5bf6341c7305c776474b45c28f7449c54b Mon Sep 17 00:00:00 2001 From: Teo Gebhard Date: Tue, 16 Feb 2021 14:42:07 +0200 Subject: [PATCH 08/12] Run DataUnion tests in band: concurrent run causes "Transaction with the same hash was already imported" error --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 417f496b5..ad5edd75e 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "test-integration": "jest --forceExit test/integration", "test-integration-no-resend": "jest --testTimeout=10000 --testPathIgnorePatterns='resend|Resend' --testNamePattern='^((?!(resend|Resend|resent|Resent)).)*$' test/integration/*.test.js", "test-integration-resend": "jest --testTimeout=15000 --testNamePattern='(resend|Resend|resent|Resent)' test/integration/*.test.js", - "test-integration-dataunions": "jest --testTimeout=15000 test/integration/DataUnionEndpoints", + "test-integration-dataunions": "jest --testTimeout=15000 --runInBand test/integration/DataUnionEndpoints", "test-flakey": "jest --forceExit test/flakey/*", "test-browser": "node ./test/browser/server.js & node node_modules/nightwatch/bin/nightwatch ./test/browser/browser.js && pkill -f server.js", "install-example": "cd examples/webpack && npm ci", From afdd50d2fc2d1774f734eb7593e5e1eb9a8b4f3c Mon Sep 17 00:00:00 2001 From: Teo Gebhard Date: Tue, 16 Feb 2021 14:42:54 +0200 Subject: [PATCH 09/12] TypeScript support to ESLint --- .eslintrc.js | 11 ++- package-lock.json | 192 +++++++++++++++++++++++++++++++++++++--------- package.json | 7 +- 3 files changed, 168 insertions(+), 42 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 18ba0b22b..543def847 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,5 +1,8 @@ module.exports = { - parser: 'babel-eslint', + parser: '@typescript-eslint/parser', + plugins: [ + '@typescript-eslint' + ], extends: [ 'streamr-nodejs' ], @@ -35,7 +38,11 @@ module.exports = { 'lines-between-class-members': 'off', 'padded-blocks': 'off', 'no-use-before-define': 'off', - 'import/order': 'off' + 'import/order': 'off', + 'no-shadow': 'off', + '@typescript-eslint/no-shadow': 'error', + 'no-unused-vars': 'off', + '@typescript-eslint/no-unused-vars': ['error'] }, settings: { 'import/resolver': { diff --git a/package-lock.json b/package-lock.json index afacd097c..59d71601e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2599,6 +2599,111 @@ "@types/node": "*" } }, + "@typescript-eslint/eslint-plugin": { + "version": "4.15.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.15.1.tgz", + "integrity": "sha512-yW2epMYZSpNJXZy22Biu+fLdTG8Mn6b22kR3TqblVk50HGNV8Zya15WAXuQCr8tKw4Qf1BL4QtI6kv6PCkLoJw==", + "dev": true, + "requires": { + "@typescript-eslint/experimental-utils": "4.15.1", + "@typescript-eslint/scope-manager": "4.15.1", + "debug": "^4.1.1", + "functional-red-black-tree": "^1.0.1", + "lodash": "^4.17.15", + "regexpp": "^3.0.0", + "semver": "^7.3.2", + "tsutils": "^3.17.1" + }, + "dependencies": { + "semver": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", + "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "@typescript-eslint/experimental-utils": { + "version": "4.15.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.15.1.tgz", + "integrity": "sha512-9LQRmOzBRI1iOdJorr4jEnQhadxK4c9R2aEAsm7WE/7dq8wkKD1suaV0S/JucTL8QlYUPU1y2yjqg+aGC0IQBQ==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.3", + "@typescript-eslint/scope-manager": "4.15.1", + "@typescript-eslint/types": "4.15.1", + "@typescript-eslint/typescript-estree": "4.15.1", + "eslint-scope": "^5.0.0", + "eslint-utils": "^2.0.0" + } + }, + "@typescript-eslint/parser": { + "version": "4.15.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.15.1.tgz", + "integrity": "sha512-V8eXYxNJ9QmXi5ETDguB7O9diAXlIyS+e3xzLoP/oVE4WCAjssxLIa0mqCLsCGXulYJUfT+GV70Jv1vHsdKwtA==", + "dev": true, + "requires": { + "@typescript-eslint/scope-manager": "4.15.1", + "@typescript-eslint/types": "4.15.1", + "@typescript-eslint/typescript-estree": "4.15.1", + "debug": "^4.1.1" + } + }, + "@typescript-eslint/scope-manager": { + "version": "4.15.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.15.1.tgz", + "integrity": "sha512-ibQrTFcAm7yG4C1iwpIYK7vDnFg+fKaZVfvyOm3sNsGAerKfwPVFtYft5EbjzByDJ4dj1WD8/34REJfw/9wdVA==", + "dev": true, + "requires": { + "@typescript-eslint/types": "4.15.1", + "@typescript-eslint/visitor-keys": "4.15.1" + } + }, + "@typescript-eslint/types": { + "version": "4.15.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.15.1.tgz", + "integrity": "sha512-iGsaUyWFyLz0mHfXhX4zO6P7O3sExQpBJ2dgXB0G5g/8PRVfBBsmQIc3r83ranEQTALLR3Vko/fnCIVqmH+mPw==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "4.15.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.15.1.tgz", + "integrity": "sha512-z8MN3CicTEumrWAEB2e2CcoZa3KP9+SMYLIA2aM49XW3cWIaiVSOAGq30ffR5XHxRirqE90fgLw3e6WmNx5uNw==", + "dev": true, + "requires": { + "@typescript-eslint/types": "4.15.1", + "@typescript-eslint/visitor-keys": "4.15.1", + "debug": "^4.1.1", + "globby": "^11.0.1", + "is-glob": "^4.0.1", + "semver": "^7.3.2", + "tsutils": "^3.17.1" + }, + "dependencies": { + "semver": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", + "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "@typescript-eslint/visitor-keys": { + "version": "4.15.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.15.1.tgz", + "integrity": "sha512-tYzaTP9plooRJY8eNlpAewTOqtWW/4ff/5wBjNVaJ0S0wC4Gpq/zDVRTJa5bq2v1pCNQ08xxMCndcvR+h7lMww==", + "dev": true, + "requires": { + "@typescript-eslint/types": "4.15.1", + "eslint-visitor-keys": "^2.0.0" + } + }, "@webassemblyjs/ast": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz", @@ -3221,20 +3326,6 @@ } } }, - "babel-eslint": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.1.0.tgz", - "integrity": "sha512-ifWaTHQ0ce+448CYop8AdrQiBsGrnC+bMgfyKFdi6EsPLTAWG+QfyDeM6OH+FmWnKvEq5NnBMLvlBUPKQZoDSg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "@babel/parser": "^7.7.0", - "@babel/traverse": "^7.7.0", - "@babel/types": "^7.7.0", - "eslint-visitor-keys": "^1.0.0", - "resolve": "^1.12.0" - } - }, "babel-helper-function-name": { "version": "6.24.1", "resolved": "https://registry.npmjs.org/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz", @@ -5380,12 +5471,12 @@ } }, "eslint": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.18.0.tgz", - "integrity": "sha512-fbgTiE8BfUJZuBeq2Yi7J3RB3WGUQ9PNuNbmgi6jt9Iv8qrkxfy19Ds3OpL1Pm7zg3BtTVhvcUZbIRQ0wmSjAQ==", + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.20.0.tgz", + "integrity": "sha512-qGi0CTcOGP2OtCQBgWZlQjcTuP0XkIpYFj25XtRTQSHC+umNnp7UMshr2G8SLsRFYDdAPFeHOsiteadmMH02Yw==", "dev": true, "requires": { - "@babel/code-frame": "^7.0.0", + "@babel/code-frame": "7.12.11", "@eslint/eslintrc": "^0.3.0", "ajv": "^6.10.0", "chalk": "^4.0.0", @@ -5397,7 +5488,7 @@ "eslint-utils": "^2.1.0", "eslint-visitor-keys": "^2.0.0", "espree": "^7.3.1", - "esquery": "^1.2.0", + "esquery": "^1.4.0", "esutils": "^2.0.2", "file-entry-cache": "^6.0.0", "functional-red-black-tree": "^1.0.1", @@ -5464,12 +5555,6 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "eslint-visitor-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz", - "integrity": "sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ==", - "dev": true - }, "glob-parent": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", @@ -5766,12 +5851,20 @@ "dev": true, "requires": { "eslint-visitor-keys": "^1.1.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true + } } }, "eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz", + "integrity": "sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ==", "dev": true }, "espree": { @@ -5783,6 +5876,14 @@ "acorn": "^7.4.0", "acorn-jsx": "^5.3.1", "eslint-visitor-keys": "^1.3.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true + } } }, "esprima": { @@ -5792,9 +5893,9 @@ "dev": true }, "esquery": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.3.1.tgz", - "integrity": "sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", + "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", "dev": true, "requires": { "estraverse": "^5.1.0" @@ -6510,9 +6611,9 @@ } }, "flatted": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.1.0.tgz", - "integrity": "sha512-tW+UkmtNg/jv9CSofAKvgVcO7c2URjhTdW1ZTkcAritblu8tajiYy7YisnIflEwtKssCtOxpnBRoCB7iap0/TA==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.1.1.tgz", + "integrity": "sha512-zAoAQiudy+r5SvnSw3KJy5os/oRJYHzrzja/tBDqrZtNhUw8bt6y8OBzMWcjWr+8liV8Eb6yOhw8WZ7VFZ5ZzA==", "dev": true }, "flush-write-stream": { @@ -13267,9 +13368,9 @@ }, "dependencies": { "ajv": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-7.0.3.tgz", - "integrity": "sha512-R50QRlXSxqXcQP5SvKUrw8VZeypvo12i2IX0EeR5PiZ7bEKeHWgzgo264LDadUsCU42lTJVhFikTqJwNeH34gQ==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-7.1.0.tgz", + "integrity": "sha512-svS9uILze/cXbH0z2myCK2Brqprx/+JJYK5pHicT/GQiBfzzhUVAIT6MwqJg8y4xV/zoGsUeuPuwtoiKSGE15g==", "dev": true, "requires": { "fast-deep-equal": "^3.1.1", @@ -13612,6 +13713,23 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz", "integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==" }, + "tsutils": { + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.20.0.tgz", + "integrity": "sha512-RYbuQuvkhuqVeXweWT3tJLKOEJ/UUw9GjNEZGWdrLLlM+611o1gwLHBpxoFJKKl25fLprp2eVthtKs5JOrNeXg==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + } + } + }, "tty-browserify": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", diff --git a/package.json b/package.json index ad5edd75e..c86672f1a 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "prepack": "npm run build", "prebuild": "npm run eslint -- --cache", "dev": "webpack --progress --colors --watch --mode=development", - "eslint": "eslint --cache-location=node_modules/.cache/.eslintcache/ . ", + "eslint": "eslint --cache-location=node_modules/.cache/.eslintcache/ '*/**/*.{js,ts}'", "test": "jest --detectOpenHandles", "test-unit": "jest test/unit --detectOpenHandles", "coverage": "jest --coverage", @@ -54,8 +54,9 @@ "@babel/preset-typescript": "^7.12.13", "@types/debug": "^4.1.5", "@types/qs": "^6.9.5", + "@typescript-eslint/eslint-plugin": "^4.15.1", + "@typescript-eslint/parser": "^4.15.1", "async-mutex": "^0.2.6", - "babel-eslint": "^10.1.0", "babel-loader": "^8.2.2", "babel-plugin-add-module-exports": "^1.0.4", "babel-plugin-transform-class-properties": "^6.24.1", @@ -63,7 +64,7 @@ "buffer": "^6.0.3", "chromedriver": "^88.0.0", "core-js": "^3.8.3", - "eslint": "^7.18.0", + "eslint": "^7.20.0", "eslint-config-airbnb": "^18.2.1", "eslint-config-streamr-nodejs": "^1.3.0", "eslint-loader": "^4.0.2", From 852973d0ac636926609b8b229164d3a6866f3e0c Mon Sep 17 00:00:00 2001 From: Teo Gebhard Date: Tue, 16 Feb 2021 15:00:23 +0200 Subject: [PATCH 10/12] Type definitions --- src/{Config.js => Config.ts} | 21 ++- src/{Session.js => Session.ts} | 82 +++++---- src/StreamrClient.ts | 179 +++++++++++++------- src/rest/DataUnionEndpoints.ts | 121 +++++++------ src/rest/LoginEndpoints.ts | 13 +- src/rest/StreamEndpoints.ts | 49 ++++-- src/stream/{StreamPart.js => StreamPart.ts} | 10 +- src/stream/index.ts | 29 +++- src/types.ts | 2 - test/integration/LoginEndpoints.test.js | 18 +- test/integration/StreamEndpoints.test.js | 2 +- test/unit/Session.test.js | 8 +- 12 files changed, 333 insertions(+), 201 deletions(-) rename src/{Config.js => Config.ts} (85%) rename src/{Session.js => Session.ts} (50%) rename src/stream/{StreamPart.js => StreamPart.ts} (62%) diff --git a/src/Config.js b/src/Config.ts similarity index 85% rename from src/Config.js rename to src/Config.ts index 0894bc733..65442c4c5 100644 --- a/src/Config.js +++ b/src/Config.ts @@ -1,16 +1,18 @@ import qs from 'qs' +// @ts-expect-error import { ControlLayer, MessageLayer } from 'streamr-client-protocol' import Debug from 'debug' import { getVersionString, counterId } from './utils' +import { StreamrClientOptions } from './StreamrClient' const { ControlMessage } = ControlLayer const { StreamMessage } = MessageLayer -export default function ClientConfig(opts = {}) { +export default function ClientConfig(opts: StreamrClientOptions = {}) { const { id = counterId('StreamrClient') } = opts - const options = { + const options: StreamrClientOptions = { debug: Debug(id), // Authentication: identity used by this StreamrClient instance auth: {}, // can contain member privateKey or (window.)ethereum @@ -39,15 +41,20 @@ export default function ClientConfig(opts = {}) { // For ethers.js provider params, see https://docs.ethers.io/ethers.js/v5-beta/api-providers.html#provider mainnet: null, // Default to ethers.js default provider settings sidechain: { + // @ts-expect-error url: null, // TODO: add our default public service sidechain node, also find good PoA params below // timeout: // pollingInterval: }, + // @ts-expect-error dataUnion: null, // Give a "default target" of all data union endpoint operations (no need to pass argument every time) tokenAddress: '0x0Cf0Ee63788A0849fE5297F3407f701E122cC023', minimumWithdrawTokenWei: '1000000', // Threshold value set in AMB configs, smallest token amount to pass over the bridge + // @ts-expect-error sidechainTokenAddress: null, // TODO // sidechain token + // @ts-expect-error factoryMainnetAddress: null, // TODO // Data Union factory that creates a new Data Union + // @ts-expect-error sidechainAmbAddress: null, // Arbitrary Message-passing Bridge (AMB), see https://github.com/poanetwork/tokenbridge payForSignatureTransport: true, // someone must pay for transporting the withdraw tx to mainnet, either us or bridge operator ...opts, @@ -58,7 +65,7 @@ export default function ClientConfig(opts = {}) { } } - const parts = options.url.split('?') + const parts = options.url!.split('?') if (parts.length === 1) { // there is no query string const controlLayer = `controlLayerVersion=${ControlMessage.LATEST_VERSION}` const messageLayer = `messageLayerVersion=${StreamMessage.LATEST_VERSION}` @@ -78,16 +85,20 @@ export default function ClientConfig(opts = {}) { options.url = `${options.url}&streamrClient=${getVersionString()}` // Backwards compatibility for option 'authKey' => 'apiKey' + // @ts-expect-error if (options.authKey && !options.apiKey) { + // @ts-expect-error options.apiKey = options.authKey } + // @ts-expect-error if (options.apiKey) { + // @ts-expect-error options.auth.apiKey = options.apiKey } - if (options.auth.privateKey && !options.auth.privateKey.startsWith('0x')) { - options.auth.privateKey = `0x${options.auth.privateKey}` + if (options.auth!.privateKey && !options.auth!.privateKey.startsWith('0x')) { + options.auth!.privateKey = `0x${options.auth!.privateKey}` } return options diff --git a/src/Session.js b/src/Session.ts similarity index 50% rename from src/Session.js rename to src/Session.ts index d4c5eaad6..f9522f131 100644 --- a/src/Session.js +++ b/src/Session.ts @@ -1,29 +1,58 @@ import EventEmitter from 'eventemitter3' import { Wallet } from '@ethersproject/wallet' -import { Web3Provider } from '@ethersproject/providers' +import { ExternalProvider, JsonRpcFetchFunc, Web3Provider } from '@ethersproject/providers' +import StreamrClient from './StreamrClient' + +enum State { + LOGGING_OUT = 'logging out', + LOGGED_OUT = 'logged out', + LOGGING_IN ='logging in', + LOGGED_IN = 'logged in', +} + +export interface SessionOptions { + privateKey?: string + ethereum?: ExternalProvider|JsonRpcFetchFunc + apiKey?: string + username?: string + password?: string + sessionToken?: string + unauthenticated?: boolean +} + +export interface TokenObject { + token: string +} export default class Session extends EventEmitter { - constructor(client, options = {}) { + + _client: StreamrClient + options: SessionOptions + state: State + loginFunction: () => Promise + sessionTokenPromise?: Promise + + constructor(client: StreamrClient, options: SessionOptions = {}) { super() this._client = client this.options = { ...options } - this.state = Session.State.LOGGED_OUT + this.state = State.LOGGED_OUT // TODO: move loginFunction to StreamrClient constructor where "auth type" is checked if (typeof this.options.privateKey !== 'undefined') { const wallet = new Wallet(this.options.privateKey) - this.loginFunction = async () => this._client.loginWithChallengeResponse((d) => wallet.signMessage(d), wallet.address) + this.loginFunction = async () => this._client.loginEndpoints.loginWithChallengeResponse((d: string) => wallet.signMessage(d), wallet.address) } else if (typeof this.options.ethereum !== 'undefined') { const provider = new Web3Provider(this.options.ethereum) const signer = provider.getSigner() - this.loginFunction = async () => this._client.loginWithChallengeResponse((d) => signer.signMessage(d), await signer.getAddress()) + this.loginFunction = async () => this._client.loginEndpoints.loginWithChallengeResponse((d: string) => signer.signMessage(d), await signer.getAddress()) } else if (typeof this.options.apiKey !== 'undefined') { - this.loginFunction = async () => this._client.loginWithApiKey(this.options.apiKey) + this.loginFunction = async () => this._client.loginEndpoints.loginWithApiKey(this.options.apiKey!) } else if (typeof this.options.username !== 'undefined' && typeof this.options.password !== 'undefined') { - this.loginFunction = async () => this._client.loginWithUsernamePassword(this.options.username, this.options.password) + this.loginFunction = async () => this._client.loginEndpoints.loginWithUsernamePassword(this.options.username!, this.options.password!) } else { if (!this.options.sessionToken) { this.options.unauthenticated = true @@ -38,7 +67,7 @@ export default class Session extends EventEmitter { return this.options.unauthenticated } - updateState(newState) { + updateState(newState: State) { this.state = newState this.emit(newState) } @@ -52,19 +81,19 @@ export default class Session extends EventEmitter { return undefined } - if (this.state !== Session.State.LOGGING_IN) { - if (this.state === Session.State.LOGGING_OUT) { + if (this.state !== State.LOGGING_IN) { + if (this.state === State.LOGGING_OUT) { this.sessionTokenPromise = new Promise((resolve) => { - this.once(Session.State.LOGGED_OUT, () => resolve(this.getSessionToken(requireNewToken))) + this.once(State.LOGGED_OUT, () => resolve(this.getSessionToken(requireNewToken))) }) } else { - this.updateState(Session.State.LOGGING_IN) - this.sessionTokenPromise = this.loginFunction().then((tokenObj) => { + this.updateState(State.LOGGING_IN) + this.sessionTokenPromise = this.loginFunction().then((tokenObj: TokenObject) => { this.options.sessionToken = tokenObj.token - this.updateState(Session.State.LOGGED_IN) + this.updateState(State.LOGGED_IN) return tokenObj.token - }, (err) => { - this.updateState(Session.State.LOGGED_OUT) + }, (err: Error) => { + this.updateState(State.LOGGED_OUT) throw err }) } @@ -73,31 +102,24 @@ export default class Session extends EventEmitter { } async logout() { - if (this.state === Session.State.LOGGED_OUT) { + if (this.state === State.LOGGED_OUT) { throw new Error('Already logged out!') } - if (this.state === Session.State.LOGGING_OUT) { + if (this.state === State.LOGGING_OUT) { throw new Error('Already logging out!') } - if (this.state === Session.State.LOGGING_IN) { + if (this.state === State.LOGGING_IN) { await new Promise((resolve) => { - this.once(Session.State.LOGGED_IN, () => resolve(this.logout())) + this.once(State.LOGGED_IN, () => resolve(this.logout())) }) return } - this.updateState(Session.State.LOGGING_OUT) - await this._client.logoutEndpoint() + this.updateState(State.LOGGING_OUT) + await this._client.loginEndpoints.logoutEndpoint() this.options.sessionToken = undefined - this.updateState(Session.State.LOGGED_OUT) + this.updateState(State.LOGGED_OUT) } } - -Session.State = { - LOGGING_OUT: 'logging out', - LOGGED_OUT: 'logged out', - LOGGING_IN: 'logging in', - LOGGED_IN: 'logged in', -} diff --git a/src/StreamrClient.ts b/src/StreamrClient.ts index 05c4d8424..a4cd0fb71 100644 --- a/src/StreamrClient.ts +++ b/src/StreamrClient.ts @@ -13,26 +13,78 @@ import Publisher from './publish' import Subscriber from './subscribe' import { getUserId } from './user' import { Todo } from './types' -import { StreamEndpoints } from './rest/StreamEndpoints' +import { StreamEndpoints, StreamListQuery } from './rest/StreamEndpoints' import { LoginEndpoints } from './rest/LoginEndpoints' -import { DataUnionEndpoints } from './rest/DataUnionEndpoints' +import { DataUnionEndpoints, DataUnionOptions } from './rest/DataUnionEndpoints' +import { BigNumber } from '@ethersproject/bignumber' +import Stream, { StreamProperties } from './stream' +import { ExternalProvider, JsonRpcFetchFunc } from '@ethersproject/providers' + +export interface StreamrClientOptions { + id?: string + debug?: Debug.Debugger, + auth?: { + privateKey?: string + ethereum?: ExternalProvider|JsonRpcFetchFunc, + apiKey?: string + username?: string + password?: string + } + url?: string + restUrl?: string + streamrNodeAddress?: string + autoConnect?: boolean + autoDisconnect?: boolean + orderMessages?: boolean, + retryResendAfter?: number, + gapFillTimeout?: number, + maxPublishQueueSize?: number, + publishWithSignature?: Todo, + verifySignatures?: Todo, + publisherStoreKeyHistory?: boolean, + groupKeys?: Todo + keyExchange?: Todo + mainnet?: Todo + sidechain?: { + url?: string + }, + dataUnion?: string + tokenAddress?: string, + minimumWithdrawTokenWei?: BigNumber|number|string, + sidechainTokenAddress?: string + factoryMainnetAddress?: string + sidechainAmbAddress?: string + payForSignatureTransport?: boolean + cache?: { + maxSize?: number, + maxAge?: number + } +} + +// TODO get metadata type from streamr-protocol-js project (it doesn't export the type definitions yet) +export type OnMessageCallback = (message: any, metadata: any) => void + +interface MessageEvent { + data: any +} /** * Wrap connection message events with message parsing. */ class StreamrConnection extends Connection { - constructor(...args: Todo) { + // TODO define args type when we convert Connection class to TypeScript + constructor(...args: any) { super(...args) this.on('message', this.onConnectionMessage) } // eslint-disable-next-line class-methods-use-this - parse(messageEvent: Todo) { + parse(messageEvent: MessageEvent) { return ControlLayer.ControlMessage.deserialize(messageEvent.data) } - onConnectionMessage(messageEvent: Todo) { + onConnectionMessage(messageEvent: MessageEvent) { let controlMessage try { controlMessage = this.parse(messageEvent) @@ -54,19 +106,20 @@ class StreamrConnection extends Connection { class StreamrCached { - client: Todo - getStream: Todo - getUserInfo: Todo - isStreamPublisher: Todo - isStreamSubscriber: Todo - getUserId: Todo + client: StreamrClient + // TODO change all "any" types in this class to valid types when CacheAsyncFn is converted to TypeScript + getStream: any + getUserInfo: any + isStreamPublisher: any + isStreamSubscriber: any + getUserId: any constructor(client: StreamrClient) { this.client = client - const cacheOptions = client.options.cache + const cacheOptions: Todo = client.options.cache this.getStream = CacheAsyncFn(client.getStream.bind(client), { ...cacheOptions, - cacheKey([maybeStreamId]: Todo) { + cacheKey([maybeStreamId]: any) { const { streamId } = validateOptions(maybeStreamId) return streamId } @@ -74,7 +127,7 @@ class StreamrCached { this.getUserInfo = CacheAsyncFn(client.getUserInfo.bind(client), cacheOptions) this.isStreamPublisher = CacheAsyncFn(client.isStreamPublisher.bind(client), { ...cacheOptions, - cacheKey([maybeStreamId, ethAddress]: Todo) { + cacheKey([maybeStreamId, ethAddress]: any) { const { streamId } = validateOptions(maybeStreamId) return `${streamId}|${ethAddress}` } @@ -82,7 +135,7 @@ class StreamrCached { this.isStreamSubscriber = CacheAsyncFn(client.isStreamSubscriber.bind(client), { ...cacheOptions, - cacheKey([maybeStreamId, ethAddress]: Todo) { + cacheKey([maybeStreamId, ethAddress]: any) { const { streamId } = validateOptions(maybeStreamId) return `${streamId}|${ethAddress}` } @@ -91,10 +144,10 @@ class StreamrCached { this.getUserId = CacheAsyncFn(client.getUserId.bind(client), cacheOptions) } - clearStream(streamId: Todo) { + clearStream(streamId: string) { this.getStream.clear() - this.isStreamPublisher.clearMatching((s: Todo) => s.startsWith(streamId)) - this.isStreamSubscriber.clearMatching((s: Todo) => s.startsWith(streamId)) + this.isStreamPublisher.clearMatching((s: string) => s.startsWith(streamId)) + this.isStreamSubscriber.clearMatching((s: string) => s.startsWith(streamId)) } clearUser() { @@ -116,7 +169,7 @@ export default class StreamrClient extends EventEmitter { id: string debug: Debug.Debugger - options: Todo + options: StreamrClientOptions session: Session connection: StreamrConnection publisher: Todo @@ -127,7 +180,7 @@ export default class StreamrClient extends EventEmitter { loginEndpoints: LoginEndpoints dataUnionEndpoints: DataUnionEndpoints - constructor(options: Todo = {}, connection?: StreamrConnection) { + constructor(options: StreamrClientOptions = {}, connection?: StreamrConnection) { super() this.id = counterId(`${this.constructor.name}:${uid}`) this.debug = Debug(this.id) @@ -284,7 +337,7 @@ export default class StreamrClient extends EventEmitter { return this.publisher.rotateGroupKey(...args) } - async subscribe(opts: Todo, onMessage: Todo) { + async subscribe(opts: Todo, onMessage: OnMessageCallback) { let subTask: Todo let sub: Todo const hasResend = !!(opts.resend || opts.from || opts.to || opts.last) @@ -319,7 +372,7 @@ export default class StreamrClient extends EventEmitter { await this.subscriber.unsubscribe(opts) } - async resend(opts: Todo, onMessage: Todo) { + async resend(opts: Todo, onMessage: OnMessageCallback) { const task = this.subscriber.resend(opts) if (typeof onMessage !== 'function') { return task @@ -360,11 +413,11 @@ export default class StreamrClient extends EventEmitter { // TODO many of these methods that use streamEndpoints/loginEndpoints/dataUnionEndpoints are private: remove those - async getStream(streamId: Todo) { + async getStream(streamId: string) { return this.streamEndpoints.getStream(streamId) } - async listStreams(query: Todo = {}) { + async listStreams(query: StreamListQuery = {}) { return this.streamEndpoints.listStreams(query) } @@ -372,46 +425,46 @@ export default class StreamrClient extends EventEmitter { return this.streamEndpoints.getStreamByName(name) } - async createStream(props: Todo) { + async createStream(props: StreamProperties) { return this.streamEndpoints.createStream(props) } - async getOrCreateStream(props: Todo) { + async getOrCreateStream(props: { id?: string, name?: string }) { return this.streamEndpoints.getOrCreateStream(props) } - async getStreamPublishers(streamId: Todo) { + async getStreamPublishers(streamId: string) { return this.streamEndpoints.getStreamPublishers(streamId) } - async isStreamPublisher(streamId: Todo, ethAddress: Todo) { + async isStreamPublisher(streamId: string, ethAddress: string) { return this.streamEndpoints.isStreamPublisher(streamId, ethAddress) } - async getStreamSubscribers(streamId: Todo) { + async getStreamSubscribers(streamId: string) { return this.streamEndpoints.getStreamSubscribers(streamId) } - async isStreamSubscriber(streamId: Todo, ethAddress: Todo) { + async isStreamSubscriber(streamId: string, ethAddress: string) { return this.streamEndpoints.isStreamSubscriber(streamId, ethAddress) } - async getStreamValidationInfo(streamId: Todo) { + async getStreamValidationInfo(streamId: string) { return this.streamEndpoints.getStreamValidationInfo(streamId) } - async getStreamLast(streamObjectOrId: Todo) { + async getStreamLast(streamObjectOrId: Stream|string) { return this.streamEndpoints.getStreamLast(streamObjectOrId) } - async getStreamPartsByStorageNode(address: Todo) { + async getStreamPartsByStorageNode(address: string) { return this.streamEndpoints.getStreamPartsByStorageNode(address) } - async publishHttp(streamObjectOrId: Todo, data: Todo, requestOptions: Todo = {}, keepAlive: Todo = true) { + async publishHttp(streamObjectOrId: Stream|string, data: Todo, requestOptions: Todo = {}, keepAlive: boolean = true) { return this.streamEndpoints.publishHttp(streamObjectOrId, data, requestOptions, keepAlive) } - + async getChallenge(address: Todo) { return this.loginEndpoints.getChallenge(address) } @@ -440,115 +493,115 @@ export default class StreamrClient extends EventEmitter { return this.loginEndpoints.logoutEndpoint() } - async calculateDataUnionMainnetAddress(dataUnionName: Todo, deployerAddress: Todo, options: Todo) { + async calculateDataUnionMainnetAddress(dataUnionName: string, deployerAddress: string, options: DataUnionOptions) { return this.dataUnionEndpoints.calculateDataUnionMainnetAddress(dataUnionName, deployerAddress, options) } - async calculateDataUnionSidechainAddress(duMainnetAddress: Todo, options: Todo) { + async calculateDataUnionSidechainAddress(duMainnetAddress: string, options: DataUnionOptions) { return this.dataUnionEndpoints.calculateDataUnionSidechainAddress(duMainnetAddress, options) } - async deployDataUnion(options: Todo = {}) { + async deployDataUnion(options: DataUnionOptions = {}) { return this.dataUnionEndpoints.deployDataUnion(options) } - async getDataUnionContract(options: Todo = {}) { + async getDataUnionContract(options: DataUnionOptions = {}) { return this.dataUnionEndpoints.getDataUnionContract(options) } - async createSecret(dataUnionMainnetAddress: Todo, name: string = 'Untitled Data Union Secret') { + async createSecret(dataUnionMainnetAddress: string, name: string = 'Untitled Data Union Secret') { return this.dataUnionEndpoints.createSecret(dataUnionMainnetAddress, name) } - async kick(memberAddressList: Todo, options: Todo = {}) { + async kick(memberAddressList: string[], options: DataUnionOptions = {}) { return this.dataUnionEndpoints.kick(memberAddressList, options) } - async addMembers(memberAddressList: Todo, options: Todo = {}) { + async addMembers(memberAddressList: string[], options: DataUnionOptions = {}) { return this.dataUnionEndpoints.addMembers(memberAddressList, options) } - async withdrawMember(memberAddress: Todo, options: Todo) { + async withdrawMember(memberAddress: string, options: DataUnionOptions) { return this.dataUnionEndpoints.withdrawMember(memberAddress, options) } - async getWithdrawMemberTx(memberAddress: Todo, options: Todo) { + async getWithdrawMemberTx(memberAddress: string, options: DataUnionOptions) { return this.dataUnionEndpoints.getWithdrawMemberTx(memberAddress, options) } - async withdrawToSigned(memberAddress: Todo, recipientAddress: Todo, signature: Todo, options: Todo) { + async withdrawToSigned(memberAddress: string, recipientAddress: string, signature: string, options: DataUnionOptions) { return this.dataUnionEndpoints.withdrawToSigned(memberAddress, recipientAddress, signature, options) } - async getWithdrawToSignedTx(memberAddress: Todo, recipientAddress: Todo, signature: Todo, options: Todo) { + async getWithdrawToSignedTx(memberAddress: string, recipientAddress: string, signature: string, options: DataUnionOptions) { return this.dataUnionEndpoints.getWithdrawToSignedTx(memberAddress, recipientAddress, signature, options) } - async setAdminFee(newFeeFraction: Todo, options: Todo) { + async setAdminFee(newFeeFraction: number, options: DataUnionOptions) { return this.dataUnionEndpoints.setAdminFee(newFeeFraction, options) } - async getAdminFee(options: Todo) { + async getAdminFee(options: DataUnionOptions) { return this.dataUnionEndpoints.getAdminFee(options) } - async getAdminAddress(options: Todo) { + async getAdminAddress(options: DataUnionOptions) { return this.dataUnionEndpoints.getAdminAddress(options) } - async joinDataUnion(options: Todo = {}) { + async joinDataUnion(options: DataUnionOptions = {}) { return this.dataUnionEndpoints.joinDataUnion(options) } - async hasJoined(memberAddress: Todo, options: Todo = {}) { + async hasJoined(memberAddress: string, options: DataUnionOptions = {}) { return this.dataUnionEndpoints.hasJoined(memberAddress, options) } - async getMembers(options: Todo) { + async getMembers(options: DataUnionOptions) { return this.dataUnionEndpoints.getMembers(options) } - async getDataUnionStats(options: Todo) { + async getDataUnionStats(options: DataUnionOptions) { return this.dataUnionEndpoints.getDataUnionStats(options) } - async getMemberStats(memberAddress: Todo, options: Todo) { + async getMemberStats(memberAddress: string, options: DataUnionOptions) { return this.dataUnionEndpoints.getMemberStats(memberAddress, options) } - async getMemberBalance(memberAddress: Todo, options: Todo) { + async getMemberBalance(memberAddress: string, options: DataUnionOptions) { return this.dataUnionEndpoints.getMemberBalance(memberAddress, options) } - async getTokenBalance(address: Todo, options: Todo) { + async getTokenBalance(address: string|null|undefined, options: DataUnionOptions) { return this.dataUnionEndpoints.getTokenBalance(address, options) } - async getDataUnionVersion(contractAddress: Todo) { + async getDataUnionVersion(contractAddress: string) { return this.dataUnionEndpoints.getDataUnionVersion(contractAddress) } - async withdraw(options: Todo = {}) { + async withdraw(options: DataUnionOptions = {}) { return this.dataUnionEndpoints.withdraw(options) } - async getWithdrawTx(options: Todo) { + async getWithdrawTx(options: DataUnionOptions) { return this.dataUnionEndpoints.getWithdrawTx(options) } - async withdrawTo(recipientAddress: Todo, options = {}) { + async withdrawTo(recipientAddress: string, options: DataUnionOptions = {}) { return this.dataUnionEndpoints.withdrawTo(recipientAddress, options) } - async getWithdrawTxTo(recipientAddress: Todo, options: Todo) { + async getWithdrawTxTo(recipientAddress: string, options: DataUnionOptions) { return this.dataUnionEndpoints.getWithdrawTxTo(recipientAddress, options) } - async signWithdrawTo(recipientAddress: Todo, options: Todo) { + async signWithdrawTo(recipientAddress: string, options: DataUnionOptions) { return this.dataUnionEndpoints.signWithdrawTo(recipientAddress, options) } - async signWithdrawAmountTo(recipientAddress: Todo, amountTokenWei: Todo, options: Todo) { + async signWithdrawAmountTo(recipientAddress: string, amountTokenWei: BigNumber|number|string, options: DataUnionOptions) { return this.dataUnionEndpoints.signWithdrawAmountTo(recipientAddress, amountTokenWei, options) } } diff --git a/src/rest/DataUnionEndpoints.ts b/src/rest/DataUnionEndpoints.ts index e31978c09..697a2347c 100644 --- a/src/rest/DataUnionEndpoints.ts +++ b/src/rest/DataUnionEndpoints.ts @@ -24,6 +24,20 @@ import { until, getEndpointUrl } from '../utils' import authFetch from './authFetch' +export interface DataUnionOptions { + wallet?: Todo, + provider?: Todo, + confirmations?: Todo, + gasPrice?: Todo, + dataUnion?: Todo, + tokenAddress?: Todo, + minimumWithdrawTokenWei?: BigNumber|number|string, + sidechainTokenAddress?: string, + factoryMainnetAddress?: string, + sidechainAmbAddress?: string, + payForSignatureTransport?: boolean +} + const log = debug('StreamrClient::DataUnionEndpoints') // const log = console.log // useful for debugging sometimes @@ -261,7 +275,7 @@ const sidechainAmbABI = [{ /** @typedef {String} EthereumAddress */ -function throwIfBadAddress(address: Todo, variableDescription: Todo) { +function throwIfBadAddress(address: string, variableDescription: Todo) { try { return getAddress(address) } catch (e) { @@ -275,8 +289,8 @@ function throwIfBadAddress(address: Todo, variableDescription: Todo) { * @param {EthereumAddress} inputAddress from user (NOT case sensitive) * @returns {EthereumAddress} with checksum case */ -function parseAddress(client: StreamrClient, inputAddress: Todo) { - if (isAddress(inputAddress)) { +function parseAddress(client: StreamrClient, inputAddress: string|null|undefined) { + if (inputAddress && isAddress(inputAddress)) { return getAddress(inputAddress) } return client.getAddress() @@ -284,12 +298,12 @@ function parseAddress(client: StreamrClient, inputAddress: Todo) { // Find the Asyncronous Message-passing Bridge sidechain ("home") contract let cachedSidechainAmb: Todo -async function getSidechainAmb(client: StreamrClient, options: Todo) { +async function getSidechainAmb(client: StreamrClient, options: DataUnionOptions) { if (!cachedSidechainAmb) { const getAmbPromise = async () => { const mainnetProvider = client.ethereum.getMainnetProvider() const factoryMainnetAddress = options.factoryMainnetAddress || client.options.factoryMainnetAddress - const factoryMainnet = new Contract(factoryMainnetAddress, factoryMainnetABI, mainnetProvider) + const factoryMainnet = new Contract(factoryMainnetAddress!, factoryMainnetABI, mainnetProvider) const sidechainProvider = client.ethereum.getSidechainProvider() const factorySidechainAddress = await factoryMainnet.data_union_sidechain_factory() const factorySidechain = new Contract(factorySidechainAddress, [{ @@ -310,15 +324,15 @@ async function getSidechainAmb(client: StreamrClient, options: Todo) { return cachedSidechainAmb } -async function getMainnetAmb(client: StreamrClient, options: Todo) { +async function getMainnetAmb(client: StreamrClient, options: DataUnionOptions) { const mainnetProvider = client.ethereum.getMainnetProvider() const factoryMainnetAddress = options.factoryMainnetAddress || client.options.factoryMainnetAddress - const factoryMainnet = new Contract(factoryMainnetAddress, factoryMainnetABI, mainnetProvider) + const factoryMainnet = new Contract(factoryMainnetAddress!, factoryMainnetABI, mainnetProvider) const mainnetAmbAddress = await factoryMainnet.amb() return new Contract(mainnetAmbAddress, mainnetAmbABI, mainnetProvider) } -async function requiredSignaturesHaveBeenCollected(client: StreamrClient, messageHash: Todo, options: Todo = {}) { +async function requiredSignaturesHaveBeenCollected(client: StreamrClient, messageHash: Todo, options: DataUnionOptions = {}) { const sidechainAmb = await getSidechainAmb(client, options) const requiredSignatureCount = await sidechainAmb.requiredSignatures() @@ -333,7 +347,7 @@ async function requiredSignaturesHaveBeenCollected(client: StreamrClient, messag } // move signatures from sidechain to mainnet -async function transportSignatures(client: StreamrClient, messageHash: Todo, options: Todo) { +async function transportSignatures(client: StreamrClient, messageHash: Todo, options: DataUnionOptions) { const sidechainAmb = await getSidechainAmb(client, options) const message = await sidechainAmb.message(messageHash) const messageId = '0x' + message.substr(2, 64) @@ -419,11 +433,11 @@ async function transportSignatures(client: StreamrClient, messageHash: Todo, opt // template for withdraw functions // client could be replaced with AMB (mainnet and sidechain) -async function untilWithdrawIsComplete(client: StreamrClient, getWithdrawTxFunc: (options: Todo) => Todo, getBalanceFunc: (options: Todo) => Todo, options: Todo = {}) { +async function untilWithdrawIsComplete(client: StreamrClient, getWithdrawTxFunc: (options: DataUnionOptions) => Todo, getBalanceFunc: (options: DataUnionOptions) => Todo, options: DataUnionOptions = {}) { const { pollingIntervalMs = 1000, retryTimeoutMs = 60000, - } = options + }: Todo = options const balanceBefore = await getBalanceFunc(options) const tx = await getWithdrawTxFunc(options) const tr = await tx.wait() @@ -473,11 +487,11 @@ async function untilWithdrawIsComplete(client: StreamrClient, getWithdrawTxFunc: // key the cache with name only, since PROBABLY one StreamrClient will ever use only one private key const mainnetAddressCache: Todo = {} // mapping: "name" -> mainnet address /** @returns {Promise} Mainnet address for Data Union */ -async function getDataUnionMainnetAddress(client: StreamrClient, dataUnionName: Todo, deployerAddress: Todo, options: Todo = {}) { +async function getDataUnionMainnetAddress(client: StreamrClient, dataUnionName: string, deployerAddress: string, options: DataUnionOptions = {}) { if (!mainnetAddressCache[dataUnionName]) { const provider = client.ethereum.getMainnetProvider() const factoryMainnetAddress = options.factoryMainnetAddress || client.options.factoryMainnetAddress - const factoryMainnet = new Contract(factoryMainnetAddress, factoryMainnetABI, provider) + const factoryMainnet = new Contract(factoryMainnetAddress!, factoryMainnetABI, provider) const addressPromise = factoryMainnet.mainnetAddress(deployerAddress, dataUnionName) mainnetAddressCache[dataUnionName] = addressPromise mainnetAddressCache[dataUnionName] = await addressPromise // eslint-disable-line require-atomic-updates @@ -488,11 +502,11 @@ async function getDataUnionMainnetAddress(client: StreamrClient, dataUnionName: // TODO: calculate addresses in JS const sidechainAddressCache: Todo = {} // mapping: mainnet address -> sidechain address /** @returns {Promise} Sidechain address for Data Union */ -async function getDataUnionSidechainAddress(client: StreamrClient, duMainnetAddress: Todo, options: Todo = {}) { +async function getDataUnionSidechainAddress(client: StreamrClient, duMainnetAddress: string, options: DataUnionOptions = {}) { if (!sidechainAddressCache[duMainnetAddress]) { const provider = client.ethereum.getMainnetProvider() const factoryMainnetAddress = options.factoryMainnetAddress || client.options.factoryMainnetAddress - const factoryMainnet = new Contract(factoryMainnetAddress, factoryMainnetABI, provider) + const factoryMainnet = new Contract(factoryMainnetAddress!, factoryMainnetABI, provider) const addressPromise = factoryMainnet.sidechainAddress(duMainnetAddress) sidechainAddressCache[duMainnetAddress] = addressPromise sidechainAddressCache[duMainnetAddress] = await addressPromise // eslint-disable-line require-atomic-updates @@ -500,7 +514,8 @@ async function getDataUnionSidechainAddress(client: StreamrClient, duMainnetAddr return sidechainAddressCache[duMainnetAddress] } -function getMainnetContractReadOnly(client: StreamrClient, options: Todo = {}) { +function getMainnetContractReadOnly(client: StreamrClient, options: DataUnionOptions = {}) { + // @ts-expect-error let dataUnion = options.dataUnion || options.dataUnionAddress || client.options.dataUnion if (isAddress(dataUnion)) { const provider = client.ethereum.getMainnetProvider() @@ -513,14 +528,14 @@ function getMainnetContractReadOnly(client: StreamrClient, options: Todo = {}) { return dataUnion } -function getMainnetContract(client: StreamrClient, options: Todo = {}) { +function getMainnetContract(client: StreamrClient, options: DataUnionOptions = {}) { const du = getMainnetContractReadOnly(client, options) const signer = client.ethereum.getSigner() // @ts-expect-error return du.connect(signer) } -async function getSidechainContract(client: StreamrClient, options: Todo = {}) { +async function getSidechainContract(client: StreamrClient, options: DataUnionOptions = {}) { const signer = await client.ethereum.getSidechainSigner() const duMainnet = getMainnetContractReadOnly(client, options) const duSidechainAddress = await getDataUnionSidechainAddress(client, duMainnet.address, options) @@ -529,7 +544,7 @@ async function getSidechainContract(client: StreamrClient, options: Todo = {}) { return duSidechain } -async function getSidechainContractReadOnly(client: StreamrClient, options: Todo = {}) { +async function getSidechainContractReadOnly(client: StreamrClient, options: DataUnionOptions = {}) { const provider = await client.ethereum.getSidechainProvider() const duMainnet = getMainnetContractReadOnly(client, options) const duSidechainAddress = await getDataUnionSidechainAddress(client, duMainnet.address, options) @@ -550,12 +565,12 @@ export class DataUnionEndpoints { // admin: DEPLOY AND SETUP DATA UNION // ////////////////////////////////////////////////////////////////// - async calculateDataUnionMainnetAddress(dataUnionName: Todo, deployerAddress: Todo, options: Todo) { + async calculateDataUnionMainnetAddress(dataUnionName: string, deployerAddress: string, options: DataUnionOptions) { const address = getAddress(deployerAddress) // throws if bad address return getDataUnionMainnetAddress(this.client, dataUnionName, address, options) } - async calculateDataUnionSidechainAddress(duMainnetAddress: Todo, options: Todo) { + async calculateDataUnionSidechainAddress(duMainnetAddress: string, options: DataUnionOptions) { const address = getAddress(duMainnetAddress) // throws if bad address return getDataUnionSidechainAddress(this.client, address, options) } @@ -590,7 +605,7 @@ export class DataUnionEndpoints { * @param {DeployOptions} options such as adminFee (default: 0) * @return {Promise} that resolves when the new DU is deployed over the bridge to side-chain */ - async deployDataUnion(options: Todo = {}) { + async deployDataUnion(options: DataUnionOptions = {}) { const { owner, joinPartAgents, @@ -598,7 +613,7 @@ export class DataUnionEndpoints { adminFee = 0, sidechainPollingIntervalMs = 1000, sidechainRetryTimeoutMs = 600000, - } = options + }: Todo = options let duName = dataUnionName if (!duName) { @@ -628,6 +643,7 @@ export class DataUnionEndpoints { } } + // @ts-expect-error const duMainnetAddress = await getDataUnionMainnetAddress(this.client, duName, ownerAddress, options) const duSidechainAddress = await getDataUnionSidechainAddress(this.client, duMainnetAddress, options) @@ -636,7 +652,7 @@ export class DataUnionEndpoints { } const factoryMainnetAddress = throwIfBadAddress( - options.factoryMainnetAddress || this.client.options.factoryMainnetAddress, + (options.factoryMainnetAddress || this.client.options.factoryMainnetAddress)!, 'StreamrClient.options.factoryMainnetAddress' ) if (await mainnetProvider.getCode(factoryMainnetAddress) === '0x') { @@ -645,7 +661,7 @@ export class DataUnionEndpoints { // function deployNewDataUnion(address owner, uint256 adminFeeFraction, address[] agents, string duName) // @ts-expect-error - const factoryMainnet = new Contract(factoryMainnetAddress, factoryMainnetABI, mainnetWallet) + const factoryMainnet = new Contract(factoryMainnetAddress!, factoryMainnetABI, mainnetWallet) const tx = await factoryMainnet.deployNewDataUnion( ownerAddress, adminFeeBN, @@ -671,7 +687,7 @@ export class DataUnionEndpoints { return dataUnion } - async getDataUnionContract(options: Todo = {}) { + async getDataUnionContract(options: DataUnionOptions = {}) { const ret = getMainnetContract(this.client, options) // @ts-expect-error ret.sidechain = await getSidechainContract(this.client, options) @@ -684,7 +700,7 @@ export class DataUnionEndpoints { * @param {String} name describes the secret * @returns {String} the server-generated secret */ - async createSecret(dataUnionMainnetAddress: Todo, name: string = 'Untitled Data Union Secret') { + async createSecret(dataUnionMainnetAddress: string, name: string = 'Untitled Data Union Secret') { const duAddress = getAddress(dataUnionMainnetAddress) // throws if bad address const url = getEndpointUrl(this.client.options.restUrl, 'dataunions', duAddress, 'secrets') const res = await authFetch( @@ -712,7 +728,7 @@ export class DataUnionEndpoints { * @param {List} memberAddressList to kick * @returns {Promise} partMembers sidechain transaction */ - async kick(memberAddressList: Todo, options: Todo = {}) { + async kick(memberAddressList: string[], options: DataUnionOptions = {}) { const members = memberAddressList.map(getAddress) // throws if there are bad addresses const duSidechain = await getSidechainContract(this.client, options) const tx = await duSidechain.partMembers(members) @@ -725,7 +741,7 @@ export class DataUnionEndpoints { * @param {List} memberAddressList to add * @returns {Promise} addMembers sidechain transaction */ - async addMembers(memberAddressList: Todo, options: Todo = {}) { + async addMembers(memberAddressList: string[], options: DataUnionOptions = {}) { const members = memberAddressList.map(getAddress) // throws if there are bad addresses const duSidechain = await getSidechainContract(this.client, options) const tx = await duSidechain.addMembers(members) @@ -740,7 +756,7 @@ export class DataUnionEndpoints { * @param {EthereumOptions} options (including e.g. `dataUnion` Contract object or address) * @returns {Promise} get receipt once withdraw transaction is confirmed */ - async withdrawMember(memberAddress: Todo, options: Todo) { + async withdrawMember(memberAddress: string, options: DataUnionOptions) { const address = getAddress(memberAddress) // throws if bad address const tr = await untilWithdrawIsComplete( this.client, @@ -758,7 +774,7 @@ export class DataUnionEndpoints { * @param {EthereumOptions} options * @returns {Promise} await on call .wait to actually send the tx */ - async getWithdrawMemberTx(memberAddress: Todo, options: Todo) { + async getWithdrawMemberTx(memberAddress: string, options: DataUnionOptions) { const a = getAddress(memberAddress) // throws if bad address const duSidechain = await getSidechainContract(this.client, options) return duSidechain.withdrawAll(a, true) // sendToMainnet=true @@ -773,7 +789,7 @@ export class DataUnionEndpoints { * @param {EthereumOptions} options * @returns {Promise} get receipt once withdraw transaction is confirmed */ - async withdrawToSigned(memberAddress: Todo, recipientAddress: Todo, signature: Todo, options: Todo) { + async withdrawToSigned(memberAddress: string, recipientAddress: string, signature: string, options: DataUnionOptions) { const from = getAddress(memberAddress) // throws if bad address const to = getAddress(recipientAddress) const tr = await untilWithdrawIsComplete( @@ -794,7 +810,7 @@ export class DataUnionEndpoints { * @param {EthereumOptions} options * @returns {Promise} await on call .wait to actually send the tx */ - async getWithdrawToSignedTx(memberAddress: Todo, recipientAddress: Todo, signature: Todo, options: Todo) { + async getWithdrawToSignedTx(memberAddress: string, recipientAddress: string, signature: string, options: DataUnionOptions) { const duSidechain = await getSidechainContract(this.client, options) return duSidechain.withdrawAllToSigned(memberAddress, recipientAddress, true, signature) // sendToMainnet=true } @@ -804,7 +820,7 @@ export class DataUnionEndpoints { * @param {number} newFeeFraction between 0.0 and 1.0 * @param {EthereumOptions} options */ - async setAdminFee(newFeeFraction: Todo, options: Todo) { + async setAdminFee(newFeeFraction: number, options: DataUnionOptions) { if (newFeeFraction < 0 || newFeeFraction > 1) { throw new Error('newFeeFraction argument must be a number between 0...1, got: ' + newFeeFraction) } @@ -818,13 +834,13 @@ export class DataUnionEndpoints { * Get data union admin fee fraction that admin gets from each revenue event * @returns {number} between 0.0 and 1.0 */ - async getAdminFee(options: Todo) { + async getAdminFee(options: DataUnionOptions) { const duMainnet = getMainnetContractReadOnly(this.client, options) const adminFeeBN = await duMainnet.adminFeeFraction() return +adminFeeBN.toString() / 1e18 } - async getAdminAddress(options: Todo) { + async getAdminAddress(options: DataUnionOptions) { const duMainnet = getMainnetContractReadOnly(this.client, options) return duMainnet.owner() } @@ -842,11 +858,11 @@ export class DataUnionEndpoints { * @property {String} member Ethereum mainnet address of the joining member. If not given, use StreamrClient authentication key * @property {String} secret if given, and correct, join the data union immediately */ - async joinDataUnion(options: Todo = {}) { + async joinDataUnion(options: DataUnionOptions = {}) { const { member, secret, - } = options + }: Todo = options const dataUnion = getMainnetContractReadOnly(this.client, options) const body = { @@ -876,11 +892,11 @@ export class DataUnionEndpoints { * @param {Number} retryTimeoutMs (optional, default: 60000) give up * @return {Promise} resolves when member is in the data union (or fails with HTTP error) */ - async hasJoined(memberAddress: Todo, options: Todo = {}) { + async hasJoined(memberAddress: string, options: DataUnionOptions = {}) { const { pollingIntervalMs = 1000, retryTimeoutMs = 60000, - } = options + }: Todo = options const address = parseAddress(this.client, memberAddress) const duSidechain = await getSidechainContractReadOnly(this.client, options) @@ -889,14 +905,14 @@ export class DataUnionEndpoints { } // TODO: this needs more thought: probably something like getEvents from sidechain? Heavy on RPC? - async getMembers(options: Todo) { + async getMembers(options: DataUnionOptions) { const duSidechain = await getSidechainContractReadOnly(this.client, options) throw new Error(`Not implemented for side-chain data union (at ${duSidechain.address})`) // event MemberJoined(address indexed); // event MemberParted(address indexed); } - async getDataUnionStats(options: Todo) { + async getDataUnionStats(options: DataUnionOptions) { const duSidechain = await getSidechainContractReadOnly(this.client, options) const [ totalEarnings, @@ -922,7 +938,7 @@ export class DataUnionEndpoints { * @param {EthereumAddress} dataUnion to query * @param {EthereumAddress} memberAddress (optional) if not supplied, get the stats of currently logged in StreamrClient (if auth: privateKey) */ - async getMemberStats(memberAddress: Todo, options: Todo) { + async getMemberStats(memberAddress: string, options: DataUnionOptions) { const address = parseAddress(this.client, memberAddress) // TODO: use duSidechain.getMemberStats(address) once it's implemented, to ensure atomic read // (so that memberData is from same block as getEarnings, otherwise withdrawable will be foobar) @@ -946,7 +962,7 @@ export class DataUnionEndpoints { * @param memberAddress whose balance is returned * @return {Promise} */ - async getMemberBalance(memberAddress: Todo, options: Todo) { + async getMemberBalance(memberAddress: string, options: DataUnionOptions) { const address = parseAddress(this.client, memberAddress) const duSidechain = await getSidechainContractReadOnly(this.client, options) return duSidechain.getWithdrawableEarnings(address) @@ -960,7 +976,7 @@ export class DataUnionEndpoints { * was given in StreamrClient constructor. * @returns {Promise} token balance in "wei" (10^-18 parts) */ - async getTokenBalance(address: Todo, options: Todo) { + async getTokenBalance(address: string|null|undefined, options: DataUnionOptions) { const a = parseAddress(this.client, address) const tokenAddressMainnet = options.tokenAddress || ( await getMainnetContractReadOnly(this.client, options).then((c: Todo) => c.token()).catch(() => null) || this.client.options.tokenAddress @@ -985,7 +1001,7 @@ export class DataUnionEndpoints { * @param {EthereumAddress} contractAddress * @returns {number} 1 for old, 2 for current, zero for "not a data union" */ - async getDataUnionVersion(contractAddress: Todo) { + async getDataUnionVersion(contractAddress: string) { const a = getAddress(contractAddress) // throws if bad address const provider = this.client.ethereum.getMainnetProvider() const du = new Contract(a, [{ @@ -1012,7 +1028,7 @@ export class DataUnionEndpoints { * @param {EthereumOptions} options (including e.g. `dataUnion` Contract object or address) * @returns {Promise} get receipt once withdraw is complete (tokens are seen in mainnet) */ - async withdraw(options: Todo = {}) { + async withdraw(options: DataUnionOptions = {}) { const tr = await untilWithdrawIsComplete( this.client, (opts) => this.getWithdrawTx(opts), @@ -1027,7 +1043,7 @@ export class DataUnionEndpoints { * @param {EthereumOptions} options (including e.g. `dataUnion` Contract object or address) * @returns {Promise} await on call .wait to actually send the tx */ - async getWithdrawTx(options: Todo) { + async getWithdrawTx(options: DataUnionOptions) { const signer = await this.client.ethereum.getSidechainSigner() // @ts-expect-error const address = await signer.getAddress() @@ -1051,7 +1067,7 @@ export class DataUnionEndpoints { * @param {EthereumOptions} options (including e.g. `dataUnion` Contract object or address) * @returns {Promise} get receipt once withdraw is complete (tokens are seen in mainnet) */ - async withdrawTo(recipientAddress: Todo, options = {}) { + async withdrawTo(recipientAddress: string, options: DataUnionOptions = {}) { const to = getAddress(recipientAddress) // throws if bad address const tr = await untilWithdrawIsComplete( this.client, @@ -1068,7 +1084,7 @@ export class DataUnionEndpoints { * @param {EthereumOptions} options (including e.g. `dataUnion` Contract object or address) * @returns {Promise} await on call .wait to actually send the tx */ - async getWithdrawTxTo(recipientAddress: Todo, options: Todo) { + async getWithdrawTxTo(recipientAddress: string, options: DataUnionOptions) { const signer = await this.client.ethereum.getSidechainSigner() // @ts-expect-error const address = await signer.getAddress() @@ -1094,7 +1110,7 @@ export class DataUnionEndpoints { * @param {EthereumOptions} options (including e.g. `dataUnion` Contract object or address) * @returns {string} signature authorizing withdrawing all earnings to given recipientAddress */ - async signWithdrawTo(recipientAddress: Todo, options: Todo) { + async signWithdrawTo(recipientAddress: string, options: DataUnionOptions) { return this.signWithdrawAmountTo(recipientAddress, BigNumber.from(0), options) } @@ -1107,7 +1123,7 @@ export class DataUnionEndpoints { * @param {EthereumOptions} options (including e.g. `dataUnion` Contract object or address) * @returns {string} signature authorizing withdrawing all earnings to given recipientAddress */ - async signWithdrawAmountTo(recipientAddress: Todo, amountTokenWei: Todo, options: Todo) { + async signWithdrawAmountTo(recipientAddress: string, amountTokenWei: BigNumber|number|string, options: DataUnionOptions) { const to = getAddress(recipientAddress) // throws if bad address const signer = this.client.ethereum.getSigner() // it shouldn't matter if it's mainnet or sidechain signer since key should be the same // @ts-expect-error @@ -1116,6 +1132,7 @@ export class DataUnionEndpoints { const memberData = await duSidechain.memberData(address) if (memberData[0] === '0') { throw new Error(`${address} is not a member in Data Union (sidechain address ${duSidechain.address})`) } const withdrawn = memberData[3] + // @ts-expect-error const message = to + hexZeroPad(amountTokenWei, 32).slice(2) + duSidechain.address.slice(2) + hexZeroPad(withdrawn, 32).slice(2) // @ts-expect-error const signature = await signer.signMessage(arrayify(message)) diff --git a/src/rest/LoginEndpoints.ts b/src/rest/LoginEndpoints.ts index 910c3b3d2..af281830f 100644 --- a/src/rest/LoginEndpoints.ts +++ b/src/rest/LoginEndpoints.ts @@ -1,10 +1,9 @@ import StreamrClient from '../StreamrClient' -import { Todo } from '../types' import { getEndpointUrl } from '../utils' import authFetch, { AuthFetchError } from './authFetch' -async function getSessionToken(url: Todo, props: Todo) { +async function getSessionToken(url: string, props: any) { return authFetch( url, undefined, @@ -26,7 +25,7 @@ export class LoginEndpoints { this.client = client } - async getChallenge(address: Todo) { + async getChallenge(address: string) { this.client.debug('getChallenge %o', { address, }) @@ -40,7 +39,7 @@ export class LoginEndpoints { ) } - async sendChallengeResponse(challenge: Todo, signature: Todo, address: Todo) { + async sendChallengeResponse(challenge: string, signature: string, address: string) { this.client.debug('sendChallengeResponse %o', { challenge, signature, @@ -55,7 +54,7 @@ export class LoginEndpoints { return getSessionToken(url, props) } - async loginWithChallengeResponse(signingFunction: Todo, address: Todo) { + async loginWithChallengeResponse(signingFunction: (challenge: string) => Promise, address: string) { this.client.debug('loginWithChallengeResponse %o', { address, }) @@ -64,7 +63,7 @@ export class LoginEndpoints { return this.sendChallengeResponse(challenge, signature, address) } - async loginWithApiKey(apiKey: Todo) { + async loginWithApiKey(apiKey: string) { this.client.debug('loginWithApiKey %o', { apiKey, }) @@ -75,7 +74,7 @@ export class LoginEndpoints { return getSessionToken(url, props) } - async loginWithUsernamePassword(username: Todo, password: Todo) { + async loginWithUsernamePassword(username: string, password: string) { this.client.debug('loginWithUsernamePassword %o', { username, }) diff --git a/src/rest/StreamEndpoints.ts b/src/rest/StreamEndpoints.ts index 3b58d1f5f..95a8071f8 100644 --- a/src/rest/StreamEndpoints.ts +++ b/src/rest/StreamEndpoints.ts @@ -6,7 +6,7 @@ import debugFactory from 'debug' import { getEndpointUrl } from '../utils' import { validateOptions } from '../stream/utils' -import Stream from '../stream' +import Stream, { StreamOperation, StreamProperties } from '../stream' import StreamPart from '../stream/StreamPart' import { isKeyExchangeStream } from '../stream/KeyExchange' @@ -16,6 +16,20 @@ import StreamrClient from '../StreamrClient' const debug = debugFactory('StreamrClient') +export interface StreamListQuery { + name?: string + uiChannel?: boolean + noConfig?: boolean + search?: string + sortBy?: string + order?: 'asc'|'desc' + max?: number + offset?: number + grantedAccess?: boolean + publicAccess?: boolean + operation?: StreamOperation +} + const agentSettings = { keepAlive: true, keepAliveMsecs: 5000, @@ -46,7 +60,7 @@ export class StreamEndpoints { this.client = client } - async getStream(streamId: Todo) { + async getStream(streamId: string) { this.client.debug('getStream %o', { streamId, }) @@ -70,7 +84,7 @@ export class StreamEndpoints { } } - async listStreams(query: Todo = {}) { + async listStreams(query: StreamListQuery = {}) { this.client.debug('listStreams %o', { query, }) @@ -85,12 +99,13 @@ export class StreamEndpoints { }) const json = await this.listStreams({ name, + // @ts-expect-error public: false, }) return json[0] ? new Stream(this.client, json[0]) : undefined } - async createStream(props: Todo) { + async createStream(props: StreamProperties) { this.client.debug('createStream %o', { props, }) @@ -106,7 +121,7 @@ export class StreamEndpoints { return json ? new Stream(this.client, json) : undefined } - async getOrCreateStream(props: Todo) { + async getOrCreateStream(props: { id?: string, name?: string }) { this.client.debug('getOrCreateStream %o', { props, }) @@ -133,7 +148,7 @@ export class StreamEndpoints { } } - async getStreamPublishers(streamId: Todo) { + async getStreamPublishers(streamId: string) { this.client.debug('getStreamPublishers %o', { streamId, }) @@ -142,7 +157,7 @@ export class StreamEndpoints { return json.addresses.map((a: string) => a.toLowerCase()) } - async isStreamPublisher(streamId: Todo, ethAddress: Todo) { + async isStreamPublisher(streamId: string, ethAddress: string) { this.client.debug('isStreamPublisher %o', { streamId, ethAddress, @@ -160,16 +175,16 @@ export class StreamEndpoints { } } - async getStreamSubscribers(streamId: Todo) { + async getStreamSubscribers(streamId: string) { this.client.debug('getStreamSubscribers %o', { streamId, }) const url = getEndpointUrl(this.client.options.restUrl, 'streams', streamId, 'subscribers') const json = await authFetch(url, this.client.session) - return json.addresses.map((a: Todo) => a.toLowerCase()) + return json.addresses.map((a: string) => a.toLowerCase()) } - async isStreamSubscriber(streamId: Todo, ethAddress: Todo) { + async isStreamSubscriber(streamId: string, ethAddress: string) { this.client.debug('isStreamSubscriber %o', { streamId, ethAddress, @@ -186,7 +201,7 @@ export class StreamEndpoints { } } - async getStreamValidationInfo(streamId: Todo) { + async getStreamValidationInfo(streamId: string) { this.client.debug('getStreamValidationInfo %o', { streamId, }) @@ -195,7 +210,7 @@ export class StreamEndpoints { return json } - async getStreamLast(streamObjectOrId: Todo) { + async getStreamLast(streamObjectOrId: Stream|string) { const { streamId, streamPartition = 0, count = 1 } = validateOptions(streamObjectOrId) this.client.debug('getStreamLast %o', { streamId, @@ -211,16 +226,16 @@ export class StreamEndpoints { return json } - async getStreamPartsByStorageNode(address: Todo) { + async getStreamPartsByStorageNode(address: string) { const json = await authFetch(getEndpointUrl(this.client.options.restUrl, 'storageNodes', address, 'streams'), this.client.session) - let result: Todo = [] - json.forEach((stream: Todo) => { + let result: StreamPart[] = [] + json.forEach((stream: { id: string, partitions: number }) => { result = result.concat(StreamPart.fromStream(stream)) }) return result } - async publishHttp(streamObjectOrId: Todo, data: Todo, requestOptions: Todo = {}, keepAlive: Todo = true) { + async publishHttp(streamObjectOrId: Stream|string, data: Todo, requestOptions: Todo = {}, keepAlive: boolean = true) { let streamId if (streamObjectOrId instanceof Stream) { streamId = streamObjectOrId.id @@ -239,7 +254,7 @@ export class StreamEndpoints { ...requestOptions, method: 'POST', body: JSON.stringify(data), - agent: keepAlive ? getKeepAliveAgentForUrl(this.client.options.restUrl) : undefined, + agent: keepAlive ? getKeepAliveAgentForUrl(this.client.options.restUrl!) : undefined, }, ) } diff --git a/src/stream/StreamPart.js b/src/stream/StreamPart.ts similarity index 62% rename from src/stream/StreamPart.js rename to src/stream/StreamPart.ts index e3a3eab4d..9371b7ae6 100644 --- a/src/stream/StreamPart.js +++ b/src/stream/StreamPart.ts @@ -1,11 +1,15 @@ export default class StreamPart { - constructor(streamId, streamPartition) { + + _streamId: string + _streamPartition: number + + constructor(streamId: string, streamPartition: number) { this._streamId = streamId this._streamPartition = streamPartition } - static fromStream({ id, partitions }) { - const result = [] + static fromStream({ id, partitions }: { id: string, partitions: number }) { + const result: StreamPart[] = [] for (let i = 0; i < partitions; i++) { result.push(new StreamPart(id, i)) } diff --git a/src/stream/index.ts b/src/stream/index.ts index 37117a808..7f9c4ac9f 100644 --- a/src/stream/index.ts +++ b/src/stream/index.ts @@ -5,6 +5,17 @@ import StorageNode from './StorageNode' import StreamrClient from '../StreamrClient' import { Todo } from '../types' +export enum StreamOperation { + STREAM_GET = 'stream_get', + STREAM_EDIT = 'stream_edit', + STREAM_DELETE = 'stream_delete', + STREAM_PUBLISH = 'stream_publish', + STREAM_SUBSCRIBE = 'stream_subscribe', + STREAM_SHARE = 'stream_share' +} + +export type StreamProperties = Todo + export default class Stream { // TODO add field definitions for all fields @@ -12,7 +23,7 @@ export default class Stream { id: string _client: StreamrClient - constructor(client: StreamrClient, props: Todo) { + constructor(client: StreamrClient, props: StreamProperties) { this._client = client Object.assign(this, props) } @@ -64,13 +75,13 @@ export default class Stream { ) } - async hasPermission(operation: Todo, userId: Todo) { + async hasPermission(operation: StreamOperation, userId: string|undefined) { // eth addresses may be in checksumcase, but userId from server has no case const userIdCaseInsensitive = typeof userId === 'string' ? userId.toLowerCase() : undefined // if not string then undefined const permissions = await this.getPermissions() - return permissions.find((p: Todo) => { + return permissions.find((p: any) => { if (p.operation !== operation) { return false } if (userIdCaseInsensitive === undefined) { @@ -80,8 +91,8 @@ export default class Stream { }) } - async grantPermission(operation: Todo, userId: Todo) { - const permissionObject: Todo = { + async grantPermission(operation: StreamOperation, userId: string|undefined) { + const permissionObject: any = { operation, } @@ -103,7 +114,7 @@ export default class Stream { ) } - async revokePermission(permissionId: Todo) { + async revokePermission(permissionId: number) { return authFetch( getEndpointUrl(this._client.options.restUrl, 'streams', this.id, 'permissions', permissionId), this._client.session, @@ -120,7 +131,7 @@ export default class Stream { ) } - async addToStorageNode(address: Todo) { + async addToStorageNode(address: string) { return authFetch( getEndpointUrl(this._client.options.restUrl, 'streams', this.id, 'storageNodes'), this._client.session, @@ -133,7 +144,7 @@ export default class Stream { ) } - async removeFromStorageNode(address: Todo) { + async removeFromStorageNode(address: string) { return authFetch( getEndpointUrl(this._client.options.restUrl, 'streams', this.id, 'storageNodes', address), this._client.session, @@ -148,7 +159,7 @@ export default class Stream { getEndpointUrl(this._client.options.restUrl, 'streams', this.id, 'storageNodes'), this._client.session, ) - return json.map((item: Todo) => new StorageNode(item.storageNodeAddress)) + return json.map((item: any) => new StorageNode(item.storageNodeAddress)) } async publish(...theArgs: Todo) { diff --git a/src/types.ts b/src/types.ts index ff7342d4a..a90812a10 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,3 +1 @@ export type Todo = any - -export type StreamrClientAndEndpoints = any \ No newline at end of file diff --git a/test/integration/LoginEndpoints.test.js b/test/integration/LoginEndpoints.test.js index 9ba86f428..a544a483b 100644 --- a/test/integration/LoginEndpoints.test.js +++ b/test/integration/LoginEndpoints.test.js @@ -27,7 +27,7 @@ describe('LoginEndpoints', () => { describe('Challenge generation', () => { it('should retrieve a challenge', async () => { - const challenge = await client.getChallenge('some-address') + const challenge = await client.loginEndpoints.getChallenge('some-address') assert(challenge) assert(challenge.id) assert(challenge.challenge) @@ -38,7 +38,7 @@ describe('LoginEndpoints', () => { describe('Challenge response', () => { it('should fail to get a session token', async () => { await expect(async () => { - await client.sendChallengeResponse({ + await client.loginEndpoints.sendChallengeResponse({ id: 'some-id', challenge: 'some-challenge', }, 'some-sig', 'some-address') @@ -47,10 +47,10 @@ describe('LoginEndpoints', () => { it('should get a session token', async () => { const wallet = ethers.Wallet.createRandom() - const challenge = await client.getChallenge(wallet.address) + const challenge = await client.loginEndpoints.getChallenge(wallet.address) assert(challenge.challenge) const signature = await wallet.signMessage(challenge.challenge) - const sessionToken = await client.sendChallengeResponse(challenge, signature, wallet.address) + const sessionToken = await client.loginEndpoints.sendChallengeResponse(challenge, signature, wallet.address) assert(sessionToken) assert(sessionToken.token) assert(sessionToken.expires) @@ -58,7 +58,7 @@ describe('LoginEndpoints', () => { it('should get a session token with combined function', async () => { const wallet = ethers.Wallet.createRandom() - const sessionToken = await client.loginWithChallengeResponse((d) => wallet.signMessage(d), wallet.address) + const sessionToken = await client.loginEndpoints.loginWithChallengeResponse((d) => wallet.signMessage(d), wallet.address) assert(sessionToken) assert(sessionToken.token) assert(sessionToken.expires) @@ -73,7 +73,7 @@ describe('LoginEndpoints', () => { }) it('should get a session token', async () => { - const sessionToken = await client.loginWithApiKey('tester1-api-key') + const sessionToken = await client.loginEndpoints.loginWithApiKey('tester1-api-key') assert(sessionToken) assert(sessionToken.token) assert(sessionToken.expires) @@ -83,14 +83,14 @@ describe('LoginEndpoints', () => { describe('Username/password login', () => { it('should fail', async () => { await expect(async () => { - await client.loginWithUsernamePassword('username', 'password') + await client.loginEndpoints.loginWithUsernamePassword('username', 'password') }).rejects.toThrow('no longer supported') }) }) describe('UserInfo', () => { it('should get user info', async () => { - const userInfo = await client.getUserInfo() + const userInfo = await client.loginEndpoints.getUserInfo() assert(userInfo.name) assert(userInfo.username) }) @@ -100,7 +100,7 @@ describe('LoginEndpoints', () => { it('should not be able to use the same session token after logout', async () => { await client.getUserInfo() // first fetches the session token, then requests the endpoint const sessionToken1 = client.session.options.sessionToken - await client.logoutEndpoint() // invalidates the session token in engine-and-editor + await client.loginEndpoints.logoutEndpoint() // invalidates the session token in engine-and-editor await client.getUserInfo() // requests the endpoint with sessionToken1, receives 401, fetches a new session token const sessionToken2 = client.session.options.sessionToken assert.notDeepStrictEqual(sessionToken1, sessionToken2) diff --git a/test/integration/StreamEndpoints.test.js b/test/integration/StreamEndpoints.test.js index e72aae728..7f50b741f 100644 --- a/test/integration/StreamEndpoints.test.js +++ b/test/integration/StreamEndpoints.test.js @@ -230,7 +230,7 @@ function TestStreamEndpoints(getName) { }) }) - describe.only('Storage node assignment', () => { + describe('Storage node assignment', () => { it('add', async () => { const storageNodeAddress = ethers.Wallet.createRandom().address const stream = await client.createStream() diff --git a/test/unit/Session.test.js b/test/unit/Session.test.js index 52309eba4..08046b45d 100644 --- a/test/unit/Session.test.js +++ b/test/unit/Session.test.js @@ -23,7 +23,9 @@ describe('Session', () => { sessionToken: 'session-token', }, }) - clientSessionToken.logoutEndpoint = sinon.stub().resolves() + clientSessionToken.loginEndpoints = { + logoutEndpoint: sinon.stub().resolves() + } session = new Session(clientSessionToken) session.options.unauthenticated = false @@ -160,7 +162,7 @@ describe('Session', () => { it('should call the logout endpoint', async () => { await session.getSessionToken() await session.logout() - expect(clientSessionToken.logoutEndpoint.calledOnce).toBeTruthy() + expect(clientSessionToken.loginEndpoints.logoutEndpoint.calledOnce).toBeTruthy() }) it('should call the logout endpoint again', async () => { @@ -169,7 +171,7 @@ describe('Session', () => { await session.logout() await session.getSessionToken() await session.logout() - expect(clientSessionToken.logoutEndpoint.calledTwice).toBeTruthy() + expect(clientSessionToken.loginEndpoints.logoutEndpoint.calledTwice).toBeTruthy() }) it('should throw if already logging out', async () => { From 5753cb5c0064499f6c01cf3a7756b08dd4bce1de Mon Sep 17 00:00:00 2001 From: Teo Gebhard Date: Tue, 16 Feb 2021 15:14:25 +0200 Subject: [PATCH 11/12] Hide private login endpoints --- src/StreamrClient.ts | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/src/StreamrClient.ts b/src/StreamrClient.ts index a4cd0fb71..a5a045960 100644 --- a/src/StreamrClient.ts +++ b/src/StreamrClient.ts @@ -464,35 +464,11 @@ export default class StreamrClient extends EventEmitter { async publishHttp(streamObjectOrId: Stream|string, data: Todo, requestOptions: Todo = {}, keepAlive: boolean = true) { return this.streamEndpoints.publishHttp(streamObjectOrId, data, requestOptions, keepAlive) } - - async getChallenge(address: Todo) { - return this.loginEndpoints.getChallenge(address) - } - - async sendChallengeResponse(challenge: Todo, signature: Todo, address: Todo) { - return this.loginEndpoints.sendChallengeResponse(challenge, signature, address) - } - - async loginWithChallengeResponse(signingFunction: Todo, address: Todo) { - return this.loginEndpoints.loginWithChallengeResponse(signingFunction, address) - } - - async loginWithApiKey(apiKey: Todo) { - return this.loginEndpoints.loginWithApiKey(apiKey) - } - - async loginWithUsernamePassword(username: Todo, password: Todo) { - return this.loginEndpoints.loginWithUsernamePassword(username, password) - } async getUserInfo() { return this.loginEndpoints.getUserInfo() } - async logoutEndpoint() { - return this.loginEndpoints.logoutEndpoint() - } - async calculateDataUnionMainnetAddress(dataUnionName: string, deployerAddress: string, options: DataUnionOptions) { return this.dataUnionEndpoints.calculateDataUnionMainnetAddress(dataUnionName, deployerAddress, options) } From beefb7cb10b8fdc071211ef9bc09f93feda5a12b Mon Sep 17 00:00:00 2001 From: Tim Oxley Date: Tue, 16 Feb 2021 16:33:09 -0500 Subject: [PATCH 12/12] Retry browser tests. --- .github/workflows/nodejs.yml | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index cda454975..5051e46c5 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -157,9 +157,14 @@ jobs: uses: streamr-dev/streamr-docker-dev-action@v1.0.0-alpha.2 with: services-to-start: "mysql redis engine-and-editor cassandra parity-node0 parity-sidechain-node0 bridge broker-node-storage-1 nginx smtp" - - name: test-browser - timeout-minutes: 2 - run: npm run test-browser + + - uses: nick-invision/retry@v2 + name: Run Test + with: + max_attempts: 3 + timeout_minutes: 3 + retry_on: error + command: npm run test-browser benchmarks: name: Test Benchmark using Node ${{ matrix.node-version }}