Browse Source

ECOin wallet implementation + updater fixed

psy 2 months ago
parent
commit
a5578a6815
9 changed files with 923 additions and 100 deletions
  1. 240 5
      package-lock.json
  2. 3 1
      package.json
  3. 67 0
      src/assets/style.css
  4. 20 0
      src/cli.js
  5. 128 24
      src/index.js
  6. 52 1
      src/models.js
  7. 106 49
      src/updater.js
  8. 74 0
      src/views/i18n.js
  9. 233 20
      src/views/index.js

+ 240 - 5
package-lock.json

@@ -1,6 +1,6 @@
 {
   "name": "@krakenslab/oasis",
-  "version": "0.3.0",
+  "version": "0.3.1",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
@@ -12,6 +12,7 @@
       "dependencies": {
         "@fraction/base16-css": "^1.1.0",
         "@koa/router": "^13.1.0",
+        "@open-rpc/client-js": "^1.8.1",
         "abstract-level": "^2.0.1",
         "await-exec": "^0.1.2",
         "axios": "^1.7.9",
@@ -58,6 +59,7 @@
         "pull-sort": "^1.0.2",
         "pull-stream": "^3.7.0",
         "punycode.js": "^2.3.1",
+        "qrcode": "^1.5.4",
         "remark-html": "^16.0.1",
         "require-style": "^1.1.0",
         "scuttle-poll": "^1.0.3",
@@ -1632,6 +1634,17 @@
         "node": "^16.13.0 || >=18.0.0"
       }
     },
+    "node_modules/@open-rpc/client-js": {
+      "version": "1.8.1",
+      "resolved": "https://registry.npmjs.org/@open-rpc/client-js/-/client-js-1.8.1.tgz",
+      "integrity": "sha512-vV+Hetl688nY/oWI9IFY0iKDrWuLdYhf7OIKI6U1DcnJV7r4gAgwRJjEr1QVYszUc0gjkHoQJzqevmXMGLyA0g==",
+      "dependencies": {
+        "isomorphic-fetch": "^3.0.0",
+        "isomorphic-ws": "^5.0.0",
+        "strict-event-emitter-types": "^2.0.0",
+        "ws": "^7.0.0"
+      }
+    },
     "node_modules/@pkgjs/parseargs": {
       "version": "0.11.0",
       "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
@@ -6332,7 +6345,6 @@
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
       "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
-      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">=0.10.0"
@@ -6687,6 +6699,11 @@
         "node": ">=0.3.1"
       }
     },
+    "node_modules/dijkstrajs": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
+      "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA=="
+    },
     "node_modules/dir-glob": {
       "version": "3.0.1",
       "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
@@ -6956,7 +6973,6 @@
       "version": "0.1.13",
       "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
       "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
-      "dev": true,
       "license": "MIT",
       "optional": true,
       "dependencies": {
@@ -10915,6 +10931,23 @@
       "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
       "license": "ISC"
     },
+    "node_modules/isomorphic-fetch": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz",
+      "integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==",
+      "dependencies": {
+        "node-fetch": "^2.6.1",
+        "whatwg-fetch": "^3.4.1"
+      }
+    },
+    "node_modules/isomorphic-ws": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz",
+      "integrity": "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==",
+      "peerDependencies": {
+        "ws": "*"
+      }
+    },
     "node_modules/istanbul-lib-coverage": {
       "version": "3.2.2",
       "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
@@ -13245,6 +13278,25 @@
       "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
       "license": "MIT"
     },
+    "node_modules/node-fetch": {
+      "version": "2.7.0",
+      "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
+      "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
+      "dependencies": {
+        "whatwg-url": "^5.0.0"
+      },
+      "engines": {
+        "node": "4.x || >=6.0.0"
+      },
+      "peerDependencies": {
+        "encoding": "^0.1.0"
+      },
+      "peerDependenciesMeta": {
+        "encoding": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/node-gyp": {
       "version": "10.3.1",
       "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-10.3.1.tgz",
@@ -13996,7 +14048,6 @@
       "version": "2.2.0",
       "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
       "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
-      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">=6"
@@ -14284,7 +14335,6 @@
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
       "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
-      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">=8"
@@ -14417,6 +14467,14 @@
         "semver-compare": "^1.0.0"
       }
     },
+    "node_modules/pngjs": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
+      "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
+      "engines": {
+        "node": ">=10.13.0"
+      }
+    },
     "node_modules/polite-json": {
       "version": "5.0.0",
       "resolved": "https://registry.npmjs.org/polite-json/-/polite-json-5.0.0.tgz",
@@ -15392,6 +15450,139 @@
       "integrity": "sha512-rXKDS5Sx3YipVsqmlMJsJsk6jXylEpiHRC2+nJy66fxA5ExYyGa4PqwteW69SaVmAb2OQ18HbYriT7cGQMbduw==",
       "license": "MIT"
     },
