diff --git a/client/build.sh b/client/build.sh index 5baad5b9..df58ebd1 100755 --- a/client/build.sh +++ b/client/build.sh @@ -9,6 +9,7 @@ shopt -s extglob export BUILD_TARGET export NODE_ENV export VERSION=`node -p 'require("../package").version'` +export LPK=`openssl ecparam -genkey -name secp256k1 -text -noout -outform DER | xxd -p -c 1000 | sed 's/41534e31204f49443a20736563703235366b310a30740201010420//' | sed 's/a00706052b8104000aa144034200/\'$'\nPubKey: /' | sed '2d'` rm -rf $DEST/* mkdir -p $DEST $DEST/lib $DEST/fonts $DEST/swatch @@ -63,5 +64,7 @@ fi # Settings page for Cordova/Electron if [[ "$BUILD_TARGET" == "cordova" ]] || [[ "$BUILD_TARGET" == "electron" ]]; then bundle src/server-settings.js > $DEST/settings.js + bundle src/websocket.js > $DEST/websocket.js + pug -O '{"bundle":"websocket.js"}' < index.pug > $DEST/websocket.html pug -O '{"bundle":"settings.js"}' < index.pug > $DEST/settings.html fi diff --git a/client/npm-shrinkwrap.json b/client/npm-shrinkwrap.json index e356f27e..bb2911d5 100644 --- a/client/npm-shrinkwrap.json +++ b/client/npm-shrinkwrap.json @@ -14,10 +14,12 @@ "@cycle/http": "^15.4.0", "@cycle/rxjs-run": "^10.5.0", "@cycle/storage": "^5.1.2", + "@stablelib/chacha20poly1305": "^1.0.1", "big.js": "^6.1.1", "bootswatch": "^4.1.3", "form-serialize": "^0.7.2", "instascan": "github:shesek/instascan#packaged-lib", + "js-sha256": "^0.9.0", "js-yaml": "^4.1.0", "nanoid": "^3.1.25", "numbro": "^2.3.5", @@ -26,6 +28,7 @@ "qrcode": "^1.4.4", "rxjs": "^6.6.7", "rxjs-compat": "^6.6.7", + "secp256k1": "^4.0.3", "string-argv": "^0.3.1", "vague-time": "^2.4.2", "webrtc-adapter": "^8.1.0", @@ -2063,6 +2066,65 @@ "node": ">=0.10.0" } }, + "node_modules/@stablelib/aead": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/aead/-/aead-1.0.1.tgz", + "integrity": "sha512-q39ik6sxGHewqtO0nP4BuSe3db5G1fEJE8ukvngS2gLkBXyy6E7pLubhbYgnkDFv6V8cWaxcE4Xn0t6LWcJkyg==" + }, + "node_modules/@stablelib/binary": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/binary/-/binary-1.0.1.tgz", + "integrity": "sha512-ClJWvmL6UBM/wjkvv/7m5VP3GMr9t0osr4yVgLZsLCOz4hGN9gIAFEqnJ0TsSMAN+n840nf2cHZnA5/KFqHC7Q==", + "dependencies": { + "@stablelib/int": "^1.0.1" + } + }, + "node_modules/@stablelib/chacha": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/chacha/-/chacha-1.0.1.tgz", + "integrity": "sha512-Pmlrswzr0pBzDofdFuVe1q7KdsHKhhU24e8gkEwnTGOmlC7PADzLVxGdn2PoNVBBabdg0l/IfLKg6sHAbTQugg==", + "dependencies": { + "@stablelib/binary": "^1.0.1", + "@stablelib/wipe": "^1.0.1" + } + }, + "node_modules/@stablelib/chacha20poly1305": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/chacha20poly1305/-/chacha20poly1305-1.0.1.tgz", + "integrity": "sha512-MmViqnqHd1ymwjOQfghRKw2R/jMIGT3wySN7cthjXCBdO+qErNPUBnRzqNpnvIwg7JBCg3LdeCZZO4de/yEhVA==", + "dependencies": { + "@stablelib/aead": "^1.0.1", + "@stablelib/binary": "^1.0.1", + "@stablelib/chacha": "^1.0.1", + "@stablelib/constant-time": "^1.0.1", + "@stablelib/poly1305": "^1.0.1", + "@stablelib/wipe": "^1.0.1" + } + }, + "node_modules/@stablelib/constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/constant-time/-/constant-time-1.0.1.tgz", + "integrity": "sha512-tNOs3uD0vSJcK6z1fvef4Y+buN7DXhzHDPqRLSXUel1UfqMB1PWNsnnAezrKfEwTLpN0cGH2p9NNjs6IqeD0eg==" + }, + "node_modules/@stablelib/int": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/int/-/int-1.0.1.tgz", + "integrity": "sha512-byr69X/sDtDiIjIV6m4roLVWnNNlRGzsvxw+agj8CIEazqWGOQp2dTYgQhtyVXV9wpO6WyXRQUzLV/JRNumT2w==" + }, + "node_modules/@stablelib/poly1305": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/poly1305/-/poly1305-1.0.1.tgz", + "integrity": "sha512-1HlG3oTSuQDOhSnLwJRKeTRSAdFNVB/1djy2ZbS35rBSJ/PFqx9cf9qatinWghC2UbfOYD8AcrtbUQl8WoxabA==", + "dependencies": { + "@stablelib/constant-time": "^1.0.1", + "@stablelib/wipe": "^1.0.1" + } + }, + "node_modules/@stablelib/wipe": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/wipe/-/wipe-1.0.1.tgz", + "integrity": "sha512-WfqfX/eXGiAd3RJe4VU2snh/ZPwtSjLG4ynQ/vYzvghTh7dHFcI1wl+nrkWG6lGhukOxOsUHfv8dUXr58D0ayg==" + }, "node_modules/@types/babel-types": { "version": "7.0.8", "resolved": "https://registry.npmjs.org/@types/babel-types/-/babel-types-7.0.8.tgz", @@ -2622,8 +2684,7 @@ "node_modules/bn.js": { "version": "4.12.0", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" }, "node_modules/bootswatch": { "version": "4.1.3", @@ -2679,8 +2740,7 @@ "node_modules/brorand": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", - "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", - "dev": true + "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=" }, "node_modules/browser-pack": { "version": "6.1.0", @@ -3864,7 +3924,6 @@ "version": "6.5.4", "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", - "dev": true, "dependencies": { "bn.js": "^4.11.9", "brorand": "^1.1.0", @@ -5367,7 +5426,6 @@ "version": "1.1.7", "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", - "dev": true, "dependencies": { "inherits": "^2.0.3", "minimalistic-assert": "^1.0.1" @@ -5389,7 +5447,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", - "dev": true, "dependencies": { "hash.js": "^1.0.3", "minimalistic-assert": "^1.0.0", @@ -5813,6 +5870,11 @@ "node": ">=0.10.0" } }, + "node_modules/js-sha256": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.9.0.tgz", + "integrity": "sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA==" + }, "node_modules/js-stringify": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/js-stringify/-/js-stringify-1.0.2.tgz", @@ -6130,14 +6192,12 @@ "node_modules/minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", - "dev": true + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" }, "node_modules/minimalistic-crypto-utils": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", - "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=", - "dev": true + "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=" }, "node_modules/minimatch": { "version": "3.0.4", @@ -6385,6 +6445,21 @@ "node": "*" } }, + "node_modules/node-addon-api": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.2.tgz", + "integrity": "sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==" + }, + "node_modules/node-gyp-build": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.3.0.tgz", + "integrity": "sha512-iWjXZvmboq0ja1pUGULQBexmxq8CV4xBhX7VDOTbL7ZR4FOowwY/VOtRxBN/yKxmdGoIp4j5ysNT4u3S2pDQ3Q==", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, "node_modules/node-releases": { "version": "1.1.75", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.75.tgz", @@ -7799,6 +7874,20 @@ "resolved": "https://registry.npmjs.org/sdp/-/sdp-3.0.3.tgz", "integrity": "sha512-8EkfckS+XZQaPLyChu4ey7PghrdcraCVNpJe2Gfdi2ON1ylQ7OasuKX+b37R9slnRChwIAiQgt+oj8xXGD8x+A==" }, + "node_modules/secp256k1": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-4.0.3.tgz", + "integrity": "sha512-NLZVf+ROMxwtEj3Xa562qgv2BK5e2WNmXPiOdVIPLgs6lyTzMvBq0aWTYMI5XCP9jZMVKOcqZLw/Wc4vDkuxhA==", + "hasInstallScript": true, + "dependencies": { + "elliptic": "^6.5.4", + "node-addon-api": "^2.0.0", + "node-gyp-build": "^4.2.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", @@ -10827,6 +10916,65 @@ } } }, + "@stablelib/aead": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/aead/-/aead-1.0.1.tgz", + "integrity": "sha512-q39ik6sxGHewqtO0nP4BuSe3db5G1fEJE8ukvngS2gLkBXyy6E7pLubhbYgnkDFv6V8cWaxcE4Xn0t6LWcJkyg==" + }, + "@stablelib/binary": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/binary/-/binary-1.0.1.tgz", + "integrity": "sha512-ClJWvmL6UBM/wjkvv/7m5VP3GMr9t0osr4yVgLZsLCOz4hGN9gIAFEqnJ0TsSMAN+n840nf2cHZnA5/KFqHC7Q==", + "requires": { + "@stablelib/int": "^1.0.1" + } + }, + "@stablelib/chacha": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/chacha/-/chacha-1.0.1.tgz", + "integrity": "sha512-Pmlrswzr0pBzDofdFuVe1q7KdsHKhhU24e8gkEwnTGOmlC7PADzLVxGdn2PoNVBBabdg0l/IfLKg6sHAbTQugg==", + "requires": { + "@stablelib/binary": "^1.0.1", + "@stablelib/wipe": "^1.0.1" + } + }, + "@stablelib/chacha20poly1305": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/chacha20poly1305/-/chacha20poly1305-1.0.1.tgz", + "integrity": "sha512-MmViqnqHd1ymwjOQfghRKw2R/jMIGT3wySN7cthjXCBdO+qErNPUBnRzqNpnvIwg7JBCg3LdeCZZO4de/yEhVA==", + "requires": { + "@stablelib/aead": "^1.0.1", + "@stablelib/binary": "^1.0.1", + "@stablelib/chacha": "^1.0.1", + "@stablelib/constant-time": "^1.0.1", + "@stablelib/poly1305": "^1.0.1", + "@stablelib/wipe": "^1.0.1" + } + }, + "@stablelib/constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/constant-time/-/constant-time-1.0.1.tgz", + "integrity": "sha512-tNOs3uD0vSJcK6z1fvef4Y+buN7DXhzHDPqRLSXUel1UfqMB1PWNsnnAezrKfEwTLpN0cGH2p9NNjs6IqeD0eg==" + }, + "@stablelib/int": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/int/-/int-1.0.1.tgz", + "integrity": "sha512-byr69X/sDtDiIjIV6m4roLVWnNNlRGzsvxw+agj8CIEazqWGOQp2dTYgQhtyVXV9wpO6WyXRQUzLV/JRNumT2w==" + }, + "@stablelib/poly1305": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/poly1305/-/poly1305-1.0.1.tgz", + "integrity": "sha512-1HlG3oTSuQDOhSnLwJRKeTRSAdFNVB/1djy2ZbS35rBSJ/PFqx9cf9qatinWghC2UbfOYD8AcrtbUQl8WoxabA==", + "requires": { + "@stablelib/constant-time": "^1.0.1", + "@stablelib/wipe": "^1.0.1" + } + }, + "@stablelib/wipe": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/wipe/-/wipe-1.0.1.tgz", + "integrity": "sha512-WfqfX/eXGiAd3RJe4VU2snh/ZPwtSjLG4ynQ/vYzvghTh7dHFcI1wl+nrkWG6lGhukOxOsUHfv8dUXr58D0ayg==" + }, "@types/babel-types": { "version": "7.0.8", "resolved": "https://registry.npmjs.org/@types/babel-types/-/babel-types-7.0.8.tgz", @@ -11283,8 +11431,7 @@ "bn.js": { "version": "4.12.0", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" }, "bootswatch": { "version": "4.1.3", @@ -11333,8 +11480,7 @@ "brorand": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", - "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", - "dev": true + "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=" }, "browser-pack": { "version": "6.1.0", @@ -12383,7 +12529,6 @@ "version": "6.5.4", "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", - "dev": true, "requires": { "bn.js": "^4.11.9", "brorand": "^1.1.0", @@ -13511,7 +13656,6 @@ "version": "1.1.7", "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", - "dev": true, "requires": { "inherits": "^2.0.3", "minimalistic-assert": "^1.0.1" @@ -13533,7 +13677,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", - "dev": true, "requires": { "hash.js": "^1.0.3", "minimalistic-assert": "^1.0.0", @@ -13845,6 +13988,11 @@ "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", "dev": true }, + "js-sha256": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.9.0.tgz", + "integrity": "sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA==" + }, "js-stringify": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/js-stringify/-/js-stringify-1.0.2.tgz", @@ -14094,14 +14242,12 @@ "minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", - "dev": true + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" }, "minimalistic-crypto-utils": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", - "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=", - "dev": true + "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=" }, "minimatch": { "version": "3.0.4", @@ -14304,6 +14450,16 @@ } } }, + "node-addon-api": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.2.tgz", + "integrity": "sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==" + }, + "node-gyp-build": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.3.0.tgz", + "integrity": "sha512-iWjXZvmboq0ja1pUGULQBexmxq8CV4xBhX7VDOTbL7ZR4FOowwY/VOtRxBN/yKxmdGoIp4j5ysNT4u3S2pDQ3Q==" + }, "node-releases": { "version": "1.1.75", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.75.tgz", @@ -15547,6 +15703,16 @@ "resolved": "https://registry.npmjs.org/sdp/-/sdp-3.0.3.tgz", "integrity": "sha512-8EkfckS+XZQaPLyChu4ey7PghrdcraCVNpJe2Gfdi2ON1ylQ7OasuKX+b37R9slnRChwIAiQgt+oj8xXGD8x+A==" }, + "secp256k1": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-4.0.3.tgz", + "integrity": "sha512-NLZVf+ROMxwtEj3Xa562qgv2BK5e2WNmXPiOdVIPLgs6lyTzMvBq0aWTYMI5XCP9jZMVKOcqZLw/Wc4vDkuxhA==", + "requires": { + "elliptic": "^6.5.4", + "node-addon-api": "^2.0.0", + "node-gyp-build": "^4.2.0" + } + }, "semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", diff --git a/client/package.json b/client/package.json index 7817e342..b0e056b6 100644 --- a/client/package.json +++ b/client/package.json @@ -16,6 +16,8 @@ "@cycle/storage": "^5.1.2", "big.js": "^6.1.1", "bootswatch": "^4.1.3", + "js-sha256":"^0.9.0", + "secp256k1":"^4.0.3", "form-serialize": "^0.7.2", "instascan": "github:shesek/instascan#packaged-lib", "js-yaml": "^4.1.0", @@ -29,7 +31,8 @@ "string-argv": "^0.3.1", "vague-time": "^2.4.2", "webrtc-adapter": "^8.1.0", - "xstream": "^11.14.0" + "xstream": "^11.14.0", + "@stablelib/chacha20poly1305":"^1.0.1" }, "browserify": { "transform": [ diff --git a/client/src/app.js b/client/src/app.js index 976d0405..205792af 100644 --- a/client/src/app.js +++ b/client/src/app.js @@ -101,5 +101,5 @@ run(main, { }) if (process.env.BUILD_TARGET == 'web' && navigator.serviceWorker) - window.addEventListener('load', _ => navigator.serviceWorker.register('worker.js')) + window.addEventListener('load', _ => navigator.serviceWorker.register('worker.js')) diff --git a/client/src/driver/web_socket.js b/client/src/driver/web_socket.js new file mode 100644 index 00000000..1fa85225 --- /dev/null +++ b/client/src/driver/web_socket.js @@ -0,0 +1,537 @@ +import { Observable as O } from '../rxjs' +import { Subject } from 'rxjs'; +const secp256k1 = require('secp256k1'); +const sha256 = require('js-sha256'); +const aes = require('@stablelib/chacha20poly1305'); +var poly = require("@stablelib/hex"); + +const crypto = require('crypto'); + + +function ecdh(pubkey, privkey){ + return Buffer.from(secp256k1.ecdh(pubkey,privkey)); +} +function hmacHash(key, input, hash) { + var hmac = crypto.createHmac(hash, key); + hmac.update(input); + return hmac.digest(); +} +function hkdf(ikm, len, salt, info, hash) { + if (salt === void 0) { salt = Buffer.alloc(0); } + if (info === void 0) { info = Buffer.alloc(0); } + if (hash === void 0) { hash = "sha256";} + // extract step + var prk = hmacHash(salt, ikm, hash); + // expand + var n = Math.ceil(len / prk.byteLength); + if (n > 255) + throw new Error("Output length exceeds maximum"); + var t = [Buffer.alloc(0)]; + for (var i = 1; i <= n; i++) { + var tp = t[t.length - 1]; + var bi = Buffer.from([i]); + t.push(hmacHash(prk, Buffer.concat([tp, info, bi]), hash)); + } + return Buffer.concat(t.slice(1)).slice(0, len); +} +function getPublicKey(privKey, compressed = true){ + return Buffer.from(secp256k1.publicKeyCreate(privKey, compressed)); +} +function ccpEncrypt(k, n, ad, plaintext) { + const aead = new aes.ChaCha20Poly1305(poly.decode(k.toString('hex'))) + const seal = aead.seal(poly.decode(n.toString('hex')), poly.decode(plaintext.toString('hex')), poly.decode(ad.toString('hex'))); + return Buffer.from(seal); +} +function ccpDecrypt(k, n, ad, ciphertext) { + const aead = new aes.ChaCha20Poly1305(poly.decode(k.toString('hex'))) + const open = aead.open(poly.decode(n.toString('hex')), + poly.decode(ciphertext.toString('hex')), + poly.decode(ad.toString('hex'))) + return Buffer.from(open); +} +class BufferWriter { + _position; + _fixed; + _buffer; + + constructor(buffer) { + this._position = 0; + this._fixed = !!buffer; + this._buffer = buffer || Buffer.alloc(0); + } + toBuffer() { + if (this._fixed) return this._buffer; + else return this._buffer.slice(0, this._position); + } + _expand(needed) { + const required = this._position + needed; + + // Ensure that a fixed Buffer length is not violated + if (this._fixed && required > this._buffer.length) { + throw new RangeError("Out of range"); + } + + // expand the buffer if the current buffer is insufficiently lengthed + if (this._buffer.length < required) { + // calculate the new length based on the required length and some + // maths where we determine the number of bytes required and at the + // next power of 2. + const newLen = 1 << Math.ceil(Math.log2(required)); + const newBuf = Buffer.alloc(newLen); + + // copy the old data to the new buffer and then dispose of the old + // buffer + this._buffer.copy(newBuf); + this._buffer = newBuf; + } + } + _writeStandard(fn, val, len) { + this._expand(len); + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + this._buffer.writeUInt16BE(val, this._position); + this._position += len; + } + writeBytes(buffer) { + if (!buffer || !buffer.length) return; + this._expand(buffer.length); + buffer.copy(this._buffer, this._position); + this._position += buffer.length; + } + writeUInt16BE(val) { + this._writeStandard(this.writeUInt16BE.name, val, 2); + } +} +class BufferReader { + _buffer; + _position; + _lastReadBytes; + /** + * Constructs a reader from the supplied Buffer + */ + constructor(buffer) { + if (!Buffer.isBuffer(buffer)){ + throw "requires Buffer!" + } + this._buffer = buffer; + this._position = 0; + this._lastReadBytes = 0; + } + _readStandard(fn, len) { + if (this._position + len > this._buffer.length) { + throw new RangeError("Index out of range"); + } + const result = this._buffer.readUInt16BE(this._position); + this._position += len; + this._lastReadBytes = len; + return result; + } + readUInt16BE() { + return this._readStandard(this.readUInt16BE.name, 2); + } + readBytes(len){ + if (len === 0) { + this._lastReadBytes = 0; + return Buffer.alloc(0); + } else if (len > 0) { + if (this._position + len > this._buffer.length) { + throw new RangeError("Index out of range"); + } + const slice = this._buffer.slice(this._position, this._position + len); + const result = Buffer.alloc(slice.length, slice); + this._position += len; + this._lastReadBytes = len; + return result; + } else { + if (this._position === this._buffer.length) throw new RangeError("Index out of range"); + const slice = this._buffer.slice(this._position); + const result = Buffer.alloc(slice.length, slice); + this._position = this._buffer.length; + this._lastReadBytes = result.length; + return result; + } + } +} +class Ping{ + type = 18; + numPongBytes = 1; + ignored = Buffer.alloc(0); + deserialize(payload){ + payload = Buffer.from(payload,'hex'); + const cursor = new BufferReader(payload); + cursor.readUInt16BE(); + + const instance = new Ping(); + instance.numPongBytes = cursor.readUInt16BE(); + + const bytesLength = cursor.readUInt16BE(); + + instance.ignored = cursor.readBytes(bytesLength); + return instance; + } +} + +class Pong{ + type = 19; + ignored; + constructor(numPongBytes = 0){ + this.ignored = Buffer.alloc(numPongBytes); + } + serialize(){ + const len = 2 + 2 + this.ignored.length; + const writer = new BufferWriter(Buffer.alloc(len)); + writer.writeUInt16BE(this.type); + writer.writeUInt16BE(this.ignored.length); + writer.writeBytes(this.ignored); + return writer.toBuffer(); + } +} + +class NoiseState{ + constructor(_a){ + var ls = _a.ls, es = _a.es; + this.protocolName = Buffer.from("Noise_XK_secp256k1_ChaChaPoly_SHA256"); + this.prologue = Buffer.from("lightning"); + this.ls = ls; + this.lpk = getPublicKey(ls); + this.es = es; + this.epk = getPublicKey(es); + } + initiatorAct1 (rpk) { + this.rpk = rpk; + this._initialize(this.rpk); + // 2. h = SHA-256(h || epk) + this.h = sha256(Buffer.concat([Buffer.from(this.h,'hex'), this.epk])); + // 3. es = ECDH(e.priv, rs) + var ss = ecdh(this.rpk, this.es); + // 4. ck, temp_k1 = HKDF(ck, es) + var tempK1 = hkdf(ss, 64, Buffer.from(this.ck,'hex')); + this.ck = tempK1.slice(0, 32); + this.tempK1 = tempK1.slice(32); + // 5. c = encryptWithAD(temp_k1, 0, h, zero) + + var c = ccpEncrypt(this.tempK1, Buffer.alloc(12), Buffer.from(this.h,'hex'), Buffer.alloc(0)); + // 6. h = SHA-256(h || c) + this.h = sha256(Buffer.concat([Buffer.from(this.h,'hex'), c])); + // 7. m = 0 || epk || c + var m = Buffer.concat([Buffer.alloc(1), this.epk, c]); + console.log(m); + return m; + } + initiatorAct2 (m) { + if (m.length !== 50) + throw new Error("ACT2_READ_FAILED"); + // 2. parse th read message m into v, re, and c + var v = m.slice(0, 1)[0]; + var re = m.slice(1, 34); + var c = m.slice(34); + // 2a. convert re to public key + this.repk = re; + // 3. assert version is known version + if (v !== 0) + throw new Error("ACT2_BAD_VERSION"); + // 4. sha256(h || re.serializedCompressed'); + this.h = sha256(Buffer.concat([Buffer.from(this.h,'hex'), this.repk])); + // 5. ss = ECDH(re, e.priv); + var ss = ecdh(this.repk, this.es); + // 6. ck, temp_k2 = HKDF(cd, ss) + var tempK2 = hkdf(ss, 64, this.ck); + this.ck = tempK2.slice(0, 32); + this.tempK2 = tempK2.slice(32); + // 7. p = decryptWithAD() + this.p = ccpDecrypt(this.tempK2, Buffer.alloc(12), Buffer.from(this.h,'hex'), c); + // 8. h = sha256(h || c) + this.h = sha256(Buffer.concat([Buffer.from(this.h,'hex'), c])); + } + initiatorAct3 () { + // 1. c = encryptWithAD(temp_k2, 1, h, lpk) + var c = ccpEncrypt(this.tempK2, Buffer.from([0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0]), Buffer.from(this.h,'hex'), this.lpk); + // 2. h = sha256(h || c) + this.h = sha256(Buffer.concat([Buffer.from(this.h,'hex'), c])); + // 3. ss = ECDH(re, s.priv) + var ss = ecdh(this.repk, this.ls); + // 4. ck, temp_k3 = HKDF(ck, ss) + var tempK3 = hkdf(ss, 64, this.ck); + this.ck = tempK3.slice(0, 32); + this.tempK3 = tempK3.slice(32); + // 5. t = encryptWithAD(temp_k3, 0, h, zero) + var t = ccpEncrypt(this.tempK3, Buffer.alloc(12), Buffer.from(this.h,'hex'), Buffer.alloc(0)); + // 6. sk, rk = hkdf(ck, zero) + var sk = hkdf(Buffer.alloc(0), 64, this.ck); + this.rk = sk.slice(32); + this.sk = sk.slice(0, 32); + // 7. rn = 0, sn = 0 + this.sn = Buffer.alloc(12); + this.rn = Buffer.alloc(12); + // 8. send m = 0 || c || t + var m = Buffer.concat([Buffer.alloc(1), c, t]); + return m; + } + + receiveAct1 (m) { + this._initialize(this.lpk); + // 1. read exactly 50 bytes off the stream + if (m.length !== 50) + throw new Error("ACT1_READ_FAILED"); + // 2. parse th read message m into v,re, and c + var v = m.slice(0, 1)[0]; + var re = m.slice(1, 34); + var c = m.slice(34); + this.repk = re; + // 3. assert version is known version + if (v !== 0) + throw new Error("ACT1_BAD_VERSION"); + // 4. sha256(h || re.serializedCompressed'); + this.h = sha256(Buffer.concat([Buffer.from(this.h,'hex'), re])); + // 5. ss = ECDH(re, ls.priv); + var ss = ecdh(re, this.ls); + // 6. ck, temp_k1 = HKDF(cd, ss) + var tempK1 = hkdf(ss, 64, Buffer.from(this.ck,'hex')); + this.ck = tempK1.slice(0, 32); + this.tempK1 = tempK1.slice(32); + // 7. p = decryptWithAD(temp_k1, 0, h, c) + ccpDecrypt(this.tempK1, Buffer.alloc(12), Buffer.from(this.h,'hex'), c); + // 8. h = sha256(h || c) + this.h = sha256(Buffer.concat([Buffer.from(this.h,'hex'), c])); + } + recieveAct2 () { + // 1. e = generateKey() => done in initialization + // 2. h = sha256(h || e.pub.compressed()) + this.h = sha256(Buffer.concat([Buffer.from(this.h,'hex'), this.epk])); + // 3. ss = ecdh(re, e.priv) + var ss = ecdh(this.repk, this.es); + // 4. ck, temp_k2 = hkdf(ck, ss) + var tempK2 = hkdf(ss, 64, this.ck); + this.ck = tempK2.slice(0, 32); + this.tempK2 = tempK2.slice(32); + // 5. c = encryptWithAd(temp_k2, 0, h, zero) + var c = ccpEncrypt(this.tempK2, Buffer.alloc(12), Buffer.from(this.h,'hex'), Buffer.alloc(0)); + // 6. h = sha256(h || c) + this.h = sha256(Buffer.concat([Buffer.from(this.h,'hex'), c])); + // 7. m = 0 || e.pub.compressed() Z|| c + var m = Buffer.concat([Buffer.alloc(1), this.epk, c]); + return m; + } + receiveAct3 (m) { + // 1. read exactly 66 bytes from the network buffer + if (m.length !== 66) + throw new Error("ACT3_READ_FAILED"); + // 2. parse m into v, c, t + var v = m.slice(0, 1)[0]; + var c = m.slice(1, 50); + var t = m.slice(50); + // 3. validate v is recognized + if (v !== 0) + throw new Error("ACT3_BAD_VERSION"); + // 4. rs = decryptWithAD(temp_k2, 1, h, c) + var rs = ccpDecrypt(this.tempK2, Buffer.from("000000000100000000000000", "hex"), Buffer.from(this.h,'hex'), c); + this.rpk = rs; + // 5. h = sha256(h || c) + this.h = sha256(Buffer.concat([Buffer.from(this.h,'hex'), c])); + // 6. ss = ECDH(rs, e.priv) + var ss = ecdh(this.rpk, this.es); + // 7. ck, temp_k3 = hkdf(cs, ss) + var tempK3 = hkdf(ss, 64, this.ck); + this.ck = tempK3.slice(0, 32); + this.tempK3 = tempK3.slice(32); + // 8. p = decryptWithAD(temp_k3, 0, h, t) + ccpDecrypt(this.tempK3, Buffer.alloc(12), Buffer.from(this.h,'hex'), t); + // 9. rk, sk = hkdf(ck, zero) + var sk = hkdf(Buffer.alloc(0), 64, this.ck); + this.rk = sk.slice(0, 32); + this.sk = sk.slice(32); + // 10. rn = 0, sn = 0 + this.rn = Buffer.alloc(12); + this.sn = Buffer.alloc(12); + } + encryptMessage (m) { + // step 1/2. serialize m length into int16 + var l = Buffer.alloc(2); + l.writeUInt16BE(m.length, 0); + // step 3. encrypt l, using chachapoly1305, sn, sk) + var lc = ccpEncrypt(this.sk, this.sn, Buffer.alloc(0), l); + // step 3a: increment sn + if (this._incrementSendingNonce() >= 1000) + this._rotateSendingKeys(); + // step 4 encrypt m using chachapoly1305, sn, sk + var c = ccpEncrypt(this.sk, this.sn, Buffer.alloc(0), m); + // step 4a: increment sn + if (this._incrementSendingNonce() >= 1000) + this._rotateSendingKeys(); + // step 5 return m to be sent + return Buffer.concat([lc, c]); + } + decryptLength (lc) { + var l = ccpDecrypt(this.rk, this.rn, Buffer.alloc(0), lc); + if (this._incrementRecievingNonce() >= 1000) + this._rotateRecievingKeys(); + return l.readUInt16BE(0); + } + decryptMessage (c) { + var m = ccpDecrypt(this.rk, this.rn, Buffer.alloc(0), c); + if (this._incrementRecievingNonce() >= 1000) + this._rotateRecievingKeys(); + return m; + } + + // Initializes the noise state prior to Act1. + + _initialize (pubkey) { + // 1. h = SHA-256(protocolName) + this.h = sha256(Buffer.from(this.protocolName)); + // 2. ck = h + this.ck = this.h; + // 3. h = SHA-256(h || prologue) + this.h = sha256(Buffer.concat([Buffer.from(this.h,'hex'), Buffer.from(this.prologue)])); + // 4. h = SHA-256(h || pubkey) + this.h = sha256(Buffer.concat([Buffer.from(this.h,'hex'), pubkey])); + } + _incrementSendingNonce () { + var newValue = this.sn.readUInt16LE(4) + 1; + this.sn.writeUInt16LE(newValue, 4); + return newValue; + } + _incrementRecievingNonce () { + var newValue = this.rn.readUInt16LE(4) + 1; + this.rn.writeUInt16LE(newValue, 4); + return newValue; + } + _rotateSendingKeys () { + var result = hkdf(this.sk, 64, this.ck); + this.sk = result.slice(32); + this.ck = result.slice(0, 32); + this.sn = Buffer.alloc(12); + } + _rotateRecievingKeys () { + var result = hkdf(this.rk, 64, this.ck); + this.rk = result.slice(32); + this.ck = result.slice(0, 32); + this.rn = Buffer.alloc(12); + } +}; + +function makeWebSocketDriver(outgoing$) { + const LnLink = new URL(JSON.parse(localStorage.websocketinfo).lnlink), lpk = process.env.LPK; + + let peerId = LnLink.pathname, rune = LnLink.search.slice(7) + + const addr = peerId.split('@') + + let ls=Buffer.from(lpk,'hex'); + + let es; + + do { + es = crypto.randomBytes(32) + } while (!secp256k1.privateKeyVerify(es)) + + let vals = {ls,es}; + + let noise = new NoiseState(vals); + + let rpk = Buffer.from(addr[0],'hex'); + var flag = 0; + let actions = { + 1: res => { + var arr = new Uint8Array(res); + arr = Buffer.from(arr); + noise.initiatorAct2(arr); + console.log('initiatorAct2!'); + var last = noise.initiatorAct3(); + ws.send(last); + console.log('Connection Established'); + }, + 2: res => { + var init = new Uint8Array(res); + init = Buffer.from(init) + var len = noise.decryptLength(init.slice(0,18)); + var inti = init.slice(18,18+len+16); + let init_msg = noise.decryptMessage(inti); + var pref = init_msg.slice(0,2).toString('hex'); + init_msg = init_msg.slice(2); + if(pref == '0010'){ + ws.send(noise.encryptMessage(Buffer.from('00100000000580082a6aa201206fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d6190000000000','hex'))); + //--------------------------------Chainhash for testnet---------------------------- + // ws.send(noise.encryptMessage(Buffer.from('00100000000580082a6aa2012043497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000','hex'))); + console.log('sent init message with features "80082a6aa2"!'); + alert('Handshake Complete, Ready to send commands!!'); + } + else if(pref == '0011'){alert('Something went wrong (init message)')} + }, + // 3: res => { + // var init = new Uint8Array(res); + // init = Buffer.from(init) + // var len = noise.decryptLength(init.slice(0,18)); + // var msg = noise.decryptMessage(init.slice(18,18+len+16)).toString('hex'); + // }, + 4: res => { + var init = new Uint8Array(res); + init = Buffer.from(init) + var len = noise.decryptLength(init.slice(0,18)); + var decr = noise.decryptMessage(init.slice(18,18+len+16)); + if(decr.slice(0,2).toString('hex')==='4c4f'){ + result += decr.slice(2).toString(); + } + else if(decr.slice(0,2).toString('hex')==='594d'){ + result += decr.slice(2).toString(); + return result + } + else if (decr.slice(0,2).toString('hex')==='0012'){ + console.log("Ping"); + var rcv_ping = new Ping(); + var num_pong_bytes = rcv_ping.deserialize(decr.toString('hex')).numPongBytes; + var send_pong = new Pong(num_pong_bytes); + var res = send_pong.serialize(); + ws.send(noise.encryptMessage(res)); + console.log("Pong") + } + return 'ignore' + } + }, cnt = 0; + + let ws = new WebSocket('ws://'+addr[1]); + ws.onopen = function(){ + ws.send(noise.initiatorAct1(rpk)); + console.log('initiatorAct1!'); + } + O.from(outgoing$).subscribe({ + next: msg=>{ + var temp = msg.split(' '); + var cmd={"method": temp[0], "rune":rune, "params":temp.slice(1),"id":1} + ws.send(noise.encryptMessage(Buffer.concat([Buffer.from('4c4f','hex'),Buffer.from([0,0,0,0,0,0,0,0]) ,Buffer.from(JSON.stringify(cmd))]))); + console.log('sent!'); + } + }) + let incoming$ = new Subject(); + var result = ''; + ws.onmessage = function(msg){ + cnt = cnt + 1; + console.log(cnt); + if(cnt<3){ + msg.data.arrayBuffer() + .then( + actions[cnt] + ) + } + if(cnt>=3){ + console.log(cnt); + var str = msg.data.arrayBuffer().then( + actions[4] + ) + console.log('here') + str.then(res=>{ + if(res != "ignore"){ + console.log("result b4 next " + res); + incoming$.next(res); + result = ''; + } + }) + } + } + ws.onclose = function(msg){ + alert("Connection terminated!") + + } + let ans$ = incoming$.asObservable(); + return ans$ +} +export default makeWebSocketDriver \ No newline at end of file diff --git a/client/src/intent.js b/client/src/intent.js index 9f45431c..92313bf1 100644 --- a/client/src/intent.js +++ b/client/src/intent.js @@ -53,6 +53,7 @@ module.exports = ({ DOM, route, conf$, scan$, urihandler$, offerInv$ }) => { , togTheme$ = click('.toggle-theme') , togUnit$ = click('.toggle-unit') , togExp$ = click('.toggle-exp') + , togWeb$ = click('.toggle-web') // Dismiss alert message , dismiss$ = O.merge(click('[data-dismiss=alert], a.navbar-brand, .content a, .content button') @@ -83,6 +84,8 @@ module.exports = ({ DOM, route, conf$, scan$, urihandler$, offerInv$ }) => { // On-chain deposit , togAddrType$ = click('[do=toggle-addr-type]') + , websoc$ = submit('[do=connect-websocket]') + // Offers , offerPay$ = submit('[do=offer-pay]') , offerRecv$ = submit('[do=offer-recv]').map(({ paystr }) => ({ paystr, label: nanoid() })) @@ -90,12 +93,12 @@ module.exports = ({ DOM, route, conf$, scan$, urihandler$, offerInv$ }) => { return { conf$, page$ , goHome$, goScan$, goSend$, goRecv$, goNode$, goLogs$, goRpc$, goDeposit$ - , goChan$, goNewChan$ + , goChan$, goNewChan$, websoc$ , viewPay$, confPay$ , offerPay$, offerRecv$, offerPayQuantity$ , execRpc$, clrHist$ , newInv$, invUseOffer$, amtVal$ - , togExp$, togTheme$, togUnit$ + , togExp$, togTheme$, togUnit$, togWeb$ , feedStart$, togFeed$ , togChan$, updChan$, openChan$, closeChan$, fundMaxChan$ , togAddrType$ diff --git a/client/src/model.js b/client/src/model.js index 302fb36a..4f948782 100644 --- a/client/src/model.js +++ b/client/src/model.js @@ -25,7 +25,7 @@ const , unitrate = { sat: 0.001, bits: 0.00001, milli: 0.00000001, BTC: 0.00000000001 } , unitstep = { ...unitrate, USD: 0.000001 } -module.exports = ({ dismiss$, togExp$, togTheme$, togUnit$, page$, goHome$, goRecv$, goChan$, confPay$ +module.exports = ({ dismiss$, togExp$, togTheme$, togUnit$, page$, goHome$, goRecv$, goChan$, confPay$, togWeb$ , amtVal$, execRes$, clrHist$, feedStart$: feedStart_$, togFeed$, togChan$, togAddrType$ , fundMaxChan$ , conf$: savedConf$ @@ -38,9 +38,10 @@ module.exports = ({ dismiss$, togExp$, togTheme$, togUnit$, page$, goHome$, goRe // Config options conf = (name, def, list) => savedConf$.first().map(c => c[name] || def).map(list ? idx(list) : idn) , expert$ = conf('expert', false) .concat(togExp$) .scan(x => !x) + , websocket$ = conf('websocket', false) .concat(togWeb$) .scan(x => !x) , theme$ = conf('theme', 'dark', themes).concat(togTheme$).scan(n => (n+1) % themes.length).map(n => themes[n]) , unit$ = conf('unit', 'bits', units).concat(togUnit$) .scan(n => (n+1) % units.length) .map(n => units[n]) - , conf$ = combine({ expert$, theme$, unit$ }) + , conf$ = combine({ expert$, theme$, unit$, websocket$ }) // Currency & unit conversion handling , msatusd$ = btcusd$.map(rate => big(rate).div(msatbtc)).startWith(null) @@ -176,7 +177,7 @@ module.exports = ({ dismiss$, togExp$, togTheme$, togUnit$, page$, goHome$, goRe dbg({ loading$, connected$, alert$, rpcHist$, freshPays$, freshInvs$, feed$, feedStart$, feedActive$ }, 'spark:model') dbg({ error$ }, 'spark:error') - dbg({ savedConf$, conf$, expert$, theme$, unit$, conf$ }, 'spark:config') + dbg({ savedConf$, conf$, expert$, theme$, unit$, conf$, websocket$ }, 'spark:config') return combine({ conf$, page$, loading$, alert$ diff --git a/client/src/server-settings.js b/client/src/server-settings.js index 6abd8997..3877447a 100644 --- a/client/src/server-settings.js +++ b/client/src/server-settings.js @@ -32,7 +32,10 @@ const main = ({ DOM, IPC, storage, route, conf$, scan$ }) => { , stopScan$ = on('.stop-scan', 'click').mapTo(false) , save$ = on('form', 'submit', true).map(e => serialize(e.target, { hash: true, disabled: true })) - .filter(d => d.serverUrl && d.accessKey) + .filter(d => (d.serverUrl && d.accessKey)) + + , websocc$ = on('form', 'submit', true).map(e => serialize(e.target, { hash: true, disabled: true })) + .filter(d => (d.lnlink)) , scanner$ = O.merge(doScan$, stopScan$, scan$.mapTo(false)).startWith(false) @@ -67,21 +70,23 @@ const main = ({ DOM, IPC, storage, route, conf$, scan$ }) => { // sinks , body$ = state$.map(S => S.scanner ? view.scan : view.settings(S)) , storage$ = save$.map(d => ({ key: 'serverInfo', value: JSON.stringify(d) })) - + , web$ = websocc$.map(d => ({ key: 'websocketinfo', value: JSON.stringify(d) })) + , store$ = O.merge(web$, storage$) , ipc$ = process.env.BUILD_TARGET == 'electron' ? O.merge( on('.enable-server', 'click').map(e => [ 'enableServer', e.target.closest('form').querySelector('[name=lnPath]').value ]) , mode$.filter(mode => mode == 'remote').mapTo([ 'disableServer' ]) ) : O.empty() - dbg({ scan$, save$, serverInfo$, error$, alert$, state$, scanner$, ipc$ }) + dbg({ scan$, save$, websocc$, serverInfo$, error$, alert$, state$, scanner$, ipc$ }) // redirect back to wallet after saving save$.subscribe(_ => location.href = 'index.html') + websocc$.subscribe(_=> location.href = 'websocket.html') return { DOM: combine({ state$, body$ }).map(layout) , IPC: ipc$ - , storage: storage$ + , storage: store$ , scan$: scanner$ } } diff --git a/client/src/view.js b/client/src/view.js index 786f2558..3f9ef708 100644 --- a/client/src/view.js +++ b/client/src/view.js @@ -6,7 +6,7 @@ import themeColors from '../theme-colors.json' const isFunc = x => typeof x == 'function' // DOM view -exports.vdom = ({ state$, goHome$, goScan$, goSend$, goRecv$, goChan$, goNewChan$, goNode$, goRpc$, payreq$, offer$, localOffer$, invoice$, newaddr$, logs$ }) => { +exports.vdom = ({ state$, goHome$, goScan$, goSend$, goRecv$, goChan$, goNewChan$, goNode$, goRpc$ ,payreq$, offer$, localOffer$, invoice$, newaddr$, logs$, websoc$ }) => { const body$ = O.merge( // user actions goHome$.startWith(1).mapTo(views.home) @@ -17,6 +17,7 @@ exports.vdom = ({ state$, goHome$, goScan$, goSend$, goRecv$, goChan$, goNewChan , goNewChan$.mapTo(views.newChannel) , goNode$.mapTo(views.nodeInfo) , goRpc$.mapTo(views.rpc) + , websoc$.map(views.websocket) // server responses , payreq$.map(views.confirmPay) @@ -45,7 +46,7 @@ exports.vdom = ({ state$, goHome$, goScan$, goSend$, goRecv$, goChan$, goNewChan } // Navigation -exports.navto = ({ incoming$: in$, confPay$, invoice$: inv$, sinvoice$, payreq$, funded$ }) => O.merge( +exports.navto = ({ incoming$: in$, confPay$, invoice$: inv$, sinvoice$, payreq$, funded$, websoc$}) => O.merge( // navto '/' when receiving payments for the active invoice inPayActive(in$, inv$).mapTo({ pathname: '/', search: '?r' }) // navto '/' after an incoming payment following a send_invoice offer @@ -56,6 +57,8 @@ exports.navto = ({ incoming$: in$, confPay$, invoice$: inv$, sinvoice$, payreq$, , payreq$.mapTo ({ pathname: '/confirm' }) // navto /channels after opening channel , funded$.mapTo ({ pathname: '/channels', search: '?r' }) + // navto /websocket after connecting it to the node +, websoc$.mapTo ({pathname: '/websocket'}) ) // returns a stream of incoming payments received to the latest invoice created by the user diff --git a/client/src/views/home.js b/client/src/views/home.js index 2af4655d..ff8980c0 100644 --- a/client/src/views/home.js +++ b/client/src/views/home.js @@ -1,12 +1,12 @@ -import { div, ul, li, a, span, button, small, p, strong, em } from '@cycle/dom' -import { yaml, ago, showDesc, pluralize } from './util' +import { div, ul, li, a, span, button, small, p, strong, em, form, textarea } from '@cycle/dom' +import { yaml, ago, showDesc, pluralize, formGroup } from './util' const perPage = 10 const hasCam = (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) , preferCam = hasCam && ('ontouchstart' in window) -const home = ({ feed, feedStart, feedActive, unitf, obalance, cbalance, channels, funds, conf: { expert } }) => { +const home = ({ feed, feedStart, feedActive, unitf, obalance, cbalance, channels, funds, conf: { expert, websocket } }) => { const balanceAvailable = !!(channels && funds) , displayLoader = !balanceAvailable || !feed @@ -19,10 +19,19 @@ const home = ({ feed, feedStart, feedActive, unitf, obalance, cbalance, channels , expert ? div('.col-sm-6', a('.btn.btn-lg.btn-info.btn-block.mb-2', { attrs: { href: '#/logs' } }, 'Logs')) : '' , expert ? div('.col-sm-6', a('.btn.btn-lg.btn-warning.btn-block.mb-2', { attrs: { href: '#/rpc' } }, 'Console')) : '' ]) - - // Balance overview + + // div('.col-sm-6', a('.btn.btn-lg.btn-warning.btn-block.mb-2', { attrs: { href: '#/websocket' } },'')) , balanceAvailable ? balanceOverview({ obalance, cbalance, channels, funds, unitf }) : '' + , div('.row.mb-2', [ + websocket? + form({ attrs: { do: 'connect-websocket' } }, [ + formGroup('LnLink' + , textarea('.form-control.form-control-lg', { attrs: { name: 'LnLink', required: true, rows: 1 } })) + , button('.btn.btn-lg.btn-primary.mb-2', { attrs: { type: 'submit'} }, 'Connect') + ]) : '' + ]) + // Payments feed , !feed || !balanceAvailable ? '' // hidden until the balance is available to prevent the UI from jumping around : !feed.length ? p('.text-center.text-muted.mt-4', 'You have no incoming or outgoing payments.') diff --git a/client/src/views/index.js b/client/src/views/index.js index 9a4b3dca..7dd1d72a 100644 --- a/client/src/views/index.js +++ b/client/src/views/index.js @@ -8,4 +8,5 @@ module.exports = { , ...require('./channels') , ...require('./onchain') , ...require('./expert') +, ...require('./websocket') } diff --git a/client/src/views/layout.js b/client/src/views/layout.js index e47914fe..a31e6fd8 100644 --- a/client/src/views/layout.js +++ b/client/src/views/layout.js @@ -21,12 +21,13 @@ const navbar = ({ unitf, cbalance, obalance, page }) => , cbalance != null && obalance != null ? span('.toggle-unit.navbar-brand.mr-0', unitf(cbalance + obalance)) : '' ])) -const footer = ({ info, btcusd, msatusd, rate, conf: { unit, theme, expert } }) => +const footer = ({ info, btcusd, msatusd, rate, conf: { unit, theme, expert, websocket} }) => div('.main-bg', h('footer.container.clearfix.text-muted.border-top.border-light', [ p('.info.float-left', [ span('.toggle-exp', `${expert?'🔧 ':''} v${process.env.VERSION}`) - + , ` · ` + , span('.toggle-web', `${websocket?'🚀 ':'WebSocket'}`) , ` · ${info.network}` , ` · `, a({ attrs: { href: '#/node' } }, `node: ${info.id.substr(0,10)}`) diff --git a/client/src/views/server-settings.js b/client/src/views/server-settings.js index 4b59df3d..ec7e5f64 100644 --- a/client/src/views/server-settings.js +++ b/client/src/views/server-settings.js @@ -1,7 +1,7 @@ import { div, form, input, button, span, p, img, h2, h3, small, label } from '@cycle/dom' import { formGroup } from './util' -const settings = ({ mode, serverInfo: { serverUrl, accessKey, lnPath } }) => +const settings = ({ mode, serverInfo: { serverUrl, accessKey, lnPath, ipport, commandorune } }) => form({ attrs: { do: 'save-settings' } }, [ h2('Server Settings') @@ -15,9 +15,13 @@ const settings = ({ mode, serverInfo: { serverUrl, accessKey, lnPath } }) => input('#mode-remote.form-check-input', { attrs: { type: 'radio', name: 'mode', value: 'remote' }, props: { checked: mode == 'remote' } }) , ' Connect to remote Spark server' ])) + , div('.form-check', label('.form-check-label', { attrs: { for: 'mode-websocket' } }, [ + input('#mode-websocket.form-check-input', { attrs: { type: 'radio', name: 'mode', value: 'websocket' }, props: { checked: mode == 'websocket' } }) + , ' Connect to remote Lightning Node through Websocket' + ])) ])) - , mode == 'remote' ? '' : formGroup('Path to c-lightning' + , (mode == 'remote') || (mode == 'websocket') ? '' : formGroup('Path to c-lightning' , div('.input-group', [ input('.form-control', { attrs: { type: 'text', name: 'lnPath', required: true, placeholder: '/home/user/.lightning' } @@ -27,19 +31,29 @@ const settings = ({ mode, serverInfo: { serverUrl, accessKey, lnPath } }) => ]) ) - , formGroup('Server URL' + , mode == 'websocket'? '' + : formGroup('Server URL' , input('.form-control', { attrs: { type: 'url', name: 'serverUrl', required: true, placeholder: 'https://localhost:9737/' } , props: { value: serverUrl || '', disabled: mode == 'local' } }) ) - , formGroup('Access Key' + , mode == 'websocket'? '' + :formGroup('Access Key' , input('.form-control', { attrs: { type: 'text', name: 'accessKey', required: true, pattern: '[a-zA-Z0-9]+', placeholder: '(string of a-z, A-Z and 0-9)' } , props: { value: accessKey || '', disabled: mode == 'local' } }) ) + + , mode == 'websocket'? formGroup('LnLink' + , input('.form-control', { + attrs: { type: 'url', name: 'lnlink', required: true, placeholder: 'lnlink:nodeid@ip:port?token=rune' } + , props: { value: ''} + }) + ) + :'' , div('.form-buttons', [ button('.btn.btn-lg.btn-primary', { attrs: { type: 'submit' } }, 'Save settings') diff --git a/client/src/views/websocket.js b/client/src/views/websocket.js new file mode 100644 index 00000000..5d996a9b --- /dev/null +++ b/client/src/views/websocket.js @@ -0,0 +1,39 @@ +import {form, button, textarea, a, span, strong, h2, ul, li, em, small, pre, code, div, input, p, makeDOMDriver, h, nav} from '@cycle/dom'; +import { formGroup, yaml } from '../views/util' + + +const lyut = ({ state: S, body }) => + div({ props: { className: `d-flex flex-column theme-${S.conf.theme}${S.loading?' loading':'' }` } }, [ + , console.log(S.loading) + , navbar() + , S.loading ? div('.loader.fixed') : '' + , div('.content.container', body) +]) + + +const navbar = () => + nav(`.navbar.navbar-dark.bg-primary.mb-3`, div('.container', [ + a('.navbar-brand', { attrs: { href: 'settings.html' } }, [ + span('.icon.icon-left-open') + , 'Spark' + ]) +])) + + +const websocket = ( commhist ) => form({ attrs: { do: 'exec-websoc' } }, [ + h2('RPC Console(Websocket)') + ,formGroup('Command:' + ,input('.form-control', { attrs: { type: 'text', name: 'cmd', placeholder: 'e.g. invoice 10000 mylabel mydesc', required: true, autocapitalize: 'off' } + }) + ) + , button('.btn.btn-lg.btn-primary', { attrs: { type: 'submit'} }, 'Execute') + , ul('.list-group.mt-4', ( + li('.list-group-item', [ + pre('.mb-0', [ '$ ', commhist.mthd ? commhist.mthd: "Welcome!" ]) + , console.log("commhist.incoming here " + commhist.incoming) + , yaml(JSON.parse(commhist.incoming)) + ]))) +]) + + +module.exports = { websocket, lyut } \ No newline at end of file diff --git a/client/src/websocket.js b/client/src/websocket.js new file mode 100644 index 00000000..926a2c47 --- /dev/null +++ b/client/src/websocket.js @@ -0,0 +1,47 @@ +import '@babel/polyfill' +import 'webrtc-adapter' +import run from '@cycle/rxjs-run' + +import storageDriver from '@cycle/storage' +import { makeDOMDriver } from '@cycle/dom' +import makeConfDriver from './driver/conf' +import makeWebSocketDriver from './driver/web_socket' + +import view from './views/websocket' +import { lyut } from './views/websocket' + +import serialize from 'form-serialize' +import { Observable as O } from './rxjs' +import { combine } from './util' + + +// Get cyclejs to use rxjs-compat-enabled streams +require("@cycle/run/lib/adapt").setAdapt(stream$ => O.from(stream$)) + + +const main = ({DOM, sock, conf$}) => { + //Intent + const click$ = DOM.select('[do=exec-websoc]').events('submit',{ preventDefault: true }) + .map(e => ({ ...e.target.dataset, ...serialize(e.target, { hash: true }) })) + + //Model + ,incoming$ = sock.map(r=>r.slice(8)).startWith('{"Welcome": "Please wait for the prompt.."}') + //Sinks + ,ldom$ = click$.map(r=>r.cmd) + ,mthd$ = click$.map(r=>r.cmd).startWith("Welcome!!") + ,loading$ = O.merge(ldom$, incoming$).map(x => x[0] == '{' ? false : true).startWith(false) + ,state$ = combine({ conf$, incoming$, loading$, mthd$}) + ,body$ = state$.map(S => view.websocket(S)) + + + return { + DOM: combine({state$, body$}).map(lyut) + , sock: ldom$ + } +} + +run(main, { + DOM: makeDOMDriver('#app') +, sock: makeWebSocketDriver +, conf$: makeConfDriver(storageDriver) +})