Repository: joinmarket-webui/joinmarket-webui
Pull request #291 (2022-05-20 10:21:02)

docs: describe 'common flow' in regtest readme
- Add "Common flow" section at the top of readme
- Note about new local Directory Node
- Note about `npm run regtest:mine`


Patch:
diff --git a/package-lock.json b/package-lock.json
index ff73e75..15e7041 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -17,8 +17,8 @@
         "@testing-library/jest-dom": "^5.16.4",
         "@testing-library/react": "^12.1.4",
         "@testing-library/user-event": "^13.5.0",
-        "@types/jest": "^27.4.1",
-        "@types/node": "^17.0.26",
+        "@types/jest": "^27.5.1",
+        "@types/node": "^17.0.35",
         "@types/react": "^17.0.43",
         "@types/react-dom": "^17.0.14",
         "bootstrap": "^5.1.3",
@@ -26,24 +26,23 @@
         "conventional-changelog": "^3.1.25",
         "formik": "^2.2.9",
         "http-proxy-middleware": "^2.0.6",
-        "husky": "^7.0.4",
-        "i18next": "^21.6.16",
+        "husky": "^8.0.1",
+        "i18next": "^21.8.4",
         "i18next-browser-languagedetector": "^6.1.4",
         "jest-watch-typeahead": "^0.6.5",
         "jest-websocket-mock": "^2.3.0",
-        "lint-staged": "^12.4.0",
+        "lint-staged": "^12.4.2",
         "moving-letters": "^1.0.1",
-        "nth-check": ">=2.0.1",
         "prettier": "^2.6.2",
         "qrcode": "^1.5.0",
         "react": "^17.0.2",
-        "react-bootstrap": "^2.3.0",
+        "react-bootstrap": "^2.4.0",
         "react-dom": "^17.0.2",
-        "react-i18next": "^11.16.7",
+        "react-i18next": "^11.16.9",
         "react-router-bootstrap": "^0.26.1",
         "react-router-dom": "^6.3.0",
         "react-scripts": "^5.0.1",