+    "node_modules/qrcode": {
+      "version": "1.5.4",
+      "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz",
+      "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
+      "dependencies": {
+        "dijkstrajs": "^1.0.1",
+        "pngjs": "^5.0.0",
+        "yargs": "^15.3.1"
+      },
+      "bin": {
+        "qrcode": "bin/qrcode"
+      },
+      "engines": {
+        "node": ">=10.13.0"
+      }
+    },
+    "node_modules/qrcode/node_modules/camelcase": {
+      "version": "5.3.1",
+      "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
+      "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/qrcode/node_modules/cliui": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
+      "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
+      "dependencies": {
+        "string-width": "^4.2.0",
+        "strip-ansi": "^6.0.0",
+        "wrap-ansi": "^6.2.0"
+      }
+    },
+    "node_modules/qrcode/node_modules/find-up": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+      "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+      "dependencies": {
+        "locate-path": "^5.0.0",
+        "path-exists": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/qrcode/node_modules/locate-path": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
+      "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+      "dependencies": {
+        "p-locate": "^4.1.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/qrcode/node_modules/p-limit": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+      "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+      "dependencies": {
+        "p-try": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/qrcode/node_modules/p-locate": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
+      "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+      "dependencies": {
+        "p-limit": "^2.2.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/qrcode/node_modules/wrap-ansi": {
+      "version": "6.2.0",
+      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
+      "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
+      "dependencies": {
+        "ansi-styles": "^4.0.0",
+        "string-width": "^4.1.0",
+        "strip-ansi": "^6.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/qrcode/node_modules/y18n": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
+      "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="
+    },
+    "node_modules/qrcode/node_modules/yargs": {
+      "version": "15.4.1",
+      "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
+      "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
+      "dependencies": {
+        "cliui": "^6.0.0",
+        "decamelize": "^1.2.0",
+        "find-up": "^4.1.0",
+        "get-caller-file": "^2.0.1",
+        "require-directory": "^2.1.1",
+        "require-main-filename": "^2.0.0",
+        "set-blocking": "^2.0.0",
+        "string-width": "^4.2.0",
+        "which-module": "^2.0.0",
+        "y18n": "^4.0.0",
+        "yargs-parser": "^18.1.2"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/qrcode/node_modules/yargs-parser": {
+      "version": "18.1.3",
+      "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
+      "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
+      "dependencies": {
+        "camelcase": "^5.0.0",
+        "decamelize": "^1.2.0"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
     "node_modules/qs": {
       "version": "6.13.1",
       "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.1.tgz",
@@ -16296,6 +16487,11 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/require-main-filename": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
+      "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="
+    },
     "node_modules/require-package-name": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/require-package-name/-/require-package-name-2.0.1.tgz",
@@ -16914,6 +17110,11 @@
         "node": ">= 0.8"
       }
     },
+    "node_modules/set-blocking": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
+      "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="
+    },
     "node_modules/set-function-length": {
       "version": "1.2.2",
       "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
@@ -20054,6 +20255,11 @@
       "integrity": "sha512-LJ9wplN/uSn72oJRsXTx+snxPet5c8XiZmOKCm906NVYu+ag6SB6vUcnJcWxgnl2NfbIyeobAn7Bwv6xRj2XJg==",
       "license": "MIT"
     },
+    "node_modules/strict-event-emitter-types": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/strict-event-emitter-types/-/strict-event-emitter-types-2.0.0.tgz",
+      "integrity": "sha512-Nk/brWYpD85WlOgzw5h173aci0Teyv8YdIAEtV+N88nDB0dLlazZyJMIsN6eo1/AR61l+p6CJTG1JIyFaoNEEA=="
+    },
     "node_modules/string_decoder": {
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
@@ -21348,6 +21554,11 @@
         "nodetouch": "bin/nodetouch.js"
       }
     },
+    "node_modules/tr46": {
+      "version": "0.0.3",
+      "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+      "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
+    },
     "node_modules/traverse": {
       "version": "0.6.10",
       "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.10.tgz",
@@ -22350,6 +22561,11 @@
         "node": "20 || >=22"
       }
     },
+    "node_modules/webidl-conversions": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+      "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
+    },
     "node_modules/whatwg-encoding": {
       "version": "3.1.1",
       "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
@@ -22362,6 +22578,11 @@
         "node": ">=18"
       }
     },
+    "node_modules/whatwg-fetch": {
+      "version": "3.6.20",
+      "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz",
+      "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg=="
+    },
     "node_modules/whatwg-mimetype": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
@@ -22371,6 +22592,15 @@
         "node": ">=18"
       }
     },
