diff --git a/package-lock.json b/package-lock.json index 4fe9e062a..ae8d486a7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "@mdi/font": "^7.4.47", "@stablelib/utf8": "^2.0.0", "@tanstack/vue-query": "^5.52.2", + "@vueuse/core": "^11.1.0", "@zxing/browser": "^0.1.5", "@zxing/library": "^0.21.3", "assert": "^2.1.0", @@ -51,6 +52,7 @@ "lodash": "^4.17.21", "marked": "^14.1.0", "mitt": "^3.0.1", + "multiformats": "^13.3.0", "notifyjs": "^3.0.0", "npm": "^10.8.3", "os-browserify": "^0.3.0", @@ -109,6 +111,7 @@ "@typescript-eslint/eslint-plugin": "^8.3.0", "@typescript-eslint/parser": "^8.3.0", "@vitejs/plugin-vue": "^5.1.2", + "@vitejs/plugin-vue-jsx": "^4.0.1", "@vue/eslint-config-prettier": "^9.0.0", "@vue/test-utils": "^2.4.6", "autoprefixer": "^10.4.20", @@ -933,6 +936,21 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.7.tgz", + "integrity": "sha512-6ddciUPe/mpMnOKv/U+RSd2vvVy+Yw/JfBB0ZHYjEZt9NLHmCUylNYlsbqCCS1Bffjlb0fCwC9Vqz+sBz6PsiQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-syntax-logical-assignment-operators": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", @@ -1043,6 +1061,21 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.4.tgz", + "integrity": "sha512-uMOCoHVU52BsSWxPOMVv5qKRdeSlPuImUCB2dlPuBSU+W2/ROE7/Zg8F2Kepbk+8yBa68LlRKxO+xgEVWorsDg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.8" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-syntax-unicode-sets-regex": { "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", @@ -1837,6 +1870,25 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.25.2.tgz", + "integrity": "sha512-lBwRvjSmqiMYe/pS0+1gggjJleUJi7NzjvQ1Fkqtt69hBa/0t1YuW/MLQMAPixfwaQOHUXsd6jeU3Z+vdGv3+A==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-create-class-features-plugin": "^7.25.0", + "@babel/helper-plugin-utils": "^7.24.8", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", + "@babel/plugin-syntax-typescript": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-unicode-escapes": { "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.24.7.tgz", @@ -4711,10 +4763,9 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", - "dev": true + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.25", @@ -6731,6 +6782,12 @@ "dev": true, "optional": true }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.20", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz", + "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==", + "license": "MIT" + }, "node_modules/@types/ws": { "version": "8.5.3", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz", @@ -6987,6 +7044,24 @@ "vue": "^3.2.25" } }, + "node_modules/@vitejs/plugin-vue-jsx": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue-jsx/-/plugin-vue-jsx-4.0.1.tgz", + "integrity": "sha512-7mg9HFGnFHMEwCdB6AY83cVK4A6sCqnrjFYF4WIlebYAQVVJ/sC/CiTruVdrRlhrFoeZ8rlMxY9wYpPTIRhhAg==", + "dev": true, + "dependencies": { + "@babel/core": "^7.24.7", + "@babel/plugin-transform-typescript": "^7.24.7", + "@vue/babel-plugin-jsx": "^1.2.2" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0", + "vue": "^3.0.0" + } + }, "node_modules/@vitest/expect": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.0.5.tgz", @@ -7113,6 +7188,110 @@ "vscode-uri": "^3.0.8" } }, + "node_modules/@vue/babel-helper-vue-transform-on": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-1.2.5.tgz", + "integrity": "sha512-lOz4t39ZdmU4DJAa2hwPYmKc8EsuGa2U0L9KaZaOJUt0UwQNjNA3AZTq6uEivhOKhhG1Wvy96SvYBoFmCg3uuw==", + "dev": true + }, + "node_modules/@vue/babel-plugin-jsx": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@vue/babel-plugin-jsx/-/babel-plugin-jsx-1.2.5.tgz", + "integrity": "sha512-zTrNmOd4939H9KsRIGmmzn3q2zvv1mjxkYZHgqHZgDrXz5B1Q3WyGEjO2f+JrmKghvl1JIRcvo63LgM1kH5zFg==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.8", + "@babel/plugin-syntax-jsx": "^7.24.7", + "@babel/template": "^7.25.0", + "@babel/traverse": "^7.25.6", + "@babel/types": "^7.25.6", + "@vue/babel-helper-vue-transform-on": "1.2.5", + "@vue/babel-plugin-resolve-type": "1.2.5", + "html-tags": "^3.3.1", + "svg-tags": "^1.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + } + } + }, + "node_modules/@vue/babel-plugin-resolve-type": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@vue/babel-plugin-resolve-type/-/babel-plugin-resolve-type-1.2.5.tgz", + "integrity": "sha512-U/ibkQrf5sx0XXRnUZD1mo5F7PkpKyTbfXM3a3rC4YnUz6crHEz9Jg09jzzL6QYlXNto/9CePdOg/c87O4Nlfg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.8", + "@babel/parser": "^7.25.6", + "@vue/compiler-sfc": "^3.5.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@vue/babel-plugin-resolve-type/node_modules/@vue/compiler-core": { + "version": "3.5.5", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.5.tgz", + "integrity": "sha512-ZrxcY8JMoV+kgDrmRwlDufz0SjDZ7jfoNZiIBluAACMBmgr55o/jTbxnyrccH6VSJXnFaDI4Ik1UFCiq9r8i7w==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.25.3", + "@vue/shared": "3.5.5", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.0" + } + }, + "node_modules/@vue/babel-plugin-resolve-type/node_modules/@vue/compiler-dom": { + "version": "3.5.5", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.5.tgz", + "integrity": "sha512-HSvK5q1gmBbxRse3S0Wt34RcKuOyjDJKDDMuF3i7NC+QkDFrbAqw8NnrEm/z7zFDxWZa4/5eUwsBOMQzm1RHBA==", + "dev": true, + "dependencies": { + "@vue/compiler-core": "3.5.5", + "@vue/shared": "3.5.5" + } + }, + "node_modules/@vue/babel-plugin-resolve-type/node_modules/@vue/compiler-sfc": { + "version": "3.5.5", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.5.tgz", + "integrity": "sha512-MzBHDxwZhgQPHrwJ5tj92gdTYRCuPDSZr8PY3+JFv8cv2UD5/WayH5yo0kKCkKfrtJhc39jNSMityHrkMSbfnA==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.25.3", + "@vue/compiler-core": "3.5.5", + "@vue/compiler-dom": "3.5.5", + "@vue/compiler-ssr": "3.5.5", + "@vue/shared": "3.5.5", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.11", + "postcss": "^8.4.44", + "source-map-js": "^1.2.0" + } + }, + "node_modules/@vue/babel-plugin-resolve-type/node_modules/@vue/compiler-ssr": { + "version": "3.5.5", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.5.tgz", + "integrity": "sha512-oFasHnpv/upubjJEmqiTKQYb4qS3ziJddf4UVWuFw6ebk/QTrTUc+AUoTJdo39x9g+AOQBzhOU0ICCRuUjvkmw==", + "dev": true, + "dependencies": { + "@vue/compiler-dom": "3.5.5", + "@vue/shared": "3.5.5" + } + }, + "node_modules/@vue/babel-plugin-resolve-type/node_modules/@vue/shared": { + "version": "3.5.5", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.5.tgz", + "integrity": "sha512-0KyMXyEgnmFAs6rNUL+6eUHtUCqCaNrVd+AW3MX3LyA0Yry5SA0Km03CDKiOua1x1WWnIr+W9+S0GMFoSDWERQ==", + "dev": true + }, "node_modules/@vue/compiler-core": { "version": "3.4.38", "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.38.tgz", @@ -7306,6 +7485,94 @@ "vue-component-type-helpers": "^2.0.0" } }, + "node_modules/@vueuse/core": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-11.1.0.tgz", + "integrity": "sha512-P6dk79QYA6sKQnghrUz/1tHi0n9mrb/iO1WTMk/ElLmTyNqgDeSZ3wcDf6fRBGzRJbeG1dxzEOvLENMjr+E3fg==", + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.20", + "@vueuse/metadata": "11.1.0", + "@vueuse/shared": "11.1.0", + "vue-demi": ">=0.14.10" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/core/node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/@vueuse/metadata": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-11.1.0.tgz", + "integrity": "sha512-l9Q502TBTaPYGanl1G+hPgd3QX5s4CGnpXriVBR5fEZ/goI6fvDaVmIl3Td8oKFurOxTmbXvBPSsgrd6eu6HYg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-11.1.0.tgz", + "integrity": "sha512-YUtIpY122q7osj+zsNMFAfMTubGz0sn5QzE5gPzAIiCmtt2ha3uQUY1+JPyL4gRCTsLPX82Y9brNbo/aqlA91w==", + "license": "MIT", + "dependencies": { + "vue-demi": ">=0.14.10" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared/node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, "node_modules/@webassemblyjs/ast": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz", @@ -14250,6 +14517,18 @@ "node": ">=18" } }, + "node_modules/html-tags": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz", + "integrity": "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/http-cache-semantics": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", @@ -16552,18 +16831,13 @@ } }, "node_modules/magic-string": { - "version": "0.30.10", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.10.tgz", - "integrity": "sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==", + "version": "0.30.11", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", + "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15" + "@jridgewell/sourcemap-codec": "^1.5.0" } }, - "node_modules/magic-string/node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" - }, "node_modules/make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", @@ -16908,6 +17182,12 @@ "dev": true, "license": "MIT" }, + "node_modules/multiformats": { + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-13.3.0.tgz", + "integrity": "sha512-CBiqvsufgmpo01VT5ze94O+uc+Pbf6f/sThlvWss0sBZmAOu6GQn5usrYV2sf2mr17FWYc0rO8c/CNe2T90QAA==", + "license": "Apache-2.0 OR MIT" + }, "node_modules/mute-stream": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", @@ -20218,10 +20498,9 @@ "dev": true }, "node_modules/picocolors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", - "license": "ISC" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==" }, "node_modules/picomatch": { "version": "2.3.1", @@ -20321,9 +20600,9 @@ } }, "node_modules/postcss": { - "version": "8.4.41", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz", - "integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==", + "version": "8.4.47", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", + "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", "funding": [ { "type": "opencollective", @@ -20338,11 +20617,10 @@ "url": "https://github.com/sponsors/ai" } ], - "license": "MIT", "dependencies": { "nanoid": "^3.3.7", - "picocolors": "^1.0.1", - "source-map-js": "^1.2.0" + "picocolors": "^1.1.0", + "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12 || >=14" @@ -22615,9 +22893,9 @@ } }, "node_modules/source-map-js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", - "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "engines": { "node": ">=0.10.0" } @@ -23057,6 +23335,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/svg-tags": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/svg-tags/-/svg-tags-1.0.0.tgz", + "integrity": "sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==", + "dev": true + }, "node_modules/swagger-schema-official": { "version": "2.0.0-bab6bed", "resolved": "https://registry.npmjs.org/swagger-schema-official/-/swagger-schema-official-2.0.0-bab6bed.tgz", diff --git a/package.json b/package.json index ef4687209..bc141adb1 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "@mdi/font": "^7.4.47", "@stablelib/utf8": "^2.0.0", "@tanstack/vue-query": "^5.52.2", + "@vueuse/core": "^11.1.0", "@zxing/browser": "^0.1.5", "@zxing/library": "^0.21.3", "assert": "^2.1.0", @@ -75,6 +76,7 @@ "lodash": "^4.17.21", "marked": "^14.1.0", "mitt": "^3.0.1", + "multiformats": "^13.3.0", "notifyjs": "^3.0.0", "npm": "^10.8.3", "os-browserify": "^0.3.0", @@ -133,6 +135,7 @@ "@typescript-eslint/eslint-plugin": "^8.3.0", "@typescript-eslint/parser": "^8.3.0", "@vitejs/plugin-vue": "^5.1.2", + "@vitejs/plugin-vue-jsx": "^4.0.1", "@vue/eslint-config-prettier": "^9.0.0", "@vue/test-utils": "^2.4.6", "autoprefixer": "^10.4.20", diff --git a/src/App.vue b/src/App.vue index f66d633dd..172ace6d1 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,5 +1,6 @@ - -``` - -## Types - -```ts -export type Message = { - id: number - senderId: string - message: string - timestamp: number - admTimestamp: number - amount: number - i18n: boolean - status: MessageStatus - type: MessageType -} - -export enum MessageType { - Message = 'message', - ADM = 'ADM', - ETH = 'ETH' -} - -export enum MessageStatus { - sent, - confirmed, - rejected -} - -export type User = { - id: string - name?: string -} -``` - -## AChat.vue - -### Props - -| Name | Type | Default | Description | -| :-----------------: | :---------: | :---------: | :------------------------------------------------------- | -| **[`messages`](#)** | `Message[]` | `[]` | Array of messages | -| **[`partners`](#)** | `User[]` | `[]` | Array of users who participate in chat (including owner) | -| **[`user-id`](#)** | `string` | `undefined` | Owner ID | -| **[`loading`](#)** | `boolean` | `false` | Show spinner | -| **[`locale`](#)** | `string` | `'en'` | Moment.js locale | - -### Events - -| Name | Arguments | Description | -| :-----------------------: | :-------: | :-------------------------------------------------------------------------------- | -| **[`@scroll:top`](#)** | | When user scrolled top of messages. Use this event to fetch messages from history | -| **[`@scroll:bottom`](#)** | | When scrolled bottom of messages | - -### Slots - -| Name | Props | Description | -| :-----------------: | :-------------------------------------------------------------------: | :----------------- | --- | -| **[`messages`](#)** | `{ messages: Message[] }` | | -| **[`message`](#)** | `{ message: Message, sender: User, userId: string], locale: string }` | | -| **[`header`](#)** | | Chat toolbar | | -| **[`form`](#)** | | Input message form | | - -### Methods - -| Name | Params | Description | -| :--------------------------------: | :----: | :------------------------------------------------------------------- | -| **[`@scrollToBottom`](#)** | | Move scrollbar-thumb to the bottom. Use this after push new message. | -| **[`@maintainScrollPosition`](#)** | | Fix scroll position after unshift new messages | - -## AChatMessage.vue - -### Props - -| Name | Type | Default | Description | -| :--------------------: | :----------------: | :-----------: | :--------------------------------------------------------- | -| **[`id`](#)** | `{string\|number}` | `undefined` | Message ID | -| **[`message`](#)** | `string` | `''` | Message text | -| **[`timestamp`](#)** | `number` | `0` | Message timestamp | -| **[`status`](#)** | `MessageStatus` | `'confirmed'` | Message status. Can be 'sent', 'CONFIRMED', 'REJECTED'. | -| **[`user-id`](#)** | `string` | `''` | Chat owner | -| **[`sender`](#)** | `User` | `undefined` | Can be accessed from `props.message.sender` of `AChat.vue` | -| **[`show-avatar`](#)** | `boolean` | `true` | Display user avatars in chat | -| **[`locale`](#)** | `string` | `'en'` | Moment.js locale | -| **[`html`](#)** | `boolean` | `false` | Uses `v-html` or `v-text` to display message | - -### Events - -| Name | Arguments | Description | -| :----------------: | :-------: | :----------------------------------- | -| **[`@resend`](#)** | | When user clicked on the resend icon | - -### Slots - -| Name | Props | Description | -| :---------------: | :---: | :------------ | -| **[`avatar`](#)** | | Custom avatar | - -## AChatTransaction.vue - -### Props - -| Name | Type | Default | Description | -| :------------------: | :-------------------------: | :------------------------------------: | :--------------------------------------------------------- | -| **[`id`](#)** | `{string\|number}` | `undefined` | Transaction ID | -| **[`message`](#)** | `string` | `''` | Transaction text | -| **[`timestamp`](#)** | `number` | `0` | Transaction timestamp | -| **[`user-id`](#)** | `string` | `''` | Chat owner | -| **[`sender`](#)** | `User` | `undefined` | Can be accessed from `props.message.sender` of `AChat.vue` | -| **[`amount`](#)** | `number` | 0 | Crypto amount | -| **[`currency`](#)** | `string` | 'ADM' | Crypto currency | -| **[`i18n`](#)** | `{ [key: string]: string }` | { sent: 'Sent', received: 'Received' } | Transaction localization | -| **[`locale`](#)** | `string` | `'en'` | Moment.js locale | - -### Events - -| Name | Arguments | Description | -| :---------------------------: | :-----------------------: | :------------------------ | -| **[`@click:transaction`](#)** | `(transactionId: string)` | On click transaction icon | -| **[`@mount`](#)** | | Emit mount when mounted | - -## AChatForm.vue - -### Props - -| Name | Type | Default | Description | -| :-----------------------: | :-------: | :--------------: | :------------------------------ | -| **[`showSendButton`](#)** | `boolean` | `true` | Show send button | -| **[`showDivider`](#)** | `boolean` | `false` | Show divider on top of the form | -| **[`sendOnEnter`](#)** | `boolean` | `true` | Send message on enter | -| **[`label`](#)** | `string` | `Type a message` | Input message placeholder | - -### Events - -| Name | Arguments | Description | -| :-----------------: | :-----------------: | :---------------------------- | -| **[`@message`](#)** | `(message: string)` | Called when sending a message | diff --git a/src/components/AChat/index.js b/src/components/AChat/index.js index 668af46bf..3defb5f09 100644 --- a/src/components/AChat/index.js +++ b/src/components/AChat/index.js @@ -8,6 +8,7 @@ import AChatMessageActionsList from './AChatMessageActionsList.vue' import AChatMessageActionsMenu from './AChatMessageActionsMenu.vue' import AChatReactionSelect from './AChatReactionSelect/AChatReactionSelect.vue' import AChatActionsOverlay from './AChatActionsOverlay.vue' +import FilesPreview from './FilesPreview/FilesPreview.vue' export { AChat, @@ -19,5 +20,6 @@ export { AChatMessageActionsList, AChatMessageActionsMenu, AChatReactionSelect, - AChatActionsOverlay + AChatActionsOverlay, + FilesPreview } diff --git a/src/components/Chat/Chat.vue b/src/components/Chat/Chat.vue index 615f4265e..e2224f107 100644 --- a/src/components/Chat/Chat.vue +++ b/src/components/Chat/Chat.vue @@ -2,7 +2,7 @@ + + + + + + + @@ -244,7 +270,7 @@ depressed fab size="small" - @click="$refs.chat.scrollToBottom()" + @click="chatRef.scrollToBottom()" > @@ -255,16 +281,18 @@ - diff --git a/src/components/Chat/ChatMenu.vue b/src/components/Chat/ChatMenu.vue index 3b6f575c7..231dded56 100644 --- a/src/components/Chat/ChatMenu.vue +++ b/src/components/Chat/ChatMenu.vue @@ -4,26 +4,41 @@ - + + - - + + - - {{ $t('chats.send_crypto', { crypto: c }) }} + {{ $t('chats.attach_image') }} - - + + + {{ $t('chats.attach_file') }} + + + + + - {{ $t(item.title) }} + {{ $t('chats.send_crypto', { crypto: c }) }} @@ -37,13 +52,16 @@ import { Cryptos } from '@/lib/constants' import ChatDialog from '@/components/Chat/ChatDialog.vue' import CryptoIcon from '@/components/icons/CryptoIcon.vue' import IconBox from '@/components/icons/IconBox.vue' +import UploadFile from '../UploadFile.vue' export default { components: { IconBox, ChatDialog, - CryptoIcon + CryptoIcon, + UploadFile }, + emits: ['files'], props: { partnerId: { type: String, @@ -54,24 +72,13 @@ export default { } }, data: () => ({ - menuItems: [ - { - type: 'action', - title: 'chats.attach_image', - icon: 'mdi-image', - disabled: true - }, - { - type: 'action', - title: 'chats.attach_file', - icon: 'mdi-file', - disabled: true - } - ], dialog: false, dialogTitle: '', dialogText: '', - crypto: '' + crypto: '', + filesList: [], + acceptImage: 'image/* , video/*', + acceptFile: 'application/* ,text/*, audio/*' }), computed: { orderedVisibleWalletSymbols() { @@ -84,6 +91,9 @@ export default { } }, methods: { + handleFileSelected(imageData) { + this.$emit('files', [imageData]) + }, sendFunds(crypto) { // check if user has crypto wallet // otherwise show dialog diff --git a/src/components/ChatPreview.vue b/src/components/ChatPreview.vue index 8eadc3541..e290f8fb2 100644 --- a/src/components/ChatPreview.vue +++ b/src/components/ChatPreview.vue @@ -57,7 +57,12 @@ - + + + + + + diff --git a/src/components/nodes/ipfs/IpfsNodesTableItem.vue b/src/components/nodes/ipfs/IpfsNodesTableItem.vue new file mode 100644 index 000000000..e1259a220 --- /dev/null +++ b/src/components/nodes/ipfs/IpfsNodesTableItem.vue @@ -0,0 +1,82 @@ + + + + + diff --git a/src/components/nodes/ipfs/index.ts b/src/components/nodes/ipfs/index.ts new file mode 100644 index 000000000..bde8905e4 --- /dev/null +++ b/src/components/nodes/ipfs/index.ts @@ -0,0 +1 @@ +export { default as IpfsNodesTable } from './IpfsNodesTable.vue' diff --git a/src/config/development.json b/src/config/development.json index 752513556..296d82938 100644 --- a/src/config/development.json +++ b/src/config/development.json @@ -63,18 +63,53 @@ "minVersion": "0.8.0" }, "services": { - "list": { - "infoService": [ + "infoService": { + "description": { + "software": "adamant-currencyinfo-services", + "github": "https://github.com/Adamant-im/adamant-currencyinfo-services", + "docs": "https://github.com/Adamant-im/adamant-currencyinfo-services/wiki/InfoServices-API-documentation" + }, + "list": [ { "url": "https://info.adamant.im", "alt_ip": "http://88.198.156.44:44099" + }, + { + "url": "https://info2.adm.im", + "alt_ip": "http://207.180.210.95:33088" } - ] + ], + "healthCheck": { + "normalUpdateInterval": 300000, + "crucialUpdateInterval": 30000, + "onScreenUpdateInterval": 10000 + } }, - "healthCheck": { - "normalUpdateInterval": 300000, - "crucialUpdateInterval": 300000, - "onScreenUpdateInterval": 300000 + "ipfsNode": { + "description": { + "software": "ipfs-node", + "github": "https://github.com/Adamant-im/ipfs-node", + "docs": "https://github.com/Adamant-im/ipfs-node/blob/master/README.md" + }, + "list": [ + { + "url": "https://ipfs4.adm.im", + "alt_ip": "http://95.216.45.88:44099" + }, + { + "url": "https://ipfs5.adamant.im", + "alt_ip": "http://62.72.43.99:44099" + }, + { + "url": "https://ipfs6.adamant.business", + "alt_ip": "http://75.119.138.235:44099" + } + ], + "healthCheck": { + "normalUpdateInterval": 300000, + "crucialUpdateInterval": 30000, + "onScreenUpdateInterval": 10000 + } } }, "explorer": "https://explorer.adamant.im", @@ -85,12 +120,12 @@ "nodes": { "list": [ { - "url": "https://btcnode1.adamant.im", - "alt_ip": "http://176.9.38.204:44099" + "url": "https://btcnode1.adamant.im/bitcoind", + "alt_ip": "http://176.9.38.204:44099/bitcoind" }, { - "url": "https://btcnode3.adamant.im", - "alt_ip": "http://195.201.242.108:44099" + "url": "https://btcnode3.adamant.im/bitcoind", + "alt_ip": "http://195.201.242.108:44099/bitcoind" } ], "healthCheck": { @@ -101,7 +136,31 @@ } }, "explorer": "https://explorer.btc.com", - "explorerAddress": "https://explorer.btc.com/btc/address/${ID}" + "explorerAddress": "https://explorer.btc.com/btc/address/${ID}", + "services": { + "btcIndexer": { + "description": { + "software": "Esplora/Electrs", + "github": "https://github.com/blockstream/electrs", + "docs": "https://github.com/blockstream/esplora/blob/master/API.md" + }, + "list": [ + { + "url": "https://btcnode1.adamant.im", + "alt_ip": "http://176.9.38.204:44099" + }, + { + "url": "https://btcnode3.adamant.im", + "alt_ip": "http://195.201.242.108:44099" + } + ], + "healthCheck": { + "normalUpdateInterval": 330000, + "crucialUpdateInterval": 30000, + "onScreenUpdateInterval": 10000 + } + } + } }, "dash": { "explorerTx": "https://dashblockexplorer.com/tx/${ID}", @@ -137,6 +196,10 @@ { "url": "https://dogenode2.adamant.im", "alt_ip": "http://176.9.32.126:44098" + }, + { + "url": "https://dogenode3.adm.im", + "alt_ip": "http://95.216.45.88:44098" } ], "healthCheck": { @@ -155,13 +218,11 @@ "list": [ { "url": "https://ethnode2.adamant.im", - "alt_ip": "http://95.216.114.252:44099", - "hasIndex": true + "alt_ip": "http://95.216.114.252:44099" }, { "url": "https://ethnode3.adamant.im", - "alt_ip": "http://46.4.37.157:44099", - "hasIndex": true + "alt_ip": "http://46.4.37.157:44099" } ], "healthCheck": { @@ -172,7 +233,31 @@ } }, "explorer": "https://etherscan.io", - "explorerAddress": "https://etherscan.io/address/${ID}" + "explorerAddress": "https://etherscan.io/address/${ID}", + "services": { + "ethIndexer": { + "description": { + "software": "ETH-transactions-storage", + "github": "https://github.com/Adamant-im/ETH-transactions-storage", + "docs": "https://github.com/Adamant-im/ETH-transactions-storage?tab=readme-ov-file#api-request-examples" + }, + "list": [ + { + "url": "https://ethnode2.adamant.im", + "alt_ip": "http://95.216.114.252:44099" + }, + { + "url": "https://ethnode3.adamant.im", + "alt_ip": "http://46.4.37.157:44099" + } + ], + "healthCheck": { + "normalUpdateInterval": 330000, + "crucialUpdateInterval": 30000, + "onScreenUpdateInterval": 10000 + } + } + } }, "kly": { "explorer": "https://explorer.klayr.xyz", @@ -180,13 +265,13 @@ "explorerAddress": "https://explorer.klayr.xyz/account/${ID}", "nodes": { "list": [ - { - "url": "https://klynode1.adamant.im", - "alt_ip": "http://195.26.255.137:44099" - }, { "url": "https://klynode2.adamant.im", "alt_ip": "http://109.176.199.130:44099" + }, + { + "url": "https://klynode3.adm.im", + "alt_ip": "http://37.27.205.78:44099" } ], "healthCheck": { @@ -197,22 +282,27 @@ } }, "services": { - "list": { - "klyService": [ - { - "url": "https://klyservice1.adamant.im", - "alt_ip": "http://195.26.255.137:44098" - }, + "klyService": { + "description": { + "software": "klayr-service", + "github": "https://github.com/KlayrHQ/klayr-service", + "docs": "https://klayr.xyz/documentation/klayr-service" + }, + "list": [ { "url": "https://klyservice2.adamant.im", "alt_ip": "http://109.176.199.130:44098" + }, + { + "url": "https://klyservice3.adm.im", + "alt_ip": "http://37.27.205.78:44098" } - ] - }, - "healthCheck": { - "normalUpdateInterval": 330000, - "crucialUpdateInterval": 30000, - "onScreenUpdateInterval": 10000 + ], + "healthCheck": { + "normalUpdateInterval": 330000, + "crucialUpdateInterval": 30000, + "onScreenUpdateInterval": 10000 + } } } } diff --git a/src/config/production.json b/src/config/production.json index eccf19824..5956798c8 100644 --- a/src/config/production.json +++ b/src/config/production.json @@ -62,18 +62,53 @@ "minVersion": "0.8.0" }, "services": { - "list": { - "infoService": [ + "infoService": { + "description": { + "software": "adamant-currencyinfo-services", + "github": "https://github.com/Adamant-im/adamant-currencyinfo-services", + "docs": "https://github.com/Adamant-im/adamant-currencyinfo-services/wiki/InfoServices-API-documentation" + }, + "list": [ { "url": "https://info.adamant.im", "alt_ip": "http://88.198.156.44:44099" + }, + { + "url": "https://info2.adm.im", + "alt_ip": "http://207.180.210.95:33088" } - ] + ], + "healthCheck": { + "normalUpdateInterval": 300000, + "crucialUpdateInterval": 30000, + "onScreenUpdateInterval": 10000 + } }, - "healthCheck": { - "normalUpdateInterval": 300000, - "crucialUpdateInterval": 300000, - "onScreenUpdateInterval": 300000 + "ipfsNode": { + "description": { + "software": "ipfs-node", + "github": "https://github.com/Adamant-im/ipfs-node", + "docs": "https://github.com/Adamant-im/ipfs-node/blob/master/README.md" + }, + "list": [ + { + "url": "https://ipfs4.adm.im", + "alt_ip": "http://95.216.45.88:44099" + }, + { + "url": "https://ipfs5.adamant.im", + "alt_ip": "http://62.72.43.99:44099" + }, + { + "url": "https://ipfs6.adamant.business", + "alt_ip": "http://75.119.138.235:44099" + } + ], + "healthCheck": { + "normalUpdateInterval": 300000, + "crucialUpdateInterval": 30000, + "onScreenUpdateInterval": 10000 + } } }, "explorerTx": "https://explorer.adamant.im/tx/${ID}", @@ -84,12 +119,12 @@ "nodes": { "list": [ { - "url": "https://btcnode1.adamant.im", - "alt_ip": "http://176.9.38.204:44099" + "url": "https://btcnode1.adamant.im/bitcoind", + "alt_ip": "http://176.9.38.204:44099/bitcoind" }, { - "url": "https://btcnode3.adamant.im", - "alt_ip": "http://195.201.242.108:44099" + "url": "https://btcnode3.adamant.im/bitcoind", + "alt_ip": "http://195.201.242.108:44099/bitcoind" } ], "healthCheck": { @@ -101,7 +136,31 @@ }, "explorerTx": "https://explorer.btc.com/btc/transaction/${ID}", "explorer": "https://explorer.btc.com", - "explorerAddress": "https://explorer.btc.com/btc/address/${ID}" + "explorerAddress": "https://explorer.btc.com/btc/address/${ID}", + "services": { + "btcIndexer": { + "description": { + "software": "Esplora/Electrs", + "github": "https://github.com/blockstream/electrs", + "docs": "https://github.com/blockstream/esplora/blob/master/API.md" + }, + "list": [ + { + "url": "https://btcnode1.adamant.im", + "alt_ip": "http://176.9.38.204:44099" + }, + { + "url": "https://btcnode3.adamant.im", + "alt_ip": "http://195.201.242.108:44099" + } + ], + "healthCheck": { + "normalUpdateInterval": 330000, + "crucialUpdateInterval": 30000, + "onScreenUpdateInterval": 10000 + } + } + } }, "dash": { "nodes": { @@ -136,6 +195,10 @@ { "url": "https://dogenode2.adamant.im", "alt_ip": "http://176.9.32.126:44098" + }, + { + "url": "https://dogenode3.adm.im", + "alt_ip": "http://95.216.45.88:44098" } ], "healthCheck": { @@ -154,13 +217,11 @@ "list": [ { "url": "https://ethnode2.adamant.im", - "alt_ip": "http://95.216.114.252:44099", - "hasIndex": true + "alt_ip": "http://95.216.114.252:44099" }, { "url": "https://ethnode3.adamant.im", - "alt_ip": "http://46.4.37.157:44099", - "hasIndex": true + "alt_ip": "http://46.4.37.157:44099" } ], "healthCheck": { @@ -172,7 +233,31 @@ }, "explorerTx": "https://etherscan.io/tx/${ID}", "explorer": "https://etherscan.io", - "explorerAddress": "https://etherscan.io/address/${ID}" + "explorerAddress": "https://etherscan.io/address/${ID}", + "services": { + "ethIndexer": { + "description": { + "software": "ETH-transactions-storage", + "github": "https://github.com/Adamant-im/ETH-transactions-storage", + "docs": "https://github.com/Adamant-im/ETH-transactions-storage?tab=readme-ov-file#api-request-examples" + }, + "list": [ + { + "url": "https://ethnode2.adamant.im", + "alt_ip": "http://95.216.114.252:44099" + }, + { + "url": "https://ethnode3.adamant.im", + "alt_ip": "http://46.4.37.157:44099" + } + ], + "healthCheck": { + "normalUpdateInterval": 330000, + "crucialUpdateInterval": 30000, + "onScreenUpdateInterval": 10000 + } + } + } }, "kly": { "explorer": "https://explorer.klayr.xyz", @@ -180,13 +265,13 @@ "explorerAddress": "https://explorer.klayr.xyz/account/${ID}", "nodes": { "list": [ - { - "url": "https://klynode1.adamant.im", - "alt_ip": "http://195.26.255.137:44099" - }, { "url": "https://klynode2.adamant.im", "alt_ip": "http://109.176.199.130:44099" + }, + { + "url": "https://klynode3.adm.im", + "alt_ip": "http://37.27.205.78:44099" } ], "healthCheck": { @@ -197,22 +282,27 @@ } }, "services": { - "list": { - "klyService": [ - { - "url": "https://klyservice1.adamant.im", - "alt_ip": "http://195.26.255.137:44098" - }, + "klyService": { + "description": { + "software": "klayr-service", + "github": "https://github.com/KlayrHQ/klayr-service", + "docs": "https://klayr.xyz/documentation/klayr-service" + }, + "list": [ { "url": "https://klyservice2.adamant.im", "alt_ip": "http://109.176.199.130:44098" + }, + { + "url": "https://klyservice3.adm.im", + "alt_ip": "http://37.27.205.78:44098" } - ] - }, - "healthCheck": { - "normalUpdateInterval": 330000, - "crucialUpdateInterval": 30000, - "onScreenUpdateInterval": 10000 + ], + "healthCheck": { + "normalUpdateInterval": 330000, + "crucialUpdateInterval": 30000, + "onScreenUpdateInterval": 10000 + } } } } diff --git a/src/config/test.json b/src/config/test.json index d880b162f..53a7c2905 100644 --- a/src/config/test.json +++ b/src/config/test.json @@ -58,6 +58,20 @@ "url": "https://info.adamant.im", "alt_ip": "http://88.198.156.44:44099" } + ], + "ipfs": [ + { + "url": "https://ipfs1test.adamant.im", + "alt_ip": "http://194.163.154.252:4000" + }, + { + "url": "https://ipfs2test.adamant.im", + "alt_ip": "http://154.26.159.245:4000" + }, + { + "url": "https://ipfs3test.adamant.im", + "alt_ip": "http://109.123.240.102:4000" + } ] }, "healthCheck": { diff --git a/src/config/tor.json b/src/config/tor.json index 608f2baea..f6ea3c24e 100644 --- a/src/config/tor.json +++ b/src/config/tor.json @@ -33,17 +33,45 @@ "minVersion": "0.8.0" }, "services": { - "list": { - "infoService": [ + "infoService": { + "description": { + "software": "adamant-currencyinfo-services", + "github": "https://github.com/Adamant-im/adamant-currencyinfo-services", + "docs": "https://github.com/Adamant-im/adamant-currencyinfo-services/wiki/InfoServices-API-documentation" + }, + "list": [ { "url": "http://czjsawp2crjmnkliw2h2kpk7wwd3a36zvvnvqgvzmi4t4vc2yzm7j2qd.onion" } - ] + ], + "healthCheck": { + "normalUpdateInterval": 300000, + "crucialUpdateInterval": 30000, + "onScreenUpdateInterval": 10000 + } }, - "healthCheck": { - "normalUpdateInterval": 300000, - "crucialUpdateInterval": 300000, - "onScreenUpdateInterval": 300000 + "ipfsNode": { + "description": { + "software": "ipfs-node", + "github": "https://github.com/Adamant-im/ipfs-node", + "docs": "https://github.com/Adamant-im/ipfs-node/blob/master/README.md" + }, + "list": [ + { + "url": "http://z455rax4mwcseyc7efog7czrbwdvphwocatl5sjcc6htcoj2k2vz7dad.onion" + }, + { + "url": "http://cds45bjd7ynxkffxpzfifnm55r6vmhgocvpvonjmry3mrskuhda6z7qd.onion" + }, + { + "url": "http://3ytwoe62bqw264v4rkaqpn5iovdg3oxly5tx2uc5qijkrdqixm6tmdyd.onion" + } + ], + "healthCheck": { + "normalUpdateInterval": 300000, + "crucialUpdateInterval": 30000, + "onScreenUpdateInterval": 10000 + } } }, "explorerTx": "http://srovpmanmrbmbqe63vp5nycsa3j3g6be3bz46ksmo35u5pw7jjtjamid.onion/tx/${ID}", @@ -54,7 +82,7 @@ "nodes": { "list": [ { - "url": "http://cc6ibzkfeseuwnmtjc6hlsd44bzg2sr3shbv7n35nj2rk2vm6dmtlnqd.onion" + "url": "http://cc6ibzkfeseuwnmtjc6hlsd44bzg2sr3shbv7n35nj2rk2vm6dmtlnqd.onion/bitcoind" }, { "url": "http://grnpvgtlrfws3424l726td5lctsod3hq2at4lhiasmedpxygbo5u2bqd.onion" @@ -69,7 +97,29 @@ }, "explorerTx": "https://explorer.btc.com/btc/transaction/${ID}", "explorer": "https://explorer.btc.com", - "explorerAddress": "https://explorer.btc.com/btc/address/${ID}" + "explorerAddress": "https://explorer.btc.com/btc/address/${ID}", + "services": { + "btcIndexer": { + "description": { + "software": "Esplora/Electrs", + "github": "https://github.com/blockstream/electrs", + "docs": "https://github.com/blockstream/esplora/blob/master/API.md" + }, + "list": [ + { + "url": "http://cc6ibzkfeseuwnmtjc6hlsd44bzg2sr3shbv7n35nj2rk2vm6dmtlnqd.onion" + }, + { + "url": "http://grnpvgtlrfws3424l726td5lctsod3hq2at4lhiasmedpxygbo5u2bqd.onion" + } + ], + "healthCheck": { + "normalUpdateInterval": 330000, + "crucialUpdateInterval": 30000, + "onScreenUpdateInterval": 10000 + } + } + } }, "dash": { "nodes": { @@ -100,6 +150,9 @@ }, { "url": "http://bfu3iiofsagyhi22zijfilbkzlzbalpylhhfcluqmezx2avdwcxut7yd.onion" + }, + { + "url": "http://tdl25bmpwystxnm6hxzqdrkaxxdicknbigs5umob2nlgcbbqgidd64qd.onion" } ], "healthCheck": { @@ -121,8 +174,7 @@ "hasIndex": true }, { - "url": "http://rekynxikhumzsme7phumocz3mquy7y3onkw33skmvk2akjkin2iopqqd.onion", - "hasIndex": true + "url": "http://rekynxikhumzsme7phumocz3mquy7y3onkw33skmvk2akjkin2iopqqd.onion" } ], "healthCheck": { @@ -134,7 +186,29 @@ }, "explorerTx": "https://etherscan.io/tx/${ID}", "explorer": "https://etherscan.io", - "explorerAddress": "https://etherscan.io/address/${ID}" + "explorerAddress": "https://etherscan.io/address/${ID}", + "services": { + "ethIndexer": { + "description": { + "software": "ETH-transactions-storage", + "github": "https://github.com/Adamant-im/ETH-transactions-storage", + "docs": "https://github.com/Adamant-im/ETH-transactions-storage?tab=readme-ov-file#api-request-examples" + }, + "list": [ + { + "url": "http://jpbrp6xapsyfnvyosrpu5wmoi62fqotazkicjeiob32yz77rt7axobqd.onion" + }, + { + "url": "http://rekynxikhumzsme7phumocz3mquy7y3onkw33skmvk2akjkin2iopqqd.onion" + } + ], + "healthCheck": { + "normalUpdateInterval": 330000, + "crucialUpdateInterval": 30000, + "onScreenUpdateInterval": 10000 + } + } + } }, "kly": { "explorer": "https://explorer.klayr.xyz", @@ -143,10 +217,10 @@ "nodes": { "list": [ { - "url": "http://7kmdhr3x3hpy7kg3gojxznypgzcya7yxpte4zz42wtljhulg5mrzkpad.onion" + "url": "http://5fr7uybpxecid5gikrm65hstsfhve2772keyqdvxirsiywlyo6zap6yd.onion" }, { - "url": "http://5fr7uybpxecid5gikrm65hstsfhve2772keyqdvxirsiywlyo6zap6yd.onion" + "url": "http://5rmyjfvazkg5gcyo3gwvdinykvycsleeebumdm6zlj6dhf6gshpelfid.onion" } ], "healthCheck": { @@ -157,20 +231,25 @@ } }, "services": { - "list": { - "klyService": [ + "klyService": { + "description": { + "software": "klayr-service", + "github": "https://github.com/KlayrHQ/klayr-service", + "docs": "https://klayr.xyz/documentation/klayr-service" + }, + "list": [ { - "url": "http://673swmi7y4cdsqw7rhl52mypuixfh5tryxjt4vzhaoy7my572ldidfad.onion" + "url": "http://3om4mobnbppxuexwufprp4cle4fivstooqqap6yoll5qk3kikesmqgad.onion" }, { - "url": "http://3om4mobnbppxuexwufprp4cle4fivstooqqap6yoll5qk3kikesmqgad.onion" + "url": "http://xif2b7cchtn27aq2qypjc6p3phamrt4fdc3gbl5e6kk2fw3ypovre5id.onion" } - ] - }, - "healthCheck": { - "normalUpdateInterval": 330000, - "crucialUpdateInterval": 30000, - "onScreenUpdateInterval": 10000 + ], + "healthCheck": { + "normalUpdateInterval": 330000, + "crucialUpdateInterval": 30000, + "onScreenUpdateInterval": 10000 + } } } } diff --git a/src/config/utils/index.ts b/src/config/utils/index.ts index af87966ea..8757f3181 100644 --- a/src/config/utils/index.ts +++ b/src/config/utils/index.ts @@ -2,4 +2,3 @@ export * from './getExplorerAddressUrl' export * from './getExplorerDelegateUrl' export * from './getExplorerTxUrl' export * from './nodes' -export * from './services' diff --git a/src/config/utils/services.ts b/src/config/utils/services.ts deleted file mode 100644 index 6b4d1fe58..000000000 --- a/src/config/utils/services.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { Service } from '@/types/wallets' -import { BlockchainSymbol, Config } from './types' -import config from '../index' - -type ServiceNames = Config[B] extends { - services: Record -} - ? keyof Config[B]['services'] - : never - -export function getService>( - blockchain: B, - serviceName: S -): Service[] { - const blockchainConfig = config[blockchain] - - if ('services' in blockchainConfig) { - const services = blockchainConfig['services']['list'] - const service = services[serviceName] - - if (service) { - return service - } - } - - throw new Error( - `${serviceName.toString()} service does not exist in "${blockchain}" configuration` - ) -} - -export function getRandomServiceUrl>( - blockchain: B, - serviceName: S -): string { - const serviceList = getService(blockchain, serviceName) - if (serviceList.length === 0) { - throw new Error(`Missing services in "${blockchain}" configuration`) - } - - const index = Math.floor(Math.random() * serviceList.length) - const service = serviceList[index] - - return service.url -} diff --git a/src/hooks/useUploadFile.ts b/src/hooks/useUploadFile.ts new file mode 100644 index 000000000..80a7a5340 --- /dev/null +++ b/src/hooks/useUploadFile.ts @@ -0,0 +1,68 @@ +import { encodeFile } from '@/lib/adamant-api' +import { watch } from 'vue' +import ipfs from '@/lib/nodes/ipfs' +import { useMutation } from '@tanstack/vue-query' + +type ParsedFile = { file: File; binary: ArrayBuffer } + +export async function readFile(file: File): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader() + reader.onload = () => { + resolve(reader.result as ArrayBuffer) + } + reader.onerror = (e) => { + reject(e) + } + reader.readAsArrayBuffer(file) + }) +} + +async function uploadFile(files: File[], to: string) { + const parsedFiles: ParsedFile[] = [] + + for (const file of files) { + const binary = await readFile(file) + const encoded = await encodeFile(new Uint8Array(binary), { to }) + + parsedFiles.push({ + file, + binary: encoded.binary + }) + } + console.log('Files', files) + + const formData = new FormData() + + for (const file of parsedFiles) { + const blob = new Blob([file.binary], { type: 'application/octet-stream' }) + formData.append('files', blob, file.file.name) + } + + const response = await ipfs.upload(formData, (progress) => { + const percentCompleted = Math.round((progress.loaded * 100) / progress.total!) + + console.log(`Progress ${percentCompleted}%`) + }) + console.log(`Uploaded CIDs`, response) + + return response +} + +export function useUploadFile() { + const { status, mutateAsync, mutate } = useMutation({ + mutationFn: (params: { files: File[]; to: string }) => { + return uploadFile(params.files, params.to) + } + }) + + watch(status, (status) => { + console.log('Status changed', status) + }) + + return { + mutate, + mutateAsync, + status + } +} diff --git a/src/lib/adamant-api/asset.d.ts b/src/lib/adamant-api/asset.d.ts deleted file mode 100644 index 02aa7596e..000000000 --- a/src/lib/adamant-api/asset.d.ts +++ /dev/null @@ -1,95 +0,0 @@ -/** - * AIP 12: Non-ADM crypto transfer messages - * @see https://aips.adamant.im/AIPS/aip-12 - */ -export interface CryptoTransferAsset { - /** - * Represents token's network and looks like `tickerSymbol_transaction`, - * e.g. `eth_transaction`. Ticker symbol must be in lower case, but apps - * must process it in any case for backwards compatibility. - */ - type: string - /** - * Transferred value in tokens of its network. Decimal separator is `.` - */ - amount: string - /** - * Transaction id in token's network. Used to check transaction status - */ - hash: string - /** - * May include comment for this transfer, shown to both recipient and sender - */ - comments?: string - /** - * Can be added to show explanation text messages on client apps that doesn't - * support specified `type` - */ - text_fallback?: string -} - -/** - * Reply to a message - * - * @see https://aips.adamant.im/AIPS/aip-16 - */ -export interface ReplyMessageAsset { - /** - * ADM transaction ID of a message which a user replies to - */ - replyto_id: string - /** - * Text of a reply - */ - reply_message: string -} - -/** - * Reply to a message with a crypto transfer - * - * @see https://aips.adamant.im/AIPS/aip-16 - */ -export interface ReplyWithCryptoTransferAsset { - /** - * ADM transaction ID of a message which a user replies to - */ - replyto_id: string - /** - * Text of a reply - */ - reply_message: CryptoTransferAsset -} - -interface CryptoTransferPayload { - cryptoSymbol: string - amount: string - hash: string - comments: string -} - -export function cryptoTransferAsset(payload: CryptoTransferPayload): CryptoTransferAsset - -interface ReplyMessagePayload { - replyToId: string - replyMessage: string -} - -export function replyMessageAsset(payload: ReplyMessagePayload): ReplyMessageAsset - -export function replyWithCryptoTransferAsset( - replyToId: string, - transferPayload: CryptoTransferPayload -): ReplyWithCryptoTransferAsset - -export interface ReactionAsset { - /** - * ADM transaction ID of the message to which the user is reacting - */ - reactto_id: string - /** - * Represents the emoji-based reaction - */ - react_message: string -} - -export function reactionAsset(reactToId: string, reactMessage: string): ReactionAsset diff --git a/src/lib/adamant-api/asset.js b/src/lib/adamant-api/asset.js deleted file mode 100644 index 363711004..000000000 --- a/src/lib/adamant-api/asset.js +++ /dev/null @@ -1,37 +0,0 @@ -import { MessageType } from '@/lib/constants' - -export function cryptoTransferAsset({ cryptoSymbol, amount, hash, comments, text_fallback }) { - const asset = { - type: MessageType.cryptoTransferMessage(cryptoSymbol), - amount, - hash, - comments - } - - if (text_fallback) { - asset.text_fallback = text_fallback - } - - return asset -} - -export function replyMessageAsset({ replyToId, replyMessage }) { - return { - replyto_id: replyToId, - reply_message: replyMessage - } -} - -export function replyWithCryptoTransferAsset(replyToId, transferPayload) { - return { - replyto_id: replyToId, - reply_message: cryptoTransferAsset(transferPayload) - } -} - -export function reactionAsset(reactToId, reactMessage) { - return { - reactto_id: reactToId, - react_message: reactMessage - } -} diff --git a/src/lib/adamant-api/asset.ts b/src/lib/adamant-api/asset.ts new file mode 100644 index 000000000..de95b14be --- /dev/null +++ b/src/lib/adamant-api/asset.ts @@ -0,0 +1,239 @@ +import { MessageType } from '@/lib/constants' +import { FileData } from '@/components/UploadFile.vue' + +interface CryptoTransferPayload { + cryptoSymbol: string + amount: string + hash: string + comments: string + text_fallback?: string +} + +export interface CryptoTransferAsset { + /** + * Represents token's network and looks like `tickerSymbol_transaction`, + * e.g. `eth_transaction`. Ticker symbol must be in lower case, but apps + * must process it in any case for backwards compatibility. + */ + type: string + /** + * Transferred value in tokens of its network. Decimal separator is `.` + */ + amount: string + /** + * Transaction id in token's network. Used to check transaction status + */ + hash: string + /** + * May include comment for this transfer, shown to both recipient and sender + */ + comments?: string + /** + * Can be added to show explanation text messages on client apps that doesn't + * support specified `type` + */ + text_fallback?: string +} + +/** + * AIP 12: Non-ADM crypto transfer messages + * @see https://aips.adamant.im/AIPS/aip-12 + */ +export function cryptoTransferAsset({ + cryptoSymbol, + amount, + hash, + comments, + text_fallback +}: CryptoTransferPayload): CryptoTransferAsset { + const asset: CryptoTransferAsset = { + type: MessageType.cryptoTransferMessage(cryptoSymbol), + amount, + hash, + comments + } + + if (text_fallback) { + asset.text_fallback = text_fallback + } + + return asset +} + +interface ReplyMessagePayload { + replyToId: string + replyMessage: string +} + +/** + * Reply to a message + * @see https://aips.adamant.im/AIPS/aip-16 + */ +export interface ReplyMessageAsset { + /** + * ADM transaction ID of a message which a user replies to + */ + replyto_id: string + /** + * Text of a reply + */ + reply_message: string +} + +/** + * Reply to a message with a crypto transfer + * @see https://aips.adamant.im/AIPS/aip-16 + */ +export interface ReplyWithCryptoTransferAsset { + /** + * ADM transaction ID of a message which a user replies to + */ + replyto_id: string + /** + * Text of a reply + */ + reply_message: CryptoTransferAsset +} + +export function replyMessageAsset({ + replyToId, + replyMessage +}: ReplyMessagePayload): ReplyMessageAsset { + return { + replyto_id: replyToId, + reply_message: replyMessage + } +} + +export function replyWithCryptoTransferAsset( + replyToId: string, + transferPayload: CryptoTransferPayload +): ReplyWithCryptoTransferAsset { + return { + replyto_id: replyToId, + reply_message: cryptoTransferAsset(transferPayload) + } +} + +export interface ReactionAsset { + /** + * ADM transaction ID of the message to which the user is reacting + */ + reactto_id: string + /** + * Represents the emoji-based reaction + */ + react_message: string +} + +export function reactionAsset(reactToId: string, reactMessage: string): ReactionAsset { + return { + reactto_id: reactToId, + react_message: reactMessage + } +} + +export interface FileAsset { + /** + * Specifies the MIME type of the file, e.g. "image/jpeg" + */ + mimeType?: string + /** + * Specifies the name of a file (without extension). + */ + name?: string + /** + * Indicates the file extension, e.g. "jpeg", "png", "pdf", etc. + */ + extension?: string + /** + * File resolution as an array of float values. + * The first value is width, the second is height. + */ + resolution?: [number, number] + /** + * Duration of the video or audio file in seconds. + */ + duration?: number + /** + * Represents the unique file identifier in the `storage` network. + * For example, for IPFS it's CID. + */ + id: string + /** + * Indicates the size of the encrypted file in bytes + */ + size: number + /** + * Nonce used for encryption + */ + nonce: string + /** + * Preview of the image. + */ + preview?: { + /** + * Represents the unique file identifier in the `storage` network. + * For example, for IPFS it's CID. + */ + id: string + /** + * Nonce used for encryption + */ + nonce: string + /** + * Indicates the file extension, e.g. "jpeg", "png", "pdf", etc. + */ + extension: string + } +} + +export interface AttachmentAsset { + files: FileAsset[] + storage: { id: 'ipfs' } + /** + * Optional comment associated with the transaction + */ + comment?: string +} + +/** + * AIP-18: https://github.com/Adamant-im/AIPs/pull/54/files + * @param {Array} files + * @param {Array<[string, string]>} [nonces] First element is the nonce of original file, second is nonce of preview + * @param {Array<[string, string]>} [ids] List of files IDs after uploading to IPFS. First element is the ID of original file, second is ID of preview. + * @param {string} [comment] + */ +export function attachmentAsset( + files: FileData[], + nonces?: [nonce: string, previewNonce: string], + ids?: [id: string, previewId: string], + comment?: string +): AttachmentAsset { + return { + files: files.map(({ file, width, height, cid, preview }, index) => { + const [name, extension] = file.name.split('.') + const resolution: FileAsset['resolution'] = width && height ? [width, height] : undefined + const [nonce, previewNonce] = nonces?.[index] || [] + const [id, previewId] = cid ? [cid, preview?.cid] : ids?.[index] || [] + + return { + mimeType: file.type, + name, + extension, + resolution, + duration: undefined, // @todo check if is a video or audio file + size: file.size, + id: id!, + nonce, + preview: { + id: previewId!, + nonce: previewNonce, + extension + } + } + }), + comment, + storage: { id: 'ipfs' } + } +} diff --git a/src/lib/adamant-api/index.d.ts b/src/lib/adamant-api/index.d.ts index be51ade35..2731347b9 100644 --- a/src/lib/adamant-api/index.d.ts +++ b/src/lib/adamant-api/index.d.ts @@ -90,6 +90,13 @@ export type SendMessageParams = { } export function sendMessage(params: SendMessageParams): Promise +export type EncodedFile = { + binary: Uint8Array + nonce: string +} + +export function encodeFile(file: Uint8Array, params: SendMessageParams): Promise + export function sendSpecialMessage( to: string, message: SendMessageParams['message'] diff --git a/src/lib/adamant-api/index.js b/src/lib/adamant-api/index.js index 18f82476e..444208f05 100644 --- a/src/lib/adamant-api/index.js +++ b/src/lib/adamant-api/index.js @@ -173,6 +173,17 @@ export function sendMessage(params) { }) } +/** + * @param {Uint8Array} file + * @param {{ to: string }} params + */ +export async function encodeFile(file, params) { + const publicKey = await getPublicKey(params.to) + const { binary, nonce } = utils.encodeBinary(file, publicKey, myKeypair.privateKey) + + return { binary, nonce } +} + /** * Sends special message with the specified payload * @param {string} to recipient address diff --git a/src/lib/adamant.js b/src/lib/adamant.js index 13acd5dbf..8b21e413b 100644 --- a/src/lib/adamant.js +++ b/src/lib/adamant.js @@ -443,6 +443,52 @@ adamant.decodeValue = function (source, privateKey, nonce) { return json.payload } +/** + * Encodes a secret binary (available for the owner only) + * @param {Uint8Array} source value to encode + * @param {Uint8Array} recipientPublicKey sender's public key + * @param {Uint8Array} privateKey private key + * @returns {{binary: string, nonce: string}} encoded binary and nonce (both as HEX-strings) + */ +adamant.encodeBinary = function (source, recipientPublicKey, privateKey) { + const nonce = Buffer.allocUnsafe(24) + sodium.randombytes(nonce) + + const publicKey = hexToBytes(recipientPublicKey) + + const DHPublicKey = ed2curve.convertPublicKey(publicKey) + const DHSecretKey = ed2curve.convertSecretKey(privateKey) + + const encrypted = nacl.box(source, nonce, DHPublicKey, DHSecretKey) + + return { + binary: encrypted, + nonce: bytesToHex(nonce) + } +} + +/** + * Decodes a secret binary + * @param {string|Uint8Array} source source to decrypt + * @param {string|Uint8Array} senderPublicKey sender's public key + * @param {Uint8Array} privateKey private key + * @param {string|Uint8Array} nonce nonce + * @returns {string} decoded value + */ +adamant.decodeBinary = function (source, senderPublicKey, privateKey, nonce) { + if (typeof nonce === 'string') { + nonce = hexToBytes(nonce) + } + + const publicKey = + typeof senderPublicKey === 'string' ? hexToBytes(senderPublicKey) : senderPublicKey + + const DHPublicKey = ed2curve.convertPublicKey(publicKey) + const DHSecretKey = ed2curve.convertSecretKey(privateKey) + + return nacl.box.open(source, nonce, DHPublicKey, DHSecretKey) +} + /** * Converts ADM amount to its internal representation * @param {number|string} admAmount amount to convert diff --git a/src/lib/attachment-api/index.ts b/src/lib/attachment-api/index.ts new file mode 100644 index 000000000..e6e858e9a --- /dev/null +++ b/src/lib/attachment-api/index.ts @@ -0,0 +1,31 @@ +import utils from '@/lib/adamant' +import { hexToBytes } from "@/lib/hex"; +import ipfs from '@/lib/nodes/ipfs' +import { Buffer } from 'buffer' + +export class AttachmentApi { + public readonly myKeypair: { publicKey: Buffer; privateKey: Buffer } + constructor(passphrase: string) { + const hash = utils.createPassphraseHash(passphrase) + this.myKeypair = utils.makeKeypair(hash) as { publicKey: Buffer; privateKey: Buffer } + } + + async getFile(cid: string, nonce: string, publicKey: string) { + const file = await ipfs.downloadFile(cid) + return utils.decodeBinary(new Uint8Array(file), publicKey, this.myKeypair.privateKey, nonce) + } + + async uploadFile(file: Uint8Array, publicKey: string) { + const formData = new FormData() + const { binary, nonce } = utils.encodeBinary(file, hexToBytes(publicKey), this.myKeypair.privateKey) + formData.append('file', binary) + + const { cids } = await ipfs.post(`api/file/upload`, formData, { + 'Content-Type': 'multipart/form-data' + }) + + console.log('File:', file) + console.log('Public key:', publicKey) + return { cids, nonce } + } +} diff --git a/src/lib/chat/helpers/createAttachment.ts b/src/lib/chat/helpers/createAttachment.ts new file mode 100644 index 000000000..38f4eb37d --- /dev/null +++ b/src/lib/chat/helpers/createAttachment.ts @@ -0,0 +1,66 @@ +import { FileData } from '@/components/UploadFile.vue' +import utils from '@/lib/adamant' +import { EncodedFile } from '@/lib/adamant-api' +import { NormalizedChatMessageTransaction } from '@/lib/chat/helpers/normalizeMessage' +import { TransactionStatus as TS, TransactionStatusType } from '@/lib/constants' +import { attachmentAsset } from '@/lib/adamant-api/asset' + +type Params = { + recipientId: string + senderId: string + files: FileData[] + nonces?: [string, string] + ids?: [string, string] + encodedFiles?: EncodedFile[] + message?: string + replyToId?: string + status?: TransactionStatusType +} +/** + * Creates a message object with uniq ID. + */ +export function createAttachment({ + recipientId, + senderId, + files, + nonces, + ids, + message, + replyToId, + status = TS.PENDING +}: Params) { + const timestamp = Date.now() + const id = utils.epochTime().toString() + + const transaction: NormalizedChatMessageTransaction = { + id, + hash: id, + recipientId, + senderId, + message: message || '', + status, + timestamp: Date.now(), + type: 'attachment', + asset: replyToId + ? { replyto_id: replyToId, reply_message: attachmentAsset(files, nonces, ids, message) } + : attachmentAsset(files, nonces, ids, message), + /** + * When sending a message, we need to store the files locally. + */ + localFiles: files.map((file) => { + return { + file, + loading: true, + error: null + } + }), + isReply: !!replyToId, + confirmations: 0, + height: 0, + admTimestamp: Math.floor(timestamp / 1000), + i18n: false, + amount: 0 + } + + return transaction +} diff --git a/src/lib/chat/helpers/index.js b/src/lib/chat/helpers/index.js index b0becaa00..b50fadea6 100644 --- a/src/lib/chat/helpers/index.js +++ b/src/lib/chat/helpers/index.js @@ -6,3 +6,4 @@ export * from './getChats' export * from './normalizeMessage' export * from './queueMessage' export * from './isEmptyReaction' +export * from './createAttachment' diff --git a/src/lib/chat/helpers/normalizeMessage.d.ts b/src/lib/chat/helpers/normalizeMessage.d.ts index 40638f83f..d5a3a3252 100644 --- a/src/lib/chat/helpers/normalizeMessage.d.ts +++ b/src/lib/chat/helpers/normalizeMessage.d.ts @@ -1,5 +1,13 @@ +import { FileData } from '@/components/UploadFile.vue' import { DecodedChatMessageTransaction } from '@/lib/adamant-api' import { TransactionStatusType } from '@/lib/constants' +import { ChatMessageTransaction } from '@/lib/schema/client' + +export type LocalFile = { + file: FileData + loading: boolean + error: string | null +} export type NormalizedChatMessageTransaction = Pick< DecodedChatMessageTransaction, @@ -15,9 +23,12 @@ export type NormalizedChatMessageTransaction = Pick< hash: string isReply?: boolean isReaction?: boolean + recipientPublicKey?: string + senderPublicKey?: string asset: any // @todo types + localFiles?: LocalFile[] // in case of attachments } export function normalizeMessage( - transaction: DecodedChatMessageTransaction + transaction: ChatMessageTransaction | DecodedChatMessageTransaction ): NormalizedChatMessageTransaction diff --git a/src/lib/chat/helpers/normalizeMessage.js b/src/lib/chat/helpers/normalizeMessage.js index 227bc0a74..fc10cba35 100644 --- a/src/lib/chat/helpers/normalizeMessage.js +++ b/src/lib/chat/helpers/normalizeMessage.js @@ -70,6 +70,16 @@ export function normalizeMessage(abstract) { transaction.type = notSupportedYetCrypto || 'UNKNOWN_CRYPTO' transaction.status = TS.UNKNOWN } + } else if (abstract.message.reply_message.files) { + transaction.asset = { + ...abstract.message.reply_message, + replyto_id: abstract.message.replyto_id + } + transaction.recipientPublicKey = abstract.recipientPublicKey + transaction.senderPublicKey = abstract.senderPublicKey + transaction.message = abstract.message.reply_message.comment || '' + transaction.hash = abstract.id + transaction.type = 'attachment' } else { // Unsupported transaction type. May require updating the PWA version. transaction.message = 'chats.unsupported_transaction_type' @@ -106,6 +116,13 @@ export function normalizeMessage(abstract) { transaction.hash = abstract.id // adm transaction id (hash) abstract.amount > 0 ? (transaction.type = 'ADM') : (transaction.type = 'message') + } else if (abstract.message?.files) { + transaction.recipientPublicKey = abstract.recipientPublicKey + transaction.senderPublicKey = abstract.senderPublicKey + transaction.asset = abstract.message + transaction.hash = abstract.id + transaction.message = abstract.message.comment || '' + transaction.type = 'attachment' } else { // Unsupported transaction type. May require updating the PWA version. transaction.message = 'chats.unsupported_transaction_type' diff --git a/src/lib/constants/cryptos/data.json b/src/lib/constants/cryptos/data.json index d5b99bae7..6a29b367a 100644 --- a/src/lib/constants/cryptos/data.json +++ b/src/lib/constants/cryptos/data.json @@ -103,12 +103,12 @@ "nodes": { "list": [ { - "url": "https://btcnode1.adamant.im", - "alt_ip": "http://176.9.38.204:44099" + "url": "https://btcnode1.adamant.im/bitcoind", + "alt_ip": "http://176.9.38.204:44099/bitcoind" }, { - "url": "https://btcnode3.adamant.im", - "alt_ip": "http://195.201.242.108:44099" + "url": "https://btcnode3.adamant.im/bitcoind", + "alt_ip": "http://195.201.242.108:44099/bitcoind" } ], "healthCheck": { @@ -233,6 +233,10 @@ { "url": "https://dogenode2.adamant.im", "alt_ip": "http://176.9.32.126:44098" + }, + { + "url": "https://dogenode3.adm.im", + "alt_ip": "http://95.216.45.88:44098" } ], "healthCheck": { @@ -280,13 +284,11 @@ "list": [ { "url": "https://ethnode2.adamant.im", - "alt_ip": "http://95.216.114.252:44099", - "hasIndex": true + "alt_ip": "http://95.216.114.252:44099" }, { "url": "https://ethnode3.adamant.im", - "alt_ip": "http://46.4.37.157:44099", - "hasIndex": true + "alt_ip": "http://46.4.37.157:44099" } ], "healthCheck": { @@ -300,7 +302,7 @@ "cryptoTransferDecimals": 6, "defaultVisibility": true, "defaultGasLimit": 22000, - "defaultGasPriceGwei": 30, + "defaultGasPriceGwei": 10, "txFetchInfo": { "newPendingInterval": 4000, "oldPendingInterval": 3000, @@ -394,13 +396,13 @@ "decimals": 8, "nodes": { "list": [ - { - "url": "https://klynode1.adamant.im", - "alt_ip": "http://195.26.255.137:44099" - }, { "url": "https://klynode2.adamant.im", "alt_ip": "http://109.176.199.130:44099" + }, + { + "url": "https://klynode3.adm.im", + "alt_ip": "http://37.27.205.78:44099" } ], "healthCheck": { diff --git a/src/lib/constants/index.d.ts b/src/lib/constants/index.d.ts deleted file mode 100644 index b15ae5c52..000000000 --- a/src/lib/constants/index.d.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { Cryptos, CryptosInfo, CryptosOrder, CryptoSymbol } from './cryptos' - -export declare const EPOCH: number - -export declare const Transactions: { - SEND: 0 - SIGNATURE: 1 - DELEGATE: 2 - VOTE: 3 - MULTI: 4 - DAPP: 5 - IN_TRANSFER: 6 - OUT_TRANSFER: 7 - CHAT_MESSAGE: 8 - STATE: 9 -} - -export declare const MessageType: { - BASIC_ENCRYPTED_MESSAGE: 1 - RICH_CONTENT_MESSAGE: 2 - cryptoTransferMessage: (cryptoSymbol: string) => string -} - -export type FiatCurrencySymbol = 'USD' | 'EUR' | 'RUB' | 'CNY' | 'JPY' - -export declare const Rates: { - [K in FiatCurrencySymbol]: K -} - -export declare const RatesNames: Record - -export declare const Fees: { - KVS: number - ADM_TRANSFER: number - NOT_ADM_TRANSFER: number -} - -export declare const base64regex: RegExp - -export declare const Symbols: { - CLOCK: string - HOURGLASS: string - CROSS: string -} - -export declare const Delegates: { - ACTIVE_DELEGATES: number -} - -export declare const WelcomeMessage: { - ADAMANT_BOUNTY: string - ADAMANT_ICO: string -} - -export declare const test: keyof Cryptos - -export declare const BTC_BASED: ReadonlyArray -export declare const KLY_BASED: ReadonlyArray -export declare const INSTANT_SEND: ReadonlyArray -export declare const ALLOW_TEXT_DATA: ReadonlyArray - -export declare const isErc20: (crypto: CryptoSymbol) => boolean -export declare const isEthBased: (crypto: CryptoSymbol) => boolean -export declare const isFeeEstimate: (crypto: CryptoSymbol) => boolean -export declare const isBtcBased: (crypto: CryptoSymbol) => boolean -export declare const isKlyBased: (crypto: CryptoSymbol) => boolean -export declare const isSelfTxAllowed: (crypto: CryptoSymbol) => boolean -export declare const isInstantSendPossible: (crypto: CryptoSymbol) => boolean -export declare const isTextDataAllowed: (crypto: CryptoSymbol) => boolean - -export declare const RE_KLY_ADDRESS_LEGACY: RegExp - -export declare const DEFAULT_ETH_TRANSFER_GAS_LIMIT: number -export declare const DEFAULT_ERC20_TRANSFER_GAS_LIMIT: number -export declare const INCREASE_FEE_MULTIPLIER: number - -export { Cryptos, CryptosInfo, CryptosOrder, CryptoSymbol } -export default { - EPOCH, - Transactions -} - -export declare const UserPasswordArticleLink: string -export declare const UserPasswordHashSettings: { - SALT: string - ITERATIONS: number - KEYLEN: number - DIGEST: string -} - -export type TransactionStatusType = - | 'CONFIRMED' - | 'PENDING' - | 'REGISTERED' - | 'REJECTED' - | 'INVALID' - | 'UNKNOWN' - -export declare const TransactionStatus: { - [K in TransactionStatusType]: K -} - -export declare const TransactionAdditionalStatus: { - NONE: boolean - INSTANT_SEND: string - ADM_REGISTERED: string -} - -export declare const tsIcon: (status: TransactionStatusType) => string -export declare const tsColor: (status: TransactionStatusType) => string -export declare const tsUpdatable: (status: TransactionStatusType, currency: CryptoSymbol) => boolean - -export function getMinAmount(crypto: CryptoSymbol): number - -export declare const SCROLL_TO_REPLIED_MESSAGE_ANIMATION_DURATION: number - -export declare const FetchStatus: { - Idle: 'idle' - Loading: 'loading' - Success: 'success' - Error: 'error' -} - -export declare const REACT_EMOJIS: Record - -export declare const AnimationReactionType = { - Incoming: 0, - Outgoing: 1 -} diff --git a/src/lib/constants/index.js b/src/lib/constants/index.ts similarity index 70% rename from src/lib/constants/index.js rename to src/lib/constants/index.ts index bd09c6eae..9b0f6ffa3 100644 --- a/src/lib/constants/index.js +++ b/src/lib/constants/index.ts @@ -1,4 +1,11 @@ -import { AllCryptos, AllCryptosOrder, Cryptos, CryptosInfo, CryptosOrder } from './cryptos' +import { + AllCryptos, + AllCryptosOrder, + Cryptos, + CryptosInfo, + CryptosOrder, + CryptoSymbol +} from './cryptos' export const EPOCH = Date.UTC(2017, 8, 2, 17, 0, 0, 0) @@ -13,7 +20,7 @@ export const Transactions = { OUT_TRANSFER: 7, CHAT_MESSAGE: 8, STATE: 9 -} +} as const /** * @see https://github.com/Adamant-im/adamant/wiki/Message-Types @@ -21,10 +28,12 @@ export const Transactions = { export const MessageType = { BASIC_ENCRYPTED_MESSAGE: 1, RICH_CONTENT_MESSAGE: 2, - cryptoTransferMessage: (cryptoSymbol) => `${cryptoSymbol.toLowerCase()}_transaction` -} + cryptoTransferMessage: (cryptoSymbol: string) => `${cryptoSymbol.toLowerCase()}_transaction` +} as const + +export type FiatCurrencySymbol = 'USD' | 'EUR' | 'RUB' | 'CNY' | 'JPY' -export const Rates = { +export const Rates: Record = { USD: 'USD', EUR: 'EUR', RUB: 'RUB', @@ -32,14 +41,16 @@ export const Rates = { JPY: 'JPY' } -export const RatesNames = { - [Rates.USD]: 'USD ($)', // United States Dollar - [Rates.EUR]: 'EUR (€)', // Euro - [Rates.RUB]: 'RUB (₽)', // Russian Ruble - [Rates.CNY]: 'CNY (¥)', // Chinese Yuan - [Rates.JPY]: 'JPY (¥)' // Japanese Yen +export const RatesNames: Record = { + USD: 'USD ($)', // United States Dollar + EUR: 'EUR (€)', // Euro + RUB: 'RUB (₽)', // Russian Ruble + CNY: 'CNY (¥)', // Chinese Yuan + JPY: 'JPY (¥)' // Japanese Yen } +export const UPLOAD_MAX_FILE_COUNT = 5 + /** Fees for the misc ADM operations */ export const Fees = { /** Storing a value into the KVS */ @@ -69,29 +80,19 @@ export const WelcomeMessage = { } export const BTC_BASED = Object.freeze([Cryptos.DOGE, Cryptos.DASH, Cryptos.BTC]) - export const KLY_BASED = Object.freeze([Cryptos.KLY]) - export const INSTANT_SEND = Object.freeze([Cryptos.DASH]) - -// Some cryptos allows to save public data with a Tx -export const ALLOW_TEXT_DATA = Object.freeze([Cryptos.KLY]) - -export const isErc20 = (crypto) => CryptosInfo[crypto]?.type === 'ERC20' - -export const isEthBased = (crypto) => isErc20(crypto) || crypto === Cryptos.ETH - -export const isFeeEstimate = (crypto) => isEthBased(crypto) - -export const isBtcBased = (crypto) => BTC_BASED.includes(crypto) - -export const isKlyBased = (crypto) => KLY_BASED.includes(crypto) - -export const isSelfTxAllowed = (crypto) => KLY_BASED.includes(crypto) || crypto === Cryptos.ADM - -export const isInstantSendPossible = (crypto) => INSTANT_SEND.includes(crypto) - -export const isTextDataAllowed = (crypto) => ALLOW_TEXT_DATA.includes(crypto) +export const ALLOW_TEXT_DATA = Object.freeze([Cryptos.KLY]) // Some blockchains allow storing a text message within a Tx + +export const isErc20 = (crypto: CryptoSymbol) => CryptosInfo[crypto]?.type === 'ERC20' +export const isEthBased = (crypto: CryptoSymbol) => isErc20(crypto) || crypto === Cryptos.ETH +export const isFeeEstimate = (crypto: CryptoSymbol) => isEthBased(crypto) +export const isBtcBased = (crypto: CryptoSymbol) => BTC_BASED.includes(crypto) +export const isKlyBased = (crypto: CryptoSymbol) => KLY_BASED.includes(crypto) +export const isSelfTxAllowed = (crypto: CryptoSymbol) => + KLY_BASED.includes(crypto) || crypto === Cryptos.ADM +export const isInstantSendPossible = (crypto: CryptoSymbol) => INSTANT_SEND.includes(crypto) +export const isTextDataAllowed = (crypto: CryptoSymbol) => ALLOW_TEXT_DATA.includes(crypto) export const RE_KLY_ADDRESS_LEGACY = /^[0-9]{2,21}L$/ @@ -102,14 +103,14 @@ export const RE_KLY_ADDRESS_LEGACY = /^[0-9]{2,21}L$/ */ /** Gas limit value for the ETH transfers */ -export const DEFAULT_ETH_TRANSFER_GAS_LIMIT = CryptosInfo['ETH'].defaultGasLimit +export const DEFAULT_ETH_TRANSFER_GAS_LIMIT = (CryptosInfo['ETH'] as any).defaultGasLimit // @todo fix type in adamant-wallets /** Gas limit value for the ERC-20 transfers */ export const DEFAULT_ERC20_TRANSFER_GAS_LIMIT = DEFAULT_ETH_TRANSFER_GAS_LIMIT * 2.4 /** Increase fee multiplier. Used as a checkbox on SendFundsForm */ export const INCREASE_FEE_MULTIPLIER = 1.5 -export { AllCryptos, AllCryptosOrder, Cryptos, CryptosInfo, CryptosOrder } +export { AllCryptos, AllCryptosOrder, Cryptos, CryptosInfo, CryptosOrder, type CryptoSymbol } export default { EPOCH, @@ -126,8 +127,16 @@ export const UserPasswordHashSettings = { DIGEST: 'sha512' } +export type TransactionStatusType = + | 'CONFIRMED' + | 'PENDING' + | 'REGISTERED' + | 'REJECTED' + | 'INVALID' + | 'UNKNOWN' + /** Status of ADM or coin transaction */ -export const TransactionStatus = { +export const TransactionStatus: Record = { CONFIRMED: 'CONFIRMED', // Tx has at least 1 network confirmation PENDING: 'PENDING', // We don't know about this Tx anything yet, it may not exist in a blockchain REGISTERED: 'REGISTERED', // This Ts is seen on a blockchain, but has 0 confirmations yet @@ -143,7 +152,7 @@ export const TransactionAdditionalStatus = { ADM_REGISTERED: 'adm_registered' // ADM tx, registered in a blockchain, but has 0 confirmations yet } -export const tsIcon = function (status) { +export const tsIcon = function (status: TransactionStatusType) { if (status === TransactionStatus.CONFIRMED) { return 'mdi-check' } else if (status === TransactionStatus.PENDING || status === TransactionStatus.REGISTERED) { @@ -157,7 +166,7 @@ export const tsIcon = function (status) { } } -export const tsColor = function (status) { +export const tsColor = function (status: TransactionStatusType) { if (status === TransactionStatus.REJECTED) { return 'red' } else if (status === TransactionStatus.INVALID || status === TransactionStatus.UNKNOWN) { @@ -166,8 +175,9 @@ export const tsColor = function (status) { return '' } -export const tsUpdatable = function (status, currency) { - currency = currency.toUpperCase() +export const tsUpdatable = function (status: TransactionStatusType, currency: CryptoSymbol) { + currency = currency.toUpperCase() as CryptoSymbol + if (currency === Cryptos.ADM) { return false } else if (status === TransactionStatus.CONFIRMED) { @@ -188,7 +198,7 @@ export const tsUpdatable = function (status, currency) { * @param {string} crypto crypto * @returns {number} */ -export function getMinAmount(crypto) { +export function getMinAmount(crypto: CryptoSymbol) { let amount = CryptosInfo[crypto].minTransferAmount if (!amount) { @@ -210,7 +220,7 @@ export const FetchStatus = { Loading: 'loading', Success: 'success', Error: 'error' -} +} as const export const REACT_EMOJIS = { FACE_WITH_TEARS_OF_JOY: '😂', @@ -225,4 +235,4 @@ export const REACT_EMOJIS = { FOLDED_HANDS: '🙏', FLUSHED_FACE: '😳', PARTY_POPPER: '🎉' -} +} as const diff --git a/src/lib/file.ts b/src/lib/file.ts new file mode 100644 index 000000000..0d5440cce --- /dev/null +++ b/src/lib/file.ts @@ -0,0 +1,138 @@ +import { FileData } from '@/components/UploadFile.vue' +import ipfs from '@/lib/nodes/ipfs' + +export function readFileAsDataURL(file: File): Promise { + return new Promise((resolve) => { + const reader = new FileReader() + reader.onload = (e: ProgressEvent) => { + resolve(e.target?.result as string) + } + reader.readAsDataURL(file) + }) +} + +export function readFileAsBuffer(file: File): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader() + + reader.onload = () => { + // Convert the ArrayBuffer to a Uint8Array + const arrayBuffer = reader.result as ArrayBuffer + const uint8Array = new Uint8Array(arrayBuffer) + resolve(uint8Array) + } + + reader.onerror = (error) => { + reject(error) + } + + reader.readAsArrayBuffer(file) + }) +} + +export async function uploadFiles( + files: FileData[], + onUploadProgress?: (progress: number) => void +) { + const formData = new FormData() + + for (const file of files) { + const blob = new Blob([file.encoded.binary], { type: 'application/octet-stream' }) + formData.append('files', blob, file.file.name) + + if (file.preview) { + const blob = new Blob([file.preview.encoded.binary], { type: 'application/octet-stream' }) + formData.append('files', blob, 'preview-' + file.file.name) + } + } + + onUploadProgress?.(0) // set initial progress to 0 + const response = await ipfs.upload(formData, (progress) => { + const percentCompleted = Math.round((progress.loaded * 100) / (progress.total || 0)) + + onUploadProgress?.(percentCompleted) + }) + + return response +} + +/** + * Compute CID for a file + */ +export async function computeCID(fileOrBytes: File | Uint8Array) { + const { CID } = await import('multiformats/cid') + const { code } = await import('multiformats/codecs/raw') + const { sha256 } = await import('multiformats/hashes/sha2') + + const bytes = + fileOrBytes instanceof File ? new Uint8Array(await fileOrBytes.arrayBuffer()) : fileOrBytes + + const hash = await sha256.digest(bytes) + const cid = CID.create(1, code, hash) + + return cid.toString() +} + +/** + * Crops an image to a specific maximum width or height while maintaining its aspect ratio. + * + * @param imageFile - The original image file to be cropped. + * @param maxSize - The maximum width or height of the cropped image. + * @returns A new File object with the cropped image. + */ +export async function cropImage(imageFile: File, maxSize = 500): Promise { + return new Promise((resolve, reject) => { + if (!imageFile.type.startsWith('image/')) { + reject(new Error('Provided file is not an image')) + return + } + + const img = new Image() + img.onload = () => { + const canvas = document.createElement('canvas') + const ctx = canvas.getContext('2d') + + // Define max size + let width = img.width + let height = img.height + + // Calculate new dimensions while keeping aspect ratio + if (width > height) { + if (width > maxSize) { + height *= maxSize / width + width = maxSize + } + } else { + if (height > maxSize) { + width *= maxSize / height + height = maxSize + } + } + + // Set canvas to the new calculated dimensions + canvas.width = width + canvas.height = height + + // Draw the resized image on the canvas + ctx!.drawImage(img, 0, 0, width, height) + + // Convert canvas to blob and create a new File + canvas.toBlob((blob) => { + if (blob) { + const resizedFile = new File([blob], imageFile.name, { + type: imageFile.type, + lastModified: Date.now() + }) + resolve(resizedFile) + } else { + reject(new Error('Canvas conversion to blob failed')) + } + }, imageFile.type) + + URL.revokeObjectURL(img.src) // clean up the Object URL once done + } + + img.onerror = () => reject(new Error('Failed to load the image')) + img.src = URL.createObjectURL(imageFile) + }) +} diff --git a/src/lib/nodes/abstract.client.ts b/src/lib/nodes/abstract.client.ts index 197285ae4..20ccaebf8 100644 --- a/src/lib/nodes/abstract.client.ts +++ b/src/lib/nodes/abstract.client.ts @@ -1,4 +1,5 @@ import type { HealthcheckInterval, NodeKind, NodeType } from '@/lib/nodes/types' +import { TNodeLabel } from '@/lib/nodes/constants' import { AllNodesDisabledError, AllNodesOfflineError } from './utils/errors' import { filterSyncedNodes } from './utils/filterSyncedNodes' import { Node } from './abstract.node' @@ -29,6 +30,10 @@ export abstract class Client { * Node type */ type: NodeType + /** + * Node label + */ + label: TNodeLabel /** * Resolves when at least one node is ready to accept requests @@ -37,9 +42,10 @@ export abstract class Client { resolve = () => {} initialized = false - constructor(type: NodeType, kind: NodeKind = 'node') { + constructor(type: NodeType, kind: NodeKind, label: TNodeLabel) { this.type = type this.kind = kind + this.label = label this.useFastest = nodesStorage.getUseFastest(type) this.ready = new Promise((resolve) => { @@ -198,7 +204,7 @@ export abstract class Client { protected updateSyncStatuses() { const nodes = this.nodes.filter((x) => x.online && x.active) - const nodesInSync = filterSyncedNodes(nodes, this.type) + const nodesInSync = filterSyncedNodes(nodes, this.label) // Finally, all the nodes from the winner list are considered to be in sync, all the // others are not diff --git a/src/lib/nodes/abstract.node.ts b/src/lib/nodes/abstract.node.ts index 05e019ffc..3618bf0eb 100644 --- a/src/lib/nodes/abstract.node.ts +++ b/src/lib/nodes/abstract.node.ts @@ -142,11 +142,7 @@ export abstract class Node { this.timer = setTimeout( () => this.startHealthcheck(), - getHealthCheckInterval( - this.type, - this.kind, - this.online ? this.healthCheckInterval : 'crucial' - ) + getHealthCheckInterval(this.label, this.online ? this.healthCheckInterval : 'crucial') ) } diff --git a/src/lib/nodes/adm/AdmClient.ts b/src/lib/nodes/adm/AdmClient.ts index a4f635077..3961c1713 100644 --- a/src/lib/nodes/adm/AdmClient.ts +++ b/src/lib/nodes/adm/AdmClient.ts @@ -1,5 +1,6 @@ import { isNodeOfflineError } from '@/lib/nodes/utils/errors' import { GetHeightResponseDto } from '@/lib/schema/client' +import { NODE_LABELS } from '@/lib/nodes/constants' import { AdmNode, Payload, RequestConfig } from './AdmNode' import { Client } from '../abstract.client' @@ -12,7 +13,7 @@ import { Client } from '../abstract.client' */ export class AdmClient extends Client { constructor(endpoints: string[] = [], minNodeVersion = '0.0.0') { - super('adm') + super('adm', 'node', NODE_LABELS.AdmNode) this.nodes = endpoints.map((endpoint) => new AdmNode(endpoint, minNodeVersion)) this.minNodeVersion = minNodeVersion diff --git a/src/lib/nodes/btc-indexer/BtcIndexerClient.ts b/src/lib/nodes/btc-indexer/BtcIndexerClient.ts index 936c0071e..46d200cb6 100644 --- a/src/lib/nodes/btc-indexer/BtcIndexerClient.ts +++ b/src/lib/nodes/btc-indexer/BtcIndexerClient.ts @@ -1,4 +1,5 @@ import type { AxiosRequestConfig } from 'axios' +import { NODE_LABELS } from '@/lib/nodes/constants' import { Client } from '../abstract.client' import { BtcIndexer } from './BtcIndexer' import { MULTIPLIER, normalizeTransaction } from './utils' @@ -17,7 +18,7 @@ import { GetUnspentsParams } from './types/api/get-unspents/get-unspents-params' */ export class BtcIndexerClient extends Client { constructor(endpoints: string[] = [], minNodeVersion = '0.0.0') { - super('btc') + super('btc', 'service', NODE_LABELS.BtcIndexer) this.nodes = endpoints.map((endpoint) => new BtcIndexer(endpoint)) this.minNodeVersion = minNodeVersion diff --git a/src/lib/nodes/btc-indexer/index.ts b/src/lib/nodes/btc-indexer/index.ts index 78bcc287c..1ceeed53d 100644 --- a/src/lib/nodes/btc-indexer/index.ts +++ b/src/lib/nodes/btc-indexer/index.ts @@ -2,7 +2,7 @@ import config from '@/config' import { NodeInfo } from '@/types/wallets' import { BtcIndexerClient } from './BtcIndexerClient' -const endpoints = (config.btc.nodes.list as NodeInfo[]).map((endpoint) => endpoint.url) +const endpoints = (config.btc.services.btcIndexer.list as NodeInfo[]).map((endpoint) => endpoint.url) export const btcIndexer = new BtcIndexerClient(endpoints) export default btcIndexer diff --git a/src/lib/nodes/btc/BtcClient.ts b/src/lib/nodes/btc/BtcClient.ts index a989090d7..0e8bf6df0 100644 --- a/src/lib/nodes/btc/BtcClient.ts +++ b/src/lib/nodes/btc/BtcClient.ts @@ -1,4 +1,5 @@ import type { AxiosRequestConfig } from 'axios' +import { NODE_LABELS } from '@/lib/nodes/constants' import { BtcNode } from './BtcNode' import { Client } from '../abstract.client' import { RpcRequest } from './types/api/common' @@ -12,7 +13,7 @@ import { RpcRequest } from './types/api/common' */ export class BtcClient extends Client { constructor(endpoints: string[] = [], minNodeVersion = '0.0.0') { - super('btc') + super('btc', 'node', NODE_LABELS.BtcNode) this.nodes = endpoints.map((endpoint) => new BtcNode(endpoint)) this.minNodeVersion = minNodeVersion diff --git a/src/lib/nodes/btc/BtcNode.ts b/src/lib/nodes/btc/BtcNode.ts index 371c78f16..ab0862997 100644 --- a/src/lib/nodes/btc/BtcNode.ts +++ b/src/lib/nodes/btc/BtcNode.ts @@ -53,7 +53,6 @@ export class BtcNode extends Node { return this.client .request>({ ...requestConfig, - url: '/bitcoind', method: 'POST', data: params }) diff --git a/src/lib/nodes/constants.ts b/src/lib/nodes/constants.ts index 170151e89..0d975beee 100644 --- a/src/lib/nodes/constants.ts +++ b/src/lib/nodes/constants.ts @@ -7,6 +7,7 @@ export type TNodeLabel = | 'doge-node' | 'doge-indexer' | 'dash-node' + | 'ipfs-node' | 'kly-node' | 'kly-indexer' | 'rates-info' @@ -36,6 +37,7 @@ export const NODE_LABELS: NodeLabels = { DogeNode: 'doge-node', DogeIndexer: 'doge-indexer', DashNode: 'dash-node', + IpfsNode: 'ipfs-node', KlyNode: 'kly-node', KlyIndexer: 'kly-indexer', RatesInfo: 'rates-info' diff --git a/src/lib/nodes/dash/DashClient.ts b/src/lib/nodes/dash/DashClient.ts index 3b1ffc842..5f6362da3 100644 --- a/src/lib/nodes/dash/DashClient.ts +++ b/src/lib/nodes/dash/DashClient.ts @@ -1,4 +1,5 @@ import { AxiosRequestConfig } from 'axios' +import { NODE_LABELS } from '@/lib/nodes/constants' import { DashNode } from './DashNode' import { Client } from '../abstract.client' import { normalizeTransaction } from './utils' @@ -9,7 +10,7 @@ import { Transaction } from './types/api/transaction' export class DashClient extends Client { constructor(endpoints: string[] = [], minNodeVersion = '0.0.0') { - super('dash') + super('dash', 'node', NODE_LABELS.DashNode) this.nodes = endpoints.map((endpoint) => new DashNode(endpoint)) this.minNodeVersion = minNodeVersion diff --git a/src/lib/nodes/doge-indexer/DogeIndexerClient.ts b/src/lib/nodes/doge-indexer/DogeIndexerClient.ts index 270342a76..a0575b860 100644 --- a/src/lib/nodes/doge-indexer/DogeIndexerClient.ts +++ b/src/lib/nodes/doge-indexer/DogeIndexerClient.ts @@ -1,4 +1,5 @@ import type { AxiosRequestConfig } from 'axios' +import { NODE_LABELS } from '@/lib/nodes/constants' import { DogeIndexer } from './DogeIndexer' import { Client } from '../abstract.client' import { NB_BLOCKS } from './constants' @@ -12,7 +13,7 @@ import { Balance } from './types/api/balance' export class DogeIndexerClient extends Client { constructor(endpoints: string[] = [], minNodeVersion = '0.0.0') { - super('doge') + super('doge', 'service', NODE_LABELS.DogeIndexer) this.nodes = endpoints.map((endpoint) => new DogeIndexer(endpoint)) this.minNodeVersion = minNodeVersion diff --git a/src/lib/nodes/doge/DogeClient.ts b/src/lib/nodes/doge/DogeClient.ts index e7b9fa6bd..f900a3fa2 100644 --- a/src/lib/nodes/doge/DogeClient.ts +++ b/src/lib/nodes/doge/DogeClient.ts @@ -1,11 +1,12 @@ import { AxiosRequestConfig } from 'axios' +import { NODE_LABELS } from '@/lib/nodes/constants' import { RpcRequest } from './types/api/common' import { DogeNode } from './DogeNode' import { Client } from '../abstract.client' export class DogeClient extends Client { constructor(endpoints: string[] = [], minNodeVersion = '0.0.0') { - super('doge') + super('doge', 'node', NODE_LABELS.DogeNode) this.nodes = endpoints.map((endpoint) => new DogeNode(endpoint)) this.minNodeVersion = minNodeVersion diff --git a/src/lib/nodes/eth-indexer/EthIndexerClient.ts b/src/lib/nodes/eth-indexer/EthIndexerClient.ts index 2e8a230d5..b123f9acc 100644 --- a/src/lib/nodes/eth-indexer/EthIndexerClient.ts +++ b/src/lib/nodes/eth-indexer/EthIndexerClient.ts @@ -1,4 +1,5 @@ import { AxiosRequestConfig } from 'axios' +import { NODE_LABELS } from '@/lib/nodes/constants' import { GetTransactionsParams } from './types/client/get-transactions-params' import { GetTransactionsRequest } from './types/api/get-transactions/get-transactions.request' import { Endpoints } from './types/api/endpoints' @@ -8,7 +9,7 @@ import { Client } from '../abstract.client' export class EthIndexerClient extends Client { constructor(endpoints: string[] = [], minNodeVersion = '0.0.0') { - super('eth') + super('eth', 'service', NODE_LABELS.EthIndexer) this.nodes = endpoints.map((endpoint) => new EthIndexer(endpoint)) this.minNodeVersion = minNodeVersion diff --git a/src/lib/nodes/eth-indexer/index.ts b/src/lib/nodes/eth-indexer/index.ts index b67bc4f5b..149d99eb0 100644 --- a/src/lib/nodes/eth-indexer/index.ts +++ b/src/lib/nodes/eth-indexer/index.ts @@ -2,9 +2,9 @@ import config from '@/config' import { NodeInfo } from '@/types/wallets' import { EthIndexerClient } from './EthIndexerClient' -const endpoints = (config.eth.nodes.list as NodeInfo[]) - .filter((node) => node.hasIndex) - .map((endpoint) => endpoint.url) +const endpoints = (config.eth.services.ethIndexer.list as NodeInfo[]).map( + (endpoint) => endpoint.url +) export const ethIndexer = new EthIndexerClient(endpoints) export default ethIndexer diff --git a/src/lib/nodes/eth/EthClient.ts b/src/lib/nodes/eth/EthClient.ts index 06c795460..2eda5b184 100644 --- a/src/lib/nodes/eth/EthClient.ts +++ b/src/lib/nodes/eth/EthClient.ts @@ -1,5 +1,6 @@ import { Web3Eth } from 'web3-eth' import { TransactionNotFound as Web3TransactionNotFound } from 'web3-errors' +import { NODE_LABELS } from '@/lib/nodes/constants' import { TransactionNotFound } from '@/lib/nodes/utils/errors' import { CryptoSymbol } from '@/lib/constants' import { bytesToHex } from '@/lib/hex' @@ -16,7 +17,7 @@ import { normalizeEthTransaction, normalizeErc20Transaction } from './utils' */ export class EthClient extends Client { constructor(endpoints: string[] = [], minNodeVersion = '0.0.0') { - super('eth') + super('eth', 'node', NODE_LABELS.EthNode) this.nodes = endpoints.map((endpoint) => new EthNode(endpoint)) this.minNodeVersion = minNodeVersion diff --git a/src/lib/nodes/index.ts b/src/lib/nodes/index.ts index a2774d915..25108d661 100644 --- a/src/lib/nodes/index.ts +++ b/src/lib/nodes/index.ts @@ -7,6 +7,7 @@ export { dogeIndexer } from './doge-indexer' export { eth } from './eth' export { kly } from './kly' export { klyIndexer } from './kly-indexer' +export { ipfs } from './ipfs' export { nodes } from './nodes' export { services } from './services' diff --git a/src/lib/nodes/ipfs/IpfsClient.ts b/src/lib/nodes/ipfs/IpfsClient.ts new file mode 100644 index 000000000..f88d5fbe0 --- /dev/null +++ b/src/lib/nodes/ipfs/IpfsClient.ts @@ -0,0 +1,85 @@ +import { isNodeOfflineError } from '@/lib/nodes/utils/errors' +import { AxiosProgressEvent } from 'axios' +import { NODE_LABELS } from '@/lib/nodes/constants' +import { IpfsNode, Payload, RequestConfig } from './IpfsNode.ts' +import { Client } from '../abstract.client' + +/** + * Provides methods for calling the ADAMANT API. + * + * The `ApiClient` instance automatically selects an ADAMANT node to + * send the API-requests to and switches to another node if the current one + * is not available at the moment. + */ +export class IpfsClient extends Client { + constructor(endpoints: string[] = [], minNodeVersion = '0.0.0') { + super('ipfs', 'service', NODE_LABELS.IpfsNode) + this.nodes = endpoints.map((endpoint) => new IpfsNode(endpoint, minNodeVersion)) + this.minNodeVersion = minNodeVersion + + void this.watchNodeStatusChange() + } + + async downloadFile(cid: string) { + return this.request({ + method: 'get', + url: `api/file/${cid}`, + responseType: 'arraybuffer' + }) + } + + async upload(payload: FormData, onUploadProgress?: (progressEvent: AxiosProgressEvent) => void) { + return this.request({ + method: 'post', + url: '/api/file/upload', + payload, + headers: { + 'Content-Type': 'multipart/form-data' + }, + onUploadProgress + }) + } + + /** + * Performs a GET API request. + * @param {String} url relative API url + * @param {any} params request params (an object) or a function that accepts `ApiNode` and returns the request params + */ + get