-        "typescript": "^4.6.3",
+        "typescript": "^4.7.2",
         "web-vitals": "^2.1.4"
       },
       "engines": {
@@ -3350,9 +3349,9 @@
       }
     },
     "node_modules/@types/jest": {
-      "version": "27.4.1",
-      "resolved": "https://registry.npmjs.org/@types/jest/-/jest-27.4.1.tgz",
-      "integrity": "sha512-23iPJADSmicDVrWk+HT58LMJtzLAnB2AgIzplQuq/bSrGaxCrlvRFjGbXmamnnk/mAmCdLStiGqggu28ocUyiw==",
+      "version": "27.5.1",
+      "resolved": "https://registry.npmjs.org/@types/jest/-/jest-27.5.1.tgz",
+      "integrity": "sha512-fUy7YRpT+rHXto1YlL+J9rs0uLGyiqVt3ZOTQR+4ROc47yNl8WLdVLgUloBRhOxP1PZvguHl44T3H0wAWxahYQ==",
       "dev": true,
       "dependencies": {
         "jest-matcher-utils": "^27.0.0",
@@ -3384,9 +3383,9 @@
       "dev": true
     },
     "node_modules/@types/node": {
-      "version": "17.0.26",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.26.tgz",
-      "integrity": "sha512-z/FG/6DUO7pnze3AE3TBGIjGGKkvCcGcWINe1C7cADY8hKLJPDYpzsNE37uExQ4md5RFtTCvg+M8Mu1Enyeg2A==",
+      "version": "17.0.35",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.35.tgz",
+      "integrity": "sha512-vu1SrqBjbbZ3J6vwY17jBs8Sr/BKA+/a/WtjRG+whKg1iuLFOosq872EXS0eXWILdO36DHQQeku/ZcL6hz2fpg==",
       "dev": true
     },
     "node_modules/@types/normalize-package-data": {
@@ -9443,24 +9442,24 @@
       }
     },
     "node_modules/husky": {
-      "version": "7.0.4",
-      "resolved": "https://registry.npmjs.org/husky/-/husky-7.0.4.tgz",
-      "integrity": "sha512-vbaCKN2QLtP/vD4yvs6iz6hBEo6wkSzs8HpRah1Z6aGmF2KW5PdYuAd7uX5a+OyBZHBhd+TFLqgjUgytQr4RvQ==",
+      "version": "8.0.1",
+      "resolved": "https://registry.npmjs.org/husky/-/husky-8.0.1.tgz",
+      "integrity": "sha512-xs7/chUH/CKdOCs7Zy0Aev9e/dKOMZf3K1Az1nar3tzlv0jfqnYtu235bstsWTmXOR0EfINrPa97yy4Lz6RiKw==",
       "dev": true,
       "bin": {
         "husky": "lib/bin.js"
       },
       "engines": {
-        "node": ">=12"
+        "node": ">=14"
       },
       "funding": {
         "url": "https://github.com/sponsors/typicode"
       }
     },
     "node_modules/i18next": {
-      "version": "21.6.16",
-      "resolved": "https://registry.npmjs.org/i18next/-/i18next-21.6.16.tgz",
-      "integrity": "sha512-xJlzrVxG9CyAGsbMP1aKuiNr1Ed2m36KiTB7hjGMG2Zo4idfw3p9THUEu+GjBwIgEZ7F11ZbCzJcfv4uyfKNuw==",
+      "version": "21.8.4",
+      "resolved": "https://registry.npmjs.org/i18next/-/i18next-21.8.4.tgz",
+      "integrity": "sha512-b3LQ5n9V1juu8UItb5x1QTI4OTvNqsNs/wetwQlBvfijEqks+N5HKMKSoevf8w0/RGUrDQ7g4cvVzF8WBp9pUw==",
       "dev": true,
       "funding": [
         {
@@ -11822,9 +11821,9 @@
       "dev": true
     },
     "node_modules/lint-staged": {
-      "version": "12.4.0",
-      "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-12.4.0.tgz",
-      "integrity": "sha512-3X7MR0h9b7qf4iXf/1n7RlVAx+EzpAZXoCEMhVSpaBlgKDfH2ewf+QUm7BddFyq29v4dgPP+8+uYpWuSWx035A==",
+      "version": "12.4.2",
+      "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-12.4.2.tgz",
+      "integrity": "sha512-JAJGIzY/OioIUtrRePr8go6qUxij//mL+RGGoFKU3VWQRtIHgWoHizSqH0QVn2OwrbXS9Q6CICQjfj+E5qvrXg==",
       "dev": true,
       "dependencies": {
         "cli-truncate": "^3.1.0",
@@ -14959,9 +14958,9 @@
       }
     },
     "node_modules/react-bootstrap": {
-      "version": "2.3.0",
-      "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-2.3.0.tgz",
-      "integrity": "sha512-O8DU/R3CHLqj1IGfqZD1mOm9Jx6tm8wmfIIshNeaIdd5AZnlO7eOPF7UyzijAxNIogN0/U3U4tWIH+9gQYlwVA==",
+      "version": "2.4.0",
+      "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-2.4.0.tgz",
+      "integrity": "sha512-dn599jNK1Fg5GGjJH+lQQDwELVzigh/MdusKpB/0el+sCjsO5MZDH5gRMmBjRhC+vb7VlCDr6OXffPIDSkNMLw==",
       "dev": true,
       "dependencies": {
         "@babel/runtime": "^7.17.2",
@@ -15119,9 +15118,9 @@
       "dev": true
     },
     "node_modules/react-i18next": {
-      "version": "11.16.7",
-      "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-11.16.7.tgz",
-      "integrity": "sha512-7yotILJLnKfvUfrl/nt9eK9vFpVFjZPLWAwBzWL6XppSZZEvlmlKk0GBGDCAPfLfs8oND7WAbry8wGzdoiW5Nw==",
+      "version": "11.16.9",
+      "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-11.16.9.tgz",
+      "integrity": "sha512-euXxWvcEAvsY7ZVkwx9ztCq4butqtsGHEkpkuo0RMj8Ru09IF9o2KxCyN+zyv51Nr0aBh/elaTIiR6fMb8YfVg==",
       "dev": true,
       "dependencies": {
         "@babel/runtime": "^7.14.5",
@@ -17753,9 +17752,9 @@
       }
     },
     "node_modules/typescript": {
-      "version": "4.6.3",
-      "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.3.tgz",
-      "integrity": "sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw==",
+      "version": "4.7.2",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.2.tgz",
+      "integrity": "sha512-Mamb1iX2FDUpcTRzltPxgWMKy3fhg0TN378ylbktPGPK/99KbDtMQ4W1hwgsbPAsG3a0xKa1vmw4VKZQbkvz5A==",
       "dev": true,
       "bin": {
         "tsc": "bin/tsc",
@@ -21420,9 +21419,9 @@
       }
     },
     "@types/jest": {
-      "version": "27.4.1",
-      "resolved": "https://registry.npmjs.org/@types/jest/-/jest-27.4.1.tgz",
-      "integrity": "sha512-23iPJADSmicDVrWk+HT58LMJtzLAnB2AgIzplQuq/bSrGaxCrlvRFjGbXmamnnk/mAmCdLStiGqggu28ocUyiw==",
+      "version": "27.5.1",
+      "resolved": "https://registry.npmjs.org/@types/jest/-/jest-27.5.1.tgz",
+      "integrity": "sha512-fUy7YRpT+rHXto1YlL+J9rs0uLGyiqVt3ZOTQR+4ROc47yNl8WLdVLgUloBRhOxP1PZvguHl44T3H0wAWxahYQ==",
       "dev": true,
       "requires": {
         "jest-matcher-utils": "^27.0.0",
@@ -21454,9 +21453,9 @@
       "dev": true
     },
     "@types/node": {
-      "version": "17.0.26",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.26.tgz",
-      "integrity": "sha512-z/FG/6DUO7pnze3AE3TBGIjGGKkvCcGcWINe1C7cADY8hKLJPDYpzsNE37uExQ4md5RFtTCvg+M8Mu1Enyeg2A==",
+      "version": "17.0.35",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.35.tgz",
+      "integrity": "sha512-vu1SrqBjbbZ3J6vwY17jBs8Sr/BKA+/a/WtjRG+whKg1iuLFOosq872EXS0eXWILdO36DHQQeku/ZcL6hz2fpg==",
       "dev": true
     },
     "@types/normalize-package-data": {
@@ -26088,15 +26087,15 @@
       "dev": true
     },
     "husky": {
-      "version": "7.0.4",
-      "resolved": "https://registry.npmjs.org/husky/-/husky-7.0.4.tgz",
-      "integrity": "sha512-vbaCKN2QLtP/vD4yvs6iz6hBEo6wkSzs8HpRah1Z6aGmF2KW5PdYuAd7uX5a+OyBZHBhd+TFLqgjUgytQr4RvQ==",
+      "version": "8.0.1",
+      "resolved": "https://registry.npmjs.org/husky/-/husky-8.0.1.tgz",
+      "integrity": "sha512-xs7/chUH/CKdOCs7Zy0Aev9e/dKOMZf3K1Az1nar3tzlv0jfqnYtu235bstsWTmXOR0EfINrPa97yy4Lz6RiKw==",
       "dev": true
     },
     "i18next": {
-      "version": "21.6.16",
-      "resolved": "https://registry.npmjs.org/i18next/-/i18next-21.6.16.tgz",
-      "integrity": "sha512-xJlzrVxG9CyAGsbMP1aKuiNr1Ed2m36KiTB7hjGMG2Zo4idfw3p9THUEu+GjBwIgEZ7F11ZbCzJcfv4uyfKNuw==",
+      "version": "21.8.4",
+      "resolved": "https://registry.npmjs.org/i18next/-/i18next-21.8.4.tgz",
+      "integrity": "sha512-b3LQ5n9V1juu8UItb5x1QTI4OTvNqsNs/wetwQlBvfijEqks+N5HKMKSoevf8w0/RGUrDQ7g4cvVzF8WBp9pUw==",
       "dev": true,
       "requires": {
         "@babel/runtime": "^7.17.2"
@@ -27937,9 +27936,9 @@
       "dev": true
     },
     "lint-staged": {
-      "version": "12.4.0",
-      "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-12.4.0.tgz",
-      "integrity": "sha512-3X7MR0h9b7qf4iXf/1n7RlVAx+EzpAZXoCEMhVSpaBlgKDfH2ewf+QUm7BddFyq29v4dgPP+8+uYpWuSWx035A==",
+      "version": "12.4.2",
+      "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-12.4.2.tgz",
+      "integrity": "sha512-JAJGIzY/OioIUtrRePr8go6qUxij//mL+RGGoFKU3VWQRtIHgWoHizSqH0QVn2OwrbXS9Q6CICQjfj+E5qvrXg==",
       "dev": true,
       "requires": {
         "cli-truncate": "^3.1.0",
@@ -30123,9 +30122,9 @@
       }
     },
     "react-bootstrap": {
-      "version": "2.3.0",
-      "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-2.3.0.tgz",
-      "integrity": "sha512-O8DU/R3CHLqj1IGfqZD1mOm9Jx6tm8wmfIIshNeaIdd5AZnlO7eOPF7UyzijAxNIogN0/U3U4tWIH+9gQYlwVA==",
+      "version": "2.4.0",
+      "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-2.4.0.tgz",
+      "integrity": "sha512-dn599jNK1Fg5GGjJH+lQQDwELVzigh/MdusKpB/0el+sCjsO5MZDH5gRMmBjRhC+vb7VlCDr6OXffPIDSkNMLw==",
       "dev": true,
       "requires": {
         "@babel/runtime": "^7.17.2",
@@ -30242,9 +30241,9 @@
       "dev": true
     },
     "react-i18next": {
-      "version": "11.16.7",
-      "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-11.16.7.tgz",
-      "integrity": "sha512-7yotILJLnKfvUfrl/nt9eK9vFpVFjZPLWAwBzWL6XppSZZEvlmlKk0GBGDCAPfLfs8oND7WAbry8wGzdoiW5Nw==",
+      "version": "11.16.9",
+      "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-11.16.9.tgz",
+      "integrity": "sha512-euXxWvcEAvsY7ZVkwx9ztCq4butqtsGHEkpkuo0RMj8Ru09IF9o2KxCyN+zyv51Nr0aBh/elaTIiR6fMb8YfVg==",
       "dev": true,
       "requires": {
         "@babel/runtime": "^7.14.5",
@@ -32203,9 +32202,9 @@
       }
     },
     "typescript": {
-      "version": "4.6.3",
-      "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.3.tgz",
-      "integrity": "sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw==",
+      "version": "4.7.2",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.2.tgz",
+      "integrity": "sha512-Mamb1iX2FDUpcTRzltPxgWMKy3fhg0TN378ylbktPGPK/99KbDtMQ4W1hwgsbPAsG3a0xKa1vmw4VKZQbkvz5A==",
       "dev": true
     },
     "uglify-js": {
diff --git a/package.json b/package.json
index a0e010f..1681127 100644
--- a/package.json
+++ b/package.json
@@ -15,8 +15,8 @@
     "@testing-library/jest-dom": "^5.16.4",
     "@testing-library/react": "^12.1.4",
     "@testing-library/user-event": "^13.5.0",
-    "@types/jest": "^27.4.1",
-    "@types/node": "^17.0.26",
+    "@types/jest": "^27.5.1",
+    "@types/node": "^17.0.35",
     "@types/react": "^17.0.43",
     "@types/react-dom": "^17.0.14",
     "bootstrap": "^5.1.3",
@@ -24,24 +24,23 @@
     "conventional-changelog": "^3.1.25",
     "formik": "^2.2.9",
     "http-proxy-middleware": "^2.0.6",
-    "husky": "^7.0.4",
-    "i18next": "^21.6.16",
+    "husky": "^8.0.1",
+    "i18next": "^21.8.4",
     "i18next-browser-languagedetector": "^6.1.4",
     "jest-watch-typeahead": "^0.6.5",
     "jest-websocket-mock": "^2.3.0",
-    "lint-staged": "^12.4.0",
+    "lint-staged": "^12.4.2",
     "moving-letters": "^1.0.1",
-    "nth-check": ">=2.0.1",
     "prettier": "^2.6.2",
     "qrcode": "^1.5.0",
     "react": "^17.0.2",
-    "react-bootstrap": "^2.3.0",
+    "react-bootstrap": "^2.4.0",
     "react-dom": "^17.0.2",
-    "react-i18next": "^11.16.7",
+    "react-i18next": "^11.16.9",
     "react-router-bootstrap": "^0.26.1",
     "react-router-dom": "^6.3.0",
     "react-scripts": "^5.0.1",
-    "typescript": "^4.6.3",
+    "typescript": "^4.7.2",
     "web-vitals": "^2.1.4"
   },
   "dependencies": {
diff --git a/src/components/App.jsx b/src/components/App.jsx
index 0937dae..9003184 100644
--- a/src/components/App.jsx
+++ b/src/components/App.jsx
@@ -100,7 +100,7 @@ export default function App() {
         </div>
       )}
       <Navbar />
-      <rb.Container as="main" className="py-5">
+      <rb.Container as="main" className="py-3 py-sm-5">
         {sessionConnectionError && (
           <rb.Alert variant="danger">
             {t('app.alert_no_connection', { connectionError: sessionConnectionError.message })}.
diff --git a/src/components/Balance.jsx b/src/components/Balance.jsx
index fba1374..8f45397 100644
--- a/src/components/Balance.jsx
+++ b/src/components/Balance.jsx
@@ -1,5 +1,5 @@
 import React, { useState, useEffect } from 'react'
-import { BTC, SATS, btcToSats, satsToBtc } from '../utils'
+import { BTC, SATS, btcToSats, satsToBtc, formatBtc, formatSats } from '../utils'
 import Sprite from './Sprite'
 import * as rb from 'react-bootstrap'
 import styles from './Balance.module.css'
@@ -8,9 +8,6 @@ const DISPLAY_MODE_BTC = 0
 const DISPLAY_MODE_SATS = 1
 const DISPLAY_MODE_HIDDEN = 2
 
-const decimalPoint = '\u002E'
-const nbHalfSpace = '\u202F'
-
 const getDisplayMode = (unit, showBalance) => {
   if (showBalance && unit === SATS) return DISPLAY_MODE_SATS
   if (showBalance && unit === BTC) return DISPLAY_MODE_BTC
@@ -18,33 +15,6 @@ const getDisplayMode = (unit, showBalance) => {
   return DISPLAY_MODE_HIDDEN
 }
 
-const formatBtc = (value) => {
-  const formatter = new Intl.NumberFormat('en-US', {
-    minimumIntegerDigits: 1,
-    minimumFractionDigits: 8,
-  })
-
-  const numberString = formatter.format(value)
-
-  const [integerPart, fractionalPart] = numberString.split(decimalPoint)
-
-  const formattedFractionalPart = fractionalPart
-    .split('')
-    .map((char, idx) => (idx === 2 || idx === 5 ? `${nbHalfSpace}${char}` : char))
-    .join('')
-
-  return integerPart + decimalPoint + formattedFractionalPart
-}
-
-const formatSats = (value) => {
-  const formatter = new Intl.NumberFormat('en-US', {
-    minimumIntegerDigits: 1,
-    minimumFractionDigits: 0,
-  })
-
-  return formatter.format(value)
-}
-
 const BalanceComponent = ({ symbol, value, symbolIsPrefix }) => {
   return (
     <span className="d-inline-flex align-items-center">
diff --git a/src/components/Cheatsheet.module.css b/src/components/Cheatsheet.module.css
new file mode 100644
index 0000000..4a1a360
--- /dev/null
+++ b/src/components/Cheatsheet.module.css
@@ -0,0 +1,52 @@
+.cheatsheet {
+  height: auto !important;
+  /* page height - navbar height - some spacing*/
+  max-height: calc(100vh - 76px - 1rem) !important;
+  margin: 0px auto !important;
+  border: none !important;
+  border-top-left-radius: 1rem;
+  border-top-right-radius: 1rem;
+  width: 528px;
+
+  box-shadow: 6px -3px 12px 3px rgba(0, 0, 0, 0.1);
+}
+
+:root[data-theme='dark'] .cheatsheet {
+  box-shadow: 6px -3px 12px 3px rgba(0, 0, 0, 0.5);
+}
+
+.cheatsheet button[aria-label='Close'] {
+  margin-bottom: auto;
+}
+
+.cheatsheet a {
+  color: inherit !important;
+}
+
+.cheatsheet .cheatsheet-list-item {
+  align-items: start;
+}
+
+.cheatsheet-list-item.upcoming-feature {
+  opacity: 0.25;
+}
+
+.cheatsheet-list-item h6 {
+  margin-bottom: 0.1rem;
+}
+
+.numbered {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  min-width: 2rem;
+  height: 2rem;
+  border-radius: 50%;
+  background-color: rgb(0, 0, 0);
+  color: white;
+}
+
+:root[data-theme='dark'] .numbered {
+  color: rgb(0, 0, 0);
+  background-color: white;
+}
diff --git a/src/components/Cheatsheet.tsx b/src/components/Cheatsheet.tsx
index f7dc0c4..b8e3993 100644
--- a/src/components/Cheatsheet.tsx
+++ b/src/components/Cheatsheet.tsx
@@ -3,6 +3,8 @@ import * as rb from 'react-bootstrap'
 import { Link } from 'react-router-dom'
 import { Trans, useTranslation } from 'react-i18next'
 import { routes } from '../constants/routes'
+import Sprite from './Sprite'
+import styles from './Cheatsheet.module.css'
 
 interface CheatsheetProps {
   show: boolean
@@ -10,17 +12,27 @@ interface CheatsheetProps {
 }
 
 type NumberedProps = {
-  number: number
+  number: number | 'last'
   className?: string
 }
 
-function Numbered({ number }: { number: number }) {
-  return <div className="numbered">{number}</div>
+function Numbered({ number }: NumberedProps) {
+  return (
+    <div className={styles.numbered}>
+      {number === 'last' ? (
+        <>
+          <Sprite symbol="checkmark" width="24" height="24" />
+        </>
+      ) : (
+        <>{number}</>
+      )}
+    </div>
+  )
 }
 
 function ListItem({ number, children, ...props }: PropsWithChildren<NumberedProps>) {
   return (
-    <rb.Stack className={`cheatsheet-list-item ${props.className || ''}`} direction="horizontal" gap={3}>
+    <rb.Stack className={`${styles['cheatsheet-list-item']} ${props.className || ''}`} direction="horizontal" gap={3}>
       <Numbered number={number} />
       <rb.Stack gap={0}>{children}</rb.Stack>
     </rb.Stack>
@@ -31,7 +43,7 @@ export default function Cheatsheet({ show = false, onHide }: CheatsheetProps) {
   const { t } = useTranslation()
 
   return (
-    <rb.Offcanvas className="cheatsheet" show={show} onHide={onHide} placement="bottom" onClick={onHide}>
+    <rb.Offcanvas className={styles.cheatsheet} show={show} onHide={onHide} placement="bottom" onClick={onHide}>
       <rb.Offcanvas.Header closeButton>
         <rb.Stack>
           <rb.Offcanvas.Title>{t('cheatsheet.title')}</rb.Offcanvas.Title>
@@ -83,7 +95,7 @@ export default function Cheatsheet({ show = false, onHide }: CheatsheetProps) {
             </h6>
             <div className="small text-secondary">{t('cheatsheet.item_2.description')}</div>
           </ListItem>
-          <ListItem number={3} className="upcoming-feature">
+          <ListItem number={3} className={styles['upcoming-feature']}>
             <h6>
               <Trans i18nKey="cheatsheet.item_3.title">
                 Optional: <Link to={routes.earn}>Lock</Link> funds in a fidelity bond.
@@ -105,9 +117,17 @@ export default function Cheatsheet({ show = false, onHide }: CheatsheetProps) {
             <div className="small text-secondary">{t('cheatsheet.item_4.description')}</div>
           </ListItem>
           <ListItem number={5}>
-            <h6>{t('cheatsheet.item_5.title')}</h6>
+            <h6>
+              <Trans i18nKey="cheatsheet.item_5.title">
+                <Link to={routes.send}>Send</Link> a collaborative transaction to yourself.
+              </Trans>
+            </h6>
+            <div className="small text-secondary">{t('cheatsheet.item_5.description')}</div>
+          </ListItem>
+          <ListItem number={'last'}>
+            <h6>{t('cheatsheet.item_last.title')}</h6>
             <div className="small text-secondary">
-              <Trans i18nKey="cheatsheet.item_5.description">
+              <Trans i18nKey="cheatsheet.item_last.description">
                 Still confused?{' '}
                 <a
                   href="https://github.com/openoms/bitcoin-tutorials/blob/master/joinmarket/joinmarket_private_flow.md#a-private-flow-through-joinmarket"
diff --git a/src/components/CurrentWalletMagic.jsx b/src/components/CurrentWalletMagic.jsx
index 1d9907f..26c6956 100644
--- a/src/components/CurrentWalletMagic.jsx
+++ b/src/components/CurrentWalletMagic.jsx
@@ -9,6 +9,7 @@ import { walletDisplayName } from '../utils'
 import styles from './CurrentWalletMagic.module.css'
 import { ExtendedLink } from './ExtendedLink'
 import { routes } from '../constants/routes'
+import { useBalanceSummary } from '../hooks/BalanceSummary'
 
 const WalletHeader = ({ name, balance, unit, showBalance, loading }) => {
   return (
@@ -38,10 +39,10 @@ const WalletHeader = ({ name, balance, unit, showBalance, loading }) => {
   )
 }
 
-const PrivacyLevels = ({ accounts, loading }) => {
+const PrivacyLevels = ({ accountBalances, loading }) => {
   const numPrivacyLevelsPalceholders = 5
-  const sortedAccounts = (accounts || []).sort((lhs, rhs) => lhs.account - rhs.account)
-  const numAccounts = sortedAccounts.length
+  const sortedAccountBalances = (accountBalances || []).sort((lhs, rhs) => lhs.accountIndex - rhs.accountIndex)
+  const numAccounts = sortedAccountBalances.length
 
   return (
     <div className="d-flex justify-content-center">
@@ -50,12 +51,12 @@ const PrivacyLevels = ({ accounts, loading }) => {
           ? Array(numPrivacyLevelsPalceholders)
               .fill('')
               .map((_, index) => <LoadingPrivacyLevel key={index} level={numPrivacyLevelsPalceholders} />)
-          : sortedAccounts.map(({ account, account_balance: balance }) => (
+          : sortedAccountBalances.map(({ accountIndex, totalBalance }) => (
               <PrivacyLevel
-                key={account}
+                key={accountIndex}
                 numAccounts={numAccounts}
-                level={parseInt(account)}
-                balance={balance}
+                level={accountIndex}
+                balance={totalBalance}
                 loading={loading}
               />
             ))}
@@ -113,7 +114,8 @@ export default function CurrentWalletMagic() {
   const settings = useSettings()
   const settingsDispatch = useSettingsDispatch()
   const currentWallet = useCurrentWallet()
-  const walletInfo = useCurrentWalletInfo()
+  const currentWalletInfo = useCurrentWalletInfo()
+  const balanceSummary = useBalanceSummary(currentWalletInfo)
   const reloadCurrentWalletInfo = useReloadCurrentWalletInfo()
 
   const [alert, setAlert] = useState(null)
@@ -148,7 +150,7 @@ export default function CurrentWalletMagic() {
         <rb.Row onClick={() => settingsDispatch({ showBalance: !settings.showBalance })} style={{ cursor: 'pointer' }}>
           <WalletHeader
             name={currentWallet?.name}
-            balance={walletInfo?.data.display.walletinfo.total_balance}
+            balance={balanceSummary?.totalBalance}
             unit={settings.unit}
             showBalance={settings.showBalance}
             loading={isLoading}
@@ -184,7 +186,7 @@ export default function CurrentWalletMagic() {
           <hr className="my-4" />
         </rb.Row>
         <rb.Row>
-          <PrivacyLevels accounts={walletInfo?.data.display.walletinfo.accounts} loading={isLoading} />
+          <PrivacyLevels accountBalances={balanceSummary?.accountBalances} loading={isLoading} />
         </rb.Row>
         <rb.Row>
           <hr className="my-4" />
diff --git a/src/components/DisplayAccounts.jsx b/src/components/DisplayAccounts.jsx
index dd856d2..5417cb1 100644
--- a/src/components/DisplayAccounts.jsx
+++ b/src/components/DisplayAccounts.jsx
@@ -37,68 +37,79 @@ export default function DisplayAccounts({ accounts, ...props }) {
 
   return (
     <rb.Accordion {...props}>
-      {Object.values(accounts).map(({ account, account_balance: balance, branches }) => (
-        <rb.Accordion.Item key={account} eventKey={account}>
-          <rb.Accordion.Header>
-            <rb.Row className="w-100  me-1">
-              <rb.Col xs={'auto'}>
-                <h5 className="mb-0">
-                  {t('current_wallet_advanced.account')} {account}
-                </h5>
-              </rb.Col>
-              <rb.Col className="d-flex align-items-center justify-content-end">
-                <Balance valueString={balance} convertToUnit={settings.unit} showBalance={settings.showBalance} />
-              </rb.Col>
-            </rb.Row>
-          </rb.Accordion.Header>
-          <rb.Accordion.Body>
-            <Link to={routes.send} state={{ account }} className="btn btn-outline-dark">
-              {t('current_wallet_advanced.account_button_send')}
-            </Link>{' '}
-            <Link to={routes.receive} state={{ account }} className="btn btn-outline-dark">
-              {t('current_wallet_advanced.account_button_receive')}
-            </Link>
-            {branches.map(({ balance, branch, entries }) => {
-              const [type, derivation, xpub] = branch.split('\t')
-              return (
-                <article key={derivation}>
-                  <rb.Row className="mt-4 pe-3">
-                    <rb.Col>
-                      {type === 'external addresses' && (
-                        <h6>{t('current_wallet_advanced.account_heading_external_addresses')}</h6>
-                      )}
-                      {type === 'internal addresses' && (
-                        <h6>{t('current_wallet_advanced.account_heading_internal_addresses')}</h6>
-                      )}
-                      {!['internal addresses', 'external addresses'].includes(type) && <h6>{type}</h6>}
-                    </rb.Col>
-                    <rb.Col className="d-flex align-items-center justify-content-end">
-                      <Balance valueString={balance} convertToUnit={settings.unit} showBalance={settings.showBalance} />
-                    </rb.Col>
-                  </rb.Row>
-                  <rb.Row className="p-3">
-                    <rb.Col xs="auto">
-                      <code className="text-break">{derivation}</code>
-                    </rb.Col>
-                    <rb.Col xs="auto">
-                      <code className="text-break">{xpub}</code>
-                    </rb.Col>
-                  </rb.Row>
-                  {entries.map((entry, index) => (
-                    <BranchEntry
-                      key={entry.address}
-                      entry={entry}
-                      className={`bg-transparent rounded-0 border-start-0 border-end-0 ${
-                        index === 0 ? 'border-top-1 mt-2' : 'border-top-0'
-                      }`}
-                    />
-                  ))}
-                </article>
-              )
-            })}
-          </rb.Accordion.Body>
-        </rb.Accordion.Item>
-      ))}
+      {Object.values(accounts).map(({ account, account_balance, branches }) => {
+        return (
+          <rb.Accordion.Item key={account} eventKey={account}>
+            <rb.Accordion.Header>
+              <rb.Row className="w-100  me-1">
+                <rb.Col xs={'auto'}>
+                  <h5 className="mb-0">
+                    {t('current_wallet_advanced.account')} {account}
+                  </h5>
+                </rb.Col>
+                <rb.Col className="d-flex align-items-center justify-content-end">
+                  <Balance
+                    valueString={account_balance}
+                    convertToUnit={settings.unit}
+                    showBalance={settings.showBalance}
+                  />
+                </rb.Col>
+              </rb.Row>
+            </rb.Accordion.Header>
+            <rb.Accordion.Body>
+              <Link to={routes.send} state={{ account }} className="btn btn-outline-dark">
+                {t('current_wallet_advanced.account_button_send')}
+              </Link>{' '}
+              <Link to={routes.receive} state={{ account }} className="btn btn-outline-dark">
+                {t('current_wallet_advanced.account_button_receive')}
+              </Link>
+              {branches.map(({ balance, branch, entries }) => {
+                const [type, derivation, xpub] = branch.split('\t')
+
+                return (
+                  <article key={derivation}>
+                    <rb.Row className="mt-4 pe-3">
+                      <rb.Col>
+                        {type === 'external addresses' && (
+                          <h6>{t('current_wallet_advanced.account_heading_external_addresses')}</h6>
+                        )}
+                        {type === 'internal addresses' && (
+                          <h6>{t('current_wallet_advanced.account_heading_internal_addresses')}</h6>
+                        )}
+                        {!['internal addresses', 'external addresses'].includes(type) && <h6>{type}</h6>}
+                      </rb.Col>
+                      <rb.Col className="d-flex align-items-center justify-content-end">
+                        <Balance
+                          valueString={balance}
+                          convertToUnit={settings.unit}
+                          showBalance={settings.showBalance}
+                        />
+                      </rb.Col>
+                    </rb.Row>
+                    <rb.Row className="p-3">
+                      <rb.Col xs="auto">
+                        <code className="text-break">{derivation}</code>
+                      </rb.Col>
+                      <rb.Col xs="auto">
+                        <code className="text-break">{xpub}</code>
+                      </rb.Col>
+                    </rb.Row>
+                    {entries.map((entry, index) => (
+                      <BranchEntry
+                        key={entry.address}
+                        entry={entry}
+                        className={`bg-transparent rounded-0 border-start-0 border-end-0 ${
+                          index === 0 ? 'border-top-1 mt-2' : 'border-top-0'
+                        }`}
+                      />
+                    ))}
+                  </article>
+                )
+              })}
+            </rb.Accordion.Body>
+          </rb.Accordion.Item>
+        )
+      })}
     </rb.Accordion>
   )
 }
diff --git a/src/components/Jam.jsx b/src/components/Jam.jsx
index b27da75..81f8b14 100644
--- a/src/components/Jam.jsx
+++ b/src/components/Jam.jsx
@@ -13,6 +13,7 @@ import ToggleSwitch from './ToggleSwitch'
 import Sprite from './Sprite'
 import Balance from './Balance'
 import ScheduleProgress from './ScheduleProgress'
+import { useBalanceSummary } from '../hooks/BalanceSummary'
 
 // When running the scheduler with internal destination addresses, the funds
 // will end up on those 3 mixdepths (one UTXO each).
@@ -104,6 +105,7 @@ export default function Jam() {
   const reloadServiceInfo = useReloadServiceInfo()
   const wallet = useCurrentWallet()
   const walletInfo = useCurrentWalletInfo()
+  const balanceSummary = useBalanceSummary(walletInfo)
   const reloadCurrentWalletInfo = useReloadCurrentWalletInfo()
 
   const [alert, setAlert] = useState(null)
@@ -366,7 +368,7 @@ export default function Jam() {
               </>
             </rb.Alert>
           </rb.Fade>
-          {!collaborativeOperationRunning && wallet && walletInfo && (
+          {!collaborativeOperationRunning && balanceSummary && (
             <>
               <div className="d-flex align-items-center justify-content-between mb-4">
                 <div className="d-flex align-items-center gap-2">
@@ -379,11 +381,8 @@ export default function Jam() {
                   </div>
                 </div>
                 <>
-                  {
-                    // Todo: Subtract frozen or locked UTXOs from amount shown.
-                  }
                   <Balance
-                    valueString={walletInfo.data.display.walletinfo.total_balance}
+                    valueString={`${balanceSummary.calculatedAvailableBalanceInSats}`}
                     convertToUnit={settings.unit}
                     showBalance={settings.showBalance}
                   />
diff --git a/src/components/Navbar.jsx b/src/components/Navbar.jsx
index 82de393..b834a92 100644
--- a/src/components/Navbar.jsx
+++ b/src/components/Navbar.jsx
@@ -10,17 +10,18 @@ import { useCurrentWallet, useCurrentWalletInfo } from '../context/WalletContext
 import { useServiceInfo, useSessionConnectionError } from '../context/ServiceInfoContext'
 import { walletDisplayName } from '../utils'
 import { routes } from '../constants/routes'
+import { useBalanceSummary } from '../hooks/BalanceSummary'
 
-const WalletPreview = ({ wallet, walletInfo, unit, showBalance }) => {
+const WalletPreview = ({ wallet, totalBalance, unit, showBalance }) => {
   return (
     <div className="d-flex align-items-center">
       <Sprite symbol="wallet" width="30" height="30" className="text-body" />
       <div className="d-flex flex-column ms-2 fs-6">
         {wallet && <div className="fw-normal">{walletDisplayName(wallet.name)}</div>}
-        {walletInfo && walletInfo?.data.display.walletinfo.total_balance && unit ? (
+        {totalBalance && unit ? (
           <div className="text-body">
             <Balance
-              valueString={walletInfo.data.display.walletinfo.total_balance}
+              valueString={totalBalance}
               convertToUnit={unit}
               showBalance={showBalance || false}
               enableVisibilityToggle={false}
@@ -139,6 +140,7 @@ export default function Navbar() {
   const settings = useSettings()
   const currentWallet = useCurrentWallet()
   const currentWalletInfo = useCurrentWalletInfo()
+  const balanceSummary = useBalanceSummary(currentWalletInfo)
 
   const serviceInfo = useServiceInfo()
   const sessionConnectionError = useSessionConnectionError()
@@ -213,7 +215,7 @@ export default function Navbar() {
                       <>
                         <WalletPreview
                           wallet={currentWallet}
-                          walletInfo={currentWalletInfo}
+                          totalBalance={balanceSummary?.totalBalance}
                           showBalance={settings.showBalance}
                           unit={settings.unit}
                         />
diff --git a/src/components/Send.jsx b/src/components/Send.jsx
index 96e31b4..e8a3e66 100644
--- a/src/components/Send.jsx
+++ b/src/components/Send.jsx
@@ -6,11 +6,12 @@ import PageTitle from './PageTitle'
 import ToggleSwitch from './ToggleSwitch'
 import Sprite from './Sprite'
 import Balance from './Balance'
-import { useCurrentWalletInfo, useReloadCurrentWalletInfo, useCurrentWallet } from '../context/WalletContext'
+import { useReloadCurrentWalletInfo, useCurrentWallet, useCurrentWalletInfo } from '../context/WalletContext'
 import { useServiceInfo, useReloadServiceInfo } from '../context/ServiceInfoContext'
 import { useSettings } from '../context/SettingsContext'
+import { useBalanceSummary } from '../hooks/BalanceSummary'
 import * as Api from '../libs/JmWalletApi'
-import { btcToSats, SATS } from '../utils'
+import { btcToSats, SATS, formatBtc, formatSats } from '../utils'
 import { routes } from '../constants/routes'
 import styles from './Send.module.css'
 
@@ -171,6 +172,7 @@ export default function Send() {
   const { t } = useTranslation()
   const wallet = useCurrentWallet()
   const walletInfo = useCurrentWalletInfo()
+
   const reloadCurrentWalletInfo = useReloadCurrentWalletInfo()
   const serviceInfo = useServiceInfo()
   const reloadServiceInfo = useReloadServiceInfo()
@@ -220,6 +222,18 @@ export default function Send() {
   const [numCollaborators, setNumCollaborators] = useState(initialNumCollaborators(minNumCollaborators))
   const [formIsValid, setFormIsValid] = useState(false)
 
+  const balanceSummary = useBalanceSummary(walletInfo)
+  const accountBalanceOrNull = useMemo(() => {
+    const eligibleAccountBalances =
+      balanceSummary && balanceSummary.accountBalances.filter((it) => it.accountIndex === account)
+
+    if (!eligibleAccountBalances || eligibleAccountBalances.length !== 1) {
+      return null
+    }
+
+    return eligibleAccountBalances[0]
+  }, [balanceSummary, account])
+
   useEffect(() => {
     if (
       isValidAddress(destination) &&
@@ -443,52 +457,19 @@ export default function Send() {
     }
   }
 
-  const balanceBreakdown = (accountNumber) => {
-    if (!walletInfo || !walletInfo.data.display.walletinfo.accounts || !walletInfo.data.utxos.utxos) {
-      return null
-    }
-
-    const filtered = walletInfo.data.display.walletinfo.accounts.filter((account) => {
-      return parseInt(account.account, 10) === accountNumber
-    })
-
-    if (filtered.length !== 1) {
-      return null
-    }
-
-    const utxosByAccount = walletInfo.data.utxos.utxos.reduce((acc, utxo) => {
-      acc[utxo.mixdepth] = acc[utxo.mixdepth] || []
-      acc[utxo.mixdepth].push(utxo)
-      return acc
-    }, {})
-    const accountUtxos = utxosByAccount[accountNumber] || []
-    const frozenOrLockedUtxos = accountUtxos.filter((utxo) => utxo.frozen || utxo.locktime)
-    const balanceFrozenOrLocked = frozenOrLockedUtxos.reduce((acc, utxo) => acc + utxo.value, 0)
-
-    return {
-      totalBalance: btcToSats(filtered[0].account_balance),
-      frozenOrLockedBalance: balanceFrozenOrLocked,
-    }
-  }
-
   const amountFieldValue = () => {
     if (amount === null || Number.isNaN(amount)) return ''
 
     if (isSweep) {
-      const breakdown = balanceBreakdown(account)
-
-      if (!breakdown) return ''
-
-      return breakdown.totalBalance - breakdown.frozenOrLockedBalance
+      if (!accountBalanceOrNull) return ''
+      return `${accountBalanceOrNull.calculatedAvailableBalanceInSats}`
     }
 
     return amount
   }
 
   const frozenOrLockedWarning = () => {
-    const breakdown = balanceBreakdown(account)
-
-    if (!breakdown) return null
+    if (!accountBalanceOrNull) return null
 
     return (
       <div className={`${styles['sweep-breakdown']} mt-2`}>
@@ -502,7 +483,7 @@ export default function Send() {
                     <td>{t('send.sweep_amount_breakdown_total_balance')}</td>
                     <td className={styles['balance-col']}>
                       <Balance
-                        valueString={breakdown.totalBalance.toString()}
+                        valueString={accountBalanceOrNull.totalBalance}
                         convertToUnit={SATS}
                         showBalance={true}
                       />
@@ -512,7 +493,7 @@ export default function Send() {
                     <td>{t('send.sweep_amount_breakdown_frozen_balance')}</td>
                     <td className={styles['balance-col']}>
                       <Balance
-                        valueString={breakdown.frozenOrLockedBalance.toString()}
+                        valueString={accountBalanceOrNull.calculatedFrozenOrLockedBalanceInSats.toString()}
                         convertToUnit={SATS}
                         showBalance={true}
                       />
@@ -628,15 +609,18 @@ export default function Send() {
                 isInvalid={!isValidAccount(account)}
                 disabled={isOperationDisabled}
               >
-                {walletInfo &&
-                  walletInfo.data.display.walletinfo.accounts
-                    .sort((lhs, rhs) => lhs.account - rhs.account)
-                    .map(({ account, account_balance: balance }) => (
-                      <option key={account} value={account}>
+                {balanceSummary &&
+                  balanceSummary.accountBalances
+                    .sort((lhs, rhs) => lhs.accountIndex - rhs.accountIndex)
+                    .map(({ accountIndex, totalBalance }) => (
+                      <option key={accountIndex} value={accountIndex}>
                         {settings.useAdvancedWalletMode
-                          ? t('send.account_selector_option_dev_mode', { number: account })
-                          : t('send.account_selector_option', { number: account })}{' '}
-                        {settings.showBalance && `(\u20BF${balance})`}
+                          ? t('send.account_selector_option_dev_mode', { number: accountIndex })
+                          : t('send.account_selector_option', { number: accountIndex })}{' '}
+                        {settings.showBalance &&
+                          (settings.unit === 'sats'
+                            ? `(${formatSats(btcToSats(totalBalance))} sats)`
+                            : `(\u20BF${formatBtc(totalBalance)})`)}
                       </option>
                     ))}
               </rb.Form.Select>
diff --git a/src/context/WalletContext.tsx b/src/context/WalletContext.tsx
index d59f8ed..a01d204 100644
--- a/src/context/WalletContext.tsx
+++ b/src/context/WalletContext.tsx
@@ -9,43 +9,65 @@ interface CurrentWallet {
 }
 
 // TODO: move these interfaces to JmWalletApi, once distinct types are used as return value instead of plain "Response"
+export type Utxo = {
+  address: string
+  path: string
+  label: string
+  value: number // in sats
+  tries: number
+  tries_remaining: number
+  external: boolean
+  mixdepth: number
+  confirmations: number
+  frozen: boolean
+  utxo: string
+  locktime?: string
+}
+
+export type Utxos = Utxo[]
 
-type Utxos = any[]
 interface UtxosResponse {
   utxos: Utxos
 }
+
 interface WalletDisplayResponse {
   walletinfo: WalletDisplayInfo
 }
 
+export type BalanceString = `${number}.${string}`
+
 interface WalletDisplayInfo {
   wallet_name: string
-  total_balance: string
+  total_balance: BalanceString
+  available_balance: BalanceString
   accounts: Account[]
 }
 
 interface Account {
   account: string
-  account_balance: string
+  account_balance: BalanceString
+  available_balance: BalanceString
   branches: Branch[]
 }
 
 interface Branch {
   branch: string
-  balance: string
+  balance: BalanceString
+  available_balance: BalanceString
   entries: BranchEntry[]
 }
 
 interface BranchEntry {
   hd_path: string
   address: string
-  amount: string
+  amount: BalanceString
+  available_balance: BalanceString
   status: string
   label: string
   extradata: string
 }
 
-interface WalletInfo {
+export interface WalletInfo {
   data: {
     utxos: UtxosResponse
     display: WalletDisplayResponse
diff --git a/src/hooks/BalanceSummary.ts b/src/hooks/BalanceSummary.ts
new file mode 100644
index 0000000..0cf2d5a
--- /dev/null
+++ b/src/hooks/BalanceSummary.ts
@@ -0,0 +1,112 @@
+import { useMemo } from 'react'
+import { btcToSats } from '../utils'
+import { WalletInfo, BalanceString, Utxos } from '../context/WalletContext'
+
+interface BalanceSummary {
+  totalBalance: BalanceString
+  /**
+   * @since clientserver v0.9.7
+   * @description available balance (total - frozen - locked)
+   *   This value can report less than available in case of address reuse.
+   *   See https://github.com/JoinMarket-Org/joinmarket-clientserver/pull/1285#issuecomment-1136438072
+   *   Utxos controlled by the same key will not be taken into account if at least one output is
+   *   frozen (last checked on 2022-05-24).
+   *
+   *   Please use {@link BalanceSummarySupport.calculatedAvailableBalanceInSats}) if applicable.
+   */
+  availableBalance: BalanceString
+}
+
+type BalanceSummarySupport = BalanceSummary & {
+  /**
+   * @description Manually calculated available balance in sats.
+   *   Same as {@link BalanceSummary.availableBalance} except address reuse is taken into account.
+   */
+  calculatedAvailableBalanceInSats: number
+  /**
+   * @description Manually calculated frozen or locked balance in sats.
+   */
+  calculatedFrozenOrLockedBalanceInSats: number
+}
+
+type AccountBalanceSummary = BalanceSummarySupport & {
+  accountIndex: number
+}
+
+export type WalletBalanceSummary = BalanceSummarySupport & {
+  accountBalances: AccountBalanceSummary[]
+}
+
+const calculateFrozenOrLockedBalance = (utxos: Utxos) => {
+  const frozenOrLockedUtxos = utxos.filter((utxo) => utxo.frozen || utxo.locktime)
+  return frozenOrLockedUtxos.reduce((acc, utxo) => acc + utxo.value, 0)
+}
+
+const useBalanceSummary = (currentWalletInfo: WalletInfo | null): WalletBalanceSummary | null => {
+  const balanceSummary = useMemo(() => {
+    if (!currentWalletInfo) {
+      return null
+    }
+
+    const accounts = currentWalletInfo.data.display.walletinfo.accounts
+    const utxos = currentWalletInfo.data.utxos.utxos
+
+    try {
+      const walletBalanceSummary: BalanceSummary = {
+        totalBalance: currentWalletInfo.data.display.walletinfo.total_balance,
+        availableBalance: currentWalletInfo.data.display.walletinfo.available_balance,
+      }
+      const utxosByAccount = utxos.reduce((acc, utxo) => {
+        const key = `${utxo.mixdepth}`
+        acc[key] = acc[key] || []
+        acc[key].push(utxo)
+        return acc
+      }, {} as { [key: string]: Utxos })
+
+      const frozenOrLockedCalculatedByAccount = Object.fromEntries(
+        Object.entries(utxosByAccount).map(([account, utxos]) => {
+          return [account, calculateFrozenOrLockedBalance(utxos)]
+        })
+      )
+
+      const accountsBalanceSummary = accounts.map(({ account, account_balance, available_balance }) => {
+        const accountBalanceSummary: BalanceSummary = {
+          totalBalance: account_balance,
+          availableBalance: available_balance,
+        }
+
+        const accountFrozenOrLockedCalculated = frozenOrLockedCalculatedByAccount[account] || 0
+        const accountAvailableCalculatedInSats =
+          btcToSats(accountBalanceSummary.totalBalance!) - accountFrozenOrLockedCalculated
+        return {
+          ...accountBalanceSummary,
+          calculatedAvailableBalanceInSats: accountAvailableCalculatedInSats,
+          calculatedFrozenOrLockedBalanceInSats: accountFrozenOrLockedCalculated,
+          accountIndex: parseInt(account, 10),
+        } as AccountBalanceSummary
+      })
+
+      const walletFrozenOrLockedCalculated = Object.values(frozenOrLockedCalculatedByAccount).reduce(
+        (acc, frozenOrLockedSats) => acc + frozenOrLockedSats,
+        0
+      )
+
+      const walletAvailableCalculatedInSats =
+        btcToSats(walletBalanceSummary.totalBalance!) - walletFrozenOrLockedCalculated
+
+      return {
+        ...walletBalanceSummary,
+        accountBalances: accountsBalanceSummary,
+        calculatedAvailableBalanceInSats: walletAvailableCalculatedInSats,
+        calculatedFrozenOrLockedBalanceInSats: walletFrozenOrLockedCalculated,
+      }
+    } catch (e) {
+      console.warn('"useBalanceSummary" hook cannot determine balance format', e)
+      return null
+    }
+  }, [currentWalletInfo])
+
+  return balanceSummary
+}
+
+export { useBalanceSummary }
diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json
index 9a85152..4697728 100644
--- a/src/i18n/locales/en/translation.json
+++ b/src/i18n/locales/en/translation.json
@@ -252,6 +252,10 @@
       "description": "Offer your sats to the marketplace. No trust or custody required—you are always in full control of your funds."
     },
     "item_5": {
+      "title": "<0>Send</0> a collaborative transaction to yourself.",
+      "description": "Collaborative transactions increase the privacy of yourself and others."
+    },
+    "item_last": {
       "title": "Go to step one and repeat.",
       "description": "Still confused? Dig into the <2>documentation</2>."
     }
diff --git a/src/i18n/testConfig.js b/src/i18n/testConfig.ts
similarity index 100%
rename from src/i18n/testConfig.js
rename to src/i18n/testConfig.ts
diff --git a/src/index.css b/src/index.css
index 6f4fa89..d062422 100644
--- a/src/index.css
+++ b/src/index.css
@@ -357,59 +357,14 @@ main {
   }
 }
 
-/* Cheatsheet Styles */
-.cheatsheet {
-  height: auto;
-  /* page height - navbar height - some spacing*/
-  max-height: calc(100vh - 76px - 1rem);
-  border-top-left-radius: 1rem;
-  border-top-right-radius: 1rem;
-  margin: 0px auto;
-  width: 528px;
-}
-
-.cheatsheet .offcanvas-header .btn-close {
-  margin-bottom: auto;
-}
-
-.cheatsheet .cheatsheet-list-item {
-  align-items: start;
-}
-
-.cheatsheet .cheatsheet-list-item.upcoming-feature {
-  opacity: 0.25;
-}
-
-.cheatsheet .cheatsheet-list-item h6 {
-  margin-bottom: 0.1rem;
-}
-.cheatsheet a {
-  color: inherit;
-}
-
-.cheatsheet .numbered {
-  display: flex;
-  justify-content: center;
-  align-items: center;
-  min-width: 2rem;
-  height: 2rem;
-  border-radius: 50%;
-  background-color: rgb(0, 0, 0);
-  color: white;
-}
-
-:root[data-theme='dark'] .cheatsheet .numbered {
-  color: rgb(0, 0, 0);
-  background-color: white;
-}
-
-.cheatsheet-link.nav-link {
+footer .cheatsheet-link.nav-link {
   color: var(--bs-gray-900);
 }
 
-:root[data-theme='dark'] .cheatsheet-link.nav-link {
+:root[data-theme='dark'] footer .cheatsheet-link.nav-link {
   color: var(--bs-gray-400);
 }
+
 /* Blurred Text Styles */
 
 .blurred-text {
diff --git a/src/utils.ts b/src/utils.ts
index 0f7e51d..fa4fd4c 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -42,3 +42,33 @@ export const copyToClipboard = (
       }
     })
 }
+
+export const formatBtc = (value: number) => {
+  const decimalPoint = '\u002E'
+  const nbHalfSpace = '\u202F'
+
+  const formatter = new Intl.NumberFormat('en-US', {
+    minimumIntegerDigits: 1,
+    minimumFractionDigits: 8,
+  })
+
+  const numberString = formatter.format(value)
+
+  const [integerPart, fractionalPart] = numberString.split(decimalPoint)
+
+  const formattedFractionalPart = fractionalPart
+    .split('')
+    .map((char, idx) => (idx === 2 || idx === 5 ? `${nbHalfSpace}${char}` : char))
+    .join('')
+
+  return integerPart + decimalPoint + formattedFractionalPart
+}
+
+export const formatSats = (value: number) => {
+  const formatter = new Intl.NumberFormat('en-US', {
+    minimumIntegerDigits: 1,
+    minimumFractionDigits: 0,
+  })
+
+  return formatter.format(value)
+}


Test patch:
diff --git a/docker/regtest/readme.md b/docker/regtest/readme.md
index 0ef192c..820767a 100644
--- a/docker/regtest/readme.md
+++ b/docker/regtest/readme.md
@@ -1,7 +1,31 @@
-# Docker setup for running JoinMarket in regtest mode
+# Docker setup for running Jam in regtest mode
 
 This setup will help you set up a regtest environment quickly.
-It starts two JoinMarket containers, hence not only API calls but also actual CoinJoin transactions can be tested.
+It starts multiple JoinMarket containers, hence not only API calls but also actual CoinJoin transactions can be tested.
+Communication between these containers is done via Tor (if internet connection is available) and IRC (locally running container).
+
+## Common flow
+```sh
+# (optional) once in a while rebuild the images
+npm run regtest:rebuild
+
+# start the regtest environment
+npm run regtest:up
+
+# fund wallets and start maker
+./docker/regtest/init-setup.sh
+
+# mine blocks in regtest periodically
+npm run regtest:mine
+
+[...]
+
+# stop the regtest environment
+npm run regtest:down
+
+# (optional) wipe all test data and start from scratch next time
+npm run regtest:clear
+```
 
 ## Commands
 
@@ -41,6 +65,8 @@ The second JoinMarket container is based on `joinmarket-webui/joinmarket-webui-d
 (username `joinmarket` and pass `joinmarket` for Basic Authentication).
 This is useful if you want to perform regression tests.
 
+The third JoinMarket container acts as [Directory Node](https://github.com/JoinMarket-Org/joinmarket-clientserver/blob/master/docs/onion-message-channels.md#directory) and exists solely to enable communication between peers.
+
 ### Rebuild
 
 In order to incorporate the current contents of `master` branch, simply rebuild the joinmarket images from scratch.
@@ -69,6 +95,13 @@ docker exec -t jm_regtest_joinmarket git log --oneline -1
 
 Some helper scripts are included to make recurring tasks and interaction with the containers easier.
 
+### `npm run regtest:mine`
+
+Mine regtest blocks in a fixed interval (current default is every 10 seconds).
+This is useful for features that await confirmations or need incoming blocks regularly.
+e.g. This is necessary for scheduled transactions to execute successfully.
+
+
 ### `init-setup.sh`
 
 This script helps in providing both JoinMarket containers a wallet with spendable coins and starting the Maker Service in the secondary container.
diff --git a/src/hooks/BalanceSummary.test.tsx b/src/hooks/BalanceSummary.test.tsx
new file mode 100644
index 0000000..2668b07
--- /dev/null
+++ b/src/hooks/BalanceSummary.test.tsx
@@ -0,0 +1,208 @@
+import React from 'react'
+import { render } from '../testUtils'
+import { act } from 'react-dom/test-utils'
+
+import { useBalanceSummary, WalletBalanceSummary } from './BalanceSummary'
+import { WalletInfo, Utxo } from '../context/WalletContext'
+
+function setup(walletInfo: WalletInfo | null) {
+  const returnVal: { data: WalletBalanceSummary | null | undefined } = { data: undefined }
+  const TestComponent: React.FunctionComponent = () => {
+    returnVal.data = useBalanceSummary(walletInfo)
+    return <></>
+  }
+
+  render(<TestComponent />)
+
+  return returnVal
+}
+
+describe('BalanceSummary', () => {
+  it('should handle missing wallet info without errors', () => {
+    let balanceSummary: WalletBalanceSummary | null | undefined
+
+    act(() => {
+      balanceSummary = setup(null).data
+    })
+
+    expect(balanceSummary).toBeNull()
+  })
+
+  it('should handle <=v0.9.6 balance format', () => {
+    let balanceSummary: WalletBalanceSummary | null | undefined
+
+    act(() => {
+      balanceSummary = setup({
+        data: {
+          utxos: {
+            utxos: [],
+          },
+          display: {
+            walletinfo: {
+              wallet_name: 'test.jmdat',
+              total_balance: '2.00000000',
+              available_balance: '1.00000000',
+              accounts: [],
+            },
+          },
+        },
+      }).data
+    })
+
+    expect(balanceSummary).not.toBeNull()
+    expect(balanceSummary!.totalBalance).toBe('2.00000000')
+    expect(balanceSummary!.availableBalance).toBe('1.00000000')
+  })
+
+  it('should populate balance properties calculated from utxo data', () => {
+    let balanceSummary: WalletBalanceSummary | null | undefined
+
+    act(() => {
+      balanceSummary = setup({
+        data: {
+          utxos: {
+            utxos: [
+              {
+                value: 1,
+                mixdepth: 0,
+                frozen: false,
+              } as Utxo,
+              {
+                value: 2,
+                mixdepth: 0,
+                frozen: false,
+                locktime: '2099-12',
+              } as Utxo,
+              {
+                value: 3,
+                mixdepth: 0,
+                confirmations: 0,
+                frozen: true,
+              } as Utxo,
+            ],
+          },
+          display: {
+            walletinfo: {
+              wallet_name: 'test.jmdat',
+              total_balance: '0.00000006',
+              available_balance: '0.00000001',
+              accounts: [],
+            },
+          },
+        },
+      }).data
+    })
+
+    expect(balanceSummary).not.toBeNull()
+    expect(balanceSummary!.totalBalance).toBe('0.00000006')
+    expect(balanceSummary!.availableBalance).toBe('0.00000001')
+    expect(balanceSummary!.calculatedAvailableBalanceInSats).toBe(1)
+    expect(balanceSummary!.calculatedFrozenOrLockedBalanceInSats).toBe(5)
+    expect(balanceSummary!.accountBalances).toEqual([])
+  })
+
+  it('should populate account balance data', () => {
+    let balanceSummary: WalletBalanceSummary | null | undefined
+
+    act(() => {
+      balanceSummary = setup({
+        data: {
+          utxos: {
+            utxos: [
+              {
+                value: 111111111,
+                mixdepth: 1,
+              } as Utxo,
+              {
+                value: 222222222,
+                mixdepth: 2,
+              } as Utxo,
+              {
+                value: 11111111,
+                mixdepth: 2,
+                frozen: true,
+              } as Utxo,
+              {
+                value: 333333333,
+                mixdepth: 3,
+                confirmations: 0,
+                frozen: true,
+              } as Utxo,
+            ],
+          },
+          display: {
+            walletinfo: {
+              wallet_name: 'test.jmdat',
+              total_balance: '6.66666666',
+              available_balance: '3.22222222',
+              accounts: [
+                {
+                  account: '0',
+                  account_balance: '0.00000000',
+                  available_balance: '0.00000000',
+                  branches: [],
+                },
+                {
+                  account: '1',
+                  account_balance: '1.11111111',
+                  available_balance: '1.11111111',
+                  branches: [],
+                },
+                {
+                  account: '2',
+                  account_balance: '2.22222222',
+                  available_balance: '2.11111111',
+                  branches: [],
+                },
+                {
+                  account: '3',
+                  account_balance: '3.33333333',
+                  available_balance: '0.00000000',
+                  branches: [],
+                },
+              ],
+            },
+          },
+        },
+      }).data
+    })
+
+    expect(balanceSummary).not.toBeNull()
+    expect(balanceSummary!.totalBalance).toBe('6.66666666')
+    expect(balanceSummary!.availableBalance).toBe('3.22222222')
+    expect(balanceSummary!.calculatedAvailableBalanceInSats).toBe(322222222)
+    expect(balanceSummary!.calculatedFrozenOrLockedBalanceInSats).toBe(344444444)
+    expect(balanceSummary!.accountBalances).toHaveLength(4)
+
+    const accountSummaryByIndex = (accountIndex: number) =>
+      balanceSummary!.accountBalances!.filter((it) => it.accountIndex === accountIndex).reduce((_, obj) => obj)
+
+    const account0 = accountSummaryByIndex(0)
+    expect(account0.accountIndex).toBe(0)
+    expect(account0.totalBalance).toBe('0.00000000')
+    expect(account0.availableBalance).toBe('0.00000000')
+    expect(account0.calculatedAvailableBalanceInSats).toBe(0)
+    expect(account0.calculatedFrozenOrLockedBalanceInSats).toBe(0)
+
+    const account1 = accountSummaryByIndex(1)
+    expect(account1.accountIndex).toBe(1)
+    expect(account1.totalBalance).toBe('1.11111111')
+    expect(account1.availableBalance).toBe('1.11111111')
+    expect(account1.calculatedAvailableBalanceInSats).toBe(111111111)
+    expect(account1.calculatedFrozenOrLockedBalanceInSats).toBe(0)
+
+    const account2 = accountSummaryByIndex(2)
+    expect(account2.accountIndex).toBe(2)
+    expect(account2.totalBalance).toBe('2.22222222')
+    expect(account2.availableBalance).toBe('2.11111111')
+    expect(account2.calculatedAvailableBalanceInSats).toBe(211111111)
+    expect(account2.calculatedFrozenOrLockedBalanceInSats).toBe(11111111)
+
+    const account3 = accountSummaryByIndex(3)
+    expect(account3.accountIndex).toBe(3)
+    expect(account3.totalBalance).toBe('3.33333333')
+    expect(account3.availableBalance).toBe('0.00000000')
+    expect(account3.calculatedAvailableBalanceInSats).toBe(0)
+    expect(account3.calculatedFrozenOrLockedBalanceInSats).toBe(333333333)
+  })
+})
diff --git a/src/setupTests.js b/src/setupTests.js
index 2a29d31..234a1f0 100644
--- a/src/setupTests.js
+++ b/src/setupTests.js
@@ -34,3 +34,19 @@ global.__DEV__.addToAppSettings = () => {
     global.__DEV__.JM_WEBSOCKET_SERVER_MOCK.close()
   })
 })()
+
+beforeAll(() => {
+  Object.defineProperty(window, 'matchMedia', {
+    writable: true,
+    value: (query) => ({
+      matches: false,
+      media: query,
+      onchange: null,
+      addListener: jest.fn(), // Deprecated, still needed by @restart/hooks (last check: 2022-05-25)
+      removeListener: jest.fn(), // Deprecated
+      addEventListener: jest.fn(),
+      removeEventListener: jest.fn(),
+      dispatchEvent: jest.fn(),
+    }),
+  })
+})
diff --git a/src/testUtils.jsx b/src/testUtils.tsx
similarity index 76%
rename from src/testUtils.jsx
rename to src/testUtils.tsx
index 5f6cc98..3ef140e 100644
--- a/src/testUtils.jsx
+++ b/src/testUtils.tsx
@@ -1,14 +1,16 @@
 import React from 'react'
-import { render } from '@testing-library/react'
+import { render, RenderOptions } from '@testing-library/react'
 import { BrowserRouter } from 'react-router-dom'
 import { I18nextProvider } from 'react-i18next'
-import { SettingsProvider } from './context/SettingsContext'
 import { WalletProvider } from './context/WalletContext'
-import { WebsocketProvider } from './context/WebsocketContext'
 import { ServiceInfoProvider } from './context/ServiceInfoContext'
+// @ts-ignore
+import { SettingsProvider } from './context/SettingsContext'
+// @ts-ignore
+import { WebsocketProvider } from './context/WebsocketContext'
 import i18n from './i18n/testConfig'
 
-const AllTheProviders = ({ children }) => {
+const AllTheProviders = ({ children }: { children: React.ReactElement }) => {
   return (
     <React.StrictMode>
       <BrowserRouter>
@@ -26,7 +28,8 @@ const AllTheProviders = ({ children }) => {
   )
 }
 
-const customRender = (ui, options) => render(ui, { wrapper: AllTheProviders, ...options })
+const customRender = (ui: React.ReactElement, options?: RenderOptions) =>
+  render(ui, { wrapper: AllTheProviders, ...options })
 
 // re-export everything
 export * from '@testing-library/react'