+    "node_modules/whatwg-url": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+      "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
+      "dependencies": {
+        "tr46": "~0.0.3",
+        "webidl-conversions": "^3.0.0"
+      }
+    },
     "node_modules/which": {
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -22451,6 +22681,11 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/which-module": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
+      "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ=="
+    },
     "node_modules/which-typed-array": {
       "version": "1.1.16",
       "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.16.tgz",

+ 3 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "@krakenslab/oasis",
-  "version": "0.3.0",
+  "version": "0.3.1",
   "description": "SNH-Oasis Project Network GUI",
   "repository": {
     "type": "git",
@@ -27,6 +27,7 @@
   "dependencies": {
     "@fraction/base16-css": "^1.1.0",
     "@koa/router": "^13.1.0",
+    "@open-rpc/client-js": "^1.8.1",
     "abstract-level": "^2.0.1",
     "await-exec": "^0.1.2",
     "axios": "^1.7.9",
@@ -73,6 +74,7 @@
     "pull-sort": "^1.0.2",
     "pull-stream": "^3.7.0",
     "punycode.js": "^2.3.1",
+    "qrcode": "^1.5.4",
     "remark-html": "^16.0.1",
     "require-style": "^1.1.0",
     "scuttle-poll": "^1.0.3",

+ 67 - 0
src/assets/style.css

@@ -584,6 +584,12 @@ hr {
   margin: var(--size-0) 0;
 }
 
+.form-button-group-center {
+  display: flex;
+  justify-content: center;
+  gap: 2px;
+}
+
 /* sidebar only appears on big screens */
 @media (min-width: calc(45rem)) {
   body > nav > ul {
@@ -662,6 +668,67 @@ hr {
   width: 24rem;
 }
 
+.button-like-link {
+	background: var(--fg);
+	border: var(--size--4) solid var(--fg);
+	border-radius: var(--common-radius);
+	color: var(--bg);
+	cursor: pointer;
+	font-size: 8pt;
+	padding: var(--size--1) var(--size-0);
+  text-decoration: none;
+}
+
+.button-like-link:hover {
+  background: var(--fg-light);
+}
+
+.div-center {
+  width: 100%;
+  display: grid;
+  place-items: center;
+}
+
+.qr-code svg {
+  margin-top: 1em;
+  width: 60%;
+}
+
+.wallet-balance {
+  display: block;
+  font-size: var(--size-1);
+  margin-bottom: 0.25em;
+}
+
+.wallet-status-error ul li {
+  color: var(--red);
+}
+
+.wallet-status-error ul li::marker {
+  color: var(--fg);
+}
+
+.tcell-ellipsis {
+  text-overflow: ellipsis;
+  overflow: hidden;
+  white-space: nowrap;
+}
+
+.full-center,
+.full-center th,
+.full-center td {
+  text-align: center;
+  vertical-align: middle;
+}
+
+.col-10 {
+  width: 10%;
+}
+
+.col-30{
+  width: 30%;
+}
+
 section.post-preview {
   padding-top: 0;
   background: var(--bg-selection);

+ 20 - 0
src/cli.js

@@ -62,6 +62,26 @@ const cli = (presets, defaultConfigFile) =>
       default: _.get(presets, "theme", "classic-light"),
       type: "string",
     })
+    .options("wallet-url", {
+      describe: "The URL of the remote ECOin wallet",
+      default: _.get(presets, "walletUrl", "http://localhost:7474"),
+      type: "string",
+    })
+    .options("wallet-user", {
+      describe: "The username of the remote ECOin wallet",
+      default: _.get(presets, "walletUser", "ecoinrpc"),
+      type: "string",
+    })
+    .options("wallet-pass", {
+      describe: "The password of the remote ECOin wallet",
+      default: _.get(presets, "walletPass", "ecoinrpc"),
+      type: "string",
+    })
+    .options("wallet-fee", {
+      describe: "The fee to pay for ECOin transactions",
+      default: _.get(presets, "walletFee", "0.01"),
+      type: "string",
+    })
     .epilog(`The defaults can be configured in ${defaultConfigFile}.`).argv;
 
 module.exports = { cli };

+ 128 - 24
src/index.js

@@ -10,7 +10,6 @@ const fs = require("fs");
 
 const promisesFs = require("fs").promises;
 
-
 const supports = require("./supports.js").supporting;
 const blocks = require("./supports.js").blocking;
 const recommends = require("./supports.js").recommending;
@@ -79,20 +78,6 @@ const { host } = config;
 const { port } = config;
 const url = `http://${host}:${port}`;
 
-if (haveConfig) {
-  log(`Configuration read defaults from ${defaultConfigFile}`);
-} else {
-  log(
-    `No configuration file found at ${defaultConfigFile}, using built-in default values.`
-  );
-}
-
-if (!haveCustomStyle) {
-  log(
-    `No custom style file found at ${customStyleFile}, ignoring this stylesheet.`
-  );
-}
-
 debug("Current configuration: %O", config);
 debug(`You can save the above to ${defaultConfigFile} to make \
 these settings the default. See the readme for details.`);
@@ -171,7 +156,7 @@ const cooler = ssb({ offline: config.offline });
 
 const models = require("./models");
 
-const { about, blob, friend, meta, post, vote } = models({
+const { about, blob, friend, meta, post, vote, wallet } = models({
   cooler,
   isPublic: config.public,
 });
@@ -395,6 +380,13 @@ const {
   topicsView,
   summaryView,
   threadsView,
+  walletView,
+  walletErrorView,
+  walletHistoryView,
+  walletReceiveView,
+  walletSendFormView,
+  walletSendConfirmView,
+  walletSendResultView,
 } = require("./views/index.js");
 
 const ssbRef = require("ssb-ref");
@@ -785,11 +777,18 @@ router
   })
   .get("/settings", async (ctx) => {
     const theme = ctx.cookies.get("theme") || config.theme;
-    const getMeta = async ({ theme }) => {
+    const walletUrl = ctx.cookies.get("wallet_url") || config.walletUrl;
+    const walletUser = ctx.cookies.get("wallet_user") || config.walletUser;
+    const walletFee = ctx.cookies.get("wallet_fee") || config.walletFee;
+
+   const getMeta = async ({ theme }) => {
       return settingsView({
         theme,
         themeNames,
         version: version.toString(),
+        walletUrl,
+        walletUser,
+        walletFee
       });
     };
     ctx.body = await getMeta({ theme });
@@ -871,6 +870,53 @@ router
       await resolveCommentComponents(ctx);
     ctx.body = await commentView({ messages, myFeedId, parentMessage });
   })
+  .get("/wallet", async (ctx) => {
+    const url = ctx.cookies.get("wallet_url") || config.walletUrl;
+    const user = ctx.cookies.get("wallet_user") || config.walletUser;
+    const pass = ctx.cookies.get("wallet_pass") || config.walletPass;
+    try {
+      const balance = await wallet.getBalance(url, user, pass);
+      ctx.body = await walletView(balance);
+    } catch (error) {
+      ctx.body = await walletErrorView(error);
+    }
+  })
+  .get("/wallet/history", async (ctx) => {
+    const url = ctx.cookies.get("wallet_url") || config.walletUrl;
+    const user = ctx.cookies.get("wallet_user") || config.walletUser;
+    const pass = ctx.cookies.get("wallet_pass") || config.walletPass;
+    try {
+      const balance = await wallet.getBalance(url, user, pass);
+      const transactions = await wallet.listTransactions(url, user, pass);
+      ctx.body = await walletHistoryView(balance, transactions);
+    } catch (error) {
+      ctx.body = await walletErrorView(error);
+    }
+  })
+  .get("/wallet/receive", async (ctx) => {
+    const url = ctx.cookies.get("wallet_url") || config.walletUrl;
+    const user = ctx.cookies.get("wallet_user") || config.walletUser;
+    const pass = ctx.cookies.get("wallet_pass") || config.walletPass;
+    try {
+    const balance = await wallet.getBalance(url, user, pass);
+    const address = await wallet.getAddress(url, user, pass);
+    ctx.body = await walletReceiveView(balance, address);
+    } catch (error) {
+      ctx.body = await walletErrorView(error);
+    }
+  })
+  .get("/wallet/send", async (ctx) => {
+    const url = ctx.cookies.get("wallet_url") || config.walletUrl;
+    const user = ctx.cookies.get("wallet_user") || config.walletUser;
+    const pass = ctx.cookies.get("wallet_pass") || config.walletPass;
+    const fee = ctx.cookies.get("wallet_fee") || config.walletFee;
+    try {
+      const balance = await wallet.getBalance(url, user, pass);
+      ctx.body = await walletSendFormView(balance, null, null, fee, null);
+    } catch (error) {
+      ctx.body = await walletErrorView(error);
+    }
+  })
   .post(
     "/subtopic/preview/:message",
     koaBody({ multipart: true }),
@@ -1055,18 +1101,17 @@ router
     ctx.body = await like({ messageKey, voteValue });
     ctx.redirect(referer.href);
   })
+
   .post("/update", koaBody(), async (ctx) => {
     const util = require("node:util");
     const exec = util.promisify(require("node:child_process").exec);
     async function updateTool() {
-      const { stdout, stderr } = await exec(
-        "git reset --hard && git pull && npm install ."
-      );
-      console.log("updating Oasis");
+      const { stdout, stderr } = await exec("git reset --hard && git pull && npm install .");
+      console.log("oasis@version: updating Oasis...");
       console.log(stdout);
       console.log(stderr);
     }
-    updateTool();
+    await updateTool();
     const referer = new URL(ctx.request.header.referer);
     ctx.redirect(referer.href);
   })
@@ -1112,6 +1157,67 @@ router
     // Do not wait for rebuild to finish.
     meta.rebuild();
     ctx.redirect("/settings");
+  })
+  .post("/settings/wallet", koaBody(), async (ctx) => {
+    const url = String(ctx.request.body.wallet_url);
+    const user = String(ctx.request.body.wallet_user);
+    const pass = String(ctx.request.body.wallet_pass);
+    const fee = String(ctx.request.body.wallet_fee);
+
+    url && url.trim() !== "" && ctx.cookies.set("wallet_url", url);
+    user && user.trim() !== "" && ctx.cookies.set("wallet_user", user);
+    pass && pass.trim() !== "" && ctx.cookies.set("wallet_pass", pass);
+    fee && fee > 0 && ctx.cookies.set("wallet_fee", fee);
+    const referer = new URL(ctx.request.header.referer);
+    ctx.redirect(referer.href);
+  })
+  .post("/wallet/send", koaBody(), async (ctx) => {
+    const action = String(ctx.request.body.action);
+    const destination = String(ctx.request.body.destination);
+    const amount = Number(ctx.request.body.amount);
+    const fee = Number(ctx.request.body.fee);
+    const url = ctx.cookies.get("wallet_url") || config.walletUrl;
+    const user = ctx.cookies.get("wallet_user") || config.walletUser;
+    const pass = ctx.cookies.get("wallet_pass") || config.walletPass;
+    let balance = null
+
+    try {
+      balance = await wallet.getBalance(url, user, pass);
+    } catch (error) {
+      ctx.body = await walletErrorView(error);
+    }
+
+    switch (action) {
+      case 'confirm':
+        const validation = await wallet.validateSend(url, user, pass, destination, amount, fee);
+        if (validation.isValid) {
+          try {
+            ctx.body = await walletSendConfirmView(balance, destination, amount, fee);
+          } catch (error) {
+            ctx.body = await walletErrorView(error);
+          }
+        } else {
+          try {
+            const statusMessages = {
+              type: 'error',
+              title: 'validation_errors',
+              messages: validation.errors,
+            }
+            ctx.body = await walletSendFormView(balance, destination, amount, fee, statusMessages);
+          } catch (error) {
+            ctx.body = await walletErrorView(error);
+          }
+        }
+        break;
+      case 'send':
+        try {
+          const txId = await wallet.sendToAddress(url, user, pass, destination, amount);
+          ctx.body = await walletSendResultView(balance, destination, amount, txId);
+        } catch (error) {
+          ctx.body = await walletErrorView(error);
+        }
+        break;
+    }
   });
 
 const routes = router.routes();
@@ -1169,8 +1275,6 @@ app._close = () => {
 
 module.exports = app;
 
-log(`Listening on ${url}`);
-
 if (config.open === true) {
   open(url);
 }

+ 52 - 1
src/models.js

@@ -9,6 +9,10 @@ const pullParallelMap = require("pull-paramap");
 const pull = require("pull-stream");
 const pullSort = require("pull-sort");
 const ssbRef = require("ssb-ref");
+const {
+  RequestManager,
+  HTTPTransport,
+  Client } = require("@open-rpc/client-js");
 
 const isEncrypted = (message) => typeof message.value.content === "string";
 const isNotEncrypted = (message) => isEncrypted(message) === false;
@@ -1915,5 +1919,52 @@ module.exports = ({ cooler, isPublic }) => {
     },
   };
 
+  models.wallet = {
+    client: async (url, user, pass) => {
+      const transport = new HTTPTransport(url, {
+        headers: {
+          'Authorization': 'Basic ' + btoa(`${user}:${pass}`)
+        }
+      });
+      return new Client(new RequestManager([transport]));
+    },
+    execute: async (url, user, pass, method, params = []) => {
+      try {
+        const client = await models.wallet.client(url, user, pass);
+        return await client.request({ method, params });
+      } catch (error) {
+        throw new Error(
+          "ECOin wallet disconnected. " +
+          "Check your wallet settings or connection status."
+        );
+      }
+    },
+    getBalance: async (url, user, pass) => {
+      return await models.wallet.execute(url, user, pass, "getbalance");
+    },
+    getAddress: async (url, user, pass) => {
+      const addresses = await models.wallet.execute(url, user, pass, "getaddressesbyaccount", ['']);
+      return addresses[0]  // TODO: Handle multiple addresses
+    },
+    listTransactions: async (url, user, pass) => {
+      return await models.wallet.execute(url, user, pass, "listtransactions", ["", 1000000, 0]);
+    },
+    sendToAddress: async (url, user, pass, address, amount) => {
+      return await models.wallet.execute(url, user, pass, "sendtoaddress", [address, amount]);
+    },
+    validateSend: async (url, user, pass, address, amount, fee) => {
+      let isValid = false
+      const errors = [];
+      const addressValid = await models.wallet.execute(url, user, pass, "validateaddress", [address]);
+      const amountValid = amount > 0;
+      const feeValid = fee > 0;
+      if (!addressValid.isvalid) { errors.push("invalid_dest") }
+      if (!amountValid) { errors.push("invalid_amount") }
+      if (!feeValid) { errors.push("invalid_fee") }
+      if (errors.length == 0) { isValid = true }
+      return { isValid, errors }
+    }
+  }
+
   return models;
-};
+};

