Compare commits
32
Commits
7520f56989
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83070ce195 | ||
|
|
e56c9fde81 | ||
|
|
515ecf0ea0 | ||
|
|
9593b8ec62 | ||
|
|
810bca8154 | ||
|
|
0c78b1ddd5 | ||
|
|
86b927b26d | ||
|
|
c66bbfa618 | ||
|
|
d4c166b557 | ||
|
|
3bd94ec41d | ||
|
|
d1e86a6c3d | ||
|
|
c576700272 | ||
|
|
4598cafb1f | ||
|
|
64e4026440 | ||
|
|
2210a525ad | ||
|
|
c9bd3adcb3 | ||
|
|
53b9fb183c | ||
|
|
cf3acaa59e | ||
|
|
f702b4f367 | ||
|
|
d68dff1707 | ||
|
|
79d37f5dd3 | ||
|
|
6e36b3d16d | ||
|
|
da8928c993 | ||
|
|
b13136b6e4 | ||
|
|
ed5213f00b | ||
|
|
40598a6e8e | ||
|
|
04221619ea | ||
|
|
badf797701 | ||
|
|
f1604758e7 | ||
|
|
00ec5ca002 | ||
|
|
9b56404e13 | ||
|
|
ab1f479ffd |
+33
-10
@@ -40,7 +40,29 @@
|
||||
- Build the backend foundation for wallet monitoring
|
||||
- Query current blockchain height through `blockchain.headers.subscribe`
|
||||
- Display Fulcrum server version and blockchain height through the status API
|
||||
- Display the current blockchain height in the Munin web interface
|
||||
- Verify live blockchain height from the physical StartOS server
|
||||
- Verify blockchain height display after rebuilding and reinstalling the S9PK package
|
||||
|
||||
### Milestone 4 — Authentication and access control
|
||||
|
||||
- Add user authentication for the Munin web interface
|
||||
- Require authentication before accessing wallet and monitoring data
|
||||
- Add password management through StartOS `Actions & Config`
|
||||
- Store the Munin admin password in the persistent StartOS `main` volume
|
||||
- Pass the stored admin password to the Munin container through `MUNIN_ADMIN_PASSWORD`
|
||||
- Protect the status API from unauthenticated access
|
||||
- Use secure, HTTP-only, SameSite session cookies
|
||||
- Use cryptographically random session tokens
|
||||
- Use constant-time password comparison
|
||||
- Expire sessions after 24 hours
|
||||
- Clean up expired sessions periodically
|
||||
- Display a login form in the web interface
|
||||
- Display authentication errors in the web interface
|
||||
- Verify authentication on the physical StartOS server
|
||||
- Verify that the configured password survives a package update
|
||||
- Verify that authenticated access to the status API continues to work after the package update
|
||||
- Verify that the existing password remains valid after rebuilding and reinstalling the S9PK package
|
||||
|
||||
## Current architecture
|
||||
|
||||
@@ -52,12 +74,22 @@
|
||||
- Fulcrum provided as a StartOS dependency
|
||||
- Electrum protocol connectivity check from Munin to Fulcrum
|
||||
- Blockchain height queried through Fulcrum
|
||||
- Blockchain height displayed in the web interface
|
||||
- StartOS `main` volume used for persistent Munin data
|
||||
- Admin password stored persistently in the StartOS `main` volume
|
||||
- Session-based authentication for the web interface
|
||||
|
||||
## Current status
|
||||
|
||||
Munin Bitcoin is running as a StartOS service and can verify connectivity to the configured Fulcrum Electrum server.
|
||||
|
||||
Munin can query the current Bitcoin blockchain height through Fulcrum.
|
||||
Munin can query the current Bitcoin blockchain height through Fulcrum and display it in the web interface.
|
||||
|
||||
The Munin web interface is protected by password authentication.
|
||||
|
||||
The admin password can be configured through StartOS `Actions & Config` and is stored in the persistent `main` volume so that it survives package updates and reinstallations.
|
||||
|
||||
Authentication and password persistence have been verified on the physical StartOS server, including verification that an existing password remains valid after rebuilding and reinstalling the package.
|
||||
|
||||
Bitcoin Core RPC is not currently used by Munin.
|
||||
|
||||
@@ -65,15 +97,6 @@ Wallet configuration, label storage, transaction scanning, and notification func
|
||||
|
||||
## Next steps
|
||||
|
||||
### Milestone 4 — Authentication and access control
|
||||
|
||||
- Add user authentication for the Munin web interface
|
||||
- Require authentication before accessing wallet and monitoring data
|
||||
- Add password management
|
||||
- Protect wallet addresses, xpubs, BSMS data, labels, and transaction information
|
||||
- Keep authentication credentials separate from wallet data
|
||||
- Integrate with StartOS authentication/security conventions where appropriate
|
||||
|
||||
### Milestone 5 — Address watchlist
|
||||
|
||||
- Add watch-only Bitcoin addresses
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
const crypto = require('crypto')
|
||||
const fs = require('fs/promises')
|
||||
|
||||
const {
|
||||
addressToScriptPubKey,
|
||||
} = require('./bitcoin-address')
|
||||
|
||||
const storePath = '/data/store.json'
|
||||
|
||||
async function readStore() {
|
||||
try {
|
||||
const data = await fs.readFile(storePath, 'utf8')
|
||||
return JSON.parse(data)
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT') {
|
||||
return {}
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async function writeStore(store) {
|
||||
const temporaryPath =
|
||||
`${storePath}.${crypto.randomBytes(8).toString('hex')}.tmp`
|
||||
|
||||
await fs.writeFile(
|
||||
temporaryPath,
|
||||
JSON.stringify(store, null, 2) + '\n',
|
||||
{
|
||||
mode: 0o600,
|
||||
},
|
||||
)
|
||||
|
||||
await fs.rename(temporaryPath, storePath)
|
||||
}
|
||||
|
||||
function normalizeAddress(address) {
|
||||
if (typeof address !== 'string') {
|
||||
return null
|
||||
}
|
||||
|
||||
const normalized = address.trim()
|
||||
|
||||
if (!normalized) {
|
||||
return null
|
||||
}
|
||||
|
||||
return normalized
|
||||
}
|
||||
|
||||
function normalizeLabel(label) {
|
||||
if (typeof label !== 'string') {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const normalized = label.trim()
|
||||
|
||||
return normalized || undefined
|
||||
}
|
||||
|
||||
function validateBitcoinAddress(address) {
|
||||
try {
|
||||
addressToScriptPubKey(address)
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: 'Invalid Bitcoin address',
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async function listAddresses() {
|
||||
const store = await readStore()
|
||||
|
||||
return Array.isArray(store.addresses)
|
||||
? store.addresses
|
||||
: []
|
||||
}
|
||||
|
||||
async function addAddress(address, label) {
|
||||
const normalizedAddress = normalizeAddress(address)
|
||||
|
||||
if (!normalizedAddress) {
|
||||
throw new Error('Bitcoin address is required')
|
||||
}
|
||||
|
||||
validateBitcoinAddress(normalizedAddress)
|
||||
|
||||
const addresses = await listAddresses()
|
||||
|
||||
if (
|
||||
addresses.some(
|
||||
(item) => item.address === normalizedAddress,
|
||||
)
|
||||
) {
|
||||
throw new Error(
|
||||
'Bitcoin address is already being watched',
|
||||
)
|
||||
}
|
||||
|
||||
const entry = {
|
||||
address: normalizedAddress,
|
||||
}
|
||||
|
||||
const normalizedLabel = normalizeLabel(label)
|
||||
|
||||
if (normalizedLabel) {
|
||||
entry.label = normalizedLabel
|
||||
}
|
||||
|
||||
const store = await readStore()
|
||||
|
||||
store.addresses = [...addresses, entry]
|
||||
|
||||
await writeStore(store)
|
||||
|
||||
return entry
|
||||
}
|
||||
|
||||
async function removeAddress(address) {
|
||||
const normalizedAddress = normalizeAddress(address)
|
||||
|
||||
if (!normalizedAddress) {
|
||||
throw new Error('Bitcoin address is required')
|
||||
}
|
||||
|
||||
const store = await readStore()
|
||||
const addresses = Array.isArray(store.addresses)
|
||||
? store.addresses
|
||||
: []
|
||||
|
||||
const filtered = addresses.filter(
|
||||
(item) => item.address !== normalizedAddress,
|
||||
)
|
||||
|
||||
if (filtered.length === addresses.length) {
|
||||
throw new Error(
|
||||
'Bitcoin address is not being watched',
|
||||
)
|
||||
}
|
||||
|
||||
store.addresses = filtered
|
||||
|
||||
await writeStore(store)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
listAddresses,
|
||||
addAddress,
|
||||
removeAddress,
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
const crypto = require('crypto')
|
||||
|
||||
const adminPassword = process.env.MUNIN_ADMIN_PASSWORD || ''
|
||||
const sessions = new Map()
|
||||
|
||||
const SESSION_TTL_MS = 24 * 60 * 60 * 1000
|
||||
|
||||
function createSession() {
|
||||
const token = crypto.randomBytes(32).toString('hex')
|
||||
|
||||
sessions.set(token, {
|
||||
expiresAt: Date.now() + SESSION_TTL_MS,
|
||||
})
|
||||
|
||||
return token
|
||||
}
|
||||
|
||||
function parseCookies(cookieHeader) {
|
||||
const cookies = {}
|
||||
|
||||
for (const part of (cookieHeader || '').split(';')) {
|
||||
const index = part.indexOf('=')
|
||||
if (index === -1) continue
|
||||
|
||||
const name = part.slice(0, index).trim()
|
||||
const value = part.slice(index + 1).trim()
|
||||
|
||||
if (name) {
|
||||
cookies[name] = decodeURIComponent(value)
|
||||
}
|
||||
}
|
||||
|
||||
return cookies
|
||||
}
|
||||
|
||||
function isValidSession(req) {
|
||||
const token = parseCookies(req.headers.cookie).munin_session
|
||||
|
||||
if (!token) return false
|
||||
|
||||
const session = sessions.get(token)
|
||||
|
||||
if (!session) return false
|
||||
|
||||
if (session.expiresAt <= Date.now()) {
|
||||
sessions.delete(token)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function passwordsEqual(a, b) {
|
||||
const left = Buffer.from(a)
|
||||
const right = Buffer.from(b)
|
||||
|
||||
if (left.length !== right.length) return false
|
||||
|
||||
return crypto.timingSafeEqual(left, right)
|
||||
}
|
||||
|
||||
function checkPassword(password) {
|
||||
if (!adminPassword || typeof password !== 'string') {
|
||||
return false
|
||||
}
|
||||
|
||||
return passwordsEqual(password, adminPassword)
|
||||
}
|
||||
|
||||
function cleanupSessions() {
|
||||
const now = Date.now()
|
||||
|
||||
for (const [token, session] of sessions) {
|
||||
if (session.expiresAt <= now) {
|
||||
sessions.delete(token)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setInterval(cleanupSessions, 60 * 60 * 1000).unref()
|
||||
|
||||
module.exports = {
|
||||
checkPassword,
|
||||
createSession,
|
||||
isValidSession,
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
const crypto = require('crypto')
|
||||
|
||||
const BECH32_CHARSET =
|
||||
'qpzry9x8gf2tvdw0s3jn54khce6mua7l'
|
||||
|
||||
function bech32Polymod(values) {
|
||||
const generators = [
|
||||
0x3b6a57b2,
|
||||
0x26508e6d,
|
||||
0x1ea119fa,
|
||||
0x3d4233dd,
|
||||
0x2a1462b3,
|
||||
]
|
||||
|
||||
let chk = 1
|
||||
|
||||
for (const value of values) {
|
||||
const top = chk >>> 25
|
||||
chk = ((chk & 0x1ffffff) << 5) ^ value
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
if ((top >>> i) & 1) {
|
||||
chk ^= generators[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return chk >>> 0
|
||||
}
|
||||
|
||||
function bech32HrpExpand(hrp) {
|
||||
const values = []
|
||||
|
||||
for (const char of hrp) {
|
||||
values.push(char.charCodeAt(0) >> 5)
|
||||
}
|
||||
|
||||
values.push(0)
|
||||
|
||||
for (const char of hrp) {
|
||||
values.push(char.charCodeAt(0) & 31)
|
||||
}
|
||||
|
||||
return values
|
||||
}
|
||||
|
||||
function bech32VerifyChecksum(hrp, data) {
|
||||
const polymod = bech32Polymod([
|
||||
...bech32HrpExpand(hrp),
|
||||
...data,
|
||||
])
|
||||
|
||||
return polymod === 1 || polymod === 0x2bc830a3
|
||||
}
|
||||
|
||||
function bech32Decode(address) {
|
||||
if (typeof address !== 'string') {
|
||||
throw new Error('Bitcoin address must be a string')
|
||||
}
|
||||
|
||||
if (address.length < 8 || address.length > 90) {
|
||||
throw new Error('Invalid Bitcoin address length')
|
||||
}
|
||||
|
||||
const hasLower = address !== address.toUpperCase()
|
||||
const hasUpper = address !== address.toLowerCase()
|
||||
|
||||
if (hasLower && hasUpper) {
|
||||
throw new Error(
|
||||
'Bitcoin address must not mix uppercase and lowercase',
|
||||
)
|
||||
}
|
||||
|
||||
const normalized = address.toLowerCase()
|
||||
const separator = normalized.lastIndexOf('1')
|
||||
|
||||
if (separator < 1 || separator + 7 > normalized.length) {
|
||||
throw new Error('Invalid Bech32 separator')
|
||||
}
|
||||
|
||||
const hrp = normalized.slice(0, separator)
|
||||
const dataPart = normalized.slice(separator + 1)
|
||||
const data = []
|
||||
|
||||
for (const char of dataPart) {
|
||||
const value = BECH32_CHARSET.indexOf(char)
|
||||
|
||||
if (value === -1) {
|
||||
throw new Error('Invalid Bech32 character')
|
||||
}
|
||||
|
||||
data.push(value)
|
||||
}
|
||||
|
||||
if (!bech32VerifyChecksum(hrp, data)) {
|
||||
throw new Error('Invalid Bitcoin address checksum')
|
||||
}
|
||||
|
||||
return {
|
||||
hrp,
|
||||
data: data.slice(0, -6),
|
||||
spec:
|
||||
bech32Polymod([
|
||||
...bech32HrpExpand(hrp),
|
||||
...data,
|
||||
]) === 1
|
||||
? 'bech32'
|
||||
: 'bech32m',
|
||||
}
|
||||
}
|
||||
|
||||
function convertBits(data, fromBits, toBits, pad) {
|
||||
let accumulator = 0
|
||||
let bits = 0
|
||||
const result = []
|
||||
const maxValue = (1 << toBits) - 1
|
||||
|
||||
for (const value of data) {
|
||||
if (value < 0 || value >> fromBits !== 0) {
|
||||
throw new Error('Invalid bit conversion input')
|
||||
}
|
||||
|
||||
accumulator =
|
||||
(accumulator << fromBits) | value
|
||||
bits += fromBits
|
||||
|
||||
while (bits >= toBits) {
|
||||
bits -= toBits
|
||||
result.push(
|
||||
(accumulator >> bits) & maxValue,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (pad) {
|
||||
if (bits > 0) {
|
||||
result.push(
|
||||
(accumulator << (toBits - bits)) & maxValue,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if (bits >= fromBits) {
|
||||
throw new Error('Invalid padding')
|
||||
}
|
||||
|
||||
if (
|
||||
((accumulator << (toBits - bits)) &
|
||||
maxValue) !==
|
||||
0
|
||||
) {
|
||||
throw new Error('Non-zero padding')
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
function addressToScriptPubKey(address) {
|
||||
const decoded = bech32Decode(address)
|
||||
|
||||
if (decoded.hrp !== 'bc') {
|
||||
throw new Error(
|
||||
'Only mainnet Bitcoin addresses are supported',
|
||||
)
|
||||
}
|
||||
|
||||
if (decoded.data.length < 1) {
|
||||
throw new Error('Invalid SegWit address')
|
||||
}
|
||||
|
||||
const witnessVersion = decoded.data[0]
|
||||
|
||||
if (witnessVersion > 16) {
|
||||
throw new Error('Invalid SegWit witness version')
|
||||
}
|
||||
|
||||
if (
|
||||
witnessVersion === 0 &&
|
||||
decoded.spec !== 'bech32'
|
||||
) {
|
||||
throw new Error(
|
||||
'Witness version 0 must use Bech32',
|
||||
)
|
||||
}
|
||||
|
||||
if (
|
||||
witnessVersion !== 0 &&
|
||||
decoded.spec !== 'bech32m'
|
||||
) {
|
||||
throw new Error(
|
||||
'Witness version 1+ must use Bech32m',
|
||||
)
|
||||
}
|
||||
|
||||
const program = convertBits(
|
||||
decoded.data.slice(1),
|
||||
5,
|
||||
8,
|
||||
false,
|
||||
)
|
||||
|
||||
if (program.length < 2 || program.length > 40) {
|
||||
throw new Error('Invalid witness program length')
|
||||
}
|
||||
|
||||
if (
|
||||
witnessVersion === 0 &&
|
||||
program.length !== 20 &&
|
||||
program.length !== 32
|
||||
) {
|
||||
throw new Error(
|
||||
'Invalid witness version 0 program length',
|
||||
)
|
||||
}
|
||||
|
||||
const versionOpcode =
|
||||
witnessVersion === 0
|
||||
? 0x00
|
||||
: 0x50 + witnessVersion
|
||||
|
||||
return Buffer.from([
|
||||
versionOpcode,
|
||||
program.length,
|
||||
...program,
|
||||
])
|
||||
}
|
||||
|
||||
function scriptPubKeyToScripthash(scriptPubKey) {
|
||||
const hash = crypto
|
||||
.createHash('sha256')
|
||||
.update(scriptPubKey)
|
||||
.digest()
|
||||
|
||||
return Buffer.from(hash)
|
||||
.reverse()
|
||||
.toString('hex')
|
||||
}
|
||||
|
||||
function addressToScripthash(address) {
|
||||
return scriptPubKeyToScripthash(
|
||||
addressToScriptPubKey(address),
|
||||
)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
addressToScriptPubKey,
|
||||
scriptPubKeyToScripthash,
|
||||
addressToScripthash,
|
||||
}
|
||||
+1120
-118
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,185 @@
|
||||
const net = require('net')
|
||||
|
||||
const fulcrumTimeoutMs = 3000
|
||||
|
||||
function queryFulcrum(fulcrumUrl, method, params = []) {
|
||||
return new Promise((resolve) => {
|
||||
if (!fulcrumUrl) {
|
||||
resolve({
|
||||
ok: false,
|
||||
result: null,
|
||||
error: 'Fulcrum URL is not configured',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
let parsed
|
||||
|
||||
try {
|
||||
parsed = new URL(fulcrumUrl)
|
||||
} catch {
|
||||
resolve({
|
||||
ok: false,
|
||||
result: null,
|
||||
error: 'Invalid Fulcrum URL',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (parsed.protocol !== 'tcp:') {
|
||||
resolve({
|
||||
ok: false,
|
||||
result: null,
|
||||
error: 'Fulcrum URL must use tcp://',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const host = parsed.hostname
|
||||
const port = Number(parsed.port)
|
||||
|
||||
if (!host || !port) {
|
||||
resolve({
|
||||
ok: false,
|
||||
result: null,
|
||||
error: 'Fulcrum URL is missing host or port',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const socket = net.createConnection({
|
||||
host,
|
||||
port,
|
||||
})
|
||||
|
||||
let response = ''
|
||||
let settled = false
|
||||
|
||||
const finish = (value) => {
|
||||
if (settled) {
|
||||
return
|
||||
}
|
||||
|
||||
settled = true
|
||||
socket.destroy()
|
||||
resolve(value)
|
||||
}
|
||||
|
||||
socket.setTimeout(fulcrumTimeoutMs)
|
||||
|
||||
socket.on('connect', () => {
|
||||
const request =
|
||||
JSON.stringify({
|
||||
jsonrpc: '2.0',
|
||||
id: 1,
|
||||
method,
|
||||
params,
|
||||
}) + '\n'
|
||||
|
||||
socket.write(request)
|
||||
})
|
||||
|
||||
socket.on('data', (data) => {
|
||||
response += data.toString()
|
||||
|
||||
const lines = response.split('\n')
|
||||
|
||||
for (const line of lines) {
|
||||
if (!line.trim()) {
|
||||
continue
|
||||
}
|
||||
|
||||
let message
|
||||
|
||||
try {
|
||||
message = JSON.parse(line)
|
||||
} catch {
|
||||
continue
|
||||
}
|
||||
|
||||
if (message.error) {
|
||||
finish({
|
||||
ok: false,
|
||||
result: null,
|
||||
error: message.error,
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
Object.prototype.hasOwnProperty.call(
|
||||
message,
|
||||
'result',
|
||||
)
|
||||
) {
|
||||
finish({
|
||||
ok: true,
|
||||
result: message.result,
|
||||
error: null,
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
socket.on('timeout', () => {
|
||||
finish({
|
||||
ok: false,
|
||||
result: null,
|
||||
error: 'Connection timed out',
|
||||
})
|
||||
})
|
||||
|
||||
socket.on('error', (error) => {
|
||||
finish({
|
||||
ok: false,
|
||||
result: null,
|
||||
error: error.message,
|
||||
})
|
||||
})
|
||||
|
||||
socket.on('close', () => {
|
||||
if (!settled) {
|
||||
finish({
|
||||
ok: false,
|
||||
result: null,
|
||||
error:
|
||||
'Connection closed before a response was received',
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async function getTransaction(txid) {
|
||||
if (typeof txid !== 'string' || !txid.trim()) {
|
||||
throw new Error('Transaction ID is required')
|
||||
}
|
||||
|
||||
const fulcrumUrl =
|
||||
process.env.MUNIN_FULCRUM_URL || null
|
||||
|
||||
if (!fulcrumUrl) {
|
||||
throw new Error('Fulcrum URL is not configured')
|
||||
}
|
||||
|
||||
const result = await queryFulcrum(
|
||||
fulcrumUrl,
|
||||
'blockchain.transaction.get',
|
||||
[txid.trim(), true],
|
||||
)
|
||||
|
||||
if (!result.ok) {
|
||||
throw new Error(
|
||||
`Failed to query transaction: ${JSON.stringify(result.error)}`,
|
||||
)
|
||||
}
|
||||
|
||||
return result.result
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getTransaction,
|
||||
}
|
||||
@@ -1,8 +1,18 @@
|
||||
import { FileHelper, z } from '@start9labs/start-sdk'
|
||||
import { sdk } from '../sdk'
|
||||
|
||||
export const store = FileHelper.json(
|
||||
'./store.json',
|
||||
{
|
||||
base: sdk.volumes.main,
|
||||
subpath: './store.json',
|
||||
},
|
||||
z.object({
|
||||
adminPassword: z.string().optional(),
|
||||
addresses: z.array(
|
||||
z.object({
|
||||
address: z.string(),
|
||||
label: z.string().optional(),
|
||||
}),
|
||||
).optional(),
|
||||
}),
|
||||
)
|
||||
+15
-5
@@ -2,6 +2,7 @@ import { i18n } from './i18n'
|
||||
import { sdk } from './sdk'
|
||||
import { uiPort } from './utils'
|
||||
import { electrumPort, mainHostId } from 'fulcrum-startos/startos/utils'
|
||||
import { store } from './file-models/store.json'
|
||||
|
||||
export const main = sdk.setupMain(async ({ effects }) => {
|
||||
console.info(i18n('Starting Munin Bitcoin!'))
|
||||
@@ -15,6 +16,8 @@ export const main = sdk.setupMain(async ({ effects }) => {
|
||||
})
|
||||
.const()
|
||||
|
||||
const credentials = await store.read().const(effects)
|
||||
|
||||
return sdk.Daemons.of(effects).addDaemon('primary', {
|
||||
subcontainer: sdk.SubContainer.of(
|
||||
effects,
|
||||
@@ -29,11 +32,18 @@ export const main = sdk.setupMain(async ({ effects }) => {
|
||||
),
|
||||
exec: {
|
||||
command: ['munin-bitcoin'],
|
||||
env: fulcrumAddress
|
||||
? {
|
||||
MUNIN_FULCRUM_URL: `tcp://${fulcrumAddress}`,
|
||||
}
|
||||
: {},
|
||||
env: {
|
||||
...(fulcrumAddress
|
||||
? {
|
||||
MUNIN_FULCRUM_URL: `tcp://${fulcrumAddress}`,
|
||||
}
|
||||
: {}),
|
||||
...(credentials?.adminPassword
|
||||
? {
|
||||
MUNIN_ADMIN_PASSWORD: credentials.adminPassword,
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
},
|
||||
ready: {
|
||||
display: i18n('Web Interface'),
|
||||
|
||||
+2136
-153
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user