(url: string, params: P) { + return this.request({ method: 'get', url, payload: params }) + } + + /** + * Performs a POST API request. + * @param {String} url relative API url + * @param {any} payload request payload (an object) or a function that accepts `ApiNode` and returns the request payload + * @param {Record} headers request headers + */ + post

(url: string, payload: P, headers: Record) { + return this.request({ method: 'post', url, payload, headers }) + } + + /** + * Performs an API request. + * @param {RequestConfig} config request config + */ + async request

(config: RequestConfig

): Promise { + const node = this.useFastest ? this.getFastestNode() : this.getRandomNode() + if (!node) { + // All nodes seem to be offline: let's refresh the statuses + this.checkHealth() + // But there's nothing we can do right now + return Promise.reject(new Error('No online nodes at the moment')) + } + + return node.request(config).catch((error) => { + if (isNodeOfflineError(error)) { + // Initiate nodes status check + this.checkHealth() + // If the selected node is not available, repeat the request with another one. + return this.request(config) + } + throw error + }) + } +} diff --git a/src/lib/nodes/ipfs/IpfsNode.ts b/src/lib/nodes/ipfs/IpfsNode.ts new file mode 100644 index 000000000..c1953206a --- /dev/null +++ b/src/lib/nodes/ipfs/IpfsNode.ts @@ -0,0 +1,126 @@ +import utils from '@/lib/adamant' +import { NodeOfflineError } from '@/lib/nodes/utils/errors' +import axios, { AxiosInstance, AxiosProgressEvent, AxiosRequestConfig, ResponseType } from 'axios' +import { Node } from '@/lib/nodes/abstract.node' +import { NODE_LABELS } from '@/lib/nodes/constants' + +type FetchNodeInfoResult = { + availableSizeInMb: number + blockstoreSizeMb: number + datastoreSizeMb: number + heliaStatus: string + timestamp: number + version: string +} + +export type Payload = + | Record + | { + (ctx: IpfsNode): Record + } +export type RequestConfig

= { + url: string + headers?: Record + method?: string + payload?: P + onUploadProgress?: (progress: AxiosProgressEvent) => void + responseType?: ResponseType +} + +/** + * Encapsulates a node. Provides methods to send API-requests + * to the node and verify is status (online/offline, version, ping, etc.) + */ +export class IpfsNode extends Node { + constructor(url: string, minNodeVersion = '0.0.0') { + super(url, 'ipfs', 'node', NODE_LABELS.IpfsNode, '', minNodeVersion) + } + + protected buildClient(): AxiosInstance { + return axios.create({ + baseURL: this.url, + timeout: 60 * 10 * 1000 + }) + } + + /** + * Performs an API request. + * + * The `payload` of the `cfg` can be either an object or a function that + * accepts `ApiNode` as a first argument and returns an object. + */ + request

(cfg: RequestConfig

): Promise { + const { url, headers, method = 'get', payload, onUploadProgress } = cfg + + const config: AxiosRequestConfig = { + url, + method: method.toLowerCase(), + headers, + // responseType: url.includes('file') ? 'arraybuffer' : 'json', + [method === 'get' ? 'params' : 'data']: + typeof payload === 'function' ? payload(this) : payload, + responseType: cfg.responseType, + onUploadProgress + } + + return this.client.request(config).then( + (response) => { + const body = response.data + // Refresh time delta on each request + if (body && isFinite(body.nodeTimestamp)) { + this.timeDelta = utils.epochTime() - body.nodeTimestamp + } + + return body + }, + (error) => { + // According to https://github.com/axios/axios#handling-errors this means, that request was sent, + // but server could not respond. + if (!error.response && error.request) { + this.online = false + throw new NodeOfflineError() + } + throw error + } + ) + } + + /** + * Fetch node version, block height and ping. + * @returns {Promise<{version: string, height: number, ping: number}>} + */ + private async fetchNodeInfo(): Promise { + const { + availableSizeInMb, + blockstoreSizeMb, + datastoreSizeMb, + heliaStatus, + timestamp, + version + } = await this.request({ + url: '/api/node/info' + }) + + this.version = version + this.height = timestamp + + return { + availableSizeInMb, + blockstoreSizeMb, + datastoreSizeMb, + heliaStatus, + timestamp, + version + } + } + + protected async checkHealth() { + const time = Date.now() + const { timestamp } = await this.fetchNodeInfo() + + return { + height: timestamp, + ping: Date.now() - time + } + } +} diff --git a/src/lib/nodes/ipfs/index.ts b/src/lib/nodes/ipfs/index.ts new file mode 100644 index 000000000..791331fbd --- /dev/null +++ b/src/lib/nodes/ipfs/index.ts @@ -0,0 +1,8 @@ +import config from '@/config' +import { NodeInfo } from '@/types/wallets' +import { IpfsClient } from './IpfsClient.ts' + +const endpoints = (config.adm.services.ipfsNode.list as NodeInfo[]).map((endpoint) => endpoint.url) +export const ipfs = new IpfsClient(endpoints, config.adm.services.minVersion) + +export default ipfs diff --git a/src/lib/nodes/kly-indexer/KlyIndexerClient.ts b/src/lib/nodes/kly-indexer/KlyIndexerClient.ts index 20b2d0237..7806a2c28 100644 --- a/src/lib/nodes/kly-indexer/KlyIndexerClient.ts +++ b/src/lib/nodes/kly-indexer/KlyIndexerClient.ts @@ -1,6 +1,7 @@ +import { AxiosRequestConfig } from 'axios' import { KLY_TOKEN_ID } from '@/lib/klayr' +import { NODE_LABELS } from '@/lib/nodes/constants' import { TransactionNotFound } from '@/lib/nodes/utils/errors' -import { AxiosRequestConfig } from 'axios' import { normalizeTransaction } from './utils' import { TransactionParams } from './types/api/transactions/transaction-params' import { Endpoints } from './types/api/endpoints' @@ -9,7 +10,7 @@ import { Client } from '../abstract.client' export class KlyIndexerClient extends Client { constructor(endpoints: string[] = [], minNodeVersion = '0.0.0') { - super('kly') + super('kly', 'service', NODE_LABELS.KlyIndexer) this.nodes = endpoints.map((endpoint) => new KlyIndexer(endpoint)) this.minNodeVersion = minNodeVersion diff --git a/src/lib/nodes/kly-indexer/index.ts b/src/lib/nodes/kly-indexer/index.ts index 0475131f1..d791926f1 100644 --- a/src/lib/nodes/kly-indexer/index.ts +++ b/src/lib/nodes/kly-indexer/index.ts @@ -1,8 +1,10 @@ import config from '@/config' -import { Service } from '@/types/wallets' +import { NodeInfo } from '@/types/wallets' import { KlyIndexerClient } from './KlyIndexerClient' -const endpoints = (config.kly.services.list.klyService as Service[]).map((endpoint) => endpoint.url) +const endpoints = (config.kly.services.klyService.list as NodeInfo[]).map( + (endpoint) => endpoint.url +) export const klyIndexer = new KlyIndexerClient(endpoints) export default klyIndexer diff --git a/src/lib/nodes/kly/KlyClient.ts b/src/lib/nodes/kly/KlyClient.ts index d864012bb..d58b66bf9 100644 --- a/src/lib/nodes/kly/KlyClient.ts +++ b/src/lib/nodes/kly/KlyClient.ts @@ -1,4 +1,5 @@ import { convertBeddowsTokly } from '@klayr/transactions' +import { NODE_LABELS } from '@/lib/nodes/constants' import { KLY_TOKEN_ID } from '@/lib/klayr' import { RpcMethod, RpcResults } from './types/api' import { KlyNode } from './KlyNode' @@ -6,7 +7,7 @@ import { Client } from '../abstract.client' export class KlyClient extends Client { constructor(endpoints: string[] = [], minNodeVersion = '0.0.0') { - super('kly') + super('kly', 'node', NODE_LABELS.KlyNode) this.nodes = endpoints.map((endpoint) => new KlyNode(endpoint)) this.minNodeVersion = minNodeVersion diff --git a/src/lib/nodes/nodes.ts b/src/lib/nodes/nodes.ts index 8541b2166..01507fae9 100644 --- a/src/lib/nodes/nodes.ts +++ b/src/lib/nodes/nodes.ts @@ -3,6 +3,7 @@ import { btc } from './btc' import { dash } from './dash' import { doge } from './doge' import { eth } from './eth' +import { ipfs } from './ipfs' import { kly } from './kly' export const nodes = { @@ -11,5 +12,6 @@ export const nodes = { dash, doge, eth, + ipfs, kly } diff --git a/src/lib/nodes/rate-info-service/RateInfoClient.ts b/src/lib/nodes/rate-info-service/RateInfoClient.ts index b9bf47c48..a19084496 100644 --- a/src/lib/nodes/rate-info-service/RateInfoClient.ts +++ b/src/lib/nodes/rate-info-service/RateInfoClient.ts @@ -1,10 +1,11 @@ +import { NODE_LABELS } from '@/lib/nodes/constants' import { Client } from '@/lib/nodes/abstract.client' import { RateInfoService } from '@/lib/nodes/rate-info-service/RateInfoService' import { RateInfoResponse } from '@/lib/nodes/rate-info-service/types/RateInfoResponse' export class RateInfoClient extends Client { constructor(endpoints: string[] = [], minNodeVersion = '0.0.0') { - super('adm', 'service') + super('adm', 'service', NODE_LABELS.RatesInfo) this.nodes = endpoints.map((endpoint) => new RateInfoService(endpoint)) this.minNodeVersion = minNodeVersion diff --git a/src/lib/nodes/rate-info-service/index.ts b/src/lib/nodes/rate-info-service/index.ts index 7fe84487f..a5e8d965b 100644 --- a/src/lib/nodes/rate-info-service/index.ts +++ b/src/lib/nodes/rate-info-service/index.ts @@ -2,7 +2,7 @@ import config from '@/config' import { NodeInfo } from '@/types/wallets' import { RateInfoClient } from '@/lib/nodes/rate-info-service/RateInfoClient' -const endpoints = (config.adm.services.list.infoService as NodeInfo[]).map( +const endpoints = (config.adm.services.infoService.list as NodeInfo[]).map( (endpoint) => endpoint.url ) export const rateInfoClient = new RateInfoClient(endpoints) diff --git a/src/lib/nodes/storage.ts b/src/lib/nodes/storage.ts index a4650b372..191a20ae7 100644 --- a/src/lib/nodes/storage.ts +++ b/src/lib/nodes/storage.ts @@ -28,7 +28,8 @@ const defaultOptions: Options = { doge: defaultConfig, dash: defaultConfig, eth: defaultConfig, - kly: defaultConfig + kly: defaultConfig, + ipfs: defaultConfig } const optionsStorage = new TypedStorage( diff --git a/src/lib/nodes/types.ts b/src/lib/nodes/types.ts index 3c6e72cde..07a8c1d34 100644 --- a/src/lib/nodes/types.ts +++ b/src/lib/nodes/types.ts @@ -5,7 +5,7 @@ export type NodeStatus = | 'sync' // node is out of sync (too low block height) | 'unsupported_version' // node version is too low -export type NodeType = 'adm' | 'eth' | 'btc' | 'doge' | 'dash' | 'kly' +export type NodeType = 'adm' | 'eth' | 'btc' | 'doge' | 'dash' | 'ipfs' | 'kly' export type NodeKind = 'node' | 'service' export type HealthcheckInterval = 'normal' | 'crucial' | 'onScreen' diff --git a/src/lib/nodes/utils/filterSyncedNodes.ts b/src/lib/nodes/utils/filterSyncedNodes.ts index 8f5022583..f57996de8 100644 --- a/src/lib/nodes/utils/filterSyncedNodes.ts +++ b/src/lib/nodes/utils/filterSyncedNodes.ts @@ -1,4 +1,4 @@ -import { NodeType } from '@/lib/nodes/types' +import { TNodeLabel } from '@/lib/nodes/constants' import { getNodeHealthcheckConfig } from './getHealthcheckConfig' /** @@ -7,10 +7,10 @@ import { getNodeHealthcheckConfig } from './getHealthcheckConfig' * If two nodes' heights differ by no more than this value, * they are considered to be in sync with each other. */ -function getHeightEpsilon(nodeType: NodeType): number { - const config = getNodeHealthcheckConfig(nodeType) +function getHeightEpsilon(nodeLabel: TNodeLabel): number { + const config = getNodeHealthcheckConfig(nodeLabel) - return config.threshold + return config.threshold || 10000 } interface Node { @@ -27,7 +27,10 @@ type GroupNodes = { * height (considering HEIGHT_EPSILON). These nodes are considered to be in sync with the network, * all the others are not. */ -export function filterSyncedNodes(nodes: N[], type: NodeType): GroupNodes { +export function filterSyncedNodes( + nodes: N[], + nodeLabel: TNodeLabel +): GroupNodes { if (nodes.length === 0) { return { height: 0, @@ -35,7 +38,7 @@ export function filterSyncedNodes(nodes: N[], type: NodeType): G } } - const heightEpsilon = getHeightEpsilon(type) + const heightEpsilon = getHeightEpsilon(nodeLabel) // For each node we take its height and list of nodes that have the same height ± epsilon const groups = nodes.map((node) => { diff --git a/src/lib/nodes/utils/getHealthcheckConfig.ts b/src/lib/nodes/utils/getHealthcheckConfig.ts index 84d8b59f1..2255b087f 100644 --- a/src/lib/nodes/utils/getHealthcheckConfig.ts +++ b/src/lib/nodes/utils/getHealthcheckConfig.ts @@ -1,27 +1,40 @@ import config from '@/config' -import { HealthcheckInterval, NodeKind, NodeType } from '@/lib/nodes/types' -import type { NodeHealthcheck, ServiceHealthcheck } from '@/types/wallets' +import { TNodeLabel } from '@/lib/nodes/constants.ts' +import { HealthcheckInterval } from '@/lib/nodes/types' +import type { NodeHealthcheck } from '@/types/wallets' -export function getNodeHealthcheckConfig(nodeType: NodeType): NodeHealthcheck { - return config[nodeType].nodes.healthCheck -} - -export function getServiceHealthcheckConfig(nodeType: NodeType): ServiceHealthcheck { - if (!config[nodeType].services) { - return config[nodeType].nodes.healthCheck // Workaround: there no configuration for ETH services +export function getNodeHealthcheckConfig(nodeLabel: TNodeLabel): NodeHealthcheck { + switch (nodeLabel) { + case 'adm-node': + return config.adm.nodes.healthCheck + case 'eth-node': + return config.eth.nodes.healthCheck + case 'eth-indexer': + return config.eth.services.ethIndexer.healthCheck + case 'btc-node': + return config.btc.nodes.healthCheck + case 'btc-indexer': + return config.btc.services.btcIndexer.healthCheck + case 'doge-node': + case 'doge-indexer': + return config.doge.nodes.healthCheck + case 'dash-node': + return config.dash.nodes.healthCheck + case 'ipfs-node': + return config.adm.services.ipfsNode.healthCheck + case 'kly-node': + return config.kly.nodes.healthCheck + case 'kly-indexer': + return config.kly.services.klyService.healthCheck + case 'rates-info': + return config.adm.services.infoService.healthCheck + default: + throw new Error(`No healthcheck configuration found for ${nodeLabel}`) } - return config[nodeType].services.healthCheck } -export function getHealthCheckInterval( - nodeType: NodeType, - nodeKind: NodeKind, - interval: HealthcheckInterval -) { - const config = - nodeKind === 'service' - ? getServiceHealthcheckConfig(nodeType) - : getNodeHealthcheckConfig(nodeType) +export function getHealthCheckInterval(nodeLabel: TNodeLabel, interval: HealthcheckInterval) { + const config = getNodeHealthcheckConfig(nodeLabel) switch (interval) { case 'normal': diff --git a/src/locales/de.json b/src/locales/de.json index 3bd260287..f9dbcbba9 100644 --- a/src/locales/de.json +++ b/src/locales/de.json @@ -135,7 +135,8 @@ "tabs": { "adm_nodes": "ADM Knooppunten", "coin_nodes": "Munt Knooppunten", - "service_nodes": "Service Knooppunten" + "service_nodes": "Service Knooppunten", + "ipfs_nodes": "IPFS-Knoten" } }, "notifications": { diff --git a/src/locales/en.json b/src/locales/en.json index f959ca233..1f79ca724 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -16,6 +16,8 @@ "incorrect_address": "Incorrect ADM address", "no_public_key": "This address is not active yet. Unable to start a chat", "unable_to_retrieve_no_public_key": "`Unable to decrypt message: no partner's public key`", + "max_files": "You can upload max {count} files", + "files": "file(s)", "me": "Me", "message": "Type a message", "message_rejected": "Message rejected", @@ -191,7 +193,8 @@ "tabs": { "adm_nodes": "ADM nodes", "coin_nodes": "Coin nodes", - "service_nodes": "Service nodes" + "service_nodes": "Service nodes", + "ipfs_nodes": "IPFS nodes" }, "unsupported": "Unsupported", "unsupported_reason_protocol": "HTTP is not allowed", diff --git a/src/locales/ru.json b/src/locales/ru.json index 7d70fecc0..55d4e6bad 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -16,6 +16,8 @@ "incorrect_address": "Неправильный ADM-адрес", "no_public_key": "Этот адрес еще не использовался. С ним нельзя начать чат", "unable_to_retrieve_no_public_key": "`Не могу прочитать сообщение: нет публичного ключа собеседника`", + "max_files": "Вы можете загрузить максимум {count} файл(ов)", + "files": "файл(ов)", "me": "Я", "message": "Введите сообщение", "message_rejected": "Сообщение отклонено", @@ -192,7 +194,8 @@ "tabs": { "adm_nodes": "Узлы ADM", "coin_nodes": "Крипто-ноды", - "service_nodes": "Крипто-сервисы" + "service_nodes": "Крипто-сервисы", + "ipfs_nodes": "IPFS ноды" }, "unsupported": "Не поддерживается", "unsupported_reason_protocol": "HTTP не поддерживается", diff --git a/src/locales/zh.json b/src/locales/zh.json index 013b86460..792014427 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -190,7 +190,8 @@ "tabs": { "adm_nodes": "ADM节点", "coin_nodes": "硬币节点", - "service_nodes": "服务节点" + "service_nodes": "服务节点", + "ipfs_nodes": "IPFS节点" }, "unsupported": "不受欢迎", "unsupported_reason_protocol": "不允许HTTP", diff --git a/src/store/index.js b/src/store/index.js index 566a74681..f9c868d54 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -20,6 +20,7 @@ import navigatorOnline from './plugins/navigatorOnline' import socketsPlugin from './plugins/socketsPlugin' import partnersModule from './modules/partners' import admModule from './modules/adm' +import attachmentModule from './modules/attachment' import botCommandsModule from './modules/bot-commands' import bitcoinModule from './modules/btc' import dashModule from './modules/dash' @@ -236,6 +237,7 @@ const store = { }, modules: { adm: admModule, // ADM transfers + attachment: attachmentModule, // Files and photos attachments doge: dogeModule, kly: klyModule, dash: dashModule, diff --git a/src/store/modules/attachment/index.ts b/src/store/modules/attachment/index.ts new file mode 100644 index 000000000..14b163eeb --- /dev/null +++ b/src/store/modules/attachment/index.ts @@ -0,0 +1,102 @@ +import { MutationTree, GetterTree, ActionTree } from 'vuex' +import { RootState } from '@/store/types' +import { AttachmentsState } from '@/store/modules/attachment/types' +import { AttachmentApi } from '@/lib/attachment-api' + +const state = (): AttachmentsState => ({ + attachments: {}, + uploadProgress: {} +}) + +const mutations: MutationTree = { + setAttachment(state, { cid, url }) { + state.attachments = { ...state.attachments, [cid]: url } + }, + + setUploadProgress(state, { cid, progress }: { cid: string; progress: number }) { + state.uploadProgress[cid] = progress + }, + + resetUploadProgress(state, { cid }: { cid: string }) { + delete state.uploadProgress[cid] + }, + + reset(state) { + state.attachments = {} + } +} + +const getters: GetterTree = { + getImageUrl: (state) => (cid: string) => { + return state.attachments[cid] + }, + getUploadProgress: (state) => (cid: string) => { + return state.uploadProgress[cid] ?? 100 + }, + isUploadInProgress(state) { + for (const progress of Object.values(state.uploadProgress)) { + if (progress < 100) return true + } + + return false + } +} + +let attachmentApi: AttachmentApi | null = null +const actions: ActionTree = { + afterLogin: { + root: true, + handler(context, passphrase) { + attachmentApi = new AttachmentApi(passphrase) + } + }, + getAttachment( + _context, + { cid, publicKey, nonce }: { cid: string; publicKey: string; nonce: string } + ) { + return attachmentApi?.getFile(cid, nonce, publicKey) + }, + async getAttachmentUrl( + { state, commit }, + { cid, publicKey, nonce }: { cid: string; publicKey: string; nonce: string } + ) { + if (state.attachments[cid]) { + return state.attachments[cid] + } else { + try { + const fileData = await attachmentApi?.getFile(cid, nonce, publicKey) + if (!fileData) { + throw new Error('Failed to fetch image') + } + + const blob = new Blob([fileData], { type: 'application/octet-stream' }) + const url = URL.createObjectURL(blob) + commit('setAttachment', { cid, url }) + return url + } catch (error) { + console.error('Error fetching image:', error) + throw error + } + } + }, + async uploadAttachment(state, { file, publicKey }: { file: Uint8Array; publicKey: string }) { + return attachmentApi?.uploadFile(file, publicKey) + }, + reset: { + root: true, + handler({ state, commit }) { + for (const fileUrl of Object.values(state.attachments)) { + URL.revokeObjectURL(fileUrl) + } + commit('reset') + } + } +} + +export default { + state, + mutations, + getters, + actions, + namespaced: true +} diff --git a/src/store/modules/attachment/types.ts b/src/store/modules/attachment/types.ts new file mode 100644 index 000000000..38d9110da --- /dev/null +++ b/src/store/modules/attachment/types.ts @@ -0,0 +1,10 @@ +export interface AttachmentsState { + /** + * Record + */ + attachments: { [cid: string]: string } + /** + * Stores upload progress of a file in % + */ + uploadProgress: { [cid: string]: number } +} diff --git a/src/store/modules/chat/index.js b/src/store/modules/chat/index.js index 0d912cd28..35d07c9d2 100644 --- a/src/store/modules/chat/index.js +++ b/src/store/modules/chat/index.js @@ -7,14 +7,15 @@ import { createMessage, createTransaction, createReaction, - normalizeMessage + normalizeMessage, + createAttachment } from '@/lib/chat/helpers' import { i18n } from '@/i18n' import { isNumeric } from '@/lib/numericHelpers' import { Cryptos, TransactionStatus as TS, MessageType } from '@/lib/constants' import { isStringEqualCI } from '@/lib/textHelpers' -import { replyMessageAsset } from '@/lib/adamant-api/asset' - +import { replyMessageAsset, attachmentAsset } from '@/lib/adamant-api/asset' +import { uploadFiles } from '../../../lib/file' import { generateAdamantChats } from './utils/generateAdamantChats' import { isAllNodesDisabledError, isAllNodesOfflineError } from '@/lib/nodes/utils/errors' @@ -459,7 +460,7 @@ const mutations = { * @param {string} realId Real id (from server) * @param {string} status Message status */ - updateMessage(state, { partnerId, id, realId, status }) { + updateMessage(state, { partnerId, id, realId, status, asset }) { const chat = state.chats[partnerId] if (chat) { @@ -472,6 +473,9 @@ const mutations = { if (status) { message.status = status } + if (asset) { + message.asset = asset + } } } }, @@ -754,6 +758,99 @@ const actions = { }) }, + /** + * Send files to the chat. + * After confirmation, `id` and `status` will be updated. + * + * @param {string} message + * @param {string} recipientId + * @param {FileData[]} files + * @param {string} replyToId Optional + * @returns {Promise} + */ + async sendAttachment({ commit, rootState }, { files, message, recipientId, replyToId }) { + let messageObject = createAttachment({ + message, + recipientId, + senderId: rootState.address, + files, + replyToId + }) + console.log('Pushed message', messageObject) + + commit('pushMessage', { + message: messageObject, + userId: rootState.address + }) + + // Updating CIDs and Nonces + const nonces = files.map((file) => [file.encoded.nonce, file.preview.encoded?.nonce]) + const cids = files.map((file) => [file.cid, file.preview?.cid]) + + let newAsset = replyToId + ? { replyto_id: replyToId, reply_message: attachmentAsset(files, nonces, cids, message) } + : attachmentAsset(files, nonces, cids, message) + commit('updateMessage', { + id: messageObject.id, + partnerId: recipientId, + asset: newAsset + }) + console.log('Updated CIDs and Nonces', newAsset) + + try { + const uploadData = await uploadFiles(files, (progress) => { + for (const [cid] of cids) { + commit('attachment/setUploadProgress', { cid, progress }, { root: true }) + } + }) + console.log('Files uploaded', uploadData) + } catch (err) { + commit('updateMessage', { + id: messageObject.id, + status: TS.REJECTED, + partnerId: recipientId + }) + + for (const [cid] of cids) { + commit('attachment/resetUploadProgress', { cid }, { root: true }) + } + + throw err + } + + for (const [cid] of cids) { + commit('attachment/resetUploadProgress', { cid }, { root: true }) + } + + return queueMessage(newAsset, recipientId, MessageType.RICH_CONTENT_MESSAGE) + .then((res) => { + if (!res.success) { + throw new Error(i18n.global.t('chats.message_rejected')) + } + + // update `message.status` to 'REGISTERED' + // and `message.id` with `realId` from server + commit('updateMessage', { + id: messageObject.id, + realId: res.transactionId, + status: TS.REGISTERED, // not confirmed yet, wait to be stored in the blockchain (optional line) + partnerId: recipientId + }) + + return res + }) + .catch((err) => { + // update `message.status` to 'REJECTED' + commit('updateMessage', { + id: messageObject.id, + status: TS.REJECTED, + partnerId: recipientId + }) + + throw err // call the error again so that it can be processed inside view + }) + }, + /** * Resend message, in case the connection fails. * @param {string} id Recipient Id diff --git a/src/store/modules/nodes/nodes-actions.js b/src/store/modules/nodes/nodes-actions.js index 3661d4e06..8e8602ef7 100644 --- a/src/store/modules/nodes/nodes-actions.js +++ b/src/store/modules/nodes/nodes-actions.js @@ -16,5 +16,8 @@ export default { }, setUseFastestCoinNode(context, payload) { context.commit('useFastestCoinNode', payload) + }, + setUseFastestIpfsNode(context, payload) { + context.commit('useFastestIpfsNode', payload) } } diff --git a/src/store/modules/nodes/nodes-getters.js b/src/store/modules/nodes/nodes-getters.js index ab9541375..1dcf567fc 100644 --- a/src/store/modules/nodes/nodes-getters.js +++ b/src/store/modules/nodes/nodes-getters.js @@ -14,6 +14,9 @@ export default { dash(state) { return Object.values(state.dash) }, + ipfs(state) { + return Object.values(state.ipfs) + }, kly(state) { return Object.values(state.kly) }, diff --git a/src/store/modules/nodes/nodes-mutations.js b/src/store/modules/nodes/nodes-mutations.js index 8c747c815..bfec4e219 100644 --- a/src/store/modules/nodes/nodes-mutations.js +++ b/src/store/modules/nodes/nodes-mutations.js @@ -5,6 +5,9 @@ export default { useFastestCoinNode(state, value) { state.useFastestCoinNode = value }, + useFastestIpfsNode(state, value) { + state.useFastestIpfsNode = value + }, toggle(state, payload) { const node = state[payload.type][payload.url] diff --git a/src/store/modules/nodes/nodes-plugin.js b/src/store/modules/nodes/nodes-plugin.js index fa930c7ff..c5b2468f5 100644 --- a/src/store/modules/nodes/nodes-plugin.js +++ b/src/store/modules/nodes/nodes-plugin.js @@ -10,6 +10,7 @@ export default (store) => { } store.commit('nodes/useFastestAdmNode', nodes.adm.useFastest) store.commit('nodes/useFastestCoinNode', nodes.btc.useFastest) + store.commit('nodes/useFastestIpfsNode', nodes.ipfs.useFastest) store.subscribe((mutation) => { const { type, payload } = mutation @@ -26,6 +27,10 @@ export default (store) => { nodes.kly.setUseFastest(!!payload) } + if (type === 'nodes/useFastestIpfsNode') { + nodes.ipfs.setUseFastest(!!payload) + } + if (type === 'nodes/toggle') { const selectedNodeType = payload.type const newStatus = nodes[selectedNodeType].toggleNode(payload.url, payload.active) diff --git a/src/store/modules/nodes/nodes-state.js b/src/store/modules/nodes/nodes-state.js index 7a6dbe2bd..03c5027d4 100644 --- a/src/store/modules/nodes/nodes-state.js +++ b/src/store/modules/nodes/nodes-state.js @@ -4,7 +4,9 @@ export default { btc: {}, doge: {}, dash: {}, + ipfs: {}, kly: {}, useFastestAdmNode: false, - useFastestCoinNode: true + useFastestCoinNode: true, + useFastestIpfsNode: true } diff --git a/src/types/wallets/index.ts b/src/types/wallets/index.ts index 2105a5709..d0cf4cf19 100644 --- a/src/types/wallets/index.ts +++ b/src/types/wallets/index.ts @@ -26,6 +26,25 @@ export interface NodeHealthcheck { threshold: number } +export interface ProjectLink { + name: string + url: string +} + +export interface Service { + /** Service node description */ + description: ServiceDescription + list: NodeInfo[] + healthCheck: ServiceHealthcheck +} + +/** Service node description */ +export interface ServiceDescription { + software: string + github?: string + docs?: string +} + export interface ServiceHealthcheck { /** Regular node status update interval in ms */ normalUpdateInterval: number @@ -35,14 +54,12 @@ export interface ServiceHealthcheck { onScreenUpdateInterval: number } -export interface ProjectLink { - name: string - url: string -} - -export interface Service { - url: string - alt_ip?: string +/** Timeouts when sending messages in chat. See [README.md](https://github.com/Adamant-im/adamant-wallets/blob/master/README.md#message-sending) for details. */ +export interface MessageTimeout { + /** Timeout for regular messages (in milliseconds) */ + message: string + /** Timeout for file transfers (in milliseconds) */ + attachment: number } export interface TokenGeneral { @@ -110,6 +127,8 @@ export interface TokenGeneral { /** Attempts to fetch Tx when its current status is "Pending" for old transactions */ oldPendingAttempts?: number } + /** Timeouts when sending messages in chat. See [README.md](https://github.com/Adamant-im/adamant-wallets/blob/master/README.md#message-sending) for details. */ + timeout?: MessageTimeout /** Time in ms when difference between in-chat transfer and Tx timestamp considered as acceptable */ txConsistencyMaxTime?: number nodes?: { @@ -122,16 +141,7 @@ export interface TokenGeneral { */ minVersion?: string } - services?: { - /** Service node links for API */ - list: Record - healthCheck: ServiceHealthcheck - /** - * Minimal service node API version - * @example "1.0.0" - */ - minVersion?: string - } + services?: Record /** Additional project links */ links?: ProjectLink[] tor?: { @@ -153,16 +163,7 @@ export interface TokenGeneral { */ minVersion?: string } - services?: { - /** Service node links for API */ - list: Record - healthCheck?: ServiceHealthcheck - /** - * Minimal service node API version - * @example "1.0.0" - */ - minVersion?: string - } + services?: Record /** Additional project links (Tor) */ links?: ProjectLink[] } diff --git a/tsconfig.json b/tsconfig.json index c15bae5e4..f7320ae6b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,6 +7,7 @@ "moduleResolution": "Node", "strict": true, "jsx": "preserve", + "jsxImportSource": "vue", "resolveJsonModule": true, "isolatedModules": true, "esModuleInterop": true, diff --git a/vite-base.config.ts b/vite-base.config.ts index 84e999cbe..74fe25ae5 100644 --- a/vite-base.config.ts +++ b/vite-base.config.ts @@ -1,5 +1,6 @@ import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' +import vueJsx from '@vitejs/plugin-vue-jsx' import wasm from 'vite-plugin-wasm' import topLevelAwait from 'vite-plugin-top-level-await' import path from 'path' @@ -16,6 +17,7 @@ export default defineConfig({ wasm(), topLevelAwait(), vue(), + vueJsx(), commonjs(), inject({ Buffer: ['buffer', 'Buffer']