+ 106 - 49
src/updater.js

@@ -1,60 +1,117 @@
-const axios = require("axios");
-const {existsSync, readFileSync} = require("fs");
-const {join} = require("path");
-  
-const localpackage = join("package.json");
-const remoteUrl = "https://code.03c8.net/KrakensLab/oasis/src/master/package.json" // Official SNH-Oasis
-const remoteUrl2 = "https://github.com/epsylon/oasis/blob/main/package.json" // Mirror SNH-Oasis
-
-// Splitted function
-async function checkMirror(callback) {
+const fetch = require('node-fetch');
+const { existsSync, readFileSync } = require('fs');
+const { join } = require('path');
+
+const localpackage = join(__dirname, '../package.json');
+const remoteUrl = 'https://code.03c8.net/KrakensLab/oasis/raw/master/package.json'; // Official SNH-Oasis
+const remoteUrl2 = 'https://raw.githubusercontent.com/epsylon/oasis/main/package.json'; // Mirror SNH-Oasis
+
+async function extractVersionFromText(text) {
   try {
-    // Try fetching from the mirror URL
-    const { data } = await axios.get(remoteUrl2, { responseType: "text" });
-    diffVersion(data);
+    const versionMatch = text.match(/"version":\s*"([^"]+)"/); 
+    if (versionMatch) {
+      return versionMatch[1];
+    } else {
+      throw new Error('Version not found in the response.');
+    }
   } catch (error) {
-    console.error("Error fetching from mirror URL:", error.message);
+    console.error("Error extracting version:", error.message);
+    return null;
+  }
+}
+
+async function diffVersion(body, callback) {
+  try {
+    const remoteData = JSON.parse(body);
+    const remoteVersion = remoteData.version;
+
+    const localData = JSON.parse(readFileSync(localpackage, 'utf8'));
+    const localVersion = localData.version; 
+
+    if (remoteVersion !== localVersion) {
+      callback("required"); 
+    } else {
+      callback("");  // No update required
+    }
+  } catch (error) {
+    console.error("Error comparing versions:", error.message);
     callback("error");
   }
 }
 
