Feature: Setup Bitcoin Core from Specter (#1007)

* Package bitcoind with specter WIP

* fixes

* Setup wizard

* Fix non default datadir

* Update bitcoind.py

* Data folder config

* UI and text fixes

* Tor setup (macOS)

* Tor setup Windows and Linux support

* Wizard!

* action btns

* About page update

* Tor controller

* Update specter.py

* tor_controller getter

* Fix wizard bug

* Fix wallet json export backup

* remove docker dependency for main

* refactoring docker-requirement out of the way

* fix import

* Fix bitcoind docker issue

* Update tor_daemon.py

* latest pruned node

* Tor options and fixes

* Fix bitcoind for wondows

* Windows process stop fixes

* Moar windows fix attempts

* Fix exit windows

* Update src/cryptoadvance/specter/cli/cli_server.py

Co-authored-by: Kim Neunert <kneunert@gmail.com>

* Cleanups

* Cleanups

* Add Bitcoin Core Tor support

* Update specter bitcoind and tord

* Small UI update

Co-authored-by: Kim Neunert <kneunert@gmail.com>
Co-authored-by: Kim Neunert <k9ert@gmx.de>
This commit is contained in:
benk10 2021-03-19 13:31:01 +02:00 committed by GitHub
parent a4210a33b4
commit c00e41f764
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 2828 additions and 534 deletions

View file

@ -67,18 +67,6 @@ let webPreferences = {
app.commandLine.appendSwitch('ignore-certificate-errors');
try {
require(
'@deadcanaries/granax')({}, {
'SocksPort': 9050,
'ControlPort': 9051
}).on('error', function () {
// Tor is probably running, ignore...
})
} catch(e) {
// Tor is probably running, ignore...
}
let platformName = ''
switch (process.platform) {
case 'darwin':
@ -181,8 +169,13 @@ function initMainWindow(specterURL) {
});
mainWindow.on('close', function (event) {
event.preventDefault();
mainWindow.hide();
if(platformName == 'win64') {
quitSpecterd()
app.quit()
} else {
event.preventDefault();
mainWindow.hide();
}
});
mainWindow.webContents.on("did-fail-load", function() {
@ -276,9 +269,11 @@ function startSpecterd(specterdPath) {
specterdArgs = specterdArgs.concat(specterdExtraArgs)
}
specterdProcess = spawn(specterdPath, specterdArgs);
specterdProcess.stdout.on('data', (_) => {
if (mainWindow) {
createWindow(appSettings.specterURL)
specterdProcess.stdout.on('data', (data) => {
if(data.toString().includes('Serving Flask app "cryptoadvance.specter.server"')) {
if (mainWindow) {
createWindow(appSettings.specterURL)
}
}
});
specterdProcess.stderr.on('data', function(_) {
@ -298,6 +293,13 @@ function startSpecterd(specterdPath) {
});
}
app.on('window-all-closed', function(){
if(platformName == 'win64') {
quitSpecterd()
app.quit()
}
});
app.on('before-quit', () => {
if (!quitted) {
quitted = true
@ -337,7 +339,8 @@ function quitSpecterd() {
if (specterdProcess) {
try {
if (platformName == 'win64') {
exec('taskkill -F -T -PID ' + specterdProcess.pid);
exec('taskkill /F /T /PID ' + specterdProcess.pid);
exec('taskkill /IM specterd.exe ');
process.kill(-specterdProcess.pid)
}
specterdProcess.kill('SIGINT')

View file

@ -4,44 +4,12 @@
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"7zip": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/7zip/-/7zip-0.0.6.tgz",
"integrity": "sha1-nK+xca+CMpSQNTtIFvAzR6oVCjA=",
"optional": true
},
"7zip-bin": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.0.3.tgz",
"integrity": "sha512-GLyWIFBbGvpKPGo55JyRZAo4lVbnBiD52cKlw/0Vt+wnmKvWJkpZvsjVoaIolyBXDeAQKSicRtqFNPem9w0WYA==",
"dev": true
},
"@deadcanaries/granax": {
"version": "3.2.5",
"resolved": "https://registry.npmjs.org/@deadcanaries/granax/-/granax-3.2.5.tgz",
"integrity": "sha512-umsEYedNQWLSlkDnGynR8bY0pVWq2hKN1JLtV9Jit7srYHqdo+XIEsUNejq1nNBiX83WhUZVe4gOqvg8ovdMMQ==",
"requires": {
"7zip": "0.0.6",
"async": "^2.3.0",
"latest-torbrowser-version": "^2.0.2",
"merge": "^1.2.1",
"mkdirp": "^0.5.1",
"mv": "^2.1.1",
"ncp": "^2.0.0",
"progress": "^2.0.3",
"rimraf": "^2.6.3"
},
"dependencies": {
"async": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
"integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
"requires": {
"lodash": "^4.17.14"
}
}
}
},
"@develar/schema-utils": {
"version": "2.6.5",
"resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz",
@ -336,7 +304,8 @@
"balanced-match": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
"dev": true
},
"bcrypt-pbkdf": {
"version": "1.0.2",
@ -361,11 +330,6 @@
"bluebird": "^3.5.5"
}
},
"boolbase": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
"integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24="
},
"boolean": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/boolean/-/boolean-3.0.2.tgz",
@ -411,6 +375,7 @@
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@ -551,19 +516,6 @@
"supports-color": "^7.1.0"
}
},
"cheerio": {
"version": "1.0.0-rc.3",
"resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.3.tgz",
"integrity": "sha512-0td5ijfUPuubwLUu0OBoe98gZj8C/AA+RW3v67GPlGOrvxWjZmBXiBCRU+I8VEiNyJzjth40POfHiz2RB3gImA==",
"requires": {
"css-select": "~1.2.0",
"dom-serializer": "~0.1.1",
"entities": "~1.1.1",
"htmlparser2": "^3.9.1",
"lodash": "^4.15.0",
"parse5": "^3.0.1"
}
},
"chromium-pickle-js": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz",
@ -652,7 +604,8 @@
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
"dev": true
},
"concat-stream": {
"version": "1.6.2",
@ -709,22 +662,6 @@
"integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==",
"dev": true
},
"css-select": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz",
"integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=",
"requires": {
"boolbase": "~1.0.0",
"css-what": "2.1",
"domutils": "1.5.1",
"nth-check": "~1.0.1"
}
},
"css-what": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.3.tgz",
"integrity": "sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg=="
},
"dashdash": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
@ -828,37 +765,6 @@
}
}
},
"dom-serializer": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.1.tgz",
"integrity": "sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==",
"requires": {
"domelementtype": "^1.3.0",
"entities": "^1.1.1"
}
},
"domelementtype": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz",
"integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w=="
},
"domhandler": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz",
"integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==",
"requires": {
"domelementtype": "1"
}
},
"domutils": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz",
"integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=",
"requires": {
"dom-serializer": "0",
"domelementtype": "1"
}
},
"dot-prop": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz",
@ -1093,11 +999,6 @@
"once": "^1.4.0"
}
},
"entities": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz",
"integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w=="
},
"env-paths": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.0.tgz",
@ -1226,11 +1127,6 @@
"trim-repeated": "^1.0.0"
}
},
"follow-redirects": {
"version": "1.13.0",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.0.tgz",
"integrity": "sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA=="
},
"forever-agent": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
@ -1257,11 +1153,6 @@
"universalify": "^0.1.0"
}
},
"fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
},
"get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
@ -1285,18 +1176,6 @@
"assert-plus": "^1.0.0"
}
},
"glob": {
"version": "6.0.4",
"resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz",
"integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=",
"requires": {
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "2 || 3",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
},
"global-agent": {
"version": "2.1.12",
"resolved": "https://registry.npmjs.org/global-agent/-/global-agent-2.1.12.tgz",
@ -1405,31 +1284,6 @@
"lru-cache": "^6.0.0"
}
},
"htmlparser2": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz",
"integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==",
"requires": {
"domelementtype": "^1.3.1",
"domhandler": "^2.3.0",
"domutils": "^1.5.1",
"entities": "^1.1.1",
"inherits": "^2.0.1",
"readable-stream": "^3.1.1"
},
"dependencies": {
"readable-stream": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
"requires": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
}
}
}
},
"http-cache-semantics": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz",
@ -1467,19 +1321,11 @@
"integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=",
"dev": true
},
"inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
"requires": {
"once": "^1.3.0",
"wrappy": "1"
}
},
"inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true
},
"ini": {
"version": "1.3.8",
@ -1707,32 +1553,6 @@
"json-buffer": "3.0.0"
}
},
"latest-torbrowser-version": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/latest-torbrowser-version/-/latest-torbrowser-version-2.0.3.tgz",
"integrity": "sha512-+6fkERFLxo2QiNn7x0txjEF199tG4CGVI9X101d7IuZAapO/hGOC+nHD9BEPNNaMqmvIUkT/o3CdZ+NS5wX/iQ==",
"requires": {
"async": "^2.6.0",
"cheerio": "^1.0.0-rc.2",
"follow-redirects": "^1.2.4",
"semver": "^5.4.1"
},
"dependencies": {
"async": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
"integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
"requires": {
"lodash": "^4.17.14"
}
},
"semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
}
}
},
"latest-version": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz",
@ -1751,7 +1571,9 @@
"lodash": {
"version": "4.17.20",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA=="
"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==",
"dev": true,
"optional": true
},
"lowercase-keys": {
"version": "1.0.1",
@ -1804,11 +1626,6 @@
}
}
},
"merge": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/merge/-/merge-1.2.1.tgz",
"integrity": "sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ=="
},
"mime": {
"version": "2.4.6",
"resolved": "https://registry.npmjs.org/mime/-/mime-2.4.6.tgz",
@ -1838,6 +1655,7 @@
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"dev": true,
"requires": {
"brace-expansion": "^1.1.7"
}
@ -1845,12 +1663,14 @@
"minimist": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
"dev": true
},
"mkdirp": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
"integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
"dev": true,
"requires": {
"minimist": "^1.2.5"
}
@ -1865,31 +1685,6 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"mv": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz",
"integrity": "sha1-rmzg1vbV4KT32JN5jQPB6pVZtqI=",
"requires": {
"mkdirp": "~0.5.1",
"ncp": "~2.0.0",
"rimraf": "~2.4.0"
},
"dependencies": {
"rimraf": {
"version": "2.4.5",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz",
"integrity": "sha1-7nEM5dk6j9uFb7Xqj/Di11k0sto=",
"requires": {
"glob": "^6.0.1"
}
}
}
},
"ncp": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz",
"integrity": "sha1-GVoh1sRuNh0vsSgbo4uR6d9727M="
},
"normalize-package-data": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
@ -1933,14 +1728,6 @@
"pify": "^3.0.0"
}
},
"nth-check": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz",
"integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==",
"requires": {
"boolbase": "~1.0.0"
}
},
"oauth-sign": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
@ -1987,24 +1774,11 @@
}
}
},
"parse5": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-3.0.3.tgz",
"integrity": "sha512-rgO9Zg5LLLkfJF9E6CCmXlSE4UVceloys8JrFqCcHloC3usd/kJCyPDwH2SOlzix2j3xaP9sUX3e8+kvkuleAA==",
"requires": {
"@types/node": "*"
}
},
"path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="
},
"path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
},
"path-parse": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
@ -2043,7 +1817,8 @@
"progress": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
"integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="
"integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
"dev": true
},
"proto-list": {
"version": "1.2.4",
@ -2193,29 +1968,6 @@
"lowercase-keys": "^1.0.0"
}
},
"rimraf": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
"integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
"requires": {
"glob": "^7.1.3"
},
"dependencies": {
"glob": {
"version": "7.1.6",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
"integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
}
}
},
"roarr": {
"version": "2.15.4",
"resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz",
@ -2451,6 +2203,7 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dev": true,
"requires": {
"safe-buffer": "~5.1.0"
}
@ -2671,7 +2424,8 @@
"util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
"dev": true
},
"uuid": {
"version": "3.4.0",

View file

@ -43,7 +43,6 @@
}
},
"dependencies": {
"@deadcanaries/granax": "^3.2.5",
"electron-context-menu": "^2.3.0",
"electron-default-menu": "^1.0.2",
"extract-zip": "^2.0.1",

View file

@ -18,3 +18,4 @@ embit==0.1.2
psutil==5.7.3
pyopenssl==20.0.1
flask_wtf==0.14.3
pgpy==0.5.3

View file

@ -87,6 +87,7 @@ cryptography==3.3.2 \
--hash=sha256:e18e6ab84dfb0ab997faf8cca25a86ff15dfea4027b986322026cc99e0a892da
# via
# noiseprotocol
# pgpy
# pyopenssl
daemonize==2.5.0 \
--hash=sha256:9b6b91311a9d934ff3f5f766666635ca280d3de8e7137e4cd7d3f052543b989f \
@ -207,6 +208,10 @@ noiseprotocol==0.3.1 \
pbkdf2==1.3 \
--hash=sha256:ac6397369f128212c43064a2b4878038dab78dab41875364554aaf2a684e6979
# via mnemonic
pgpy==0.5.3 \
--hash=sha256:a49c269cedcaf82ac6999bcae5fd3f543ecb1c759f9d48a15ad8d8fa4ac03987 \
--hash=sha256:cba6fbbb44a896a8a4f5807b3d8d4943a8f7a6607be11587f4a27734c711c1dd
# via -r requirements.in
protobuf==3.13.0 \
--hash=sha256:0bba42f439bf45c0f600c3c5993666fcb88e8441d011fad80a11df6f324eef33 \
--hash=sha256:1e834076dfef9e585815757a2c7e4560c7ccc5962b9d09f831214c693a91b463 \
@ -243,6 +248,10 @@ psutil==5.7.3 \
pyaes==1.6.1 \
--hash=sha256:02c1b1405c38d3c370b085fb952dd8bea3fadcee6411ad99f312cc129c536d8f
# via hwi
pyasn1==0.4.8 \
--hash=sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d \
--hash=sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba
# via pgpy
pycparser==2.20 \
--hash=sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0 \
--hash=sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705
@ -279,6 +288,7 @@ six==1.12.0 \
# -r requirements.in
# cryptography
# flask-cors
# pgpy
# protobuf
# pyopenssl
stem==1.8.0 \

View file

@ -1,5 +1,4 @@
""" Stuff to control a bitcoind-instance. Either directly by access to a bitcoind-executable or
via docker.
""" Stuff to control a bitcoind-instance.
"""
import atexit
import logging
@ -11,8 +10,8 @@ import subprocess
import tempfile
import time
import json
import platform
import docker
from .util.shell import which
from .rpc import RpcError
@ -80,8 +79,13 @@ class Btcd_conn:
class BitcoindController:
""" A kind of abstract class to simplify running a bitcoind with or without docker """
def __init__(self, rpcport=18443):
self.rpcconn = Btcd_conn(rpcport=rpcport)
def __init__(
self, rpcport=18443, network="regtest", rpcuser="bitcoin", rpcpassword="secret"
):
self.rpcconn = Btcd_conn(
rpcuser=rpcuser, rpcpassword=rpcpassword, rpcport=rpcport
)
self.network = network
def start_bitcoind(
self, cleanup_at_exit=False, cleanup_hard=False, datadir=None, extra_args=[]
@ -103,7 +107,8 @@ class BitcoindController:
)
self.wait_for_bitcoind(self.rpcconn)
self.mine(block_count=100)
if self.network == "regtest":
self.mine(block_count=100)
return self.rpcconn
def version(self):
@ -160,23 +165,24 @@ class BitcoindController:
try:
rpcconn.get_rpc() # that call will also check the connection
return True
except ConnectionRefusedError:
except ConnectionRefusedError as e:
return False
except TypeError:
except TypeError as e:
return False
except Exception:
except Exception as e:
print(f"couldn't reach bitcoind - message returned: {e}")
return False
@staticmethod
def wait_for_bitcoind(rpcconn):
""" tries to reach the bitcoind via rpc. Will timeout after 10 seconds """
""" tries to reach the bitcoind via rpc. Will timeout after 30 seconds """
i = 0
while True:
if BitcoindController.check_bitcoind(rpcconn):
break
time.sleep(0.5)
i = i + 1
if i > 20:
if i > 60:
raise Exception(
"Timeout while trying to reach bitcoind at rpcport {} !".format(
rpcconn
@ -197,11 +203,13 @@ class BitcoindController:
run_docker=True,
datadir=None,
bitcoind_path="bitcoind",
network="regtest",
extra_args=[],
):
""" returns a bitcoind-command to run bitcoind """
btcd_cmd = "{} ".format(bitcoind_path)
btcd_cmd += " -regtest "
btcd_cmd = '"{}" '.format(bitcoind_path)
if network != "mainnet":
btcd_cmd += " -{} ".format(network)
btcd_cmd += " -fallbackfee=0.0002 "
btcd_cmd += " -port={} -rpcport={} -rpcbind=0.0.0.0 -rpcbind=0.0.0.0".format(
rpcconn.rpcport - 1, rpcconn.rpcport
@ -214,7 +222,7 @@ class BitcoindController:
btcd_cmd += " -noprinttoconsole"
if datadir == None:
datadir = tempfile.mkdtemp(prefix="bitcoind_datadir")
btcd_cmd += " -datadir={} ".format(datadir)
btcd_cmd += ' -datadir="{}" '.format(datadir)
if extra_args:
btcd_cmd += " {}".format(" ".join(extra_args))
logger.debug("constructed bitcoind-command: %s", btcd_cmd)
@ -222,10 +230,19 @@ class BitcoindController:
class BitcoindPlainController(BitcoindController):
""" A class controlling the bicoind-process directly on the machine """
""" A class controlling the bitcoind-process directly on the machine """
def __init__(self, bitcoind_path="bitcoind", rpcport=18443):
super().__init__(rpcport=rpcport)
def __init__(
self,
bitcoind_path="bitcoind",
rpcport=18443,
network="regtest",
rpcuser="bitcoin",
rpcpassword="secret",
):
super().__init__(
rpcport=rpcport, network=network, rpcuser=rpcuser, rpcpassword=rpcpassword
)
self.bitcoind_path = bitcoind_path
self.rpcconn.ipaddress = "localhost"
@ -240,11 +257,15 @@ class BitcoindPlainController(BitcoindController):
run_docker=False,
datadir=datadir,
bitcoind_path=self.bitcoind_path,
network=self.network,
extra_args=extra_args,
)
logger.debug("About to execute: {}".format(bitcoind_cmd))
# exec will prevent creating a child-process and will make bitcoind_proc.terminate() work as expected
self.bitcoind_proc = subprocess.Popen("exec " + bitcoind_cmd, shell=True)
self.bitcoind_proc = subprocess.Popen(
("exec " if platform.system() != "Windows" else "") + bitcoind_cmd,
shell=True,
)
logger.debug(
"Running bitcoind-process with pid {}".format(self.bitcoind_proc.pid)
)
@ -278,6 +299,8 @@ class BitcoindPlainController(BitcoindController):
for p in alive:
logger.info("bitcoind did not terminated in time, killing!")
p.kill()
if platform.system() == "Windows":
subprocess.run("Taskkill /IM bitcoind.exe /F")
def stop_bitcoind(self):
self.cleanup_bitcoind()
@ -292,187 +315,6 @@ class BitcoindPlainController(BitcoindController):
return True
class BitcoindDockerController(BitcoindController):
""" A class specifically controlling a docker-based bitcoind-container """
def __init__(self, rpcport=18443, docker_tag="latest"):
self.btcd_container = None
super().__init__(rpcport=rpcport)
self.docker_tag = docker_tag
if self.detect_bitcoind_container(rpcport) != None:
rpcconn, btcd_container = self.detect_bitcoind_container(rpcport)
logger.debug("Detected old container ... deleting it")
btcd_container.stop()
btcd_container.remove()
def _start_bitcoind(
self, cleanup_at_exit, cleanup_hard=False, datadir=None, extra_args=[]
):
if datadir != None:
# ignored
pass
bitcoind_path = self.construct_bitcoind_cmd(self.rpcconn, extra_args=extra_args)
dclient = docker.from_env()
logger.debug("Running (in docker): {}".format(bitcoind_path))
ports = {
"{}/tcp".format(self.rpcconn.rpcport - 1): self.rpcconn.rpcport - 1,
"{}/tcp".format(self.rpcconn.rpcport): self.rpcconn.rpcport,
}
logger.debug("portmapping: {}".format(ports))
image = dclient.images.get(
"registry.gitlab.com/cryptoadvance/specter-desktop/python-bitcoind:{}".format(
self.docker_tag
)
)
self.btcd_container = dclient.containers.run(
image,
bitcoind_path,
ports=ports,
detach=True,
)
def cleanup_docker_bitcoind(*args):
logger.info("Cleaning up bitcoind-docker-container")
self.btcd_container.stop()
self.btcd_container.remove()
if cleanup_at_exit:
logger.debug(
"Register function cleanup_docker_bitcoind for SIGINT and SIGTERM"
)
# This is for CTRL-C --> SIGINT
signal.signal(signal.SIGINT, cleanup_docker_bitcoind)
# This is for kill $pid --> SIGTERM
signal.signal(signal.SIGTERM, cleanup_docker_bitcoind)
logger.debug(
"Waiting for container {} to come up".format(self.btcd_container.id)
)
self.wait_for_container()
rpcconn, _ = self.detect_bitcoind_container(self.rpcconn.rpcport)
if rpcconn == None:
raise Exception(
"Couldn't find container or it died already. Check the logs!"
)
else:
self.rpcconn = rpcconn
return
def stop_bitcoind(self):
if self.btcd_container != None:
self.btcd_container.reload()
if self.btcd_container.status == "running":
_, container = self.detect_bitcoind_container(self.rpcconn.rpcport)
if container == self.btcd_container:
self.btcd_container.stop()
logger.info("Stopped btcd_container {}".format(self.btcd_container))
self.btcd_container.remove()
return
raise Exception("Ambigious Container running")
def check_existing(self):
""" Checks whether self.btcd_container is up2date and not ambigious """
if self.btcd_container != None:
self.btcd_container.reload()
if self.btcd_container.status == "running":
rpcconn, container = self.detect_bitcoind_container(
self.rpcconn.rpcport
)
if container == self.btcd_container:
return rpcconn
raise Exception("Ambigious Container running")
return None
@staticmethod
def search_bitcoind_container(all=False):
""" returns a list of containers which are running bitcoind """
d_client = docker.from_env()
return [
c
for c in d_client.containers.list(all)
if (c.attrs["Config"].get("Cmd") or [""])[0] == "bitcoind"
]
@staticmethod
def detect_bitcoind_container(with_rpcport):
"""checks all the containers for a bitcoind one, parses the arguments and initializes
the object accordingly
returns rpcconn, btcd_container
"""
d_client = docker.from_env()
potential_btcd_containers = BitcoindDockerController.search_bitcoind_container()
if len(potential_btcd_containers) == 0:
logger.debug(
"could not detect container. Candidates: {}".format(
d_client.containers.list()
)
)
all_candidates = BitcoindDockerController.search_bitcoind_container(
all=True
)
logger.debug(
"could not detect container. All Candidates: {}".format(all_candidates)
)
if len(all_candidates) > 0:
logger.debug("100 chars of logs of first candidate")
logger.debug(all_candidates[0].logs()[0:100])
return None
for btcd_container in potential_btcd_containers:
rpcport = int(
[
arg
for arg in btcd_container.attrs["Config"]["Cmd"]
if "rpcport" in arg
][0].split("=")[1]
)
if rpcport != with_rpcport:
logger.debug(
"checking port {} against searched port {}".format(
type(rpcport), type(with_rpcport)
)
)
continue
rpcpassword = [
arg
for arg in btcd_container.attrs["Config"]["Cmd"]
if "rpcpassword" in arg
][0].split("=")[1]
rpcuser = [
arg for arg in btcd_container.attrs["Config"]["Cmd"] if "rpcuser" in arg
][0].split("=")[1]
if "CI" in os.environ: # this is a predefined variable in gitlab
# This works on Linux (direct docker) and gitlab-CI but not on MAC
ipaddress = btcd_container.attrs["NetworkSettings"]["IPAddress"]
else:
# This works on most machines but not on gitlab-CI
ipaddress = "127.0.0.1"
rpcconn = Btcd_conn(
rpcuser=rpcuser,
rpcpassword=rpcpassword,
rpcport=rpcport,
ipaddress=ipaddress,
)
logger.info("detected container {}".format(btcd_container.id))
return rpcconn, btcd_container
logger.debug("No matching container found")
return None
def wait_for_container(self):
""" waits for the docker-container to come up. Times out after 10 seconds """
i = 0
while True:
ip_address = self.btcd_container.attrs["NetworkSettings"]["IPAddress"]
if ip_address.startswith("172"):
self.rpcconn.ipaddress = ip_address
break
self.btcd_container.reload()
time.sleep(0.5)
i = i + 1
if i > 20:
raise Exception("Timeout while starting bitcoind-docker-container!")
def fetch_wallet_addresses_for_mining(data_folder):
"""parses all the wallet-jsons in the folder (default ~/.specter/wallets/regtest)
and returns an array with the addresses

View file

@ -0,0 +1,193 @@
import logging
# the docker-dependency is special as it's only used with pytest --docker or
# if you manually start bitcoind --docker
# If this fails, you need to pip install the test-requirements
import docker
import os
from .bitcoind import BitcoindController, Btcd_conn
import signal
import time
logger = logging.getLogger(__name__)
class BitcoindDockerController(BitcoindController):
""" A class specifically controlling a docker-based bitcoind-container """
def __init__(self, rpcport=18443, docker_tag="latest"):
self.btcd_container = None
super().__init__(rpcport=rpcport)
self.docker_tag = docker_tag
if self.detect_bitcoind_container(rpcport) != None:
rpcconn, btcd_container = self.detect_bitcoind_container(rpcport)
logger.debug("Detected old container ... deleting it")
btcd_container.stop()
btcd_container.remove()
def _start_bitcoind(
self, cleanup_at_exit, cleanup_hard=False, datadir=None, extra_args=[]
):
if datadir != None:
# ignored
pass
bitcoind_path = self.construct_bitcoind_cmd(self.rpcconn, extra_args=extra_args)
dclient = docker.from_env()
logger.debug("Running (in docker): {}".format(bitcoind_path))
ports = {
"{}/tcp".format(self.rpcconn.rpcport - 1): self.rpcconn.rpcport - 1,
"{}/tcp".format(self.rpcconn.rpcport): self.rpcconn.rpcport,
}
logger.debug("portmapping: {}".format(ports))
image = dclient.images.get(
"registry.gitlab.com/cryptoadvance/specter-desktop/python-bitcoind:{}".format(
self.docker_tag
)
)
self.btcd_container = dclient.containers.run(
image,
bitcoind_path,
ports=ports,
detach=True,
)
def cleanup_docker_bitcoind(*args):
logger.info("Cleaning up bitcoind-docker-container")
self.btcd_container.stop()
self.btcd_container.remove()
if cleanup_at_exit:
logger.debug(
"Register function cleanup_docker_bitcoind for SIGINT and SIGTERM"
)
# This is for CTRL-C --> SIGINT
signal.signal(signal.SIGINT, cleanup_docker_bitcoind)
# This is for kill $pid --> SIGTERM
signal.signal(signal.SIGTERM, cleanup_docker_bitcoind)
logger.debug(
"Waiting for container {} to come up".format(self.btcd_container.id)
)
self.wait_for_container()
rpcconn, _ = self.detect_bitcoind_container(self.rpcconn.rpcport)
if rpcconn == None:
raise Exception(
"Couldn't find container or it died already. Check the logs!"
)
else:
self.rpcconn = rpcconn
return
def stop_bitcoind(self):
if self.btcd_container != None:
self.btcd_container.reload()
if self.btcd_container.status == "running":
_, container = self.detect_bitcoind_container(self.rpcconn.rpcport)
if container == self.btcd_container:
self.btcd_container.stop()
logger.info("Stopped btcd_container {}".format(self.btcd_container))
self.btcd_container.remove()
return
raise Exception("Ambigious Container running")
def check_existing(self):
""" Checks whether self.btcd_container is up2date and not ambigious """
if self.btcd_container != None:
self.btcd_container.reload()
if self.btcd_container.status == "running":
rpcconn, container = self.detect_bitcoind_container(
self.rpcconn.rpcport
)
if container == self.btcd_container:
return rpcconn
raise Exception("Ambigious Container running")
return None
@staticmethod
def search_bitcoind_container(all=False):
""" returns a list of containers which are running bitcoind """
d_client = docker.from_env()
return [
c
for c in d_client.containers.list(all)
if (c.attrs["Config"].get("Cmd") or [""])[0] == "bitcoind"
]
@staticmethod
def detect_bitcoind_container(with_rpcport):
"""checks all the containers for a bitcoind one, parses the arguments and initializes
the object accordingly
returns rpcconn, btcd_container
"""
d_client = docker.from_env()
potential_btcd_containers = BitcoindDockerController.search_bitcoind_container()
if len(potential_btcd_containers) == 0:
logger.debug(
"could not detect container. Candidates: {}".format(
d_client.containers.list()
)
)
all_candidates = BitcoindDockerController.search_bitcoind_container(
all=True
)
logger.debug(
"could not detect container. All Candidates: {}".format(all_candidates)
)
if len(all_candidates) > 0:
logger.debug("100 chars of logs of first candidate")
logger.debug(all_candidates[0].logs()[0:100])
return None
for btcd_container in potential_btcd_containers:
rpcport = int(
[
arg
for arg in btcd_container.attrs["Config"]["Cmd"]
if "rpcport" in arg
][0].split("=")[1]
)
if rpcport != with_rpcport:
logger.debug(
"checking port {} against searched port {}".format(
type(rpcport), type(with_rpcport)
)
)
continue
rpcpassword = [
arg
for arg in btcd_container.attrs["Config"]["Cmd"]
if "rpcpassword" in arg
][0].split("=")[1]
rpcuser = [
arg for arg in btcd_container.attrs["Config"]["Cmd"] if "rpcuser" in arg
][0].split("=")[1]
if "CI" in os.environ: # this is a predefined variable in gitlab
# This works on Linux (direct docker) and gitlab-CI but not on MAC
ipaddress = btcd_container.attrs["NetworkSettings"]["IPAddress"]
else:
# This works on most machines but not on gitlab-CI
ipaddress = "127.0.0.1"
rpcconn = Btcd_conn(
rpcuser=rpcuser,
rpcpassword=rpcpassword,
rpcport=rpcport,
ipaddress=ipaddress,
)
logger.info("detected container {}".format(btcd_container.id))
return rpcconn, btcd_container
logger.debug("No matching container found")
return None
def wait_for_container(self):
""" waits for the docker-container to come up. Times out after 10 seconds """
i = 0
while True:
ip_address = self.btcd_container.attrs["NetworkSettings"]["IPAddress"]
if ip_address.startswith("172"):
self.rpcconn.ipaddress = ip_address
break
self.btcd_container.reload()
time.sleep(0.5)
i = i + 1
if i > 20:
raise Exception("Timeout while starting bitcoind-docker-container!")

View file

@ -98,7 +98,7 @@ def bitcoind(
# In order to avoid these dependencies for production use, we're importing them here:
import docker
from ..bitcoind import BitcoindDockerController, BitcoindPlainController
from ..bitcoind import BitcoindPlainController
if config is None:
config = DEFAULT_CONFIG
@ -159,6 +159,8 @@ def bitcoind(
Path(config_obj["BTCD_REGTEST_DATA_DIR"]).mkdir(parents=True, exist_ok=True)
else:
echo("starting container")
from ..bitcoind_docker import BitcoindDockerController
my_bitcoind = BitcoindDockerController(docker_tag=docker_tag)
try:
my_bitcoind.start_bitcoind(

View file

@ -13,6 +13,7 @@ from urllib.parse import urlparse
from ..server import create_app, init_app
from ..util.tor import start_hidden_service, stop_hidden_services
from ..specter_error import SpecterError
logger = logging.getLogger(__name__)
@ -197,18 +198,6 @@ def server(
# debug is false by default
def run(debug=debug):
try:
tor_control_address = urlparse(app.specter.proxy_url).netloc.split(":")[0]
if tor_control_address == "localhost":
tor_control_address = "127.0.0.1"
app.controller = Controller.from_port(
address=tor_control_address,
port=int(app.specter.tor_control_port)
if app.specter.tor_control_port
else "default",
)
except Exception:
app.controller = None
try:
# if we have certificates
if "ssl_context" in kwargs:
@ -245,8 +234,12 @@ def server(
app.run(debug=debug, **kwargs)
stop_hidden_services(app)
finally:
if app.controller is not None:
app.controller.close()
try:
if app.specter.tor_controller is not None:
app.specter.tor_controller.close()
except SpecterError as se:
# no reason to break startup here
logger.error("Could not initialize tor-system")
# check if we should run a daemon or not
if daemon or restart:

View file

@ -66,6 +66,7 @@ class BaseConfig(object):
SPECTER_SSL_CERT_SERIAL_NUMBER = int(
os.getenv("SPECTER_SSL_CERT_SERIAL_NUMBER", random.randrange(1, 100000))
)
INTERNAL_BITCOIND_VERSION = os.getenv("INTERNAL_BITCOIND_VERSION", "0.21.0")
class DevelopmentConfig(BaseConfig):

View file

@ -1,4 +1,4 @@
import random, traceback, socket
import random, traceback, socket, threading, os
from datetime import datetime
from binascii import unhexlify
from flask import make_response
@ -18,7 +18,13 @@ from ..helpers import (
from ..specter import Specter
from ..specter_error import SpecterError
from ..util.tor import start_hidden_service, stop_hidden_services
from ..util.bitcoind_setup_tasks import (
setup_bitcoind_thread,
setup_bitcoind_directory_thread,
)
from ..util.tor_setup_tasks import (
setup_tor_thread,
)
from pathlib import Path
env_path = Path(".") / ".flaskenv"
@ -104,6 +110,133 @@ def about():
return render_template("base.jinja", specter=app.specter, rand=rand)
@app.route("/node_setup_wizard/", defaults={"step": 0}, methods=["GET", "POST"])
@app.route("/node_setup_wizard/<step>", methods=["GET", "POST"])
@login_required
def node_setup_wizard(step):
app.specter.config["bitcoind_setup"]["error"] = ""
app.specter.config["bitcoind_setup"]["stage"] = ""
app.specter.config["bitcoind_setup"]["stage_progress"] = -1
app.specter.config["torbrowser_setup"]["error"] = ""
app.specter.config["torbrowser_setup"]["stage"] = ""
app.specter.config["torbrowser_setup"]["stage_progress"] = -1
app.specter._save()
return render_template(
"node_setup_wizard.jinja", step=step, specter=app.specter, rand=rand
)
@app.route("/setup_bitcoind", methods=["POST"])
@login_required
def setup_bitcoind():
bitcoind_setup_status = app.specter.config.get(
"bitcoind_setup", {"stage_progress": -1}
)
if (
not os.path.isfile(app.specter.bitcoind_path)
and bitcoind_setup_status["stage_progress"] == -1
):
app.specter.config["rpc"]["datadir"] = request.form.get(
"bitcoin_core_datadir", app.specter.config["rpc"]["datadir"]
)
app.specter.config["bitcoind_setup"]["stage_progress"] = 0
app.specter.config["bitcoind_setup"]["error"] = ""
app.specter._save()
t = threading.Thread(
target=setup_bitcoind_thread,
args=(app.specter, app.config["INTERNAL_BITCOIND_VERSION"]),
)
t.start()
elif os.path.isfile(app.specter.bitcoind_path):
return {"error": "bitcoind already installed"}
elif bitcoind_setup_status["stage_progress"] != -1:
return {"error": "bitcoind installation is still under progress"}
return {"success": "Starting Bitcoin Core setup!"}
@app.route("/setup_bitcoind_directory", methods=["POST"])
@login_required
def setup_bitcoind_directory():
bitcoind_setup_status = app.specter.config.get(
"bitcoind_setup", {"stage_progress": -1}
)
if (
os.path.isfile(app.specter.bitcoind_path)
and bitcoind_setup_status["stage_progress"] == -1
):
app.specter.config["bitcoind_setup"]["stage_progress"] = 0
app.specter.config["bitcoind_setup"]["error"] = ""
app.specter._save()
quicksync = request.form["quicksync"] == "true"
pruned = request.form["nodetype"] == "pruned"
t = threading.Thread(
target=setup_bitcoind_directory_thread,
args=(app.specter, quicksync, pruned),
)
t.start()
elif not os.path.isfile(app.specter.bitcoind_path):
return {"error": "bitcoind in not installed but required for this step"}
elif bitcoind_setup_status["stage_progress"] != -1:
return {"error": "bitcoind installation is still under progress"}
return {"success": "Starting Bitcoin Core setup!"}
@app.route("/setup_tor", methods=["POST"])
@login_required
def setup_tor():
tor_setup_status = app.specter.config.get(
"torbrowser_setup", {"stage_progress": -1}
)
if (
not os.path.isfile(app.specter.torbrowser_path)
and tor_setup_status["stage_progress"] == -1
):
app.specter.config["torbrowser_setup"]["stage_progress"] = 0
app.specter.config["torbrowser_setup"]["error"] = ""
app.specter._save()
t = threading.Thread(
target=setup_tor_thread,
args=(app.specter,),
)
t.start()
elif os.path.isfile(app.specter.torbrowser_path):
return {"error": "tor is already installed"}
elif tor_setup_status["stage_progress"] != -1:
return {"error": "tor installation is still under progress"}
return {"success": "Starting Tor setup!"}
@app.route("/get_node_setup_status")
@login_required
@app.csrf.exempt
def get_node_setup_status():
return app.specter.config.get(
"bitcoind_setup",
{
"installed": os.path.isfile(app.specter.bitcoind_path),
"stage_progress": -1,
"stage": "none",
"error": "",
},
)
@app.route("/get_tor_setup_status")
@login_required
@app.csrf.exempt
def get_tor_setup_status():
return app.specter.config.get(
"torbrowser_setup",
{
"installed": os.path.isfile(app.specter.torbrowser_path),
"stage_progress": -1,
"stage": "none",
"error": "",
},
)
# TODO: Move all these below to REST API
################ Utils ####################

View file

@ -1,5 +1,6 @@
import json, os, time, random, requests, secrets
import json, os, time, random, requests, secrets, platform, tarfile, zipfile, sys, shutil
import pgpy
from pathlib import Path
from flask import (
Flask,
Blueprint,
@ -23,6 +24,8 @@ from ..helpers import (
from ..persistence import write_devices, write_wallet
from ..user import hash_password
from ..util.tor import start_hidden_service, stop_hidden_services
from ..util.sha256sum import sha256sum
from ..util.error_handling import handle_exception
rand = random.randint(0, 1e32) # to force style refresh
@ -58,6 +61,8 @@ def bitcoin_core():
protocol = "http"
autodetect = rpc["autodetect"]
datadir = rpc["datadir"]
external_node = rpc["external_node"]
node_view = "external" if external_node else "internal"
err = None
if "protocol" in rpc:
@ -65,14 +70,13 @@ def bitcoin_core():
test = None
if request.method == "POST":
action = request.form["action"]
if current_user.is_admin:
autodetect = "autodetect" in request.form
if autodetect:
datadir = request.form["datadir"]
user = request.form["username"]
password = request.form["password"]
port = request.form["port"]
host = request.form["host"].rstrip("/")
autodetect = "autodetect" in request.form
if autodetect:
datadir = request.form["datadir"]
user = request.form["username"]
password = request.form["password"]
port = request.form["port"]
host = request.form["host"].rstrip("/")
# protocol://host
if "://" in host:
@ -93,6 +97,7 @@ def bitcoin_core():
autodetect=autodetect,
datadir=datadir,
)
node_view = "external"
if "tests" in test:
# If any test has failed, we notify the user that the test has not passed
@ -102,6 +107,7 @@ def bitcoin_core():
flash("Test passed", "info")
elif action == "save":
if current_user.is_admin:
node_view = "external"
success = app.specter.update_rpc(
user=user,
password=password,
@ -114,6 +120,38 @@ def bitcoin_core():
if not success:
flash("Failed connecting to the node", "error")
app.specter.check()
# Internal Node actions
elif action == "useinternal":
app.specter.update_use_external_node(False)
external_node = False
node_view = "internal"
elif action == "useexternal":
app.specter.update_use_external_node(True)
external_node = True
node_view = "external"
elif action == "stopbitcoind":
node_view = "internal"
try:
app.specter.bitcoind.stop_bitcoind()
app.specter.set_bitcoind_pid(False)
time.sleep(5)
flash("Specter stopped Bitcoin Core successfully")
except Exception:
try:
flash("Stopping Bitcoin Core, this might take a few moments.")
app.specter.rpc.stop()
except Exception as e:
flash(f"Failed to stop Bitcoin Core {e}", "error")
elif action == "startbitcoind":
node_view = "internal"
app.specter.bitcoind.start_bitcoind(
datadir=os.path.expanduser(app.specter.config["rpc"]["datadir"])
)
app.specter.set_bitcoind_pid(app.specter.bitcoind.bitcoind_proc.pid)
time.sleep(15)
flash("Specter has started Bitcoin Core")
app.specter.check()
return render_template(
"settings/bitcoin_core_settings.jinja",
@ -127,6 +165,10 @@ def bitcoin_core():
protocol=protocol,
specter=app.specter,
current_version=current_version,
bitcoind_exists=os.path.isfile(app.specter.bitcoind_path),
is_running=app.specter.is_bitcoind_running(),
node_view=node_view,
external_node=external_node,
error=err,
rand=rand,
)
@ -254,6 +296,9 @@ def tor():
if not current_user.is_admin:
flash("Only an admin is allowed to access this page.", "error")
return redirect("")
app.specter.config["torbrowser_setup"]["stage"] = ""
app.specter.config["torbrowser_setup"]["stage_progress"] = -1
app.specter._save()
current_version = notify_upgrade(app, flash)
proxy_url = app.specter.proxy_url
only_tor = app.specter.only_tor
@ -269,13 +314,36 @@ def tor():
app.specter.update_only_tor(only_tor, current_user)
app.specter.update_tor_control_port(tor_control_port, current_user)
app.specter.check()
elif action == "starttor":
try:
app.specter.tor_daemon.start_tor_daemon()
flash("Specter has started Tor")
except Exception as e:
flash(f"Failed to start Tor, error: {e}", "error")
elif action == "stoptor":
try:
app.specter.tor_daemon.stop_tor_daemon()
time.sleep(1)
flash("Specter stopped Tor successfully")
except Exception as e:
flash(f"Failed to stop Tor, error: {e}", "error")
elif action == "uninstalltor":
try:
if app.specter.is_tor_dameon_running():
app.specter.tor_daemon.stop_tor_daemon()
shutil.rmtree(os.path.join(app.specter.data_folder, "tor-binaries"))
os.remove(os.path.join(app.specter.data_folder, "torrc"))
flash(f"Tor uninstalled successfully")
except Exception as e:
flash(f"Failed to stop Tor, error: {e}", "error")
elif action == "test_tor":
try:
requests_session = requests.Session()
requests_session.proxies["http"] = proxy_url
requests_session.proxies["https"] = proxy_url
res = requests_session.get(
"http://expyuzz4wqqyqhjn.onion", # Tor Project onion website
# "http://expyuzz4wqqyqhjn.onion", # Tor Project onion website (seems to be down)
"https://protonirockerxow.onion", # Proton mail onion website
)
tor_connectable = res.status_code == 200
if tor_connectable:
@ -283,7 +351,7 @@ def tor():
else:
flash("Failed to make test request over Tor.", "error")
except Exception as e:
flash("Failed to make test request over Tor. Error: %s" % e, "error")
flash("Failed to make test request over Tor.\nError: %s" % e, "error")
tor_connectable = False
elif action == "toggle_hidden_service":
if not app.config["DEBUG"]:
@ -296,9 +364,10 @@ def tor():
if hasattr(current_user, "is_admin") and current_user.is_admin:
try:
current_hidden_services = (
app.controller.list_ephemeral_hidden_services()
app.specter.tor_controller.list_ephemeral_hidden_services()
)
except Exception:
except Exception as e:
handle_exception(e)
current_hidden_services = []
if len(current_hidden_services) != 0:
stop_hidden_services(app)
@ -310,6 +379,7 @@ def tor():
app.specter.toggle_tor_status()
flash("Tor hidden service turn on successfully", "info")
except Exception as e:
handle_exception(e)
flash(
"Failed to start Tor hidden service. Make sure you have Tor running with ControlPort configured and try again. Error returned: {}".format(
e
@ -328,6 +398,8 @@ def tor():
only_tor=only_tor,
tor_control_port=tor_control_port,
tor_service_id=app.tor_service_id,
torbrowser_installed=os.path.isfile(app.specter.torbrowser_path),
torbrowser_running=app.specter.is_tor_dameon_running,
specter=app.specter,
current_version=current_version,
rand=rand,

View file

@ -118,7 +118,9 @@ def new_wallet(wallet_type):
action = request.form["action"]
if action == "importwallet":
try:
wallet_data = json.loads(request.form["wallet_data"].replace("'", "h"))
wallet_data = json.loads(
request.form["wallet_data"].replace("\\'", "").replace("'", "h")
)
(
wallet_name,
recv_descriptor,

View file

@ -6,11 +6,15 @@ import traceback
import random
import time
import zipfile
import platform
import secrets
import requests
from io import BytesIO
from .helpers import migrate_config, deep_update, clean_psbt, is_testnet
from .util.checker import Checker
from .rpc import autodetect_rpc_confs, get_default_datadir, RpcError
from .rpc import autodetect_rpc_confs, detect_rpc_confs, get_default_datadir, RpcError
from .bitcoind import BitcoindPlainController
from .tor_daemon import TorDaemonController
from urllib3.exceptions import NewConnectionError
from requests.exceptions import ConnectionError
from .rpc import BitcoinRPC
@ -20,7 +24,11 @@ from .user_manager import UserManager
from .persistence import write_json_file, read_json_file
from .user import User
from .util.price_providers import update_price
from .util.tor import get_tor_daemon_suffix
import threading
from urllib.parse import urlparse
from stem.control import Controller
from .specter_error import SpecterError
logger = logging.getLogger(__name__)
@ -113,6 +121,7 @@ class Specter:
"port": "",
"host": "localhost", # localhost
"protocol": "http", # https for the future
"external_node": True,
},
"auth": {
"method": "none",
@ -135,14 +144,63 @@ class Specter:
"alt_symbol": "BTC",
"price_provider": "",
"validate_merkle_proofs": False,
"bitcoind": False,
"bitcoind_internal_version": "",
"bitcoind_setup": {
"stage_progress": -1,
},
"torbrowser_setup": {
"stage_progress": -1,
},
}
self.torbrowser_path = os.path.join(
self.data_folder, f"tor-binaries/tor{get_tor_daemon_suffix()}"
)
self.bitcoind_path = os.path.join(
self.data_folder, "bitcoin-binaries/bin/bitcoind"
)
if platform.system() == "Windows":
self.bitcoind_path += ".exe"
self._bitcoind = None
self._tor_daemon = None
# health check: loads config, tests rpc
# also loads and checks wallets for all users
try:
self.check(check_all=True)
rpc_conf = next(
(
conf
for conf in detect_rpc_confs(
datadir=os.path.expanduser(self.config["rpc"]["datadir"])
)
if conf["port"] == 8332
),
None,
)
if not rpc_conf:
if not self.config["rpc"]["user"]:
self.config["rpc"]["user"] = "bitcoin"
self._save()
if not self.config["rpc"]["password"]:
self.config["rpc"]["password"] = secrets.token_urlsafe(16)
self._save()
if os.path.isfile(self.torbrowser_path):
self.tor_daemon.start_tor_daemon()
if not self.config["rpc"].get("external_node", True):
self.bitcoind.start_bitcoind(
datadir=os.path.expanduser(self.config["rpc"]["datadir"])
)
self.set_bitcoind_pid(self.bitcoind.bitcoind_proc.pid)
time.sleep(15)
except Exception as e:
logger.error(e)
self.update_tor_controller()
self.checker = Checker(lambda: self.check(check_all=True), desc="health")
self.checker.start()
self.price_checker = Checker(
@ -443,6 +501,17 @@ class Specter:
self.check(check_all=True)
return self.rpc is not None
def set_bitcoind_pid(self, pid):
""" set the control pid of the bitcoind daemon """
if self.config.get("bitcoind", False) != pid:
self.config["bitcoind"] = pid
self._save()
def update_use_external_node(self, use_external_node):
""" set whatever specter should connect to internal or external node """
self.config["rpc"]["external_node"] = use_external_node
self._save()
def update_auth(self, method, rate_limit, registration_link_timeout):
""" simply persisting the current auth-choice """
auth = self.config["auth"]
@ -493,6 +562,71 @@ class Specter:
if self.config["tor_control_port"] != tor_control_port:
self.config["tor_control_port"] = tor_control_port
self._save()
self.update_tor_controller()
def update_tor_controller(self):
try:
tor_control_address = urlparse(self.proxy_url).netloc.split(":")[0]
if tor_control_address == "localhost":
tor_control_address = "127.0.0.1"
self._tor_controller = Controller.from_port(
address=tor_control_address,
port=int(self.tor_control_port) if self.tor_control_port else "default",
)
self._tor_controller.authenticate(
password=self.config.get("torrc_password", "")
)
except Exception as e:
logger.warning(f"Failed to connect to Tor control port. Error: {e}")
self._tor_controller = None
@property
def tor_daemon(self):
if os.path.isfile(self.torbrowser_path) and os.path.join(
self.data_folder, "torrc"
):
if not self._tor_daemon:
self._tor_daemon = TorDaemonController(
tor_daemon_path=self.torbrowser_path,
tor_config_path=os.path.join(self.data_folder, "torrc"),
)
return self._tor_daemon
raise SpecterError(
"Tor daemon files missing. Make sure Tor is installed within Specter"
)
@property
def bitcoind(self):
if os.path.isfile(self.bitcoind_path):
if not self._bitcoind:
self._bitcoind = BitcoindPlainController(
bitcoind_path=self.bitcoind_path,
rpcport=8332,
network="mainnet",
rpcuser=self.config["rpc"]["user"],
rpcpassword=self.config["rpc"]["password"],
)
return self._bitcoind
raise SpecterError(
"Bitcoin Core files missing. Make sure Bitcoin Core is installed within Specter"
)
def is_tor_dameon_running(self):
return self._tor_daemon and self._tor_daemon.is_running()
def is_bitcoind_running(self):
return self._bitcoind and self._bitcoind.check_existing()
@property
def tor_controller(self):
if self._tor_controller:
return self._tor_controller
self.update_tor_controller()
if self._tor_controller:
return self._tor_controller
raise SpecterError(
"Failed to connect to the Tor daemon. Make sure ControlPort is properly configured."
)
def update_hwi_bridge_url(self, url, user):
""" update the hwi bridge url to use """

View file

@ -0,0 +1,784 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
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
S9Pb9RpTJvsxc4FTeX7vTHtCosUVKvvK
=D7TT
-----END PGP PUBLIC KEY BLOCK-----

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View file

@ -0,0 +1,12 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
mE8EX3YN+BMFK4EEAAoCAwRhTEFptcVn6WAE4PbCTCeeHRmqQ5Yd49wCdycs9Sje
2B/rBPEGADac9PRmIXhjtWn3pifZ/sG2hFzl7G3VGIDttElTdGVwYW4gU25pZ2ly
ZXYgKFNwZWN0ZXIgcmVsZWFzZSBzaWduaW5nIGtleSkgPHNuaWdpcmV2LnN0ZXBh
bkBnbWFpbC5jb20+iJYEExMIAD4WIQRvFuNU+DOT1uUuwl827TV6skuRXwUCX3YN
+AIbAwUJAeEzgAULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRA27TV6skuRX9P2
AP9LUiI/IapEK0YcRFczaBGyRC2/p8AvrKKrK/8hY6evfgEAo00jrdk10duMaY7N
kpfNcQIQy6Qhv3HDg8AcY1RgUyk=
=9s5S
-----END PGP PUBLIC KEY BLOCK-----

View file

@ -0,0 +1,332 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
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
qAtUENvs4LC7dEE=
=Jqal
-----END PGP PUBLIC KEY BLOCK-----

View file

@ -35,6 +35,9 @@
<a class="settings-bar-btn" href="{{ url_for('settings_endpoint.settings') }}">
<img src="{{ url_for('static', filename='img/settings-status-bar.svg') }}" style="width: 22px;"/>
</a>
<a class="settings-bar-btn" href="{{ url_for('about') }}" title="About and help">
<img src="{{ url_for('static', filename='img/icon.png') }}" style="width: 22px;"/>
</a>
{% if specter.config.auth.method != "none" %}
<a class="settings-bar-btn" href="{{ url_for('auth_endpoint.logout') }}">
<img src="{{ url_for('static', filename='img/logout-status-bar.svg') }}" style="width: 22px;"/>
@ -69,7 +72,11 @@
<img src="{{ url_for('static', filename='img/ghost.png') }}" width="40px" style="vertical-align: top; margin-left: 20px;"/>
</h1>
<p style="font-size: 1.2em; font-weight: bolder;">Specter Desktop is a convenient wallet interface to better use your Bitcoin Core node. Its good for singlesig wallets, but comes with a special focus on multisignature setups for different hardware wallets and air-gapped signing devices.</p>
<br><br><br>
<br><br>
{% if not specter.chain %}
<a href="{{ url_for('node_setup_wizard') }}" class="btn action centered" style="max-width: 200px;">Get started!</a>
<br><br>
{% else %}
<h2 style="font-size: 1.6em;">Getting Started</h2><br>
<table>
<tbody>
@ -78,8 +85,8 @@
<img src="{{ url_for('static', filename='img/checkbox-' + ('un' if not specter.chain else '') + 'tick.svg') }}" width="30px">
</td>
<td style="text-align: left;">
<a href="{{ url_for('settings_endpoint.settings') }}">
<h2>Connect Specter to your Bitcoin Core node.</h2>
<a href="{{ url_for('node_setup_wizard') }}">
<h2>Connect Specter with Bitcoin Core node.</h2>
</a>
</td>
<td style="text-align: left;">
@ -175,6 +182,7 @@
</tbody>
</table>
<br>
{% endif %}
Having issues? Try reading <a href="https://github.com/cryptoadvance/specter-desktop/blob/master/docs/faq.md" target="_blank" style="color: #fff; font-size: 1.05em; font-weight: bolder;">the FAQ</a><br><br>
Still having issues?<br>
Feel free to <a href="https://github.com/cryptoadvance/specter-desktop/issues/new" target="_blank" style="color: #fff;">open an issue on the GitHub</a> or ask in the <a href="https://t.me/spectersupport" target="_blank" style="color: #fff;">Specter Community telegram chat</a>

View file

@ -228,9 +228,9 @@
if (jsonResponse.label) {
self.label.innerText = jsonResponse.label;
}
return
};
showError('Failed to fetch address label data.')
return;
}
showError('Failed to fetch address label data.');
} catch(e) {
console.log("Caught error: ", e);
showError(e);

View file

@ -0,0 +1,365 @@
{% extends "base.jinja" %}
{% block main %}
<style>
.wizard-btn {
width: 200px;
min-width: 200px;
max-width: 200px;
height: 36px;
margin: auto;
}
.helper-btn {
text-decoration: underline;
margin: 10px 50px;
}
</style>
{% from "components/bitcoin_svg.jinja" import bitcoin_svg %}
<div class="card center hidden" id="step0">
<h1>Specter Quickstart!</h1>
<h2>Get up and running with Specter in just a few clicks!</h2><br>
<img style="width: 50px;" src="{{ url_for('static', filename='img/tor.svg') }}"/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
{{ bitcoin_svg('main', 50) }}&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<img src="{{ url_for('static', filename='img/icon.png') }}" style="width: 50px;"/>
<br><br>
<p style="max-width: 400px; margin: auto;">
Here, we'll take you through the process of setting up Specter with your own Bitcoin Core node and Tor.<br><br>
Everything is completely automated, but you make choices to customize your setup to fit your needs.
</p>
<br><br>
<div class="row">
<button type="button" class="btn wizard-btn action" onclick="showStep(1)">Get Started!</button>
</div>
</div>
<div class="card center hidden" id="step1">
<h1><img src="{{ url_for('static', filename='img/icon.png') }}" style="width: 60px;"/> <img src="{{ url_for('static', filename='img/arrow_right.svg') }}" style="width: 60px;"/>&nbsp;<img style="width: 60px;" src="{{ url_for('static', filename='img/tor.svg') }}"/><br>
<br>Download and setup Tor daemon</h1>
<h2 style="color: #ccc;">(optional, recommended)</h2>
<p>This should take between a few seconds to a few minutes, depending on your internet connection...</p>
<p class="hidden warning" id="step1-helper-text">Here, Specter will download the Tor daemon software, verify signatures to ensure download files can be trusted, and start your Tor daemon to allow you to make calls over the Tor network.</p>
<div class="row center">
<p class="explorer-link" id="step1-helper" onclick="toggleHelper('step1')">Unsure? Click here for more info!</p>
<a class="explorer-link hidden" id="step1-support" href="https://t.me/spectersupport" target="_blank">Still need help?</a>
</div>
<br><br>
<div class="row">
<button type="button" class="btn wizard-btn" onclick="showStep(2)">Skip</button>
<button type="button" class="btn wizard-btn action" id="setup-tor-button" onclick="setupTorDaemon()">Setup Tor!</button>
</div>
</div>
<div class="card center hidden" id="step2">
<h1><img src="{{ url_for('static', filename='img/icon.png') }}" style="width: 60px;"/> <img src="{{ url_for('static', filename='img/arrow_right.svg') }}" style="width: 60px;"/>&nbsp;{{ bitcoin_svg('main', 60) }}<br>
<br>Would you like to setup a new Bitcoin node or connect to an existing one?</h1>
<p class="hidden warning" id="step2-helper-text">If you have an existing Bitcoin Core node you would like to use, either running locally on this computer or somewhere else, like on Umbrel, myNode, or RaspiBlitz, you can just connect and use it.<br><br>
Don't have a node? Specter can help you setup a new one! This guide will help you get it up and running!</p>
<div class="row center">
<p class="explorer-link" id="step2-helper" onclick="toggleHelper('step2')">Unsure? Click here for more info!</p>
<a class="explorer-link hidden" id="step2-support" href="https://t.me/spectersupport" target="_blank">Still need help?</a>
</div>
<br><br>
<div class="row">
<a href="{{url_for('settings_endpoint.bitcoin_core')}}" class="btn wizard-btn">Connect existing node</a>
<button type="button" class="btn wizard-btn action" onclick="showStep(3)">Setup a new node</button>
</div>
</div>
<div class="card center hidden" id="step3">
<h1><img src="{{ url_for('static', filename='img/icon.png') }}" style="width: 60px;"/> <img src="{{ url_for('static', filename='img/arrow_right.svg') }}" style="width: 60px;"/>&nbsp;{{ bitcoin_svg('main', 60) }}<br>
<br>Download and setup Bitcoin Core</h1>
<p>This should take between a few seconds to a few minutes, depending on your internet connection...</p>
<p class="hidden warning" id="step3-helper-text">Here, Specter will download the Bitcoin Core release software, verify signatures to ensure download files can be trusted, and prepare basic configurations of your new Bitcoin Core node.</p>
<div class="row center">
<p class="explorer-link" id="step3-helper" onclick="toggleHelper('step3')">Unsure? Click here for more info!</p>
<a class="explorer-link hidden" id="step3-support" href="https://t.me/spectersupport" target="_blank">Still need help?</a>
</div><br>
<span id="toggle_advanced_3" style="cursor: pointer;" onclick="toggleAdvanced(3)">Advanced &#9654;</span>
<br>
<br>
<div id="advanced_settings_3" class="card hidden">
<h2 style="text-decoration: underline;">Advanced configurations</h2><br>
<p style="text-align: left;">
Bitcoin Core data directory path:<br>
<input type="text" id="bitcoin_core_datadir" name="bitcoin_core_datadir" type="text" value="{{ specter.config.rpc.datadir }}">
</p>
</div>
<br><br>
<div class="row">
<button type="button" class="btn wizard-btn action" id="setup-bitcoind-button" onclick="setupBitcoind()">Start the Setup!</button>
</div>
</div>
<div class="card center hidden" id="step4">
<h1><img src="{{ url_for('static', filename='img/icon.png') }}" style="width: 60px;"/> <img src="{{ url_for('static', filename='img/arrow_right.svg') }}" style="width: 60px;"/>&nbsp;{{ bitcoin_svg('main', 60) }}<br>
<br>Configue your node</h1>
<p>QuickSync?&nbsp;&nbsp;
<label class="switch">
<input type="checkbox" name="quicksync" id="quicksync" onchange="toggleNodeTypeSelect()" checked>
<span class="slider"></span>
</label>&nbsp;&nbsp;
<tool-tip title="What is QuickSync?">
QuickSync will set up your new node from a trusted state of the blockchain.<br><br>
This means all block validation will be skipped until the most recent block in the snapshot.<br><br>
Later blocks will be normally validated like with a regular full node.
</tool-tip>
</p>
<div class="hidden" id="select-node-type-div">
<br>
<h1>Select node type:</h1>
<div class="row">
<label style="position: relative;">
<input type="radio" name="node-type" id="node-type-archival" class="hidden" value="archival">
<div class="small-card radio">
<span style="font-size: 1.2em; font-weight: bold;">Archival Node</span><br>
Requires about 400GB of available storage
</div>
</label>
<label style="position: relative;">
<input type="radio" name="node-type" class="hidden" value="pruned" checked>
<div class="small-card radio">
<span style="font-size: 1.2em; font-weight: bold;">Pruned Node</span><br>
Requires about 10GB of available storage<br><br>
</div>
</label>
</div>
</div>
<p class="hidden warning" id="step4-helper-text">
Now that you have Bitcoin Core installed, your next step will be to sync with the network.<br><br>
Specter allows you to choose between normal syncing (IBD) - which will download and validate the entire history of the blockchain, or downloading a trusted snapshot of the current state of the network.<br><br>
This snapshot is produced and signed by Specter team - which means you will be trusting it to get the present state of the blockchain, instead of verifying the history yourself.<br><br>
It takes about 5GB to download and will setup a fully synced pruned node for you to use.
You can also disable the QuickSync option, and choose between setting up an archival node (which keeps the entire history) or a pruned node.<br><br>
<b>Please note: it is recommended to use a full archival node, or if storage is a constraint, a pruned node synced from scratch. The option for QuickSync is useful for getting started quickly, but is not recommended in the long term or when using Specter with large amounts.</b>
</p>
<div class="row center">
<p class="explorer-link" id="step4-helper" onclick="toggleHelper('step4')">Unsure? Click here for more info!</p>
<a class="explorer-link hidden" id="step4-support" href="https://t.me/spectersupport" target="_blank">Still need help?</a>
</div>
<br><br>
<div class="row">
<button type="button" class="btn wizard-btn action" id="setup-bitcoind-dir-button" onclick="setupBitcoindDirectory()">Start Syncing!</button>
</div>
</div>
<div class="card center hidden" id="step5">
<img style="width: 100px;" src="{{ url_for('static', filename='img/party.png') }}"/><br><br>
<h1 style="font-size: 1.8em;">Setup Completed Successfully!</h1>
<p>Your new node is up and running!</p><br>
<a href="{{url_for('about')}}" class="btn wizard-btn action">Done</a>
</div>
<br>
<button type="button" class="btn hidden action" id="show-progress-details" style="max-height: 36px;" onclick="showPageOverlay('stage-progress-details');">View Progress</button>
<h1 id="stage-progress-details" class="hidden" style="margin: auto;"></h1>
<br><br>
{% endblock %}
{% block scripts %}
<script>
let currentStep = parseInt('{{step}}');
let torSetup = false;
document.addEventListener("DOMContentLoaded", function(){
showStep(currentStep);
});
function showStep(step) {
currentStep = step;
document.getElementById('step0').classList.add('hidden');
document.getElementById('step1').classList.add('hidden');
document.getElementById('step2').classList.add('hidden');
document.getElementById('step3').classList.add('hidden');
document.getElementById('step4').classList.add('hidden');
document.getElementById('step5').classList.add('hidden');
document.getElementById(`step${step}`).classList.remove('hidden');
}
function toggleHelper(step) {
let helper = document.getElementById(step + "-helper");
let helperText = document.getElementById(step + "-helper-text");
let support = document.getElementById(step + "-support");
if (helperText.classList.contains("hidden")) {
helper.innerText = "Got it!";
helperText.classList.remove("hidden");
support.classList.remove("hidden");
helper.classList.add("helper-btn");
support.classList.add("helper-btn");
} else {
helper.innerText = "Unsure? Click here for more info!";
helperText.classList.add("hidden");
support.classList.add("hidden");
helper.classList.remove("helper-btn");
support.classList.remove("helper-btn");
}
}
function toggleAdvanced(step) {
let advancedButton = document.getElementById(`toggle_advanced_${step}`);
let advancedSettings = document.getElementById(`advanced_settings_${step}`);
if (advancedSettings.classList.contains("hidden")) {
advancedSettings.classList.remove("hidden");
advancedButton.innerHTML = 'Advanced &#9660;';
} else {
advancedSettings.classList.add("hidden");
advancedButton.innerHTML = 'Advanced &#9654;';
}
}
function toggleNodeTypeSelect() {
let nodeTypeDiv = document.getElementById('select-node-type-div');
if (document.getElementById('quicksync').checked) {
nodeTypeDiv.classList.add('hidden');
} else {
nodeTypeDiv.classList.remove('hidden');
}
}
async function setupTorDaemon() {
torSetup = true;
let url = "{{ url_for('setup_tor') }}"
var formData = new FormData();
formData.append('csrf_token', '{{ csrf_token() }}');
try {
const response = await fetch(
url,
{
method: 'POST',
body: formData
}
);
if(response.status != 200){
showError(await response.text());
return;
}
const jsonResponse = await response.json();
if ("success" in jsonResponse) {
showNotification(jsonResponse.success);
document.getElementById('setup-tor-button').classList.add('hidden');
startProgressCheck();
return;
} if (jsonResponse.error == "tor is already installed") {
showNotification('Tor already installed, skipped step.');
showStep(++currentStep);
return;
}
showError(jsonResponse.error, 4000);
} catch(e) {
showError("Failed to download Tor daemon...");
showError(e);
}
}
async function setupBitcoind() {
torSetup = false;
let url = "{{ url_for('setup_bitcoind') }}"
var formData = new FormData();
formData.append('csrf_token', '{{ csrf_token() }}');
formData.append('bitcoin_core_datadir', document.getElementById('bitcoin_core_datadir').value);
try {
const response = await fetch(
url,
{
method: 'POST',
body: formData
}
);
if(response.status != 200){
showError(await response.text());
return;
}
const jsonResponse = await response.json();
if ("success" in jsonResponse) {
showNotification(jsonResponse.success);
document.getElementById('setup-bitcoind-button').classList.add('hidden');
startProgressCheck();
return;
} if (jsonResponse.error == "bitcoind already installed") {
showNotification('Bitcoin Core already installed, skipped step.');
showStep(++currentStep);
return;
}
showError(jsonResponse.error, 4000);
} catch(e) {
showError("Failed to download Bitcoin Core...");
showError(e);
}
}
async function setupBitcoindDirectory() {
torSetup = false;
let url = "{{ url_for('setup_bitcoind_directory') }}"
var formData = new FormData();
formData.append('csrf_token', '{{ csrf_token() }}');
formData.append('quicksync', document.getElementById('quicksync').checked);
formData.append('nodetype', document.getElementById('node-type-archival').checked ? 'archival' : 'pruned');
try {
const response = await fetch(
url,
{
method: 'POST',
body: formData
}
);
if(response.status != 200){
showError(await response.text());
return;
}
const jsonResponse = await response.json();
if ("success" in jsonResponse) {
showNotification(jsonResponse.success);
document.getElementById('setup-bitcoind-dir-button').classList.add('hidden');
startProgressCheck();
return;
}
showError(jsonResponse.error, 4000);
} catch(e) {
showError("Failed to start Bitcoin Core...");
showError(e);
}
}
function startProgressCheck() {
async function fetchStageProgress() {
try {
let url = "{{ url_for('get_node_setup_status') }}"
if (torSetup) {
url = "{{ url_for('get_tor_setup_status') }}"
}
const response = await fetch(
url,
{
method: 'GET'
}
);
let result = await response.json();
if (result.error) {
showError('Encountered an error:' + result.error);
document.getElementById('stage-progress-details').classList.add('hidden');
hidePageOverlay('stage-progress-details');
return;
}
let stage = result.stage;
let progress = parseFloat(result.stage_progress);
if (progress == -1) {
showNotification('Finished successfully');
showStep(++currentStep);
document.getElementById('stage-progress-details').classList.add('hidden');
document.getElementById('show-progress-details').classList.add('hidden');
hidePageOverlay('stage-progress-details');
return;
}
document.getElementById('stage-progress-details').innerHTML = `${stage}<br>`;
if (progress > 0) {
document.getElementById('stage-progress-details').innerHTML += `<br><br><b style="font-size:0.8em;">Progress: ${progress}%</b>`;
}
setTimeout(fetchStageProgress, 1000);
} catch(e) {
console.log('Caught error:', e);
return { success: false, error: e };
}
}
document.getElementById('stage-progress-details').classList.remove('hidden');
document.getElementById('show-progress-details').classList.remove('hidden');
showPageOverlay('stage-progress-details');
document.getElementById('stage-progress-details').innerHTML = 'Stating up...';
setTimeout(fetchStageProgress, 1000);
}
</script>
{% endblock %}

View file

@ -7,7 +7,48 @@
<h1 id="title" class="settings-title">Bitcoin Core settings - Specter Desktop {{ current_version }}</h1>
{% from 'settings/components/settings_menu.jinja' import settings_menu %}
{{ settings_menu('bitcoin_core', current_user) }}
<div class="card" style="margin: 20px auto 120px;">
<nav class="row">
<button type="button" id="external_node_view_btn" class="btn radio left checked" onclick="setNodeTypeView('external')"> Existing Node </button>
<button type="button" id="internal_node_view_btn" class="btn radio right" onclick="setNodeTypeView('internal')"> Built In Node </button>
</nav>
<div id="internal_node_setup_view" class="card" style="margin: 20px auto 120px; text-align: center;">
{% if external_node %}
<button type="submit" class="btn centered" name="action" value="useinternal">Switch to built-in node</button>
<br><br>
{% endif %}
{% if bitcoind_exists %}
<h2>Built in Bitcoin Node Status: {% if is_running %}Running{% else %}Down{% endif %}</h2><br>
<span class="note" style="font-size: 1.3em;">Bitcoin Core Version: {{specter.config.bitcoind_internal_version}}</span>
<br><br>
{% if not external_node %}
{% if is_running %}
<button type="submit" class="btn centered" name="action" value="stopbitcoind">Stop Bitcoin Core</button>
{% else %}
<button type="submit" class="btn centered" name="action" value="startbitcoind">Start Bitcoin Core</button>
<p class="warning">
If this is your first time starting the node, and you haven't used the QuickSync, your node will start with Initial Block Downloading (IBD) to sync with the network.<br>
This process of IBD syncing may take several days. If you'd like the QuickSync option, please click on it before starting your node, or if it's running, stop it and the click the QuickSync.<br>
<b>(Warning: QuickSync will override any existing data of your Bitcoin Node!)</b>
</p>
{% endif %}
{% else %}
<p>Please switch node type to start the built in node.</p>
{% endif %}
<br><br>
{% else %}
<div class="card">
<h1>Setup Bitcoin Core Node</h1><br>
<a href="{{url_for('node_setup_wizard', step=3)}}" class="btn wizard-btn" style="width: 200px; margin: auto;">Setup new Bitcoin node</a>
</div>
{% endif %}
</div>
<div id="external_node_setup_view" class="card" style="margin: 20px auto 120px;">
{% if not external_node %}
<button type="submit" class="btn centered" name="action" value="useexternal">Switch to external node</button>
<br><br>
{% endif %}
<h2>Bitcoin JSON-RPC configuration</h2>
<br>
<label style="height: 25px; vertical-align: sub;">Auto-detect:&nbsp;</label>
@ -140,6 +181,7 @@
{% block scripts %}
<script>
document.addEventListener("DOMContentLoaded", function(){
setNodeTypeView("{{ node_view }}")
var autodetect = document.getElementById("autodetect");
if (autodetect != null) {
function toggleAutoDetectMode() {
@ -175,6 +217,20 @@
toggleAutoDetectMode();
}
});
function setNodeTypeView(nodeType) {
if (nodeType == "external") {
document.getElementById("external_node_view_btn").classList.add('checked');
document.getElementById("internal_node_view_btn").classList.remove('checked');
document.getElementById("external_node_setup_view").classList.remove('hidden');
document.getElementById("internal_node_setup_view").classList.add('hidden');
} else {
document.getElementById("external_node_view_btn").classList.remove('checked');
document.getElementById("internal_node_view_btn").classList.add('checked');
document.getElementById("external_node_setup_view").classList.add('hidden');
document.getElementById("internal_node_setup_view").classList.remove('hidden');
}
}
</script>
<script type="text/javascript">

View file

@ -10,6 +10,24 @@
<img style="width: 25px; margin-right: 7px; vertical-align: middle;" src="{{ url_for('static', filename='img/tor.svg') }}"/><span style="vertical-align: bottom; margin-right: 10px;">
Tor configurations
</h1>
<div class="card center">
<h2>Specer built-in Tor</h2><br>
{% if torbrowser_installed %}
<h2><b>Status: {% if torbrowser_running %}Running{% else %}Down{% endif %}</b></h2>
<br>
<div class="row">
<button type="submit" class="btn" name="action" value="uninstalltor">Uninstall Tor</button>&nbsp;&nbsp;
{% if torbrowser_running %}
<button type="submit" class="btn" name="action" value="stoptor">Stop Tor</button>
{% else %}
<button type="submit" class="btn" name="action" value="starttor">Start Tor</button>
{% endif %}
</div>
{% else %}
<button type="button" class="btn centered" id="setup-tor-button" onclick="setupTorDaemon()">Setup Tor!</button>
{% endif %}
</div>
<br><br>
Use Tor proxy running at:
<input id="proxy-url" name="proxy_url" value="{{ proxy_url }}" type="url" placeholder="i.e. socks5://127.0.0.1:9050" />
<br>
@ -72,6 +90,83 @@
{% endif %}
</div>
</form>
<h1 id="stage-progress-details" class="hidden" style="margin: auto;"></h1>
{% endblock %}
{% block scripts %}
<script>
async function setupTorDaemon() {
let url = "{{ url_for('setup_tor') }}"
var formData = new FormData();
formData.append('csrf_token', '{{ csrf_token() }}');
try {
const response = await fetch(
url,
{
method: 'POST',
body: formData
}
);
if(response.status != 200){
showError(await response.text());
return;
}
const jsonResponse = await response.json();
if ("success" in jsonResponse) {
showNotification(jsonResponse.success);
document.getElementById('setup-tor-button').classList.add('hidden');
startProgressCheck();
return;
} if (jsonResponse.error == "tor is already installed") {
showNotification('Tor daemon already installed.');
return;
}
showError(jsonResponse.error, 4000);
} catch(e) {
showError("Failed to download Tor daemon...");
showError(e);
}
}
function startProgressCheck() {
async function fetchStageProgress() {
try {
let url="{{ url_for('get_tor_setup_status') }}"
const response = await fetch(
url,
{
method: 'GET'
}
);
let result = await response.json();
let stage = result.stage;
if (result.error) {
showError('Encountered an error:' + result.error);
document.getElementById('stage-progress-details').classList.add('hidden');
hidePageOverlay('stage-progress-details');
return;
}
let progress = parseFloat(result.stage_progress);
if (progress == -1) {
showNotification('Finished successfully');
document.getElementById('stage-progress-details').classList.add('hidden');
hidePageOverlay('stage-progress-details');
location.reload();
return;
}
document.getElementById('stage-progress-details').innerHTML = `${stage}<br>`;
if (progress > 0) {
document.getElementById('stage-progress-details').innerHTML += `<br><br><b style="font-size:0.8em;">Progress: ${progress}%</b>`;
}
setTimeout(fetchStageProgress, 1000);
} catch(e) {
console.log('Caught error:', e);
return { success: false, error: e };
}
}
document.getElementById('stage-progress-details').classList.remove('hidden');
showPageOverlay('stage-progress-details');
document.getElementById('stage-progress-details').innerHTML = 'Stating up...';
setTimeout(fetchStageProgress, 1000);
}
</script>
{% endblock %}

View file

@ -24,7 +24,8 @@
</div>
<script>
document.addEventListener("DOMContentLoaded", function(){
document.getElementById('export_wallet_file').href = 'data:text/json,' + encodeURIComponent('{{ wallet.account_map }}');
let accountMap = encodeURIComponent(`{{ wallet.account_map|tojson }}`)
document.getElementById('export_wallet_file').href = "data:text/json;charset=utf-8," + accountMap.substring(3, accountMap.length - 3);
});
document.addEventListener("DOMContentLoaded", function(){
document.getElementById('electrum_export').href = 'data:text/plain,' + encodeURIComponent('{{ wallet.get_electrum_file()|tojson }}');

View file

@ -0,0 +1,51 @@
import logging
import subprocess
import platform
import os
import signal
logger = logging.getLogger(__name__)
class TorDaemonController:
""" A class controlling the tor-daemon process directly on the machine """
def __init__(
self,
tor_daemon_path,
tor_config_path,
):
self.tor_daemon_path = tor_daemon_path
self.tor_config_path = tor_config_path
self.tor_daemon_proc = None
def start_tor_daemon(self, cleanup_at_exit=True):
if self.is_running():
return
self.tor_daemon_proc = subprocess.Popen(
f'{"exec " if platform.system() != "Windows" else ""}"{self.tor_daemon_path}" --defaults-torrc {self.tor_config_path}',
shell=True,
)
logger.debug(
"Running tor-daemon process with pid {}".format(self.tor_daemon_proc.pid)
)
def get_hashed_password(self, password):
hashed_pw = subprocess.check_output(
f'{"exec " if platform.system() != "Windows" else ""}"{self.tor_daemon_path}" --hash-password {password}',
shell=True,
).decode("utf8")
if not hashed_pw.startswith("16:"):
return "16:" + hashed_pw.split("\n16:")[1]
return hashed_pw
def is_running(self):
return self.tor_daemon_proc and self.tor_daemon_proc.poll() is None
def stop_tor_daemon(self):
if self.tor_daemon_proc:
if platform.system() == "Windows":
subprocess.run("Taskkill /IM tor.exe /F")
self.tor_daemon_proc.terminate()

View file

@ -0,0 +1,199 @@
import os, time, requests, secrets, platform, tarfile, zipfile, sys
from ..bitcoind import BitcoindPlainController
import pgpy
from pathlib import Path
from .sha256sum import sha256sum
import logging
from .file_download import download_file
logger = logging.getLogger(__name__)
def setup_bitcoind_thread(specter=None, internal_bitcoind_version=""):
try:
BITCOIND_OS_SUFFIX = {
"Windows": "win64.zip",
"Linux": "x86_64-linux-gnu.tar.gz",
"Darwin": "osx64.tar.gz",
}
bitcoind_url = f"https://bitcoincore.org/bin/bitcoin-core-{internal_bitcoind_version}/bitcoin-{internal_bitcoind_version}-{BITCOIND_OS_SUFFIX[platform.system()]}"
packed_name = os.path.join(
specter.data_folder,
f"bitcoind-{BITCOIND_OS_SUFFIX[platform.system()]}",
)
download_file(
specter,
bitcoind_url,
packed_name,
"bitcoind",
"Downloading Bitcoin Core release files...",
)
bitcoind_sha256sums_url = f"https://bitcoincore.org/bin/bitcoin-core-{internal_bitcoind_version}/SHA256SUMS.asc"
bitcoind_sha256sums_file = os.path.join(
specter.data_folder, "bitcoind-sha256sums.asc"
)
download_file(
specter,
bitcoind_sha256sums_url,
bitcoind_sha256sums_file,
"bitcoind",
"Downloading Bitcoin Core signatures...",
)
specter.config["bitcoind_setup"]["stage"] = "Verifying signatures..."
specter._save()
with open(bitcoind_sha256sums_file, "r") as f:
signed_sums = f.read()
bitcoind_release_pgp_key, _ = pgpy.PGPKey.from_file(
os.path.join(sys._MEIPASS, "static/bitcoin-release-pubkey.asc")
if getattr(sys, "frozen", False)
else Path(__file__).parent / "../static/bitcoin-release-pubkey.asc"
)
bitcoind_sha256sums_msg = pgpy.PGPMessage.from_file(
bitcoind_sha256sums_file
)
if not bitcoind_release_pgp_key.verify(bitcoind_sha256sums_msg):
raise Exception("Failed to verify Bitcoin Core PGP signature")
bitcoind_hash = sha256sum(packed_name)
if bitcoind_hash not in signed_sums:
raise Exception("Failed to verify bitcoind hash is in SHA265SUMS.asc")
bitcoin_binaries_folder = os.path.join(specter.data_folder, "bitcoin-binaries")
if packed_name.endswith("tar.gz"):
with tarfile.open(packed_name, "r:gz") as so:
so.extractall(specter.data_folder)
else:
with zipfile.ZipFile(packed_name, "r") as zip_ref:
zip_ref.extractall(specter.data_folder)
os.rename(
os.path.join(specter.data_folder, f"bitcoin-{internal_bitcoind_version}"),
bitcoin_binaries_folder,
)
os.remove(packed_name)
specter.config["rpc"]["user"] = "bitcoin"
specter.config["rpc"]["password"] = secrets.token_urlsafe(16)
specter._save()
if not os.path.exists(specter.config["rpc"]["datadir"]):
os.makedirs(specter.config["rpc"]["datadir"])
with open(
os.path.join(specter.config["rpc"]["datadir"], "bitcoin.conf"),
"w",
) as file:
file.write(f'\nrpcuser={specter.config["rpc"]["user"]}')
file.write(f'\nrpcpassword={specter.config["rpc"]["password"]}')
file.write(f"\nserver=1")
file.write(f"\nlisten=1")
file.write(f"\nproxy=127.0.0.1:9050")
file.write(f"\nbind=127.0.0.1")
file.write(f"\ntorcontrol=127.0.0.1:9051")
file.write(f"\ntorpassword={specter.config['torrc_password']}")
specter.config["bitcoind_internal_version"] = internal_bitcoind_version
specter._save()
except Exception as e:
logger.error(f"Failed to install Bitcoin Core. Error: {e}")
specter.config["bitcoind_setup"]["error"] = str(e)
specter._save()
finally:
specter.config["bitcoind_setup"]["stage_progress"] = -1
specter._save()
def setup_bitcoind_directory_thread(specter=None, quicksync=True, pruned=True):
try:
if quicksync:
prunednode_file = os.path.join(
os.path.join(specter.data_folder, "snapshot-prunednode.zip")
)
prunednode_sha256sums_file = os.path.join(
specter.data_folder, "prunednode-sha256sums.asc"
)
download_file(
specter,
"https://prunednode.today/latest.zip",
prunednode_file,
"bitcoind",
"Downloading QuickSync files...",
)
download_file(
specter,
"https://prunednode.today/latest.signed.txt",
prunednode_sha256sums_file,
"bitcoind",
"Downloading Quicksync signature...",
)
specter.config["bitcoind_setup"]["stage"] = "Verifying signatures..."
specter._save()
with open(prunednode_sha256sums_file, "r") as f:
signed_sums = f.read()
prunednode_release_pgp_key, _ = pgpy.PGPKey.from_file(
os.path.join(
sys._MEIPASS, "static/pruned-node-today-release-pubkey.asc"
)
if getattr(sys, "frozen", False)
else Path(__file__).parent
/ "../static/pruned-node-today-release-pubkey.asc"
)
prunednode_sha256sums_msg = pgpy.PGPMessage.from_file(
prunednode_sha256sums_file
)
if not prunednode_release_pgp_key.verify(prunednode_sha256sums_msg):
raise Exception("Failed to verify prunednode.today PGP signature")
prunednode_hash = sha256sum(prunednode_file)
if prunednode_hash not in signed_sums:
raise Exception(
"Failed to verify prunednode.today hash is in SHA265SUMS.asc"
)
with zipfile.ZipFile(prunednode_file, "r") as zip_ref:
zip_ref.extractall(
os.path.expanduser(specter.config["rpc"]["datadir"])
)
os.remove(prunednode_file)
with open(
os.path.join(specter.config["rpc"]["datadir"], "bitcoin.conf"),
"a",
) as file:
file.write(f'\nrpcuser={specter.config["rpc"]["user"]}')
file.write(f'\nrpcpassword={specter.config["rpc"]["password"]}')
else:
with open(
os.path.join(specter.config["rpc"]["datadir"], "bitcoin.conf"),
"a",
) as file:
if pruned:
file.write(f"\nprune=1000")
else:
file.write(f"\nblockfilterindex=1")
specter.config["bitcoind_setup"]["stage"] = "Starting up Bitcoin Core..."
specter._save()
specter.bitcoind = BitcoindPlainController(
bitcoind_path=specter.bitcoind_path,
rpcport=8332,
network="mainnet",
rpcuser=specter.config["rpc"]["user"],
rpcpassword=specter.config["rpc"]["password"],
)
specter.bitcoind.start_bitcoind(
datadir=os.path.expanduser(specter.config["rpc"]["datadir"])
)
specter.set_bitcoind_pid(specter.bitcoind.bitcoind_proc.pid)
specter.update_use_external_node(False)
time.sleep(15)
success = specter.update_rpc(
port=8332,
autodetect=True,
user=specter.config["rpc"]["user"],
password=specter.config["rpc"]["password"],
)
if not success:
specter.config["bitcoind_setup"][
"stage"
] = "Failed to start Bitcoin Core..."
specter._save()
specter.check()
except Exception as e:
logger.error(f"Failed to setup Bitcoin Core. Error: {e}")
specter.config["bitcoind_setup"]["error"] = str(e)
specter._save()
finally:
specter.config["bitcoind_setup"]["stage_progress"] = -1
specter._save()

View file

@ -0,0 +1,15 @@
import logging
logger = logging.getLogger(__name__)
def handle_exception(exception, user=None):
""" prints the exception and most important the stacktrace """
logger.error("Unexpected error:")
logger.error(
"----START-TRACEBACK-----------------------------------------------------------------"
)
logger.exception(exception) # the exception instance
logger.error(
"----END---TRACEBACK-----------------------------------------------------------------"
)

View file

@ -0,0 +1,23 @@
import os
def download_file(specter, url, file_path, setup_status_name, setup_status_stage):
response = specter.requests_session().get(url, stream=True)
with open(file_path, "wb") as f:
total_length = float(response.headers["content-length"])
downloaded = 0.0
old_progress = 0
specter.config[f"{setup_status_name}_setup"]["stage"] = setup_status_stage
specter._save()
for chunk in response.iter_content(chunk_size=4096):
downloaded += len(chunk)
f.write(chunk)
new_progress = int((downloaded / total_length) * 10000) / 100
if new_progress > old_progress:
if new_progress > 100:
new_progress = 100
old_progress = new_progress
specter.config[f"{setup_status_name}_setup"][
"stage_progress"
] = new_progress
specter._save()

View file

@ -0,0 +1,55 @@
#!/usr/bin/env python3
# Copyright (c) 2015-2018 The Bitcoin Core developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
from argparse import ArgumentParser
from base64 import urlsafe_b64encode
from binascii import hexlify
from getpass import getpass
from os import urandom
import hmac
def generate_salt(size):
"""Create size byte hex salt"""
return hexlify(urandom(size)).decode()
def generate_password():
"""Create 32 byte b64 password"""
return urlsafe_b64encode(urandom(32)).decode("utf-8")
def password_to_hmac(salt, password):
m = hmac.new(bytearray(salt, "utf-8"), bytearray(password, "utf-8"), "SHA256")
return m.hexdigest()
def main():
parser = ArgumentParser(description="Create login credentials for a JSON-RPC user")
parser.add_argument("username", help="the username for authentication")
parser.add_argument(
"password",
help='leave empty to generate a random password or specify "-" to prompt for password',
nargs="?",
)
args = parser.parse_args()
if not args.password:
args.password = generate_password()
elif args.password == "-":
args.password = getpass()
# Create 16 byte hex salt
salt = generate_salt(16)
password_hmac = password_to_hmac(salt, args.password)
print("String to be appended to bitcoin.conf:")
print("rpcauth={0}:{1}${2}".format(args.username, salt, password_hmac))
print("Your password:\n{0}".format(args.password))
if __name__ == "__main__":
main()

View file

@ -0,0 +1,11 @@
import hashlib
def sha256sum(filename):
h = hashlib.sha256()
b = bytearray(128 * 1024)
mv = memoryview(b)
with open(filename, "rb", buffering=0) as f:
for n in iter(lambda: f.readinto(mv), 0):
h.update(mv[:n])
return h.hexdigest()

View file

@ -1,16 +1,26 @@
import os
import os, platform
from stem.control import Controller
def get_tor_daemon_suffix():
if platform.system() == "Darwin":
return ".real"
elif platform.system() == "Windows":
return ".exe"
return ""
def start_hidden_service(app):
app.controller.reconnect()
app.specter.tor_controller.authenticate(
password=app.specter.config.get("torrc_password", "")
)
key_path = os.path.abspath(
os.path.join(app.specter.data_folder, ".tor_service_key")
)
app.tor_service_id = None
if not os.path.exists(key_path):
service = app.controller.create_ephemeral_hidden_service(
service = app.specter.tor_controller.create_ephemeral_hidden_service(
{app.tor_port: app.port}, await_publication=True
)
app.tor_service_id = service.service_id
@ -25,7 +35,7 @@ def start_hidden_service(app):
with open(key_path) as key_file:
key_type, key_content = key_file.read().split(":", 1)
service = app.controller.create_ephemeral_hidden_service(
service = app.specter.tor_controller.create_ephemeral_hidden_service(
{app.tor_port: app.port},
key_type=key_type,
key_content=key_content,
@ -62,13 +72,15 @@ def start_hidden_service(app):
def stop_hidden_services(app):
try:
app.controller.reconnect()
hidden_services = app.controller.list_ephemeral_hidden_services()
app.specter.tor_controller.authenticate(
password=app.specter.config.get("torrc_password", "")
)
hidden_services = app.specter.tor_controller.list_ephemeral_hidden_services()
print(" * Shutting down our hidden service")
for tor_service_id in hidden_services:
app.controller.remove_ephemeral_hidden_service(tor_service_id)
app.specter.tor_controller.remove_ephemeral_hidden_service(tor_service_id)
# Sanity
if len(app.controller.list_ephemeral_hidden_services()) != 0:
if len(app.specter.tor_controller.list_ephemeral_hidden_services()) != 0:
print(" * Failed to shut down our hidden services...")
else:
print(" * Hidden services were shut down successfully")

View file

@ -0,0 +1,132 @@
import os, time, requests, secrets, platform, tarfile, zipfile, sys, subprocess, shutil, stat, zipfile, logging
import pgpy
from pathlib import Path
from .sha256sum import sha256sum
from .file_download import download_file
logger = logging.getLogger(__name__)
def copytree(src, dst, symlinks=False, ignore=None):
for item in os.listdir(src):
s = os.path.join(src, item)
d = os.path.join(dst, item)
if os.path.isdir(s):
shutil.copytree(s, d, symlinks, ignore)
else:
shutil.copy2(s, d)
def setup_tor_thread(specter=None):
try:
specter.config["torbrowser_setup"]["stage"] = "Starting Tor setup process..."
specter._save()
TOR_OS_SUFFIX = {
"Windows": "tor-win64-0.4.5.6.zip",
"Linux": "tor-browser-linux64-10.0.12_en-US.tar.xz",
"Darwin": "TorBrowser-10.0.12-osx64_en-US.dmg",
}
torbrowser_url = f"https://www.torproject.org/dist/torbrowser/10.0.12/{TOR_OS_SUFFIX[platform.system()]}"
packed_name = os.path.join(
specter.data_folder,
TOR_OS_SUFFIX[platform.system()],
)
torbrowser_signed_url = f"https://www.torproject.org/dist/torbrowser/10.0.12/{TOR_OS_SUFFIX[platform.system()]}.asc"
torbrowser_signed_file = os.path.join(
specter.data_folder, f"{TOR_OS_SUFFIX[platform.system()]}.asc"
)
download_file(
specter,
torbrowser_url,
packed_name,
"torbrowser",
"Downloading Tor release files...",
)
download_file(
specter,
torbrowser_signed_url,
torbrowser_signed_file,
"torbrowser",
"Downloading Tor signatures...",
)
specter.config["torbrowser_setup"]["stage"] = "Verifying signatures..."
specter._save()
torbrowser_release_pgp_key, _ = pgpy.PGPKey.from_file(
os.path.join(sys._MEIPASS, "static/torbrowser-release-pubkey.asc")
if getattr(sys, "frozen", False)
else Path(__file__).parent / "../static/torbrowser-release-pubkey.asc"
)
torbrowser_file_sig = pgpy.PGPSignature.from_file(torbrowser_signed_file)
with open(packed_name, "rb") as binary_file:
signed_data = binary_file.read()
if not torbrowser_release_pgp_key.verify(signed_data, torbrowser_file_sig):
raise Exception("Failed to verify Tor PGP signature")
os.makedirs(os.path.join(specter.data_folder, "tor-binaries"), exist_ok=True)
if packed_name.endswith(".dmg"):
result = subprocess.run(
[f'hdiutil attach "{packed_name}"'], shell=True, capture_output=True
)
if result.stderr == b"hdiutil: attach failed - no mountable file systems\n":
os.remove(packed_name)
return
torbrowser_source_path = os.path.join(
"/Volumes", "Tor Browser", "Tor Browser.app", "Contents", "MacOS", "Tor"
)
copytree(
src=torbrowser_source_path,
dst=os.path.join(specter.data_folder, "tor-binaries"),
)
disk_image_path = "/Volumes/Tor\ Browser"
subprocess.run([f"hdiutil detach {disk_image_path}"], shell=True)
elif packed_name.endswith(".zip"):
with zipfile.ZipFile(packed_name) as zip_file:
for file in zip_file.filelist:
if file.filename.endswith("dll") or file.filename.endswith("exe"):
destination_exe = os.path.join(
os.path.join(specter.data_folder, "tor-binaries"),
file.filename.split("/")[-1],
)
with zip_file.open(file.filename) as zf, open(
destination_exe, "wb"
) as f:
shutil.copyfileobj(zf, f)
elif packed_name.endswith(".tar.xz"):
with tarfile.open(packed_name) as tar:
tor_files = [
"libcrypto.so.1.1",
"libevent-2.1.so.7",
"libssl.so.1.1",
"tor",
]
for tor_file in tor_files:
file_name = "tor-browser_en-US/Browser/TorBrowser/Tor/" + tor_file
destination_file = os.path.join(
os.path.join(specter.data_folder, "tor-binaries"), tor_file
)
extracted_file = tar.extractfile(file_name)
with open(destination_file, "wb") as f:
shutil.copyfileobj(extracted_file, f)
if tor_file == "tor":
st = os.stat(destination_file)
os.chmod(destination_file, st.st_mode | stat.S_IEXEC)
os.remove(packed_name)
if "torrc_password" not in specter.config:
specter.config["torrc_password"] = secrets.token_urlsafe(16)
specter._save()
with open(os.path.join(specter.data_folder, "torrc"), "w") as file:
file.write("ControlPort 9051")
file.write(
f"\nHashedControlPassword {specter.tor_daemon.get_hashed_password(specter.config['torrc_password'])}"
)
specter.tor_daemon.start_tor_daemon()
specter.update_tor_controller()
except Exception as e:
logger.error(f"Failed to install Tor. Error: {e}")
specter.config["torbrowser_setup"]["error"] = str(e)
specter._save()
finally:
specter.config["torbrowser_setup"]["stage_progress"] = -1
specter._save()

View file

@ -10,9 +10,9 @@ import time
import pytest
import docker
from cryptoadvance.specter.bitcoind import (
BitcoindDockerController,
BitcoindPlainController,
)
from cryptoadvance.specter.bitcoind_docker import BitcoindDockerController
from cryptoadvance.specter.device_manager import DeviceManager
from cryptoadvance.specter.specter import Specter
from cryptoadvance.specter.server import create_app, init_app
@ -48,6 +48,8 @@ def instantiate_bitcoind_controller(docker, request, rpcport=18543, extra_args=[
# logging.getLogger().setLevel(logging.DEBUG)
requested_version = request.config.getoption("--bitcoind-version")
if docker:
from cryptoadvance.specter.bitcoind_docker import BitcoindDockerController
bitcoind_controller = BitcoindDockerController(
rpcport=rpcport, docker_tag=requested_version
)

View file

@ -2,8 +2,8 @@ import logging
import os
from cryptoadvance.specter.util.shell import which
from cryptoadvance.specter.bitcoind import BitcoindPlainController
from cryptoadvance.specter.bitcoind import BitcoindDockerController
from cryptoadvance.specter.bitcoind import fetch_wallet_addresses_for_mining
from cryptoadvance.specter.bitcoind_docker import BitcoindDockerController
def test_bitcoinddocker_running(caplog, docker, request):
@ -13,6 +13,8 @@ def test_bitcoinddocker_running(caplog, docker, request):
caplog.set_level(logging.DEBUG, logger="cryptoadvance.specter")
requested_version = request.config.getoption("--bitcoind-version")
if docker:
from cryptoadvance.specter.bitcoind_docker import BitcoindDockerController
my_bitcoind = BitcoindDockerController(
rpcport=18999, docker_tag=requested_version
) # completly different port to not interfere