diff --git a/.gitignore b/.gitignore index 810e854..2603f70 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ -manager/target/ -**/*.rs.bk *.s9pk +startos/*.js +node_modules/ .DS_Store .vscode/ -scripts/embassy.js -docker-images/ +docker-images +javascript \ No newline at end of file diff --git a/.gitmodules b/.gitmodules index fabdfe5..94204a9 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "bitcoin"] path = bitcoin - url = https://github.com/bitcoin/bitcoin + url = https://github.com/bitcoin/bitcoin/ diff --git a/.github/workflows/buildService.yml b/Old/.github/workflows/buildService.yml similarity index 100% rename from .github/workflows/buildService.yml rename to Old/.github/workflows/buildService.yml diff --git a/.github/workflows/releaseService.yml b/Old/.github/workflows/releaseService.yml similarity index 100% rename from .github/workflows/releaseService.yml rename to Old/.github/workflows/releaseService.yml diff --git a/Old/.gitignore b/Old/.gitignore new file mode 100644 index 0000000..810e854 --- /dev/null +++ b/Old/.gitignore @@ -0,0 +1,7 @@ +manager/target/ +**/*.rs.bk +*.s9pk +.DS_Store +.vscode/ +scripts/embassy.js +docker-images/ diff --git a/Old/.gitmodules b/Old/.gitmodules new file mode 100644 index 0000000..fabdfe5 --- /dev/null +++ b/Old/.gitmodules @@ -0,0 +1,3 @@ +[submodule "bitcoin"] + path = bitcoin + url = https://github.com/bitcoin/bitcoin diff --git a/Dockerfile b/Old/Dockerfile similarity index 100% rename from Dockerfile rename to Old/Dockerfile diff --git a/actions/reindex.sh b/Old/actions/reindex.sh similarity index 100% rename from actions/reindex.sh rename to Old/actions/reindex.sh diff --git a/actions/reindex_chainstate.sh b/Old/actions/reindex_chainstate.sh similarity index 100% rename from actions/reindex_chainstate.sh rename to Old/actions/reindex_chainstate.sh diff --git a/assets/compat/bitcoin.conf.template b/Old/assets/compat/bitcoin.conf.template similarity index 100% rename from assets/compat/bitcoin.conf.template rename to Old/assets/compat/bitcoin.conf.template diff --git a/check-rpc.sh b/Old/check-rpc.sh similarity index 100% rename from check-rpc.sh rename to Old/check-rpc.sh diff --git a/check-synced.sh b/Old/check-synced.sh similarity index 100% rename from check-synced.sh rename to Old/check-synced.sh diff --git a/docker_entrypoint.sh b/Old/docker_entrypoint.sh similarity index 100% rename from docker_entrypoint.sh rename to Old/docker_entrypoint.sh diff --git a/manager/.gitignore b/Old/manager/.gitignore similarity index 100% rename from manager/.gitignore rename to Old/manager/.gitignore diff --git a/manager/Cargo.lock b/Old/manager/Cargo.lock similarity index 100% rename from manager/Cargo.lock rename to Old/manager/Cargo.lock diff --git a/manager/Cargo.toml b/Old/manager/Cargo.toml similarity index 100% rename from manager/Cargo.toml rename to Old/manager/Cargo.toml diff --git a/manager/src/main.rs b/Old/manager/src/main.rs similarity index 100% rename from manager/src/main.rs rename to Old/manager/src/main.rs diff --git a/manifest.yaml b/Old/manifest.yaml similarity index 100% rename from manifest.yaml rename to Old/manifest.yaml diff --git a/scripts/dependencies.ts b/Old/scripts/dependencies.ts similarity index 100% rename from scripts/dependencies.ts rename to Old/scripts/dependencies.ts diff --git a/scripts/embassy.ts b/Old/scripts/embassy.ts similarity index 100% rename from scripts/embassy.ts rename to Old/scripts/embassy.ts diff --git a/scripts/services/action.ts b/Old/scripts/services/action.ts similarity index 100% rename from scripts/services/action.ts rename to Old/scripts/services/action.ts diff --git a/scripts/services/getConfig.ts b/Old/scripts/services/getConfig.ts similarity index 98% rename from scripts/services/getConfig.ts rename to Old/scripts/services/getConfig.ts index 3bcc08c..618db90 100644 --- a/scripts/services/getConfig.ts +++ b/Old/scripts/services/getConfig.ts @@ -123,13 +123,13 @@ export const getConfig: T.ExpectedExports.getConfig = async (effects) => { "zmq-enabled": { type: "boolean", name: "ZeroMQ Enabled", - description: "The ZeroMQ interface is useful for some applications which might require data related to block and transaction events from Bitcoin Core. For example, LND requires ZeroMQ be enabled for LND to get the latest block data", + description: "The ZeroMQ interface is useful for some applications which might require data related to block and transaction events from Bitcoin. For example, LND requires ZeroMQ be enabled for LND to get the latest block data", default: true, }, txindex: { type: "boolean", name: "Transaction Index", - description: "By enabling Transaction Index (txindex) Bitcoin Core will build a complete transaction index. This allows Bitcoin Core to access any transaction with commands like `gettransaction`.", + description: "By enabling Transaction Index (txindex) Bitcoin will build a complete transaction index. This allows Bitcoin to access any transaction with commands like `gettransaction`.", default: allowUnpruned, }, coinstatsindex: { @@ -302,7 +302,7 @@ export const getConfig: T.ExpectedExports.getConfig = async (effects) => { }, }, }, - pruning: { + prune: { type: "union", name: "Pruning Settings", description: diff --git a/scripts/services/migrations.ts b/Old/scripts/services/migrations.ts similarity index 100% rename from scripts/services/migrations.ts rename to Old/scripts/services/migrations.ts diff --git a/scripts/services/properties.ts b/Old/scripts/services/properties.ts similarity index 100% rename from scripts/services/properties.ts rename to Old/scripts/services/properties.ts diff --git a/scripts/services/setConfig.ts b/Old/scripts/services/setConfig.ts similarity index 100% rename from scripts/services/setConfig.ts rename to Old/scripts/services/setConfig.ts diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..96892a6 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,412 @@ +{ + "name": "bitcoind", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "bitcoind", + "dependencies": { + "@start9labs/start-sdk": "0.3.6-alpha8", + "diskusage": "^1.2.0", + "js-yaml": "^4.1.0" + }, + "devDependencies": { + "@types/js-yaml": "^4.0.5", + "@types/node": "^20.11.30", + "@vercel/ncc": "^0.38.1", + "prettier": "^3.2.5", + "typescript": "^5.4.3" + } + }, + "node_modules/@iarna/toml": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-2.2.5.tgz", + "integrity": "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==" + }, + "node_modules/@noble/curves": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.2.tgz", + "integrity": "sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw==", + "dependencies": { + "@noble/hashes": "1.4.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@start9labs/start-sdk": { + "version": "0.3.6-alpha8", + "resolved": "https://registry.npmjs.org/@start9labs/start-sdk/-/start-sdk-0.3.6-alpha8.tgz", + "integrity": "sha512-rbwPIvB2NHZoQBaBZQrfljAgUJKpGegnWgAgEBOpT0+1+PTliQ8x98IxYj7rBGyWL9DJkrRh5GlAxNe+I1YPdQ==", + "dependencies": { + "@iarna/toml": "^2.2.5", + "@noble/curves": "^1.4.0", + "@noble/hashes": "^1.4.0", + "isomorphic-fetch": "^3.0.0", + "lodash.merge": "^4.6.2", + "mime": "^4.0.3", + "ts-matches": "^5.5.1", + "yaml": "^2.2.2" + } + }, + "node_modules/@types/js-yaml": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", + "dev": true + }, + "node_modules/@types/node": { + "version": "20.14.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.11.tgz", + "integrity": "sha512-kprQpL8MMeszbz6ojB5/tU8PLN4kesnN8Gjzw349rDlNgsSzg90lAVj3llK99Dh7JON+t9AuscPPFW6mPbTnSA==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@vercel/ncc": { + "version": "0.38.1", + "resolved": "https://registry.npmjs.org/@vercel/ncc/-/ncc-0.38.1.tgz", + "integrity": "sha512-IBBb+iI2NLu4VQn3Vwldyi2QwaXt5+hTyh58ggAMoCGE6DJmPvwL3KPBWcJl1m9LYPChBLE980Jw+CS4Wokqxw==", + "dev": true, + "bin": { + "ncc": "dist/ncc/cli.js" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "node_modules/diskusage": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/diskusage/-/diskusage-1.2.0.tgz", + "integrity": "sha512-2u3OG3xuf5MFyzc4MctNRUKjjwK+UkovRYdD2ed/NZNZPrt0lqHnLKxGhlFVvAb4/oufIgQG3nWgwmeTbHOvXA==", + "hasInstallScript": true, + "dependencies": { + "es6-promise": "^4.2.8", + "nan": "^2.18.0" + } + }, + "node_modules/es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" + }, + "node_modules/isomorphic-fetch": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz", + "integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==", + "dependencies": { + "node-fetch": "^2.6.1", + "whatwg-fetch": "^3.4.1" + } + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" + }, + "node_modules/mime": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/mime/-/mime-4.0.4.tgz", + "integrity": "sha512-v8yqInVjhXyqP6+Kw4fV3ZzeMRqEW6FotRsKXjRS5VMTNIuXsdRoAvklpoRgSqXm6o9VNH4/C0mgedko9DdLsQ==", + "funding": [ + "https://github.com/sponsors/broofa" + ], + "bin": { + "mime": "bin/cli.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/nan": { + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.20.0.tgz", + "integrity": "sha512-bk3gXBZDGILuuo/6sKtr0DQmSThYHLtNCdSdXk9YkxD/jK6X2vmCyyXBBxyqZ4XcnzTyYEAThfX3DCEnLf6igw==" + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/prettier": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/ts-matches": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/ts-matches/-/ts-matches-5.5.1.tgz", + "integrity": "sha512-UFYaKgfqlg9FROK7bdpYqFwG1CJvP4kOJdjXuWoqxo9jCmANoDw1GxkSCpJgoTeIiSTaTH5Qr1klSspb8c+ydg==" + }, + "node_modules/typescript": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", + "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-fetch": { + "version": "3.6.20", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", + "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/yaml": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.5.tgz", + "integrity": "sha512-aBx2bnqDzVOyNKfsysjA2ms5ZlnjSAW2eG3/L5G/CSujfjLJTJsEw1bGw8kCf04KodQWk1pxlGnZ56CRxiawmg==", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + } + }, + "dependencies": { + "@iarna/toml": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-2.2.5.tgz", + "integrity": "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==" + }, + "@noble/curves": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.2.tgz", + "integrity": "sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw==", + "requires": { + "@noble/hashes": "1.4.0" + } + }, + "@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==" + }, + "@start9labs/start-sdk": { + "version": "0.3.6-alpha8", + "resolved": "https://registry.npmjs.org/@start9labs/start-sdk/-/start-sdk-0.3.6-alpha8.tgz", + "integrity": "sha512-rbwPIvB2NHZoQBaBZQrfljAgUJKpGegnWgAgEBOpT0+1+PTliQ8x98IxYj7rBGyWL9DJkrRh5GlAxNe+I1YPdQ==", + "requires": { + "@iarna/toml": "^2.2.5", + "@noble/curves": "^1.4.0", + "@noble/hashes": "^1.4.0", + "isomorphic-fetch": "^3.0.0", + "lodash.merge": "^4.6.2", + "mime": "^4.0.3", + "ts-matches": "^5.5.1", + "yaml": "^2.2.2" + } + }, + "@types/js-yaml": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", + "dev": true + }, + "@types/node": { + "version": "20.14.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.11.tgz", + "integrity": "sha512-kprQpL8MMeszbz6ojB5/tU8PLN4kesnN8Gjzw349rDlNgsSzg90lAVj3llK99Dh7JON+t9AuscPPFW6mPbTnSA==", + "dev": true, + "requires": { + "undici-types": "~5.26.4" + } + }, + "@vercel/ncc": { + "version": "0.38.1", + "resolved": "https://registry.npmjs.org/@vercel/ncc/-/ncc-0.38.1.tgz", + "integrity": "sha512-IBBb+iI2NLu4VQn3Vwldyi2QwaXt5+hTyh58ggAMoCGE6DJmPvwL3KPBWcJl1m9LYPChBLE980Jw+CS4Wokqxw==", + "dev": true + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "diskusage": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/diskusage/-/diskusage-1.2.0.tgz", + "integrity": "sha512-2u3OG3xuf5MFyzc4MctNRUKjjwK+UkovRYdD2ed/NZNZPrt0lqHnLKxGhlFVvAb4/oufIgQG3nWgwmeTbHOvXA==", + "requires": { + "es6-promise": "^4.2.8", + "nan": "^2.18.0" + } + }, + "es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" + }, + "isomorphic-fetch": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz", + "integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==", + "requires": { + "node-fetch": "^2.6.1", + "whatwg-fetch": "^3.4.1" + } + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "requires": { + "argparse": "^2.0.1" + } + }, + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" + }, + "mime": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/mime/-/mime-4.0.4.tgz", + "integrity": "sha512-v8yqInVjhXyqP6+Kw4fV3ZzeMRqEW6FotRsKXjRS5VMTNIuXsdRoAvklpoRgSqXm6o9VNH4/C0mgedko9DdLsQ==" + }, + "nan": { + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.20.0.tgz", + "integrity": "sha512-bk3gXBZDGILuuo/6sKtr0DQmSThYHLtNCdSdXk9YkxD/jK6X2vmCyyXBBxyqZ4XcnzTyYEAThfX3DCEnLf6igw==" + }, + "node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "requires": { + "whatwg-url": "^5.0.0" + } + }, + "prettier": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", + "dev": true + }, + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "ts-matches": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/ts-matches/-/ts-matches-5.5.1.tgz", + "integrity": "sha512-UFYaKgfqlg9FROK7bdpYqFwG1CJvP4kOJdjXuWoqxo9jCmANoDw1GxkSCpJgoTeIiSTaTH5Qr1klSspb8c+ydg==" + }, + "typescript": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", + "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==", + "dev": true + }, + "undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "whatwg-fetch": { + "version": "3.6.20", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", + "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==" + }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "yaml": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.5.tgz", + "integrity": "sha512-aBx2bnqDzVOyNKfsysjA2ms5ZlnjSAW2eG3/L5G/CSujfjLJTJsEw1bGw8kCf04KodQWk1pxlGnZ56CRxiawmg==" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..fbd6ecf --- /dev/null +++ b/package.json @@ -0,0 +1,26 @@ +{ + "name": "bitcoind", + "scripts": { + "build": "ncc build startos/index.ts -o ./", + "prettier": "prettier --write startos", + "check": "tsc --noEmit" + }, + "dependencies": { + "@start9labs/start-sdk": "0.3.6-alpha.10", + "diskusage": "^1.2.0", + "js-yaml": "^4.1.0" + }, + "devDependencies": { + "@types/js-yaml": "^4.0.5", + "@types/node": "^20.11.30", + "@vercel/ncc": "^0.38.1", + "prettier": "^3.2.5", + "typescript": "^5.4.3" + }, + "prettier": { + "trailingComma": "all", + "tabWidth": 2, + "semi": false, + "singleQuote": true + } +} diff --git a/startos/actions/config/config.ts b/startos/actions/config/config.ts new file mode 100644 index 0000000..84292dc --- /dev/null +++ b/startos/actions/config/config.ts @@ -0,0 +1,28 @@ +import { sdk } from '../../sdk' +import { read } from './read' +import { write } from './write' +import { configSpec } from './spec' + +export const config = sdk.Action.withInput( + // id + 'config', + + // metadata + async ({ effects }) => ({ + name: 'Customize Bitcoin', + description: 'Edit the bitcoin.conf configuration file', + warning: null, + allowedStatuses: 'any', + group: null, + visibility: 'enabled', + }), + + // form input specification + configSpec, + + // optionally pre-fill the input form + ({ effects }) => read(effects), + + // the execution function + ({ effects, input }) => write(input), +) diff --git a/startos/actions/config/read.ts b/startos/actions/config/read.ts new file mode 100644 index 0000000..a027d73 --- /dev/null +++ b/startos/actions/config/read.ts @@ -0,0 +1,72 @@ +import { + bitcoinConfFile, + toTypedBitcoinConf, +} from '../../file-models/bitcoin.conf' +import { ConfigSpec } from './spec' + +export async function read(effects: any): Promise { + const bitcoinConf = await bitcoinConfFile.read + .const(effects) + .then((conf) => toTypedBitcoinConf(conf || {})) + + return { + rpc: { + enable: Object.keys(bitcoinConf).includes('rpcbind'), + username: bitcoinConf.rpcuser, + password: bitcoinConf.rpcpassword, + advanced: { + auth: bitcoinConf.rpcauth, + servertimeout: bitcoinConf.rpcservertimeout, + threads: bitcoinConf.rpcthreads, + workqueue: bitcoinConf.rpcworkqueue, + }, + }, + zmqEnabled: Object.keys(bitcoinConf).includes('zmqpubrawblock'), + txindex: bitcoinConf.txindex === 1, + coinstatsindex: bitcoinConf.coinstatsindex === 1, + testnet: bitcoinConf.testnet === 1, + wallet: { + enable: bitcoinConf.disablewallet === 0, + avoidpartialspends: bitcoinConf.avoidpartialspends === 1, + discardfee: bitcoinConf.discardfee, + }, + mempool: { + persistmempool: bitcoinConf.persistmempool === 1, + maxmempool: bitcoinConf.maxmempool, + mempoolexpiry: bitcoinConf.mempoolexpiry, + mempoolfullrbf: bitcoinConf.mempoolfullrbf === 1, + permitbaremultisig: bitcoinConf.permitbaremultisig === 1, + datacarrier: bitcoinConf.datacarrier === 1, + datacarriersize: bitcoinConf.datacarriersize, + }, + peers: { + listen: bitcoinConf.listen === 1, + onlyonion: bitcoinConf.onlynet === 'onion', + v2transport: bitcoinConf.v2transport === 1, + connectpeer: Object.keys(bitcoinConf).includes('connect') + ? { + selection: 'connect' as const, + value: { + peers: bitcoinConf['connect'] || [], + }, + } + : { + selection: 'addnode' as const, + value: { + peers: bitcoinConf['addnode'] || [], + }, + }, + }, + advanced: { + prune: bitcoinConf.prune, + dbcache: bitcoinConf.dbcache, + blockfilters: { + blockfilterindex: bitcoinConf.blockfilterindex === 'basic', + peerblockfilters: bitcoinConf.peerblockfilters === 1, + }, + bloomfilters: { + peerbloomfilters: bitcoinConf.peerbloomfilters === 1, + }, + }, + } +} diff --git a/startos/actions/config/spec.ts b/startos/actions/config/spec.ts new file mode 100644 index 0000000..9a26753 --- /dev/null +++ b/startos/actions/config/spec.ts @@ -0,0 +1,469 @@ +import { once } from '@start9labs/start-sdk/cjs/lib/util/once' +import { sdk } from '../../sdk' +import * as diskusage from 'diskusage' + +const { InputSpec, Value, List, Variants } = sdk + +const diskUsage = once(() => diskusage.check('/')) + +export const configSpec = sdk.InputSpec.of({ + rpc: Value.object( + { + name: 'RPC Settings', + description: 'RPC configuration options.', + }, + InputSpec.of({ + enable: Value.toggle({ + name: 'Enable', + default: true, + description: 'Allow remote RPC requests.', + warning: null, + }), + username: Value.text({ + name: 'Username', + required: { + default: 'bitcoin', + }, + description: 'The username for connecting to Bitcoin over RPC.', + warning: + 'You will need to restart all services that depend on Bitcoin.', + masked: true, + placeholder: null, + inputmode: 'text', + patterns: [ + { + regex: '^[a-zA-Z0-9_]+$', + description: 'Must be alphanumeric (can contain underscore).', + }, + ], + minLength: null, + maxLength: null, + }), + password: Value.text({ + name: 'RPC Password', + required: { + default: { + charset: 'a-z,2-7', + len: 20, + }, + }, + description: 'The password for connecting to Bitcoin over RPC.', + warning: + 'You will need to restart all services that depend on Bitcoin.', + masked: true, + placeholder: null, + inputmode: 'text', + patterns: [ + { + regex: '^[a-zA-Z0-9_]+$', + description: 'Must be alphanumeric (can contain underscore).', + }, + ], + minLength: null, + maxLength: null, + }), + advanced: Value.object( + { + name: 'Advanced', + description: 'Advanced RPC Settings', + }, + InputSpec.of({ + auth: Value.list( + List.text( + { + name: 'Authorization', + minLength: null, + maxLength: null, + default: [], + description: + 'Username and hashed password for JSON-RPC connections. RPC clients connect using the usual http basic authentication.', + warning: null, + }, + { + masked: false, + placeholder: null, + patterns: [ + { + regex: + '^[a-zA-Z0-9_-]+:([0-9a-fA-F]{2})+\\$([0-9a-fA-F]{2})+$', + description: + 'Each item must be of the form ":$".', + }, + ], + minLength: 0, + maxLength: null, + }, + ), + ), + servertimeout: Value.number({ + name: 'Rpc Server Timeout', + description: + 'Number of seconds after which an uncompleted RPC call will time out.', + warning: null, + required: { + default: 30, + }, + min: 5, + max: 300, + step: null, + integer: true, + units: 'seconds', + placeholder: null, + }), + threads: Value.number({ + name: 'Threads', + description: + 'Set the number of threads for handling RPC calls. You may wish to increase this if you are making lots of calls via an integration.', + warning: null, + required: { + default: 16, + }, + min: 4, + max: 64, + step: null, + integer: true, + units: null, + placeholder: null, + }), + workqueue: Value.number({ + name: 'Work Queue', + description: + 'Set the depth of the work queue to service RPC calls. Determines how long the backlog of RPC requests can get before it just rejects new ones.', + warning: null, + required: { + default: 128, + }, + min: 8, + max: 256, + step: null, + integer: true, + units: 'requests', + placeholder: null, + }), + }), + ), + }), + ), + zmqEnabled: Value.toggle({ + name: 'ZeroMQ Enabled', + default: true, + description: + 'The ZeroMQ interface is useful for some applications which might require data related to block and transaction events from Bitcoin Core. For example, LND requires ZeroMQ be enabled for LND to get the latest block data', + warning: null, + }), + txindex: Value.dynamicToggle(async ({ effects }) => { + const disk = await diskUsage() + return { + name: 'Transaction Index', + default: disk.total >= 900_000_000_000, + description: + 'By enabling Transaction Index (txindex) Bitcoin Core will build a complete transaction index. This allows Bitcoin Core to access any transaction with commands like `getrawtransaction`.', + warning: null, + } + }), + coinstatsindex: Value.toggle({ + name: 'Coinstats Index', + default: false, + description: + 'Enabling Coinstats Index reduces the time for the gettxoutsetinfo RPC to complete at the cost of using additional disk space', + warning: null, + }), + testnet: Value.toggle({ + name: 'Testnet', + default: false, + description: + 'Testnet is an alternative Bitcoin block chain to be used for testing. Testnet coins are separate and distinct from actual bitcoins, and are never supposed to have any value. This allows application developers or bitcoin testers to experiment, without having to use real bitcoins or worrying about breaking the main bitcoin chain.', + }), + wallet: Value.object( + { + name: 'Wallet', + description: 'Wallet Settings', + }, + InputSpec.of({ + enable: Value.toggle({ + name: 'Enable Wallet', + default: true, + description: 'Load the wallet and enable wallet RPC calls.', + warning: null, + }), + avoidpartialspends: Value.toggle({ + name: 'Avoid Partial Spends', + default: true, + description: + 'Group outputs by address, selecting all or none, instead of selecting on a per-output basis. This improves privacy at the expense of higher transaction fees.', + warning: null, + }), + discardfee: Value.number({ + name: 'Discard Change Tolerance', + description: + 'The fee rate (in BTC/kB) that indicates your tolerance for discarding change by adding it to the fee.', + warning: null, + required: { + default: 0.0001, + }, + min: 0, + max: 0.01, + step: null, + integer: false, + units: 'BTC/kB', + placeholder: null, + }), + }), + ), + mempool: Value.object( + { + name: 'Mempool', + description: 'Mempool Settings', + }, + InputSpec.of({ + persistmempool: Value.toggle({ + name: 'Persist Mempool', + default: true, + description: 'Save the mempool on shutdown and load on restart.', + warning: null, + }), + maxmempool: Value.number({ + name: 'Max Mempool Size', + description: 'Keep the transaction memory pool below megabytes.', + warning: null, + required: { + default: 300, + }, + min: 1, + max: null, + step: null, + integer: true, + units: 'MiB', + placeholder: null, + }), + mempoolexpiry: Value.number({ + name: 'Mempool Expiration', + description: + 'Do not keep transactions in the mempool longer than hours.', + warning: null, + required: { + default: 336, + }, + min: 1, + max: null, + step: null, + integer: true, + units: 'Hr', + placeholder: null, + }), + mempoolfullrbf: Value.toggle({ + name: 'Enable Full RBF', + default: true, + description: + 'Policy for your node to use for relaying and mining unconfirmed transactions. For details, see https://github.com/bitcoin/bitcoin/blob/master/doc/release-notes/release-notes-24.0.1.md#notice-of-new-option-for-transaction-replacement-policies', + warning: null, + }), + permitbaremultisig: Value.toggle({ + name: 'Permit Bare Multisig', + default: true, + description: 'Relay non-P2SH multisig transactions', + warning: null, + }), + datacarrier: Value.toggle({ + name: 'Relay OP_RETURN Transactions', + default: true, + description: 'Relay transactions with OP_RETURN outputs', + warning: null, + }), + datacarriersize: Value.number({ + name: 'Max OP_RETURN Size', + description: 'Maximum size of data in OP_RETURN outputs to relay', + warning: null, + required: { + default: 83, + }, + min: 0, + max: 10_000, + step: null, + integer: true, + units: 'bytes', + placeholder: null, + }), + }), + ), + peers: Value.object( + { + name: 'Peers', + description: 'Peer Connection Settings', + }, + InputSpec.of({ + listen: Value.toggle({ + name: 'Make Public', + default: true, + description: 'Allow other nodes to find your server on the network.', + warning: null, + }), + onlyonion: Value.toggle({ + name: 'Disable Clearnet', + default: false, + description: 'Only connect to peers over Tor.', + warning: null, + }), + v2transport: Value.toggle({ + name: 'Use V2 P2P Transport Protocol', + default: true, + description: + 'Enable or disable the use of BIP324 V2 P2P transport protocol.', + warning: null, + }), + connectpeer: Value.union( + { + name: 'Connect Peer', + required: { default: 'addnode' }, + }, + Variants.of({ + connect: { + name: 'Connect', + spec: InputSpec.of({ + peers: Value.list( + List.text( + { + name: 'Connect Nodes', + minLength: 1, + maxLength: null, + default: [], + description: + 'Add addresses of nodes for Bitcoin to EXCLUSIVELY connect to.', + warning: null, + }, + { + masked: false, + placeholder: null, + inputmode: 'text', + patterns: [ + { + regex: + '(^s*((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?).(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?).(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?).(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?:[0-9]{1,5}))s*$)|(^s*((?=.{1,255}$)[0-9A-Za-z](?:(?:[0-9A-Za-z]|\b-){0,61}[0-9A-Za-z])?(?:.[0-9A-Za-z](?:(?:[0-9A-Za-z]|\b-){0,61}[0-9A-Za-z])?)*.?:[0-9]{1,5})s*$)|(^s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:)))(%.+)?:[0-9]{1,5}s*$)', + description: + "Must be either a domain name, or an IPv4 or IPv6 address. Be sure to include the port number, but do not include protocol scheme (eg 'http://').", + }, + ], + minLength: null, + maxLength: null, + }, + ), + ), + }), + }, + addnode: { + name: 'Connect', + spec: InputSpec.of({ + peers: Value.list( + List.text( + { + name: 'Connect Nodes', + minLength: 0, + maxLength: null, + default: [], + description: + 'Add addresses of nodes for Bitcoin to EXCLUSIVELY connect to.', + warning: null, + }, + { + masked: false, + placeholder: null, + inputmode: 'text', + patterns: [ + { + regex: + '(^s*((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?).(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?).(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?).(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?:[0-9]{1,5}))s*$)|(^s*((?=.{1,255}$)[0-9A-Za-z](?:(?:[0-9A-Za-z]|\b-){0,61}[0-9A-Za-z])?(?:.[0-9A-Za-z](?:(?:[0-9A-Za-z]|\b-){0,61}[0-9A-Za-z])?)*.?:[0-9]{1,5})s*$)|(^s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:)))(%.+)?:[0-9]{1,5}s*$)', + description: + "Must be either a domain name, or an IPv4 or IPv6 address. Be sure to include the port number, but do not include protocol scheme (eg 'http://').", + }, + ], + minLength: null, + maxLength: null, + }, + ), + ), + }), + }, + }), + ), + }), + ), + advanced: Value.object( + { + name: 'Advanced', + description: 'Advanced Settings', + }, + InputSpec.of({ + prune: Value.dynamicNumber(async ({ effects }) => { + const disk = await diskUsage() + + return { + name: 'Pruning', + description: + 'Set the maximum size of the blockchain you wish to store on disk.', + warning: 'Increasing this value will require re-syncing your node.', + placeholder: 'Enter max blockchain size', + required: disk.total < 900_000_000_000 ? { default: 550 } : false, + integer: true, + units: 'MiB', + min: 550, + max: (disk.total * 0.9) / 1_000_000, + } + }), + dbcache: Value.number({ + name: 'Database Cache', + description: + "How much RAM to allocate for caching the TXO set. Higher values improve syncing performance, but increase your chance of using up all your system's memory or corrupting your database in the event of an ungraceful shutdown. Set this high but comfortably below your system's total RAM during IBD, then turn down to 450 (or leave blank) once the sync completes.", + warning: + 'WARNING: Increasing this value results in a higher chance of ungraceful shutdowns, which can leave your node unusable if it happens during the initial block download. Use this setting with caution. Be sure to set this back to the default (450 or leave blank) once your node is synced. DO NOT press the STOP button if your dbcache is large. Instead, set this number back to the default, hit save, and wait for bitcoind to restart on its own.', + required: false, + min: 0, + max: null, + step: null, + integer: true, + units: 'MiB', + placeholder: null, + }), + blockfilters: Value.object( + { + name: 'Block Filters', + description: 'Settings for storing and serving compact block filters', + }, + InputSpec.of({ + blockfilterindex: Value.toggle({ + name: 'Compute Compact Block Filters (BIP158)', + default: true, + description: + "Generate Compact Block Filters during initial sync (IBD) to enable 'getblockfilter' RPC. This is useful if dependent services need block filters to efficiently scan for addresses/transactions etc.", + warning: null, + }), + peerblockfilters: Value.toggle({ + name: 'Serve Compact Block Filters to Peers (BIP157)', + default: false, + description: + "Serve Compact Block Filters as a peer service to other nodes on the network. This is useful if you wish to connect an SPV client to your node to make it efficient to scan transactions without having to download all block data. 'Compute Compact Block Filters (BIP158)' is required.", + warning: null, + }), + }), + ), + bloomfilters: Value.object( + { + name: 'Bloom Filters (BIP37)', + description: 'Setting for serving Bloom Filters', + }, + InputSpec.of({ + peerbloomfilters: Value.toggle({ + name: 'Serve Bloom Filters to Peers', + default: false, + description: + 'Peers have the option of setting filters on each connection they make after the version handshake has completed. Bloom filters are for clients implementing SPV (Simplified Payment Verification) that want to check that block headers connect together correctly, without needing to verify the full blockchain. The client must trust that the transactions in the chain are in fact valid. It is highly recommended AGAINST using for anything except Bisq integration.', + warning: + 'This is ONLY for use with Bisq integration, please use Block Filters for all other applications.', + }), + }), + ), + }), + ), +}) + +export const matchConfigSpec = configSpec.validator +export type ConfigSpec = typeof matchConfigSpec._TYPE diff --git a/startos/actions/config/write.ts b/startos/actions/config/write.ts new file mode 100644 index 0000000..23012f8 --- /dev/null +++ b/startos/actions/config/write.ts @@ -0,0 +1,93 @@ +import { + bitcoinConfFile, + fromTypedBitcoinConf, + TypedBitcoinConf, +} from '../../file-models/bitcoin.conf' +import { ConfigSpec } from './spec' + +export async function write(input: ConfigSpec) { + const { + rpc, + wallet, + txindex, + coinstatsindex, + testnet, + mempool, + peers, + advanced: { prune, dbcache, bloomfilters, blockfilters }, + } = input + + const typed: TypedBitcoinConf = { + // RPC + rpcuser: rpc.username, + rpcpassword: rpc.password, + rpcauth: rpc.advanced.auth, + rpcservertimeout: rpc.advanced.servertimeout, + rpcthreads: rpc.advanced.threads, + rpcworkqueue: rpc.advanced.workqueue, + rpcbind: rpc.enable && prune ? '127.0.0.1:18332' : '0.0.0.0:8332', + rpcallowip: rpc.enable && prune ? '127.0.0.1/32' : '0.0.0.0/0', + + // Mempool + mempoolfullrbf: mempool.mempoolfullrbf === true ? 1 : 0, + persistmempool: mempool.persistmempool === true ? 1 : 0, + maxmempool: mempool.maxmempool, + mempoolexpiry: mempool.mempoolexpiry, + datacarrier: mempool.datacarrier === true ? 1 : 0, + datacarriersize: mempool.datacarriersize, + permitbaremultisig: mempool.permitbaremultisig === true ? 1 : 0, + + // Peers + listen: peers.listen ? 1 : 0, + v2transport: peers.v2transport ? 1 : 0, + whitelist: '172.18.0.0/16', + + // Wallet + disablewallet: wallet.enable ? 0 : 1, + avoidpartialspends: wallet.avoidpartialspends ? 1 : 0, + discardfee: wallet.discardfee, + + testnet: testnet ? 1 : 0, + } + + if (peers.listen) typed.bind = '0.0.0.0:8333' + + if (peers.connectpeer.selection === 'addnode') { + typed.addnode = peers.connectpeer.value.peers + } else { + typed.connect = peers.connectpeer.value.peers + } + + if (peers.onlyonion) typed.onlynet = 'onion' + + if (prune) typed.prune = prune + + if (dbcache) typed.dbcache = dbcache + + if (wallet.enable) typed.deprecatedrpc = 'create_bdb' + + // Zero MQ + if (input.zmqEnabled) { + typed.zmqpubrawblock = 'tcp://0.0.0.0:28332' + typed.zmqpubhashblock = 'tcp://0.0.0.0:28332' + typed.zmqpubrawtx = 'tcp://0.0.0.0:28333' + typed.zmqpubhashtx = 'tcp://0.0.0.0:28333' + typed.zmqpubsequence = 'tcp://0.0.0.0:28333' + } + + // TxIndex + if (txindex) typed.txindex = 1 + + // CoinStatsIndex + if (coinstatsindex) typed.coinstatsindex = 1 + + // BIP37 + if (bloomfilters) typed.peerbloomfilters = 1 + + // BIP157 + if (blockfilters.blockfilterindex) typed.blockfilterindex = 'basic' + + if (blockfilters.peerblockfilters) typed.peerblockfilters = 1 + + await bitcoinConfFile.merge(fromTypedBitcoinConf(typed)) +} diff --git a/startos/actions/credentials.ts b/startos/actions/credentials.ts new file mode 100644 index 0000000..7fce067 --- /dev/null +++ b/startos/actions/credentials.ts @@ -0,0 +1,76 @@ +import { bitcoinConfFile } from '../file-models/bitcoin.conf' +import { rpcInterfaceId } from '../interfaces' +import { sdk } from '../sdk' + +export const credentials = sdk.Action.withoutInput( + // id + 'credentials', + + // metadata + async ({ effects }) => ({ + name: 'Credentials', + description: 'Access credentials for Bitcoin RPC, quick connect, auth, etc', + warning: null, + allowedStatuses: 'any', + group: null, + visibility: 'enabled', + }), + + // execution function + async ({ effects }) => { + const conf = (await bitcoinConfFile.read.const(effects))! + + const { rpcuser, rpcpassword } = conf + + const addressInfoRes = ( + await sdk.serviceInterface.getOwn(effects, rpcInterfaceId).const() + )?.addressInfo + + return { + version: '1', + title: 'Credentials', + message: null, + result: { + type: 'group', + value: [ + { + name: 'RPC Username', + type: 'single', + value: rpcuser, + description: 'Bitcoin RPC Username', + copyable: true, + masked: false, + qr: false, + }, + { + name: 'RPC Password', + type: 'single', + value: rpcpassword, + description: 'Bitcoin RPC Password', + copyable: true, + masked: true, + qr: false, + }, + { + name: 'Tor Quick Connect', + type: 'single', + value: `btcstandup://${rpcuser}:${rpcpassword}@${addressInfoRes?.onionHostnames[0]}:8332`, + description: 'Bitcoin-Standup Tor Quick Connect URL', + copyable: true, + qr: true, + masked: true, + }, + { + name: 'Lan Quick Connect', + type: 'single', + value: `btcstandup://${rpcuser}:${rpcpassword}@${addressInfoRes?.localHostnames[0]}:8332`, + description: 'Bitcoin-Standup Lan Quick Connect URL', + copyable: true, + qr: true, + masked: true, + }, + ], + }, + } + }, +) diff --git a/startos/actions/deleteCoinstatsIndex.ts b/startos/actions/deleteCoinstatsIndex.ts new file mode 100644 index 0000000..9ddabbf --- /dev/null +++ b/startos/actions/deleteCoinstatsIndex.ts @@ -0,0 +1,31 @@ +import { sdk } from '../sdk' +import * as fs from 'fs/promises' + +export const deleteCoinstatsIndex = sdk.Action.withoutInput( + // id + 'deleteCoinstatsIndex', + + // metadata + async ({ effects }) => ({ + name: 'Delete Coinstats Index', + description: + 'Deletes the Coinstats Index (coinstatsindex) in case it gets corrupted.', + warning: + "The Coinstats Index will be rebuilt once Bitcoin Core is started again, unless 'Transaction Index' is disabled in the config settings. Please don't do this unless you fully understand what you are doing.", + allowedStatuses: 'only-stopped', + group: 'Delete Corrupted Files', + visibility: 'enabled', + }), + + // execution function + async ({ effects }) => { + await fs.rmdir('/root/.bitcoin/indexes/coinstats', { recursive: true }) + + return { + version: '1', + title: 'Success', + message: 'Successfully deleted coinstats index', + result: null, + } + }, +) diff --git a/startos/actions/deletePeers.ts b/startos/actions/deletePeers.ts new file mode 100644 index 0000000..6da9dbc --- /dev/null +++ b/startos/actions/deletePeers.ts @@ -0,0 +1,29 @@ +import { sdk } from '../sdk' +import * as fs from 'fs/promises' + +export const deletePeers = sdk.Action.withoutInput( + // id + 'deletePeers', + + // metadata + async ({ effects }) => ({ + name: 'Delete Peer List', + description: 'Deletes the Peer List (peers.dat) in case it gets corrupted.', + warning: null, + allowedStatuses: 'only-stopped', + group: 'Delete Corrupted Files', + visibility: 'enabled', + }), + + // execution function + async ({ effects }) => { + await fs.rm('/root/.bitcoin/peers.dat') + + return { + version: '1', + title: 'Success', + message: 'Successfully deleted peers.dat', + result: null, + } + }, +) diff --git a/startos/actions/deleteTxIndex.ts b/startos/actions/deleteTxIndex.ts new file mode 100644 index 0000000..a61ccb7 --- /dev/null +++ b/startos/actions/deleteTxIndex.ts @@ -0,0 +1,31 @@ +import { sdk } from '../sdk' +import * as fs from 'fs/promises' + +export const deleteTxIndex = sdk.Action.withoutInput( + // id + 'deleteTxIndex', + + // metadata + async ({ effects }) => ({ + name: 'Delete Transaction Index', + description: + 'Deletes the Transaction Index (txindex) in the event it gets corrupted.', + warning: + "The Transaction Index will be rebuilt once Bitcoin Core is started again, unless 'Coinstats Index' is disabled in the config settings. Please don't do this unless you fully understand what you are doing.", + allowedStatuses: 'only-stopped', + group: 'Delete Corrupted Files', + visibility: 'enabled', + }), + + // execution function + async ({ effects }) => { + await fs.rmdir('/root/.bitcoin/indexes/txindex', { recursive: true }) + + return { + version: '1', + title: 'Success', + message: 'Successfully deleted txindex', + result: null, + } + }, +) diff --git a/startos/actions/index.ts b/startos/actions/index.ts new file mode 100644 index 0000000..246f4a0 --- /dev/null +++ b/startos/actions/index.ts @@ -0,0 +1,18 @@ +import { sdk } from '../sdk' +import { credentials } from './credentials' +import { deleteCoinstatsIndex } from './deleteCoinstatsIndex' +import { deletePeers } from './deletePeers' +import { deleteTxIndex } from './deleteTxIndex' +import { reindexBlockchain } from './reindexBlockchain' +import { reindexChainstate } from './reindexChainstate' +import { runtimeInfo } from './runtime-info' + +export const { actions, actionsMetadata } = sdk.setupActions( + credentials, + runtimeInfo, + deleteCoinstatsIndex, + deletePeers, + deleteTxIndex, + reindexBlockchain, + reindexChainstate, +) diff --git a/startos/actions/reindexBlockchain.ts b/startos/actions/reindexBlockchain.ts new file mode 100644 index 0000000..629c2c5 --- /dev/null +++ b/startos/actions/reindexBlockchain.ts @@ -0,0 +1,30 @@ +import { sdk } from '../sdk' + +export const reindexBlockchain = sdk.Action.withoutInput( + // id + 'reindexBlockchain', + + // metadata + async ({ effects }) => ({ + name: 'Reindex Blockchain', + description: + 'Rebuilds the block and chainstate databases starting from genesis. If blocks already exist on disk, these are used rather than being re-downloaded. For pruned nodes, this means downloading the entire blockchain over again.', + warning: + 'Blocks not stored on disk will be re-downloaded in order to rebuild the database. If your node is pruned, this action is equivalent to syncing the node from scratch, so this process could take weeks on low-end hardware.', + allowedStatuses: 'any', + group: 'Reindex', + visibility: 'enabled', + }), + + // execution function + async ({ effects }) => { + await sdk.store.setOwn(effects, sdk.StorePath.reindexBlockchain, true) + return { + version: '1', + title: 'Success', + message: + 'Blockchain will be reindexed on next startup. If Bitcoin is already running, it will be automatically restarted now.', + result: null, + } + }, +) diff --git a/startos/actions/reindexChainstate.ts b/startos/actions/reindexChainstate.ts new file mode 100644 index 0000000..2c1b9f1 --- /dev/null +++ b/startos/actions/reindexChainstate.ts @@ -0,0 +1,34 @@ +import { bitcoinConfFile } from '../file-models/bitcoin.conf' +import { sdk } from '../sdk' + +export const reindexChainstate = sdk.Action.withoutInput( + // id + 'reindexChainstate', + + // metadata + async ({ effects }) => ({ + name: 'Reindex Chainstate', + description: + "Rebuilds the chainstate database using existing block index data; as the block index is not rebuilt, 'reindex_chainstate' should be strictly faster than 'reindex'. This action should only be used in the case of chainstate corruption; if the blocks stored on disk are corrupted, the 'reindex' action will need to be run instead.", + warning: + "While faster than 'Reindex', 'Reindex Chainstate' can still take several days or more to complete.", + allowedStatuses: 'any', + group: 'Reindex', + visibility: + (await bitcoinConfFile.read.const(effects))?.prune === 'enabled' + ? 'hidden' + : 'enabled', + }), + + // execution function + async ({ effects }) => { + await sdk.store.setOwn(effects, sdk.StorePath.reindexChainstate, true) + return { + version: '1', + title: 'Success', + message: + 'Chainstate will be reindexed on next startup. If Bitcoin was already running, it will be automatically restarted now.', + result: null, + } + }, +) diff --git a/startos/actions/runtime-info.ts b/startos/actions/runtime-info.ts new file mode 100644 index 0000000..a856026 --- /dev/null +++ b/startos/actions/runtime-info.ts @@ -0,0 +1,316 @@ +import { T } from '@start9labs/start-sdk' +import { sdk } from '../sdk' +import { GetBlockchainInfo, GetNetworkInfo } from '../utils' + +export const runtimeInfo = sdk.Action.withoutInput( + // id + 'runtime-info', + + // metadata + async ({ effects }) => ({ + name: 'Runtime Information', + description: + 'Network and other runtime information about this Bitcoin node', + warning: null, + allowedStatuses: 'any', + group: null, + visibility: 'enabled', + }), + + // execution function + async ({ effects }) => { + // getnetowrkinfo + + const networkInfoRes = await sdk.runCommand( + effects, + { id: 'main' }, + ['bitcoin-cli', '-conf=/root/.bitcoin/bitcoin.conf', 'getnetworkinfo'], + {}, + 'getnetworkinfo', + ) + + const networkInfoRaw: GetNetworkInfo = JSON.parse( + networkInfoRes.stdout as string, + ) + + // getblockchaininfo + + const blockchainInfoRes = await sdk.runCommand( + effects, + { id: 'main' }, + ['bitcoin-cli', '-conf=/root/.bitcoin/bitcoin.conf', 'getblockchaininfo'], + {}, + 'getblockchaininfo', + ) + + const blockchainInfoRaw: GetBlockchainInfo = JSON.parse( + blockchainInfoRes.stdout as string, + ) + + // return + + return { + version: '1', + title: 'Node Runtime Info', + message: null, + result: { + type: 'group', + value: [ + getConnections(networkInfoRaw), + getBlockchainInfo(blockchainInfoRaw), + getSoftforkInfo(blockchainInfoRaw), + ], + }, + } + }, +) + +function getConnections(networkInfoRaw: GetNetworkInfo): T.ActionResultMember { + return { + type: 'single', + name: 'Connections', + description: 'The number of peers connected (inbound and outbound)', + value: `${networkInfoRaw.connections} (${networkInfoRaw.connectionsIn} in / ${networkInfoRaw.connectionsOut} out)`, + copyable: false, + masked: false, + qr: false, + } +} + +function getBlockchainInfo( + blockchainInfoRaw: GetBlockchainInfo, +): T.ActionResultMember { + return { + type: 'group', + name: 'Blockchain Info', + description: null, + value: [ + { + type: 'single', + name: 'Block Height', + value: String(blockchainInfoRaw.headers), + description: 'The current block height for the network', + copyable: false, + masked: false, + qr: false, + }, + { + type: 'single', + name: 'Synced Block Height', + value: String(blockchainInfoRaw.blocks), + description: 'The number of blocks the node has verified', + copyable: false, + masked: false, + qr: false, + }, + { + type: 'single', + name: 'Sync Progress', + value: + blockchainInfoRaw.blocks < blockchainInfoRaw.headers + ? `${(blockchainInfoRaw.verificationprogress * 100).toFixed(2)}%` + : '100%', + description: 'The percentage of the blockchain that has been verified', + copyable: false, + masked: false, + qr: false, + }, + ], + } +} + +function getSoftforkInfo( + blockchainInfoRaw: GetBlockchainInfo, +): T.ActionResultMember { + return { + type: 'group', + name: 'Softfork Info', + description: null, + value: [ + { + type: 'group', + name: 'Softforks', + description: null, + value: getSoftforks(blockchainInfoRaw), + }, + ], + } +} + +function getSoftforks( + blockchainInfoRaw: GetBlockchainInfo, +): T.ActionResultMember[] { + return Object.entries(blockchainInfoRaw.softforks).map(([key, val]) => { + const value: T.ActionResultMember[] = [ + { + type: 'single', + name: 'Type', + value: val.type, + description: 'Either "buried", "bip9"', + copyable: false, + masked: false, + qr: false, + }, + { + type: 'single', + name: 'Height', + value: val.height ? String(val.height) : 'N/A', + description: + 'height of the first block which the rules are or will be enforced (only for "buried" type, or "bip9" type with "active" status)', + copyable: false, + masked: false, + qr: false, + }, + { + type: 'single', + name: 'Active', + value: String(val.active), + description: + 'true if the rules are enforced for the mempool and the next block', + copyable: false, + masked: false, + qr: false, + }, + ] + + if (val.bip9) { + value.push(getBip9Info(val.bip9)) + + if (val.bip9.statistics) { + value.push(getBip9Statistics(val.bip9.statistics)) + } + } + + return { + type: 'group', + name: key, + description: null, + value, + } + }) +} + +function getBip9Info(bip9: Bip9): T.ActionResultMember { + const { status, bit, start_time, timeout, since } = bip9 + + return { + type: 'group', + name: 'Bip9', + description: null, + value: [ + { + type: 'single', + name: 'Status', + value: status, + description: + 'One of "defined", "started", "locked_in", "active", "failed"', + copyable: false, + masked: false, + qr: false, + }, + { + type: 'single', + name: 'Bit', + value: bit ? String(bit) : 'N/A', + description: + 'The bit (0-28) in the block version field used to signal this softfork (only for "started" status)', + copyable: false, + masked: false, + qr: false, + }, + { + type: 'single', + name: 'Start Time', + value: String(start_time), + description: + 'The minimum median time past of a block at which the bit gains its meaning', + copyable: false, + masked: false, + qr: false, + }, + { + type: 'single', + name: 'Timeout', + value: String(timeout), + description: + 'The median time past of a block at which the deployment is considered failed if not yet locked in', + copyable: false, + masked: false, + qr: false, + }, + { + type: 'single', + name: 'Since', + value: String(since), + description: 'height of the first block to which the status applies', + copyable: false, + masked: false, + qr: false, + }, + ], + } +} + +function getBip9Statistics(statistics: Bip9Stats): T.ActionResultMember { + const { period, threshold, elapsed, count, possible } = statistics + + return { + type: 'group', + name: 'Statistics', + description: null, + value: [ + { + type: 'single', + name: 'Period', + value: String(period), + description: 'The length in blocks of the BIP9 signalling period', + copyable: false, + masked: false, + qr: false, + }, + { + type: 'single', + name: 'Threshold', + value: String(threshold), + description: + 'The number of blocks with the version bit set required to activate the feature', + copyable: false, + masked: false, + qr: false, + }, + { + type: 'single', + name: 'Elapsed', + value: String(elapsed), + description: + 'The number of blocks elapsed since the beginning of the current period', + copyable: false, + masked: false, + qr: false, + }, + { + type: 'single', + name: 'Count', + value: String(count), + description: + 'The number of blocks with the version bit set in the current period', + copyable: false, + masked: false, + qr: false, + }, + { + type: 'single', + name: 'Possible', + value: String(possible), + description: + 'returns false if there are not enough blocks left in this period to pass activation threshold', + copyable: false, + masked: false, + qr: false, + }, + ], + } +} + +type Bip9 = NonNullable +type Bip9Stats = NonNullable diff --git a/startos/backups.ts b/startos/backups.ts new file mode 100644 index 0000000..45dd7fa --- /dev/null +++ b/startos/backups.ts @@ -0,0 +1,7 @@ +import { sdk } from './sdk' + +export const { createBackup, restoreBackup } = sdk.setupBackups(async () => + sdk.Backups.volumes('main').setOptions({ + exclude: ['blocks/', 'chainstate/', 'indexes/', 'testnet3/'], + }), +) diff --git a/startos/dependencies.ts b/startos/dependencies.ts new file mode 100644 index 0000000..7221c4b --- /dev/null +++ b/startos/dependencies.ts @@ -0,0 +1,5 @@ +import { sdk } from './sdk' + +export const setDependencies = sdk.setupDependencies( + async ({ effects }) => ({}), +) diff --git a/startos/file-models/bitcoin.conf.ts b/startos/file-models/bitcoin.conf.ts new file mode 100644 index 0000000..b6bdddd --- /dev/null +++ b/startos/file-models/bitcoin.conf.ts @@ -0,0 +1,144 @@ +import { FileHelper } from '@start9labs/start-sdk' + +export type TypedBitcoinConf = { + // RPC + rpcbind: string + rpcallowip: string + rpcuser: string + rpcpassword: string + rpcauth: string[] + // list of objects + // null pw = noop + // index = identifier for rpcauth entries + rpcservertimeout: number + rpcthreads: number + rpcworkqueue: number + + // Mempool + mempoolfullrbf: 0 | 1 + persistmempool: 0 | 1 + maxmempool: number + mempoolexpiry: number + datacarrier: 0 | 1 + datacarriersize: number + permitbaremultisig: 0 | 1 + + // Peers + listen: 0 | 1 + bind?: string + connect?: string[] + addnode?: string[] + onlynet?: 'onion' + v2transport: 0 | 1 + + // Whitelist + whitelist: string + + // Pruning + prune?: number + + // Performance Tuning + dbcache?: number + + // Wallet + disablewallet: 0 | 1 + deprecatedrpc?: string + avoidpartialspends: 0 | 1 + discardfee: number + + // Zero MQ + zmqpubrawblock?: string + zmqpubhashblock?: string + zmqpubrawtx?: string + zmqpubhashtx?: string + zmqpubsequence?: string + + // TxIndex + txindex?: 1 + + // CoinstatsIndex + coinstatsindex?: 1 + + // BIP37 + peerbloomfilters?: 1 + + // BIP157 + blockfilterindex?: 'basic' + peerblockfilters?: 1 + + // Testnet + testnet: 0 | 1 +} + +function fromBitcoinConf(text: string): Record { + const lines = text.split('/n') + const dictionary = {} as Record + + for (const line of lines) { + const [key, value] = line.split('=', 2) + const trimmedKey = key.trim() + const trimmedValue = value.trim() + + if (!dictionary[trimmedKey]) { + dictionary[trimmedKey] = [] + } + dictionary[trimmedKey].push(trimmedValue) + } + + return dictionary +} + +function toBitcoinConf(conf: Record): string { + let bitcoinConfStr = '' + + Object.entries(conf).forEach(([key, value]) => { + if (Array.isArray(value)) { + for (const v of value) { + bitcoinConfStr += `${key}=${v}\n` + } + } else { + bitcoinConfStr += `${key}=${value}\n` + } + }) + return bitcoinConfStr +} + +export const bitcoinConfFile = FileHelper.raw( + './bitcoin/bitcoin.conf', + (obj: Record) => toBitcoinConf(obj), // BitcoinConf.typeof + (str) => fromBitcoinConf(str), +) + +export function toTypedBitcoinConf( + obj: Record, +): TypedBitcoinConf { + const typed = {} as TypedBitcoinConf + + Object.keys(obj).forEach((key) => { + if (TypedBitcoinConf.contains(key)) { + const expectedType = TypedBitcoinConf.typeof(key) + let val: string | string[] | number + if (expectedType === 'array') { + val = obj[key] + } else if (expectedType === 'number') { + val = Number(obj[key]) + } else { + val = obj[key][0 || -1] // @TODO 0 or -1 depends on Bitcoin's behavior + } + typed[key] = val + } + }) + return typed +} + +export function fromTypedBitcoinConf( + typed: Partial, +): Record { + return Object.entries(typed).reduce( + (obj, [key, val]) => ({ + ...obj, + [key]: [typeof val === 'number' ? String(val) : val].flat(), + }), + {}, + ) +} diff --git a/startos/index.ts b/startos/index.ts new file mode 100644 index 0000000..8d32c10 --- /dev/null +++ b/startos/index.ts @@ -0,0 +1,11 @@ +/** + * Plumbing. DO NOT EDIT. + */ +export { createBackup, restoreBackup } from './backups' +export { main } from './main' +export { packageInit, packageUninit, containerInit } from './init' +export { actions } from './actions' +import { buildManifest } from '@start9labs/start-sdk' +import { manifest as sdkManifest } from './manifest' +import { versions } from './versions' +export const manifest = buildManifest(versions, sdkManifest) diff --git a/startos/init.ts b/startos/init.ts new file mode 100644 index 0000000..e821594 --- /dev/null +++ b/startos/init.ts @@ -0,0 +1,23 @@ +import { sdk } from './sdk' +import { exposedStore } from './store' +import { setDependencies } from './dependencies' +import { setInterfaces } from './interfaces' +import { versions } from './versions' +import { actions } from './actions' + +const install = sdk.setupInstall(async ({ effects }) => {}) + +const uninstall = sdk.setupUninstall(async ({ effects }) => {}) + +/** + * Plumbing. DO NOT EDIT. + */ +export const { packageInit, packageUninit, containerInit } = sdk.setupInit( + versions, + install, + uninstall, + setInterfaces, + setDependencies, + actions, + exposedStore, +) diff --git a/startos/interfaces.ts b/startos/interfaces.ts new file mode 100644 index 0000000..c369588 --- /dev/null +++ b/startos/interfaces.ts @@ -0,0 +1,90 @@ +import { sdk } from './sdk' +import { BindOptions } from '@start9labs/start-sdk/cjs/lib/osBindings' +import { bitcoinConfFile, toTypedBitcoinConf } from './file-models/bitcoin.conf' +import { getPeerPort, getRpcPort } from './utils' + +export const rpcInterfaceId = 'rpc' +export const peerInterfaceId = 'peer' +export const zmqPort = 28332 +export const zmqInterfaceId = 'zmq' + +const zmqProtocol = { + preferredExternalPort: 28332, +} as BindOptions + +export const setInterfaces = sdk.setupInterfaces(async ({ effects }) => { + let conf = await bitcoinConfFile.read.const(effects) + + if (!conf) return [] + + const config = toTypedBitcoinConf(conf) + + // RPC + const rpcPort = getRpcPort(config.testnet) + const rpcMulti = sdk.host.multi(effects, 'rpc') + const rpcMultiOrigin = await rpcMulti.bindPort(rpcPort, { + protocol: 'grpc', + }) + const rpc = sdk.createInterface(effects, { + name: 'RPC Interface', + id: rpcInterfaceId, + description: 'Listens for JSON-RPC commands', + type: 'api', + hasPrimary: false, + masked: false, + schemeOverride: null, + username: null, + path: '', + search: {}, + }) + const rpcReceipt = await rpcMultiOrigin.export([rpc]) + + const receipts = [rpcReceipt] + + // PEER + const peerPort = getPeerPort(config.testnet) + const peerMulti = sdk.host.multi(effects, 'peer') + const peerMultiOrigin = await peerMulti.bindPort(peerPort, { + protocol: 'bitcoin', + }) + const peer = sdk.createInterface(effects, { + name: 'Peer Interface', + id: peerInterfaceId, + description: + 'Listens for incoming connections from peers on the bitcoin network', + type: 'p2p', + hasPrimary: false, + masked: false, + schemeOverride: null, + username: null, + path: '', + search: {}, + }) + const peerReceipt = await peerMultiOrigin.export([peer]) + + receipts.push(peerReceipt) + + // ZMQ (conditional) + if (config.zmqpubhashblock) { + const zmqMulti = sdk.host.multi(effects, 'zmq') + const zmqMultiOrigin = await zmqMulti.bindPort(zmqPort, zmqProtocol) + const zmq = sdk.createInterface(effects, { + name: 'ZeroMQ Interface', + id: zmqInterfaceId, + description: + 'Listens for incoming connections from peers on the bitcoin network', + type: 'api', + hasPrimary: false, + masked: false, + schemeOverride: null, + username: null, + path: '', + search: {}, + }) + const zmqReceipt = await zmqMultiOrigin.export([zmq]) + + receipts.push(zmqReceipt) + } + + return receipts +}) diff --git a/startos/main.ts b/startos/main.ts new file mode 100644 index 0000000..d854e7e --- /dev/null +++ b/startos/main.ts @@ -0,0 +1,136 @@ +import { sdk } from './sdk' +import { T } from '@start9labs/start-sdk' +import { peerInterfaceId } from './interfaces' +import { GetBlockchainInfo, getRpcPort } from './utils' +import { bitcoinConfFile, toTypedBitcoinConf } from './file-models/bitcoin.conf' + +export const main = sdk.setupMain(async ({ effects, started }) => { + /** + * ======================== Setup (optional) ======================== + */ + + // TODO if pruned: create proxy container, util.subcontainer + const conf = (await bitcoinConfFile.read.const(effects))! + const config = toTypedBitcoinConf(conf) + + const rpcPort = getRpcPort(config.testnet) + const containerIp = await effects.getContainerIp() + const peerAddr = ( + await sdk.serviceInterface.getOwn(effects, peerInterfaceId).once() + )?.addressInfo?.onionUrls[0] + + const bitcoinArgs: string[] = [] + + bitcoinArgs.push(`-onion=${containerIp}:9050`) + bitcoinArgs.push(`-externalip=${peerAddr}`) + bitcoinArgs.push('-datadir=/root/.bitcoin"') + bitcoinArgs.push('-conf=/root/.bitcoin/bitcoin.conf') + + for await (const reindexBlockchain of sdk.store + .getOwn(effects, sdk.StorePath.reindexBlockchain) + .watch()) { + if (reindexBlockchain) { + bitcoinArgs.push('-reindex') // @TODO confirm syntax for re-indexing from specific block height + await sdk.store.setOwn(effects, sdk.StorePath.reindexBlockchain, false) + await sdk.restart(effects) + } + } + + for await (const reindexChainstate of sdk.store + .getOwn(effects, sdk.StorePath.reindexChainstate) + .watch()) { + if (reindexChainstate) { + bitcoinArgs.push('-reindex-chainstate') + await sdk.store.setOwn(effects, sdk.StorePath.reindexChainstate, false) + await sdk.restart(effects) + } + } + + /** + * ======================== Additional Health Checks (optional) ======================== + */ + + const syncCheck = sdk.HealthCheck.of(effects, { + name: 'Blockchain Sync Progress', + fn: async () => { + const res = await sdk.runCommand( + effects, + { id: 'main' }, + [ + 'bitcoin-cli', + '-conf=/root/.bitcoin/bitcoin.conf', + 'getblockchaininfo', + ], + {}, + 'getblockchaininfo', + ) + + if (res.stdout && typeof res.stdout === 'string') { + const info: GetBlockchainInfo = JSON.parse(res.stdout) + + if (info.initialblockdownload) { + const percentage = (info.blocks / info.headers).toFixed(2) + return { + message: `Syncing blocks...${percentage}%`, + result: 'loading', + } + } + + return { + message: 'Bitcoin is fully synced', + result: 'success', + } + } + + return { + message: null, + result: + typeof res.stderr === 'string' && JSON.parse(res.stderr).code === 28 + ? 'starting' + : 'failure', + } + }, + }) + + const healthReceipts: T.HealthReceipt[] = [syncCheck] + + /** + * ======================== Daemons ======================== + */ + + const daemons = sdk.Daemons.of(effects, started, healthReceipts).addDaemon( + 'primary', + { + image: { id: 'main' }, + command: ['bitcoind', ...bitcoinArgs], + mounts: sdk.Mounts.of().addVolume('main', null, '/data', false), + ready: { + display: 'RPC Interface', + fn: () => + sdk.healthCheck.checkPortListening(effects, rpcPort, { + successMessage: 'The Bitcoin RPC interface is ready', + errorMessage: 'The Bitcoin RPC interface is not ready', + }), + }, + requires: [], + }, + ) + + if (config.prune) { + return daemons.addDaemon('proxy', { + image: { id: 'proxy' }, // subcontainer: + command: ['btc-rpc-proxy'], + mounts: sdk.Mounts.of().addVolume('proxy', null, '/data', false), // add mount for toml file + ready: { + display: 'RPC Proxy', + fn: () => + sdk.healthCheck.checkPortListening(effects, 28332, { + successMessage: 'The Bitcoin RPC Proxy is ready', + errorMessage: 'The Bitcoin RPC Proxy is not ready', + }), + }, + requires: [], + }) + } + return daemons +}) diff --git a/startos/manifest.ts b/startos/manifest.ts new file mode 100644 index 0000000..09769bf --- /dev/null +++ b/startos/manifest.ts @@ -0,0 +1,45 @@ +import { setupManifest } from '@start9labs/start-sdk' + +export const manifest = setupManifest({ + id: 'bitcoind', + title: 'Bitcoin Core', + license: 'MIT', + donationUrl: null, + wrapperRepo: 'https://github.com/Start9Labs/bitcoind-startos', + upstreamRepo: 'https://github.com/bitcoin/bitcoin', + supportSite: 'https://github.com/bitcoin/bitcoin/issues', + marketingSite: 'https://bitcoincore.org/', + description: { + short: 'A Bitcoin Full Node by Bitcoin Core', + long: 'Bitcoin is an innovative payment network and a new kind of money. Bitcoin uses peer-to-peer technology to operate with no central authority or banks; managing transactions and the issuing of bitcoins is carried out collectively by the network. Bitcoin is open-source; its design is public, nobody owns or controls Bitcoin and everyone can take part. Through many of its unique properties, Bitcoin allows exciting uses that could not be covered by any previous payment system.', + }, + assets: [], + volumes: ['main', 'proxy'], + images: { + main: { + source: { + dockerBuild: { + workdir: '../bitcoin', + dockerfile: 'Dockerfile', + }, + }, + }, + proxy: { + source: { + dockerTag: 'ghcr.io/start9labs/btc-rpc-proxy:0.4.0', + }, + }, + }, + hardwareRequirements: {}, + alerts: { + install: null, + update: null, + uninstall: + "Uninstalling Bitcoin Core will result in permanent loss of data. Without a backup, any funds stored on your node's default hot wallet will be lost forever. If you are unsure, we recommend making a backup, just to be safe.", + restore: + 'Restoring Bitcoin Core will overwrite its current data. You will lose any transactions recorded in watch-only wallets, and any funds you have received to the hot wallet, since the last backup.', + start: null, + stop: null, + }, + dependencies: {}, +}) diff --git a/startos/sdk.ts b/startos/sdk.ts new file mode 100644 index 0000000..5146d32 --- /dev/null +++ b/startos/sdk.ts @@ -0,0 +1,13 @@ +import { StartSdk } from '@start9labs/start-sdk' +import { manifest } from './manifest' +import { Store } from './store' + +/** + * Plumbing. DO NOT EDIT. + * + * The exported "sdk" const will be imported and used throughout the package codebase. + */ +export const sdk = StartSdk.of() + .withManifest(manifest) + .withStore() + .build(true) diff --git a/startos/store.ts b/startos/store.ts new file mode 100644 index 0000000..39d96f4 --- /dev/null +++ b/startos/store.ts @@ -0,0 +1,8 @@ +import { setupExposeStore } from '@start9labs/start-sdk' + +export type Store = { + reindexBlockchain: boolean + reindexChainstate: boolean +} + +export const exposedStore = setupExposeStore((_pathBuilder) => []) diff --git a/startos/utils.ts b/startos/utils.ts new file mode 100644 index 0000000..9c67491 --- /dev/null +++ b/startos/utils.ts @@ -0,0 +1,53 @@ +export type GetNetworkInfo = { + connections: number + connectionsIn: number + connectionsOut: number +} + +export type GetBlockchainInfo = { + chain: string + blocks: number + headers: number + bestblockhash: string + difficulty: number + mediantime: number + verificationprogress: number + initialblockdownload: boolean + chainwork: string + size_on_disk: number + pruned: boolean + pruneheight?: number + automatic_pruning?: boolean + prune_target_size?: number + softforks: Record< + string, + { + type: string + bip9?: { + status: string + bit?: number + start_time: number + timeout: number + since: number + statistics?: { + period: number + threshold: number + elapsed: number + count: number + possible: boolean + } + } + height?: number + active: boolean + } + > + warnings: string +} + +export function getRpcPort(testnet: 0 | 1) { + return testnet ? 18332 : 8332 +} + +export function getPeerPort(testnet: 0 | 1) { + return testnet ? 18333 : 8333 +} diff --git a/startos/versions/index.ts b/startos/versions/index.ts new file mode 100644 index 0000000..5f58b4b --- /dev/null +++ b/startos/versions/index.ts @@ -0,0 +1,4 @@ +import { VersionGraph } from '@start9labs/start-sdk' +import { v27_1_0 } from './v27_1_0' + +export const versions = VersionGraph.of(v27_1_0) diff --git a/startos/versions/v27_1_0.ts b/startos/versions/v27_1_0.ts new file mode 100644 index 0000000..ce26131 --- /dev/null +++ b/startos/versions/v27_1_0.ts @@ -0,0 +1,17 @@ +import { VersionInfo, IMPOSSIBLE } from '@start9labs/start-sdk' +import { + bitcoinConfFile, + fromTypedBitcoinConf, +} from '../file-models/bitcoin.conf' + +export const v27_1_0 = VersionInfo.of({ + version: '27.1.0:0', + releaseNotes: 'Revamped for StartOS 0.3.6', + migrations: { + up: async ({ effects }) => { + // TODO: migrate existing rpcuser and rpcpass to rpcauth + await bitcoinConfFile.merge(fromTypedBitcoinConf({ testnet: 0 })) + }, + down: IMPOSSIBLE, + }, +}) diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..b814af3 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,11 @@ +{ + "include": ["startos/**/*.ts"], + "compilerOptions": { + "target": "es2022", + "module": "None", + "moduleResolution": "node", + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true + } +}