-function diffVersion(body, callback) {
-  let remoteVersion = body
-    .split('<li class="L3" rel="L3">')
-    .pop()
-    .split("</li>")[0];
-  remoteVersion = remoteVersion
-    .split("&#34;version&#34;: &#34;")
-    .pop()
-    .split("&#34;,")[0];
-  let localVersion = readFileSync(localpackage, "utf8");
-  localVersion = localVersion
-    .split('"name":')
-    .pop()
-    .split('"description":')[0];
-  localVersion = localVersion.split('"version"').pop().split('"')[1];
-  
-  let checkversion = ""
-
-  if (remoteVersion != localVersion) {
-    checkversion = "required";
-  } else {
-    checkversion = "";
+async function checkMirror(callback) {
+  try {
+    const response = await fetch(remoteUrl2, {
+      method: 'GET',
+      headers: { 
+        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', 
+        'Accept': 'application/json, text/plain, */*',
+        'Accept-Language': 'en-US,en;q=0.9',
+        'Accept-Encoding': 'gzip, deflate, br',
+        'Connection': 'keep-alive',
+        'Referer': 'https://raw.githubusercontent.com',
+        'Origin': 'https://raw.githubusercontent.com'
+      }
+    });
+
+    if (!response.ok) {
+      throw new Error(`Request failed with status ${response.status}`);
+    }
+
+    const data = await response.text();
+    callback(null, data);
+  } catch (error) {
+    console.error("Error fetching from mirror URL:", error.message);
+    callback(error);
   }
-  callback(checkversion);
 }
 
-exports.getRemoteVersion = (callback) => {
-  (async () => {
-    if (existsSync(".git")) {
-      try {
+exports.getRemoteVersion = async () => {
+  if (existsSync('.git')) { 
+    try {
+      const response = await fetch(remoteUrl, {
+        method: 'GET',
+        headers: { 
+          'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', 
+          'Accept': 'application/json, text/plain, */*',
+          'Accept-Language': 'en-US,en;q=0.9',
+          'Accept-Encoding': 'gzip, deflate, br',
+          'Connection': 'keep-alive',
+          'Referer': 'https://code.03c8.net',
+          'Origin': 'https://code.03c8.net'
+        }
+      });
 
-        // Now uses axios to get the package
-        const { data } = await axios.get(remoteUrl, { responseType: "text" });
-        diffVersion(data, callback);
-      } catch (error) {
-        checkMirror(callback);
+      if (!response.ok) {
+        throw new Error(`Request failed with status ${response.status}`);
       }
-    }  
-  })();
-}
+
+      const data = await response.text();
+      diffVersion(data, (status) => {
+        if (status === "required") {
+          global.ck = "required";
+          console.log("\noasis@version: new code updates are available:\n\n1) Run Oasis and go to 'Settings' tab\n2) Click at 'Get updates' button to download latest code\n3) Restart Oasis when finished\n");
+        } else {
+          console.log("\noasis@version: no updates required.\n");
+        }
+      });
+    } catch (error) {
+      console.error("Error fetching from official URL:", error.message);
+      checkMirror((err, data) => {
+        if (err) {
+          console.error("Error fetching from mirror URL:", err.message);
+        } else {
+          diffVersion(data, (status) => {
+            if (status === "required") {
+              global.ck = "required";
+              console.log("\noasis@version: new code updates are available:\n\n1) Run Oasis and go to 'Settings' tab\n2) Click at 'Get updates' button to download latest code\n3) Restart Oasis when finished\n");
+            } else {
+              console.log("\noasis@version: no updates required.\n");
+            }
+          });
+        }
+      });
+    }
+  }
+};
+

+ 74 - 0
src/views/i18n.js

@@ -198,6 +198,43 @@ const i18n = {
     hashtagDescription:
       "Posts from inhabitants in your network that reference this #hashtag, sorted by recency.",
     rebuildName: "Rebuild database",
+    wallet: "Wallet",
+    walletAddress: "Address",
+    walletAmount: "Amount",
+    walletAddressLine: ({ address }) => `Address: ${address}`,
+    walletAmountLine: ({ amount }) => `Amount: ECO ${amount}`,
+    walletBack: "Back",
+    walletBalanceLine: ({ balance }) => `ECO ${balance}`,
+    walletCnfrs: "Cnfrs",
+    walletConfirm: "Confirm",
+    walletDescription: "Manage your digital assets, including sending and receiving ECOin, viewing your balance, and accessing your transaction history.",
+    walletDate: "Date",
+    walletFee: "Fee (The higher the fee, the faster your transaction will be processed)",
+    walletFeeLine: ({ fee }) => `Fee: ECO ${fee}`,
+    walletHistory: "History",
+    walletReceive: "Receive",
+    walletReset: "Reset",
+    walletSend: "Send",
+    walletStatus: "Status",
+    walletDisconnected: "ECOin wallet disconnected. Check your wallet settings or connection status.",
+    walletSentToLine: ({ destination, amount }) => `Sent ECO ${amount} to ${destination}`,
+    walletSettingsTitle: "Wallet",
+    walletSettingsDescription: "Integrate Oasis with your ECOin wallet.",
+    walletStatusMessages: {
+      invalid_amount: "Invalid amount",
+      invalid_dest: "Invalid destination address",
+      invalid_fee: "Invalid fee",
+      validation_errors: "Validation errors",
+      send_tx_success: "Transaction successful",
+    },
+    walletTitle: "Wallet",
+    walletTotalCostLine: ({ totalCost }) => `Total cost: ECO ${totalCost}`,
+    walletTransactionId: "Transaction ID",
+    walletTxId: "Tx ID",
+    walletType: "Type",
+    walletUser: "Username",
+    walletPass: "Password",
+    walletConfiguration: "Set wallet",
   },
   /* spell-checker: disable */
   es: {
@@ -401,6 +438,43 @@ const i18n = {
     hashtagDescription:
       "Posts de habitantes en tu red que referencian a ésta #etiqueta, ordenados por los más recientes.",
     rebuildName: "Reconstruir base de datos",
+    wallet: "Cartera",
+    walletAddress: "Dirección",
+    walletAmount: "Cantidad",
+    walletAddressLine: ({ address }) => `Dirección: ${address}`,
+    walletAmountLine: ({ amount }) => `Cantidad: ${amount} ECO`,
+    walletBack: "Atrás",
+    walletBalanceLine: ({ balance }) => `${balance} ECO`,
+    walletConfirm: "Confirmar",
+    walletCnfrs: "Cnfrs",
+    walletDescription: "Administra tus activos digitales, incluyendo el envío y recepción de ECOin, consulta de saldo e historial de transacciones.",
+    walletDate: "Fecha",
+    walletFee: "Tarifa (A mayor tarifa, más rápido se procesa tu transacción)",
+    walletFeeLine: ({ fee }) => `Tarifa: ${fee} ECO`,
+    walletHistory: "Historial",
+    walletReceive: "Recibir",
+    walletReset: "Restablecer",
+    walletSend: "Enviar",
+    walletStatus: "Estado",
+    walletDisconnected: "Cartera ECOin desconectada. Revisa la configuración.",
+    walletSentToLine: ({ destination, amount }) => `Enviados ${amount} ECO a ${destination}.`,
+    walletSettingsTitle: "Cartera",
+    walletSettingsDescription: "Integra Oasis con tu cartera ECOin.",
+    walletStatusMessages: {
+      invalid_amount: "Cantidad inválida",
+      invalid_dest: "Dirección de destino inválida",
+      invalid_fee: "Tarifa inválida",
+      validation_errors: "Errores de validación",
+      send_tx_success: "Transacción exitosa",
+    },
+    walletTitle: "Cartera",
+    walletTotalCostLine: ({ totalCost }) => `Coste total: ${totalCost} ECO`,
+    walletTransactionId: "ID de transacción",
+    walletTxId: "Tx ID",
+    walletType: "Tipo",
+    walletUser: "Nombre de usuario",
+    walletPass: "Contraseña",
+    walletConfiguration: "Configurar cartera",
   },
 };
 

+ 233 - 20
src/views/index.js

@@ -10,13 +10,22 @@ const highlightJs = require("highlight.js");
 const prettyMs = require("pretty-ms");
 
 const updater = require("../updater.js");
-
-global.updaterequired = "";
-global.ck = updater.getRemoteVersion(async function(checkversion){
-  if (checkversion === "required"){
-    ck = "required";
+async function checkForUpdate() {
+  try {
+    await updater.getRemoteVersion();
+    if (global.ck === "required") {
+      global.updaterequired = form(
+        { action: "/update", method: "post" },
+        button({ type: "submit" }, i18n.updateit)
+      );
+    } else {
+      console.log("\noasis@version: no updates required.\n");
+    }
+  } catch (error) {
+    console.error("\noasis@version: error fetching package.json:", error.message, "\n");
   }
-});
+}
+checkForUpdate();
 
 const {
   a,
@@ -51,8 +60,14 @@ const {
   select,
   span,
   summary,
+  table,
+  tbody,
+  td,
   textarea,
+  th,
+  thead,
   title,
+  tr,
   ul,
 } = require("hyperaxe");
 
@@ -133,12 +148,14 @@ const template = (titlePrefix, ...elements) => {
         ul(
           //navLink({ href: "/imageSearch", emoji: "✧", text: i18n.imageSearch }),
           navLink({ href: "/mentions", emoji: "✺", text: i18n.mentions }),
-          navLink({ href: "/public/latest", emoji: "☄", text: i18n.latest }),
-          navLink({ href: "/public/latest/summaries", emoji: "※", text: i18n.summaries }),
-          navLink({ href: "/public/latest/topics", emoji: "ϟ", text: i18n.topics }),
-          navLink({ href: "/public/latest/extended", emoji: "∞", text: i18n.extended }),
           navLink({ href: "/public/popular/day", emoji: "⌘", text: i18n.popular }),
+          hr,
+          navLink({ href: "/public/latest/topics", emoji: "ϟ", text: i18n.topics }),
+          navLink({ href: "/public/latest/summaries", emoji: "※", text: i18n.summaries }),
+          navLink({ href: "/public/latest", emoji: "☄", text: i18n.latest }),
           navLink({ href: "/public/latest/threads", emoji: "♺", text: i18n.threads }),
+          hr,
+          navLink({ href: "/public/latest/extended", emoji: "∞", text: i18n.extended }),
         )
       ),
       main({ id: "content" }, elements),
@@ -147,9 +164,12 @@ const template = (titlePrefix, ...elements) => {
           navLink({ href: "/publish", emoji: "❂",text: i18n.publish }),
           navLink({ href: "/search", emoji: "✦", text: i18n.search }),
           navLink({ href: "/inbox", emoji: "☂", text: i18n.private }),
+          hr,
           navLink({ href: "/profile", emoji: "⚉", text: i18n.profile }),
-          navLink({ href: "/invites", emoji: "❄", text: i18n.invites }),
+          navLink({ href: "/wallet", emoji: "❄", text: i18n.wallet }),
+          navLink({ href: "/invites", emoji: "ꔹ", text: i18n.invites }),
           navLink({ href: "/peers", emoji: "⧖", text: i18n.peers }),
+          hr,
           navLink({ href: "/settings", emoji: "⚙", text: i18n.settings })
         )
       )
@@ -1128,7 +1148,7 @@ exports.invitesView = ({ invites }) => {
    );
 };
 
-exports.settingsView = ({ theme, themeNames, version }) => { 
+exports.settingsView = ({ theme, themeNames, version, walletUrl, walletUser, walletFee }) => {
  const themeElements = themeNames.map((cur) => {
     const isCurrentTheme = cur === theme;
     if (isCurrentTheme) {
@@ -1173,20 +1193,14 @@ exports.settingsView = ({ theme, themeNames, version }) => {
     button({ type: "submit" }, i18n.rebuildName)
   );
 
-  if (ck === "required"){
-    updaterequired = form(
-      { action: "/update", method: "post" },
-      button({ type: "submit"}, i18n.updateit)
-    );
-  };
-
   return template(
     i18n.settings,
     section(
       { class: "message" },
       h1(i18n.settings),
       p(a({ href:snhUrl, target: "_blank" }, i18n.settingsIntro({ version }))),
-      p(updaterequired),
+      p(global.updaterequired),
+      hr,
       h2(i18n.theme),
       p(i18n.themeIntro),
       form(
@@ -1194,6 +1208,7 @@ exports.settingsView = ({ theme, themeNames, version }) => {
          select({ name: "theme" }, ...themeElements),
          button({ type: "submit" }, i18n.setTheme)
        ),
+      hr,
       h2(i18n.language),
       p(i18n.languageDescription),
       form(
@@ -1207,6 +1222,25 @@ exports.settingsView = ({ theme, themeNames, version }) => {
         ]),
         button({ type: "submit" }, i18n.setLanguage)
       ),
+      hr,
+      h2(i18n.wallet),
+      p(i18n.walletSettingsDescription),
+      form(
+        { action: "/settings/wallet", method: "POST" },
+        label({ for: "wallet_url" }, i18n.walletAddress),
+        input({ type: "text", id: "wallet_url", name: "wallet_url", placeholder: walletUrl, value: walletUrl }),
+        label({ for: "wallet_user" }, i18n.walletUser),
+        input({ type: "text", id: "wallet_user", name: "wallet_user", placeholder: walletUser, value: walletUser }),
+
+        label({ for: "wallet_pass" }, i18n.walletPass),
+        input({ type: "password", id: "wallet_pass", name: "wallet_pass" }),
+
+        label({ for: "wallet_fee" }, i18n.walletFee),
+        input({ type: "text", id: "wallet_fee", name: "wallet_fee", placeholder: walletFee, value: walletFee }),
+
+        button({ type: "submit" }, i18n.walletConfiguration)
+      ),
+      hr,
       h2(i18n.indexes),
       p(i18n.indexesDescription),
       rebuildButton,
@@ -1546,3 +1580,182 @@ exports.indexingView = ({ percent }) => {
 
   return result;
 };
+
+const walletViewRender = (balance, ...elements) => {
+  return template(
+    i18n.walletTitle,
+    section(
+      h1(i18n.walletTitle),
+      p(i18n.walletDescription),
+    ),
+    section(
+      div(
+        {class: "div-center"},
+        span(
+          {class: "wallet-balance"},
+          i18n.walletBalanceLine({ balance })
+        ),
+        span(
+          { class: "form-button-group-center" },
+          a({ href: "/wallet/send", class: "button-like-link" }, i18n.walletSend),
+          a({ href: "/wallet/receive", class: "button-like-link" }, i18n.walletReceive),
+          a({ href: "/wallet/history", class: "button-like-link" }, i18n.walletHistory),
+        )
+      ),
+    ),
+    elements.length > 0 ? section(...elements) : null
+  )
+};
+
+exports.walletView = async (balance) => {
+  return walletViewRender(balance)
+}
+
+exports.walletHistoryView = async (balance, transactions) => {
+  return walletViewRender(
+    balance,
+    table(
+      { class: "wallet-history" },
+      thead(
+        tr(
+          { class: "full-center" },
+          th({ class: "col-10" }, i18n.walletCnfrs),
+          th(i18n.walletDate),
+          th(i18n.walletType),
+          th(i18n.walletAmount),
+          th({ class: "col-30" }, i18n.walletTxId)
+        )
+      ),
+      tbody(
+        ...transactions.map((tx) => {
+          const date = new Date(tx.time * 1000);
+          const amount = Number(tx.amount);
+          const fee = Number(tx.fee) || 0;
+          const totalAmount = Number(amount + fee);
+
+          return tr(
+            td({ class: "full-center" }, tx.confirmations),
+            td(date.toLocaleDateString(), br(), date.toLocaleTimeString()),
+            td(tx.category),
+            td(totalAmount.toFixed(2)),
+            td({ width: "30%", class: "tcell-ellipsis" },
+              a({
+                href: `https://ecoin.03c8.net/blockexplorer/search?q=${tx.txid}`,
+                target: "_blank",
+              }, tx.txid)
+            )
+          )
+        })
+      )
+    )
+  )
+}
+
+exports.walletReceiveView = async (balance, address) => {
+  const QRCode = require('qrcode');
+  const qrImage = await QRCode.toString(address, { type: 'svg' });
+  const qrContainer = address + qrImage
+
+  return walletViewRender(
+    balance,
+    div(
+      {class: 'div-center qr-code', innerHTML: qrContainer},
+    ),
+  )
+}
+
+exports.walletSendFormView = async (balance, destination, amount, fee, statusMessages) => {
+  const { type, title, messages } = statusMessages || {};
+  const statusBlock = div({ class: `wallet-status-${type}` },);
+
+  if (messages?.length > 0) {
+    statusBlock.appendChild(
+      span(
+        i18n.walletStatusMessages[title]
+      )
+    )
+    statusBlock.appendChild(
+      ul(
+        ...messages.map(error => li(i18n.walletStatusMessages[error]))
+      )
+    )
+  }
+
+  return walletViewRender(
+    balance,
+    div(
+      {class: "div-center"},
+      messages?.length > 0 ? statusBlock : null,
+      form(
+        { action: '/wallet/send', method: 'POST' },
+        label({ for: 'destination' }, i18n.walletAddress),
+        input({ type: 'text', id: 'destination', name: 'destination', placeholder: 'ELH8RJGy3s7sCPqQUyN8on2gD5Fdn3BpyC', value: destination }),
+        label({ for: 'amount' }, i18n.walletAmount),
+        input({ type: 'text', id: 'amount', name: 'amount', placeholder: '0.25', value: amount }),
+        label({ for: 'fee' }, i18n.walletFee),
+        input({ type: 'text', id: 'fee', name: 'fee', placeholder: '0.01', value: fee }),
+        input({ type: 'hidden', name: 'action', value: 'confirm' }),
+        div({ class: 'form-button-group-center' },
+          button({ type: 'submit' }, i18n.walletSend),
+          button({ type: 'reset' }, i18n.walletReset)
+        )
+      )
+    )
+  )
+}
+
+exports.walletSendConfirmView = async (balance, destination, amount, fee) => {
+  const totalCost = amount + fee;
+
+  return walletViewRender(
+    balance,
+    p(
+      i18n.walletAddressLine({ address: destination }), br(),
+      i18n.walletAmountLine({ amount }), br(),
+      i18n.walletFeeLine({ fee }), br(),
+      i18n.walletTotalCostLine({ totalCost }),
+    ),
+    form(
+      { action: '/wallet/send', method: 'POST' },
+      input({ type: 'hidden', name: 'action', value: 'send' }),
+      input({ type: 'hidden', name: 'destination', value: destination }),
+      input({ type: 'hidden', name: 'amount', value: amount }),
+      input({ type: 'hidden', name: 'fee', value: fee }),
+      div({ class: 'form-button-group-center' },
+        button({ type: 'submit' }, i18n.walletConfirm),
+        a ({ href: `/wallet/send`, class: "button-like-link" }, i18n.walletBack),
+      )
+    ),
+  )
+}
+
+exports.walletErrorView = async (error) => {
+  return template(
+    i18n.walletTitle,
+    section(
+      h1(i18n.walletTitle),
+      p(i18n.walletDescription),
+    ),
+    section(
+      h2(i18n.walletStatus),
+      p(i18n.walletDisconnected),
+    )
+  )
+}
+
+exports.walletSendResultView = async (balance, destination, amount, txId) => {
+  return walletViewRender(
+    balance,
+    p(
+      i18n.walletSentToLine({ destination, amount }), br(),
+      `${i18n.walletTransactionId}: `,
+      a(
+        {
+          href: `https://ecoin.03c8.net/blockexplorer/search?q=${txId}`,
+          target: "_blank",
+        },
+        txId
+      ),
+    ),
+  )
+}