@1sat/cli 0.0.83 → 0.0.86
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +48 -47
- package/src/commands/serve.ts +201 -61
- package/src/config.ts +77 -5
- package/src/help.ts +3 -8
- package/src/repricer/buildPriceUpdateTask.ts +39 -29
- package/src/repricer/index.ts +4 -1
- package/src/commands/serve-messagebox.ts +0 -221
package/package.json
CHANGED
|
@@ -1,49 +1,50 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
2
|
+
"name": "@1sat/cli",
|
|
3
|
+
"version": "0.0.86",
|
|
4
|
+
"description": "CLI for 1Sat Ordinals SDK",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/cli.ts",
|
|
7
|
+
"bin": {
|
|
8
|
+
"1sat": "./src/cli.ts"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src",
|
|
12
|
+
"scripts"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"preinstall": "node scripts/check-bun.cjs",
|
|
16
|
+
"build": "bun build ./src/cli.ts --compile --outfile=bin/1sat --external pg --external pg-native --external pg-query-stream --external tedious --external oracledb --external mysql --external mariadb --external 'mariadb/*'",
|
|
17
|
+
"dev": "bun run src/cli.ts",
|
|
18
|
+
"lint": "biome check src",
|
|
19
|
+
"test": "bun test"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"1sat",
|
|
23
|
+
"bsv",
|
|
24
|
+
"ordinals",
|
|
25
|
+
"cli"
|
|
26
|
+
],
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@1sat/actions": "0.0.189",
|
|
30
|
+
"@1sat/client": "0.0.45",
|
|
31
|
+
"@1sat/types": "0.0.36",
|
|
32
|
+
"@1sat/wallet-node": "0.0.62",
|
|
33
|
+
"@1sat/wallet-server": "0.0.32",
|
|
34
|
+
"@bsv/sdk": "^2.1.6",
|
|
35
|
+
"@bsv/wallet-toolbox": "2.1.24",
|
|
36
|
+
"chalk": "^5.0.0",
|
|
37
|
+
"@clack/prompts": "^0.8.0",
|
|
38
|
+
"bitcoin-backup": "^0.0.11",
|
|
39
|
+
"dotenv": "^17.0.0",
|
|
40
|
+
"evlog": "^2.10.0",
|
|
41
|
+
"knex": "^3.1.0",
|
|
42
|
+
"@bopen-io/messagebox-server": "^1.3.2",
|
|
43
|
+
"pg": "^8.11.3",
|
|
44
|
+
"sqlite3": "^6.0.1"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@types/bun": "^1.3.9",
|
|
48
|
+
"typescript": "^5.9.3"
|
|
49
|
+
}
|
|
49
50
|
}
|
package/src/commands/serve.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `1sat serve` command — launch
|
|
2
|
+
* `1sat serve` command — launch the unified host server and/or monitor.
|
|
3
3
|
*
|
|
4
|
-
* 1sat serve
|
|
5
|
-
* 1sat serve wallet Wallet server only
|
|
4
|
+
* 1sat serve Host server (storage + hosting + paymail + messagebox) + monitor
|
|
5
|
+
* 1sat serve wallet Wallet storage server only
|
|
6
6
|
* 1sat serve monitor Monitor daemon only
|
|
7
|
-
* 1sat serve messagebox BSV message-box server
|
|
8
7
|
*
|
|
9
8
|
* The server wraps the same wallet instance the CLI uses. Storage, active
|
|
10
9
|
* remote, and backups all come from `~/.1sat/cli/config.json` via the same
|
|
@@ -14,7 +13,9 @@
|
|
|
14
13
|
* 1sat config set server.port 8100
|
|
15
14
|
* 1sat config set server.host 0.0.0.0
|
|
16
15
|
* 1sat config set server.accounts.enabled true
|
|
17
|
-
* 1sat config set server.
|
|
16
|
+
* 1sat config set server.hosting.enabled true
|
|
17
|
+
* 1sat config set server.paymail.baseUrl https://1sat.app
|
|
18
|
+
* 1sat config set server.monitor.enabled false
|
|
18
19
|
*/
|
|
19
20
|
|
|
20
21
|
import { join } from 'node:path'
|
|
@@ -23,12 +24,19 @@ import {
|
|
|
23
24
|
type NodeWalletStorageConfig,
|
|
24
25
|
createNodeWallet,
|
|
25
26
|
} from '@1sat/wallet-node'
|
|
26
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
KnexPendingStore,
|
|
29
|
+
createHostServer,
|
|
30
|
+
createWalletServer,
|
|
31
|
+
} from '@1sat/wallet-server'
|
|
27
32
|
import type { PrivateKey } from '@bsv/sdk'
|
|
28
33
|
import { initLogger } from 'evlog'
|
|
34
|
+
import knexLib from 'knex'
|
|
29
35
|
import type { GlobalFlags } from '../args'
|
|
30
36
|
import {
|
|
37
|
+
type RepricerConfig,
|
|
31
38
|
type ServerAccountsConfig,
|
|
39
|
+
type ServerMessageboxConfig,
|
|
32
40
|
type ServerStorageConfig,
|
|
33
41
|
loadConfig,
|
|
34
42
|
setConfigPath,
|
|
@@ -39,11 +47,11 @@ import { loadKey } from '../keys'
|
|
|
39
47
|
import { clearMonitorPid, writeMonitorPid } from '../monitor-lock'
|
|
40
48
|
import { fatal } from '../output'
|
|
41
49
|
import {
|
|
50
|
+
type RepriceTarget,
|
|
42
51
|
buildPriceUpdateTask,
|
|
43
52
|
createAccountsConfigLoader,
|
|
44
53
|
resolveRateProvider,
|
|
45
54
|
} from '../repricer'
|
|
46
|
-
import { startMessagebox } from './serve-messagebox'
|
|
47
55
|
|
|
48
56
|
const DEFAULT_HOST = '127.0.0.1'
|
|
49
57
|
const DEFAULT_PORT = 8100
|
|
@@ -51,10 +59,11 @@ const DEFAULT_ONESAT_URL = 'https://api.1sat.app/1sat'
|
|
|
51
59
|
const DEFAULT_BASELINE_BYTES = 1024 * 1024 * 1024 // 1 GB
|
|
52
60
|
const DEFAULT_PURCHASE_UNIT_BYTES = 1_073_741_824 // 1 GB chunks for production
|
|
53
61
|
const DEFAULT_SATS_PER_UNIT = 1_000_000
|
|
62
|
+
const DEFAULT_HOSTING_PRICE_SATS = 10_000
|
|
54
63
|
const DEFAULT_DURATION_BLOCKS = 4383
|
|
55
64
|
const DEFAULT_STORAGE_IDENTITY_KEY = '1sat-cli-default'
|
|
56
65
|
|
|
57
|
-
type ServeMode = 'all' | 'wallet' | 'monitor'
|
|
66
|
+
type ServeMode = 'all' | 'wallet' | 'monitor'
|
|
58
67
|
|
|
59
68
|
interface ResolvedServe {
|
|
60
69
|
chain: 'main' | 'test'
|
|
@@ -68,6 +77,8 @@ interface ResolvedServe {
|
|
|
68
77
|
activeRemote?: string
|
|
69
78
|
backups?: string[]
|
|
70
79
|
accounts: ResolvedAccounts
|
|
80
|
+
repricer?: RepricerConfig
|
|
81
|
+
monitorEnabled: boolean
|
|
71
82
|
privateKey: PrivateKey
|
|
72
83
|
}
|
|
73
84
|
|
|
@@ -78,14 +89,7 @@ interface ResolvedAccounts {
|
|
|
78
89
|
satsPerUnit: number
|
|
79
90
|
durationBlocks: number
|
|
80
91
|
freeIdentityKeys: string[]
|
|
81
|
-
|
|
82
|
-
enabled?: boolean
|
|
83
|
-
targetUsd?: number
|
|
84
|
-
intervalMs?: number
|
|
85
|
-
provider?: string
|
|
86
|
-
maxMovePct?: number
|
|
87
|
-
minSats?: number
|
|
88
|
-
}
|
|
92
|
+
targetUsd?: number
|
|
89
93
|
}
|
|
90
94
|
|
|
91
95
|
export async function handleServeCommand(
|
|
@@ -103,20 +107,6 @@ export async function handleServeCommand(
|
|
|
103
107
|
|
|
104
108
|
initLogger({ env: { service: `1sat-cli-serve-${mode}` } })
|
|
105
109
|
|
|
106
|
-
if (mode === 'messagebox') {
|
|
107
|
-
const handle = await startMessagebox(opts)
|
|
108
|
-
try {
|
|
109
|
-
await waitForShutdown()
|
|
110
|
-
} finally {
|
|
111
|
-
try {
|
|
112
|
-
await handle.stop()
|
|
113
|
-
} catch (err) {
|
|
114
|
-
console.error(`Error during shutdown: ${(err as Error).message}`)
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
return
|
|
118
|
-
}
|
|
119
|
-
|
|
120
110
|
const resolved = await resolveServe(opts)
|
|
121
111
|
const handles: Stoppable[] = []
|
|
122
112
|
|
|
@@ -139,7 +129,6 @@ function resolveMode(subcommand: string | undefined): ServeMode | null {
|
|
|
139
129
|
switch (subcommand) {
|
|
140
130
|
case 'wallet':
|
|
141
131
|
case 'monitor':
|
|
142
|
-
case 'messagebox':
|
|
143
132
|
return subcommand
|
|
144
133
|
default:
|
|
145
134
|
return null
|
|
@@ -187,6 +176,8 @@ async function resolveServe(opts: GlobalFlags): Promise<ResolvedServe> {
|
|
|
187
176
|
activeRemote: config.activeRemote,
|
|
188
177
|
backups: config.backups,
|
|
189
178
|
accounts: resolveAccounts(server.accounts),
|
|
179
|
+
repricer: server.repricer,
|
|
180
|
+
monitorEnabled: resolveMonitorEnabled(server.monitor?.enabled),
|
|
190
181
|
privateKey,
|
|
191
182
|
}
|
|
192
183
|
}
|
|
@@ -195,6 +186,19 @@ function deriveSqliteFilename(dataDir: string, chain: string): string {
|
|
|
195
186
|
return join(dataDir, `wallet-${chain}.db`)
|
|
196
187
|
}
|
|
197
188
|
|
|
189
|
+
/**
|
|
190
|
+
* Env beats config, default on. ONESAT_MONITOR=false is set per-process by
|
|
191
|
+
* clustered deployments (PM2 ecosystem) that run a dedicated `serve monitor`;
|
|
192
|
+
* a plain `1sat serve` runs all-in-one.
|
|
193
|
+
*/
|
|
194
|
+
function resolveMonitorEnabled(configured: boolean | undefined): boolean {
|
|
195
|
+
const fromEnv = process.env.ONESAT_MONITOR
|
|
196
|
+
if (fromEnv && fromEnv.trim() !== '') {
|
|
197
|
+
return !['false', '0', 'off'].includes(fromEnv.trim().toLowerCase())
|
|
198
|
+
}
|
|
199
|
+
return configured !== false
|
|
200
|
+
}
|
|
201
|
+
|
|
198
202
|
function resolvePort(configured: number | undefined): number {
|
|
199
203
|
const fromEnv = process.env.ONESAT_PORT
|
|
200
204
|
if (fromEnv && fromEnv.trim() !== '') {
|
|
@@ -231,7 +235,7 @@ function resolveAccounts(accounts?: ServerAccountsConfig): ResolvedAccounts {
|
|
|
231
235
|
satsPerUnit: accounts?.satsPerUnit ?? DEFAULT_SATS_PER_UNIT,
|
|
232
236
|
durationBlocks: accounts?.durationBlocks ?? DEFAULT_DURATION_BLOCKS,
|
|
233
237
|
freeIdentityKeys: accounts?.freeIdentityKeys ?? [],
|
|
234
|
-
|
|
238
|
+
targetUsd: accounts?.targetUsd,
|
|
235
239
|
}
|
|
236
240
|
}
|
|
237
241
|
|
|
@@ -258,12 +262,17 @@ async function runWithStorage(
|
|
|
258
262
|
storage,
|
|
259
263
|
activeRemote: resolved.activeRemote,
|
|
260
264
|
backups: resolved.backups,
|
|
261
|
-
//
|
|
262
|
-
//
|
|
263
|
-
// initial runOnce is redundant in all three cases.
|
|
265
|
+
// Each serve mode manages monitor work explicitly (see runMonitor
|
|
266
|
+
// below); the factory's initial runOnce is redundant in every case.
|
|
264
267
|
skipInitialMonitor: true,
|
|
265
268
|
})
|
|
266
269
|
|
|
270
|
+
// One active monitor per deployment: wallet mode never runs it, monitor
|
|
271
|
+
// mode always does, all mode runs it unless server.monitor.enabled=false
|
|
272
|
+
// (set on redundant host instances that share a dedicated serve monitor).
|
|
273
|
+
const runMonitor =
|
|
274
|
+
mode === 'monitor' || (mode === 'all' && resolved.monitorEnabled)
|
|
275
|
+
|
|
267
276
|
const accounts =
|
|
268
277
|
mode === 'monitor'
|
|
269
278
|
? undefined
|
|
@@ -272,17 +281,35 @@ async function runWithStorage(
|
|
|
272
281
|
const serverHandle =
|
|
273
282
|
mode === 'monitor'
|
|
274
283
|
? undefined
|
|
275
|
-
: await startWalletServer(resolved, walletResult, accounts)
|
|
276
|
-
|
|
277
|
-
if (
|
|
278
|
-
const
|
|
279
|
-
const
|
|
280
|
-
if (
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
284
|
+
: await startWalletServer(resolved, walletResult, accounts, mode)
|
|
285
|
+
|
|
286
|
+
if (runMonitor) {
|
|
287
|
+
const r = resolved.repricer
|
|
288
|
+
const targets: RepriceTarget[] = []
|
|
289
|
+
if (resolved.accounts.enabled && (resolved.accounts.targetUsd ?? 0) > 0) {
|
|
290
|
+
targets.push({
|
|
291
|
+
name: 'accounts',
|
|
292
|
+
targetUsd: resolved.accounts.targetUsd!,
|
|
293
|
+
readCurrentSats: () =>
|
|
294
|
+
loadConfig().server?.accounts?.satsPerUnit ?? DEFAULT_SATS_PER_UNIT,
|
|
295
|
+
onPersist: async (sats) => {
|
|
296
|
+
setConfigPath('server.accounts.satsPerUnit', sats)
|
|
297
|
+
},
|
|
298
|
+
})
|
|
299
|
+
}
|
|
300
|
+
const hostingCfg = loadConfig().server?.hosting
|
|
301
|
+
if (hostingCfg?.enabled === true && (hostingCfg.targetUsd ?? 0) > 0) {
|
|
302
|
+
targets.push({
|
|
303
|
+
name: 'hosting',
|
|
304
|
+
targetUsd: hostingCfg.targetUsd!,
|
|
305
|
+
readCurrentSats: () =>
|
|
306
|
+
loadConfig().server?.hosting?.priceSats ?? DEFAULT_HOSTING_PRICE_SATS,
|
|
307
|
+
onPersist: async (sats) => {
|
|
308
|
+
setConfigPath('server.hosting.priceSats', sats)
|
|
309
|
+
},
|
|
310
|
+
})
|
|
311
|
+
}
|
|
312
|
+
if (r?.enabled && targets.length > 0) {
|
|
286
313
|
walletResult.monitor.addTask(
|
|
287
314
|
buildPriceUpdateTask({
|
|
288
315
|
monitor: walletResult.monitor,
|
|
@@ -290,27 +317,24 @@ async function runWithStorage(
|
|
|
290
317
|
chain: resolved.chain,
|
|
291
318
|
}),
|
|
292
319
|
intervalMs: r.intervalMs ?? 15 * 60 * 1000,
|
|
293
|
-
targetUsd: r.targetUsd,
|
|
294
320
|
bounds: {
|
|
295
321
|
maxMovePct: r.maxMovePct ?? 25,
|
|
296
322
|
minSats: r.minSats ?? 1,
|
|
297
323
|
},
|
|
298
|
-
|
|
299
|
-
loadConfig().server?.accounts?.satsPerUnit ?? DEFAULT_SATS_PER_UNIT,
|
|
300
|
-
onPersist: async (sats) => {
|
|
301
|
-
setConfigPath('server.accounts.satsPerUnit', sats)
|
|
302
|
-
},
|
|
324
|
+
targets,
|
|
303
325
|
}),
|
|
304
326
|
)
|
|
305
327
|
console.log(
|
|
306
|
-
`[repricer] enabled —
|
|
328
|
+
`[repricer] enabled — ${targets
|
|
329
|
+
.map((t) => `${t.name} $${t.targetUsd}`)
|
|
330
|
+
.join(', ')} every ${Math.round(
|
|
307
331
|
(r.intervalMs ?? 900_000) / 1000,
|
|
308
332
|
)}s via ${r.provider ?? 'whatsonchain'}`,
|
|
309
333
|
)
|
|
310
334
|
}
|
|
311
335
|
}
|
|
312
336
|
|
|
313
|
-
if (
|
|
337
|
+
if (runMonitor) {
|
|
314
338
|
// startTasks loops until stopTasks flips its flag. Fire without
|
|
315
339
|
// awaiting so the caller can install shutdown handlers and write
|
|
316
340
|
// the monitor pid file.
|
|
@@ -323,7 +347,7 @@ async function runWithStorage(
|
|
|
323
347
|
|
|
324
348
|
return {
|
|
325
349
|
async stop() {
|
|
326
|
-
if (
|
|
350
|
+
if (runMonitor) {
|
|
327
351
|
walletResult.monitor.stopTasks()
|
|
328
352
|
clearMonitorPid(resolved.dataDir, process.pid)
|
|
329
353
|
}
|
|
@@ -339,20 +363,136 @@ async function startWalletServer(
|
|
|
339
363
|
resolved: ResolvedServe,
|
|
340
364
|
walletResult: NodeWalletResult,
|
|
341
365
|
accounts: AccountsRuntime | undefined,
|
|
366
|
+
mode: ServeMode,
|
|
342
367
|
): Promise<{ stop(): Promise<void> }> {
|
|
343
|
-
const
|
|
368
|
+
const config = loadConfig()
|
|
369
|
+
const hostingCfg = config.server?.hosting
|
|
370
|
+
const hostingEnabled = hostingCfg?.enabled === true
|
|
371
|
+
const hosting = hostingEnabled
|
|
372
|
+
? {
|
|
373
|
+
getConfig: () => {
|
|
374
|
+
const h = loadConfig().server?.hosting
|
|
375
|
+
return {
|
|
376
|
+
enabled: h?.enabled === true,
|
|
377
|
+
priceSats: h?.priceSats ?? DEFAULT_HOSTING_PRICE_SATS,
|
|
378
|
+
priceUsd: h?.targetUsd,
|
|
379
|
+
periodSeconds: h?.periodSeconds ?? 2_592_000,
|
|
380
|
+
}
|
|
381
|
+
},
|
|
382
|
+
}
|
|
383
|
+
: undefined
|
|
384
|
+
|
|
385
|
+
if (mode === 'wallet') {
|
|
386
|
+
const handle = createWalletServer({
|
|
387
|
+
wallet: walletResult.wallet,
|
|
388
|
+
storage: walletResult.getActiveStorage(),
|
|
389
|
+
serverIdentityKey: walletResult.wallet.identityKey,
|
|
390
|
+
listen: { port: resolved.port, host: resolved.host },
|
|
391
|
+
publicPath: '/',
|
|
392
|
+
internalPath: null,
|
|
393
|
+
accounts: accounts?.walletServerAccounts,
|
|
394
|
+
hosting,
|
|
395
|
+
})
|
|
396
|
+
const port = await handle.start()
|
|
397
|
+
const notes = [
|
|
398
|
+
resolved.accounts.enabled ? 'accounts: on' : '',
|
|
399
|
+
hostingEnabled ? 'hosting: on' : '',
|
|
400
|
+
]
|
|
401
|
+
.filter(Boolean)
|
|
402
|
+
.join(', ')
|
|
403
|
+
console.log(
|
|
404
|
+
`[wallet] listening on ${resolved.host}:${port}${notes ? ` (${notes})` : ''}`,
|
|
405
|
+
)
|
|
406
|
+
return { stop: () => handle.stop() }
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Unified host: storage + messagebox + paymail + hosting
|
|
410
|
+
const serverCfg = config.server ?? {}
|
|
411
|
+
const paymailCfg = serverCfg.paymail
|
|
412
|
+
const messageboxCfg = serverCfg.messagebox
|
|
413
|
+
|
|
414
|
+
const knex = createMessageboxKnex(
|
|
415
|
+
serverCfg.storage ?? { provider: 'bun-sqlite' },
|
|
416
|
+
messageboxCfg,
|
|
417
|
+
resolved,
|
|
418
|
+
)
|
|
419
|
+
const pendingStore = new KnexPendingStore(knex)
|
|
420
|
+
await pendingStore.init()
|
|
421
|
+
|
|
422
|
+
const handle = await createHostServer({
|
|
344
423
|
wallet: walletResult.wallet,
|
|
345
424
|
storage: walletResult.getActiveStorage(),
|
|
346
425
|
serverIdentityKey: walletResult.wallet.identityKey,
|
|
347
426
|
listen: { port: resolved.port, host: resolved.host },
|
|
348
|
-
publicPath: '/',
|
|
349
|
-
internalPath: null,
|
|
350
427
|
accounts: accounts?.walletServerAccounts,
|
|
428
|
+
hosting,
|
|
429
|
+
paymail: {
|
|
430
|
+
baseUrl:
|
|
431
|
+
paymailCfg?.baseUrl ?? `http://${resolved.host}:${resolved.port}`,
|
|
432
|
+
stackUrl: paymailCfg?.stackUrl ?? 'https://api.1sat.app',
|
|
433
|
+
pendingStore,
|
|
434
|
+
hostWallet: hostingEnabled ? walletResult.wallet : undefined,
|
|
435
|
+
requireEntitlement: hostingEnabled,
|
|
436
|
+
messageboxUrl: `http://127.0.0.1:${resolved.port}/messagebox`,
|
|
437
|
+
hostPrivateKey: resolved.privateKey,
|
|
438
|
+
},
|
|
439
|
+
messagebox: {
|
|
440
|
+
knex,
|
|
441
|
+
websockets: messageboxCfg?.websockets !== false,
|
|
442
|
+
},
|
|
351
443
|
})
|
|
352
444
|
const port = await handle.start()
|
|
353
|
-
const
|
|
354
|
-
|
|
355
|
-
|
|
445
|
+
const notes = [
|
|
446
|
+
resolved.accounts.enabled ? 'accounts: on' : '',
|
|
447
|
+
hostingEnabled ? 'hosting: on' : '',
|
|
448
|
+
]
|
|
449
|
+
.filter(Boolean)
|
|
450
|
+
.join(', ')
|
|
451
|
+
console.log(
|
|
452
|
+
`[host] listening on ${resolved.host}:${port}${notes ? ` (${notes})` : ''}`,
|
|
453
|
+
)
|
|
454
|
+
return {
|
|
455
|
+
stop: async () => {
|
|
456
|
+
await handle.stop()
|
|
457
|
+
if (knex) await knex.destroy()
|
|
458
|
+
},
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const DEFAULT_PG_SCHEMA = 'messagebox'
|
|
463
|
+
|
|
464
|
+
function createMessageboxKnex(
|
|
465
|
+
storage: ServerStorageConfig,
|
|
466
|
+
messagebox: ServerMessageboxConfig | undefined,
|
|
467
|
+
resolved: ResolvedServe,
|
|
468
|
+
) {
|
|
469
|
+
if (storage.provider === 'bun-sqlite') {
|
|
470
|
+
const filename =
|
|
471
|
+
messagebox?.dbPath ??
|
|
472
|
+
join(resolved.dataDir, `messagebox-${resolved.chain}.db`)
|
|
473
|
+
return (knexLib as any)({
|
|
474
|
+
client: 'sqlite3',
|
|
475
|
+
connection: { filename },
|
|
476
|
+
useNullAsDefault: true,
|
|
477
|
+
})
|
|
478
|
+
}
|
|
479
|
+
if (storage.provider === 'pg') {
|
|
480
|
+
if (messagebox?.dbUrl) {
|
|
481
|
+
return (knexLib as any)({
|
|
482
|
+
client: 'pg',
|
|
483
|
+
connection: { connectionString: messagebox.dbUrl },
|
|
484
|
+
})
|
|
485
|
+
}
|
|
486
|
+
const schema = messagebox?.pgSchema ?? DEFAULT_PG_SCHEMA
|
|
487
|
+
return (knexLib as any)({
|
|
488
|
+
client: 'pg',
|
|
489
|
+
connection: { connectionString: storage.dbUrl },
|
|
490
|
+
searchPath: [schema],
|
|
491
|
+
})
|
|
492
|
+
}
|
|
493
|
+
fatal(
|
|
494
|
+
`server.storage.provider '${(storage as { provider: string }).provider}' is not supported for messagebox storage.`,
|
|
495
|
+
)
|
|
356
496
|
}
|
|
357
497
|
|
|
358
498
|
interface AccountsRuntime {
|
package/src/config.ts
CHANGED
|
@@ -25,18 +25,23 @@ export type ServerStorageConfig =
|
|
|
25
25
|
| ServerStorageBunSqliteConfig
|
|
26
26
|
| ServerStoragePgConfig
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Shared repricer engine (`server.repricer`). When enabled, a monitor task
|
|
30
|
+
* fetches the BSV/USD rate on the interval and rewrites each service's sats
|
|
31
|
+
* price toward its own `targetUsd` (`server.accounts.targetUsd` →
|
|
32
|
+
* `satsPerUnit`, `server.hosting.targetUsd` → `priceSats`). Services with no
|
|
33
|
+
* `targetUsd` set are left alone.
|
|
34
|
+
*/
|
|
28
35
|
export interface RepricerConfig {
|
|
29
36
|
/** Master toggle. Defaults to false. */
|
|
30
37
|
enabled?: boolean
|
|
31
|
-
/** Target price per `purchaseUnitBytes` chunk, in USD. */
|
|
32
|
-
targetUsd?: number
|
|
33
38
|
/** Interval between rate checks in ms. Defaults to 900000 (15 min). */
|
|
34
39
|
intervalMs?: number
|
|
35
40
|
/** Rate provider name. Defaults to "whatsonchain". */
|
|
36
41
|
provider?: string
|
|
37
42
|
/** Max percent change allowed per update. Larger moves are skipped. Defaults to 25. */
|
|
38
43
|
maxMovePct?: number
|
|
39
|
-
/** Lower bound for
|
|
44
|
+
/** Lower bound for repriced sats values. Defaults to 1. */
|
|
40
45
|
minSats?: number
|
|
41
46
|
}
|
|
42
47
|
|
|
@@ -49,12 +54,12 @@ export interface ServerAccountsConfig {
|
|
|
49
54
|
purchaseUnitBytes?: number
|
|
50
55
|
/** Sats charged per purchase unit. */
|
|
51
56
|
satsPerUnit?: number
|
|
57
|
+
/** USD target per purchase unit, maintained by `server.repricer`. */
|
|
58
|
+
targetUsd?: number
|
|
52
59
|
/** Block window a payment remains valid for. */
|
|
53
60
|
durationBlocks?: number
|
|
54
61
|
/** Identity keys that bypass metering (server's own key is auto-added). */
|
|
55
62
|
freeIdentityKeys?: string[]
|
|
56
|
-
/** Optional auto-repricer. When enabled, updates `satsPerUnit` from a live BSV/USD rate. */
|
|
57
|
-
repricer?: RepricerConfig
|
|
58
63
|
}
|
|
59
64
|
|
|
60
65
|
export interface ServerMessageboxConfig {
|
|
@@ -85,6 +90,65 @@ export interface ServerMessageboxConfig {
|
|
|
85
90
|
walletStorageUrl?: string
|
|
86
91
|
}
|
|
87
92
|
|
|
93
|
+
export interface ServerPaymailConfig {
|
|
94
|
+
/** Port to bind. Defaults to `8466` or ONESAT_PAYMAIL_PORT. */
|
|
95
|
+
port?: number
|
|
96
|
+
/**
|
|
97
|
+
* Public base URL used in /.well-known/bsvalias capability links
|
|
98
|
+
* (e.g. https://1sat.app). Required for serve paymail.
|
|
99
|
+
*/
|
|
100
|
+
baseUrl?: string
|
|
101
|
+
/** Stack API root (OpNS, beef, tx). Defaults to https://api.1sat.app */
|
|
102
|
+
stackUrl?: string
|
|
103
|
+
/**
|
|
104
|
+
* SQLite path for in-flight P2P payment references (Go paymail port).
|
|
105
|
+
* Defaults to `<dataDir>/paymail-pending-{chain}.db`.
|
|
106
|
+
*/
|
|
107
|
+
pendingDbPath?: string
|
|
108
|
+
/**
|
|
109
|
+
* Messagebox URL for payment_inbox delivery after receive.
|
|
110
|
+
* Defaults to https://messagebox.1sat.app when unset is OK to skip —
|
|
111
|
+
* set explicitly to enable (same as Go messagebox_url).
|
|
112
|
+
*/
|
|
113
|
+
messageboxUrl?: string
|
|
114
|
+
/**
|
|
115
|
+
* When true, open host wallet storage and require active HOSTING_BASKET
|
|
116
|
+
* receipt for the bound identity. Default false until subscribe path ships.
|
|
117
|
+
*/
|
|
118
|
+
requireEntitlement?: boolean
|
|
119
|
+
/**
|
|
120
|
+
* Wallet storage URL for entitlement listOutputs (and optional later
|
|
121
|
+
* subscribe). Defaults to local wallet serve URL like messagebox.
|
|
122
|
+
*/
|
|
123
|
+
walletStorageUrl?: string
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface ServerHostingConfig {
|
|
127
|
+
/** Master toggle. Defaults to false. */
|
|
128
|
+
enabled?: boolean
|
|
129
|
+
/** Sats per subscription period. */
|
|
130
|
+
priceSats?: number
|
|
131
|
+
/**
|
|
132
|
+
* USD price per subscription period. Returned as `priceUsd` on the
|
|
133
|
+
* pricing API for display, and maintained as the repricing target for
|
|
134
|
+
* `priceSats` when `server.repricer` is enabled.
|
|
135
|
+
*/
|
|
136
|
+
targetUsd?: number
|
|
137
|
+
/** Period length in seconds (e.g. 2592000 ≈ 30 days). */
|
|
138
|
+
periodSeconds?: number
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export interface ServerMonitorConfig {
|
|
142
|
+
/**
|
|
143
|
+
* Run the background monitor task loop (post waiting txs, check for
|
|
144
|
+
* proofs, account repricing). Defaults to true. Set false on redundant
|
|
145
|
+
* host instances that run alongside a dedicated `serve monitor`, so a
|
|
146
|
+
* single active monitor runs per deployment. `serve monitor` always runs
|
|
147
|
+
* the loop regardless of this flag.
|
|
148
|
+
*/
|
|
149
|
+
enabled?: boolean
|
|
150
|
+
}
|
|
151
|
+
|
|
88
152
|
export interface ServerConfig {
|
|
89
153
|
/** Hostname to bind. Defaults to `127.0.0.1`. */
|
|
90
154
|
host?: string
|
|
@@ -96,6 +160,14 @@ export interface ServerConfig {
|
|
|
96
160
|
accounts?: ServerAccountsConfig
|
|
97
161
|
/** Messagebox subcommand settings. */
|
|
98
162
|
messagebox?: ServerMessageboxConfig
|
|
163
|
+
/** Paymail subcommand settings. */
|
|
164
|
+
paymail?: ServerPaymailConfig
|
|
165
|
+
/** Hosted paymail subscription (wallet serve /hosting/*). */
|
|
166
|
+
hosting?: ServerHostingConfig
|
|
167
|
+
/** Shared BSV/USD repricer engine for accounts + hosting prices. */
|
|
168
|
+
repricer?: RepricerConfig
|
|
169
|
+
/** Background monitor task loop. Defaults to enabled. */
|
|
170
|
+
monitor?: ServerMonitorConfig
|
|
99
171
|
}
|
|
100
172
|
|
|
101
173
|
export interface OneSatCliConfig {
|
package/src/help.ts
CHANGED
|
@@ -560,19 +560,14 @@ export const COMMANDS: CommandSpec[] = [
|
|
|
560
560
|
group: 'Server',
|
|
561
561
|
name: 'serve',
|
|
562
562
|
description:
|
|
563
|
-
'Run
|
|
563
|
+
'Run unified host server (storage + hosting + paymail + messagebox) and/or monitor (config under server.* in config.json)',
|
|
564
564
|
subcommands: [
|
|
565
565
|
{
|
|
566
566
|
name: '(no subcommand)',
|
|
567
|
-
description: '
|
|
567
|
+
description: 'Host server + monitor daemon',
|
|
568
568
|
},
|
|
569
|
-
{ name: 'wallet', description: 'Wallet server only (BRC-100 HTTP)' },
|
|
569
|
+
{ name: 'wallet', description: 'Wallet storage server only (BRC-100 HTTP)' },
|
|
570
570
|
{ name: 'monitor', description: 'Monitor daemon only' },
|
|
571
|
-
{
|
|
572
|
-
name: 'messagebox',
|
|
573
|
-
description:
|
|
574
|
-
'BSV message-box server (port 8771 default; uses wallet identity)',
|
|
575
|
-
},
|
|
576
571
|
],
|
|
577
572
|
},
|
|
578
573
|
{
|
|
@@ -1,18 +1,25 @@
|
|
|
1
1
|
import { computeReprice } from './computeReprice'
|
|
2
2
|
import type { RateProvider, RepricerBounds } from './types'
|
|
3
3
|
|
|
4
|
+
/** One price the repricer maintains from the shared BSV/USD quote. */
|
|
5
|
+
export interface RepriceTarget {
|
|
6
|
+
/** Label for task result logs, e.g. 'accounts' or 'hosting'. */
|
|
7
|
+
name: string
|
|
8
|
+
/** USD target for this price. Targets with 0 or negative are ignored. */
|
|
9
|
+
targetUsd: number
|
|
10
|
+
/** Read the current sats value (typically from disk via the same loader the servers use). */
|
|
11
|
+
readCurrentSats: () => number
|
|
12
|
+
/** Persist the new sats value (wraps `setConfigPath`). */
|
|
13
|
+
onPersist: (newSats: number) => Promise<void>
|
|
14
|
+
}
|
|
15
|
+
|
|
4
16
|
export interface PriceUpdateTaskOptions {
|
|
5
17
|
monitor: unknown
|
|
6
18
|
rateProvider: RateProvider
|
|
7
19
|
/** Interval between successful tick attempts. */
|
|
8
20
|
intervalMs: number
|
|
9
|
-
/** USD target per purchase unit. Task no-ops when this is 0. */
|
|
10
|
-
targetUsd: number
|
|
11
21
|
bounds: RepricerBounds
|
|
12
|
-
|
|
13
|
-
readCurrentSats: () => number
|
|
14
|
-
/** Persist the new `satsPerUnit` value. Wraps `setConfigPath('server.accounts.satsPerUnit', sats)`. */
|
|
15
|
-
onPersist: (newSats: number) => Promise<void>
|
|
22
|
+
targets: RepriceTarget[]
|
|
16
23
|
}
|
|
17
24
|
|
|
18
25
|
export interface PriceUpdateTask {
|
|
@@ -28,15 +35,8 @@ export interface PriceUpdateTask {
|
|
|
28
35
|
export function buildPriceUpdateTask(
|
|
29
36
|
options: PriceUpdateTaskOptions,
|
|
30
37
|
): PriceUpdateTask {
|
|
31
|
-
const {
|
|
32
|
-
|
|
33
|
-
rateProvider,
|
|
34
|
-
intervalMs,
|
|
35
|
-
targetUsd,
|
|
36
|
-
bounds,
|
|
37
|
-
readCurrentSats,
|
|
38
|
-
onPersist,
|
|
39
|
-
} = options
|
|
38
|
+
const { monitor, rateProvider, intervalMs, bounds } = options
|
|
39
|
+
const targets = options.targets.filter((t) => t.targetUsd > 0)
|
|
40
40
|
|
|
41
41
|
const storage = (monitor as { storage?: unknown } | null | undefined)?.storage
|
|
42
42
|
|
|
@@ -47,7 +47,7 @@ export function buildPriceUpdateTask(
|
|
|
47
47
|
lastRunMsecsSinceEpoch: 0,
|
|
48
48
|
async asyncSetup() {},
|
|
49
49
|
trigger(now: number): { run: boolean } {
|
|
50
|
-
if (
|
|
50
|
+
if (targets.length === 0) return { run: false }
|
|
51
51
|
if (now - this.lastRunMsecsSinceEpoch < intervalMs) return { run: false }
|
|
52
52
|
return { run: true }
|
|
53
53
|
},
|
|
@@ -59,22 +59,32 @@ export function buildPriceUpdateTask(
|
|
|
59
59
|
return `rate fetch failed: ${(err as Error).message}`
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
const
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
62
|
+
const parts: string[] = []
|
|
63
|
+
for (const target of targets) {
|
|
64
|
+
const currentSats = target.readCurrentSats()
|
|
65
|
+
const result = computeReprice({
|
|
66
|
+
targetUsd: target.targetUsd,
|
|
67
|
+
bsvUsd: quote.bsvUsd,
|
|
68
|
+
currentSats,
|
|
69
|
+
bounds,
|
|
70
|
+
})
|
|
69
71
|
|
|
70
|
-
|
|
72
|
+
if (result.status === 'skipped') {
|
|
73
|
+
parts.push(`${target.name}: skipped: ${result.reason}`)
|
|
74
|
+
continue
|
|
75
|
+
}
|
|
71
76
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
77
|
+
try {
|
|
78
|
+
await target.onPersist(result.newSats)
|
|
79
|
+
} catch (err) {
|
|
80
|
+
parts.push(
|
|
81
|
+
`${target.name}: persist failed: ${(err as Error).message}`,
|
|
82
|
+
)
|
|
83
|
+
continue
|
|
84
|
+
}
|
|
85
|
+
parts.push(`${target.name}: ${currentSats} → ${result.newSats} sats`)
|
|
76
86
|
}
|
|
77
|
-
return `${
|
|
87
|
+
return `${parts.join('; ')} @ $${quote.bsvUsd}/BSV`
|
|
78
88
|
},
|
|
79
89
|
}
|
|
80
90
|
}
|
package/src/repricer/index.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
export { buildPriceUpdateTask } from './buildPriceUpdateTask'
|
|
2
|
-
export type {
|
|
2
|
+
export type {
|
|
3
|
+
PriceUpdateTaskOptions,
|
|
4
|
+
RepriceTarget,
|
|
5
|
+
} from './buildPriceUpdateTask'
|
|
3
6
|
export { computeReprice } from './computeReprice'
|
|
4
7
|
export { createAccountsConfigLoader } from './configLoader'
|
|
5
8
|
export type { AccountsConfigLoaderOptions } from './configLoader'
|
|
@@ -1,221 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `1sat serve messagebox` — launch the BSV message-box server using the
|
|
3
|
-
* CLI's wallet identity.
|
|
4
|
-
*
|
|
5
|
-
* messagebox-server boots itself as a side effect of `import` once env vars
|
|
6
|
-
* are populated, so this handler:
|
|
7
|
-
* 1. Reads CLI config + private key
|
|
8
|
-
* 2. Composes env (SERVER_PRIVATE_KEY, PORT, KNEX_DB_*, WALLET_STORAGE_URL, …)
|
|
9
|
-
* 3. Switches cwd to the messagebox-server install dir so its knexfile's
|
|
10
|
-
* relative `./out/src/migrations` path resolves correctly
|
|
11
|
-
* 4. Dynamically imports `@bopen-io/messagebox-server`
|
|
12
|
-
* 5. Awaits SIGINT/SIGTERM, then closes the HTTP and WebSocket servers
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import { dirname, join } from 'node:path'
|
|
16
|
-
import { fileURLToPath } from 'node:url'
|
|
17
|
-
import type { PrivateKey } from '@bsv/sdk'
|
|
18
|
-
import type { GlobalFlags } from '../args'
|
|
19
|
-
import {
|
|
20
|
-
type ServerMessageboxConfig,
|
|
21
|
-
type ServerStorageConfig,
|
|
22
|
-
ensureDataDir,
|
|
23
|
-
loadConfig,
|
|
24
|
-
} from '../config'
|
|
25
|
-
import { loadKey } from '../keys'
|
|
26
|
-
import { fatal } from '../output'
|
|
27
|
-
|
|
28
|
-
const DEFAULT_MESSAGEBOX_PORT = 8771
|
|
29
|
-
const DEFAULT_WALLET_HOST = '127.0.0.1'
|
|
30
|
-
const DEFAULT_WALLET_PORT = 8100
|
|
31
|
-
const DEFAULT_PG_SCHEMA = 'messagebox'
|
|
32
|
-
|
|
33
|
-
interface ResolvedMessagebox {
|
|
34
|
-
chain: 'main' | 'test'
|
|
35
|
-
port: number
|
|
36
|
-
websockets: boolean
|
|
37
|
-
walletStorageUrl: string
|
|
38
|
-
knexClient: string
|
|
39
|
-
knexConnection: Record<string, unknown>
|
|
40
|
-
identityHex: string
|
|
41
|
-
identityPub: string
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
interface MessageboxModule {
|
|
45
|
-
http: { close(cb?: (err?: Error) => void): void }
|
|
46
|
-
io: { close(cb?: () => void): void } | null
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export interface MessageboxStoppable {
|
|
50
|
-
stop(): Promise<void>
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export async function startMessagebox(
|
|
54
|
-
opts: GlobalFlags,
|
|
55
|
-
): Promise<MessageboxStoppable> {
|
|
56
|
-
const resolved = await resolveMessagebox(opts)
|
|
57
|
-
applyEnv(resolved)
|
|
58
|
-
|
|
59
|
-
console.log(`[messagebox] starting on 0.0.0.0:${resolved.port}`)
|
|
60
|
-
console.log(`[messagebox] identity: ${resolved.identityPub.slice(0, 16)}…`)
|
|
61
|
-
console.log(`[messagebox] storage: ${resolved.knexClient}`)
|
|
62
|
-
console.log(`[messagebox] wallet: ${resolved.walletStorageUrl}`)
|
|
63
|
-
console.log(
|
|
64
|
-
`[messagebox] websockets: ${resolved.websockets ? 'enabled' : 'disabled'}`,
|
|
65
|
-
)
|
|
66
|
-
|
|
67
|
-
// messagebox-server's knexfile uses `./out/src/migrations` relative to
|
|
68
|
-
// cwd. Switch cwd to its install dir so that path resolves correctly
|
|
69
|
-
// regardless of where the CLI was invoked from.
|
|
70
|
-
const packageJsonUrl = import.meta.resolve(
|
|
71
|
-
'@bopen-io/messagebox-server/package.json',
|
|
72
|
-
)
|
|
73
|
-
const messageboxDir = dirname(fileURLToPath(packageJsonUrl))
|
|
74
|
-
process.chdir(messageboxDir)
|
|
75
|
-
|
|
76
|
-
// messagebox-server boots itself when its index module is loaded.
|
|
77
|
-
const mbox = (await import(
|
|
78
|
-
'@bopen-io/messagebox-server'
|
|
79
|
-
)) as unknown as MessageboxModule
|
|
80
|
-
|
|
81
|
-
return {
|
|
82
|
-
async stop() {
|
|
83
|
-
await new Promise<void>((resolve) => {
|
|
84
|
-
try {
|
|
85
|
-
mbox.http.close(() => resolve())
|
|
86
|
-
} catch {
|
|
87
|
-
resolve()
|
|
88
|
-
}
|
|
89
|
-
})
|
|
90
|
-
if (mbox.io) {
|
|
91
|
-
await new Promise<void>((resolve) => {
|
|
92
|
-
try {
|
|
93
|
-
mbox.io?.close(() => resolve())
|
|
94
|
-
} catch {
|
|
95
|
-
resolve()
|
|
96
|
-
}
|
|
97
|
-
})
|
|
98
|
-
}
|
|
99
|
-
},
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
async function resolveMessagebox(
|
|
104
|
-
opts: GlobalFlags,
|
|
105
|
-
): Promise<ResolvedMessagebox> {
|
|
106
|
-
const config = loadConfig()
|
|
107
|
-
const server = config.server ?? {}
|
|
108
|
-
const messagebox: ServerMessageboxConfig = server.messagebox ?? {}
|
|
109
|
-
|
|
110
|
-
const chain = opts.chain ?? config.chain ?? 'main'
|
|
111
|
-
const port = resolvePort(messagebox.port)
|
|
112
|
-
const websockets = messagebox.websockets !== false
|
|
113
|
-
|
|
114
|
-
let privateKey: PrivateKey
|
|
115
|
-
try {
|
|
116
|
-
privateKey = await loadKey()
|
|
117
|
-
} catch (err) {
|
|
118
|
-
fatal((err as Error).message)
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const identityHex = privateKey.toString()
|
|
122
|
-
const identityPub = privateKey.toPublicKey().toString()
|
|
123
|
-
|
|
124
|
-
const walletHost = server.host ?? DEFAULT_WALLET_HOST
|
|
125
|
-
const walletPort = server.port ?? DEFAULT_WALLET_PORT
|
|
126
|
-
const walletStorageUrl =
|
|
127
|
-
messagebox.walletStorageUrl ?? `http://${walletHost}:${walletPort}/`
|
|
128
|
-
|
|
129
|
-
const { knexClient, knexConnection } = resolveStorage(
|
|
130
|
-
server.storage ?? { provider: 'bun-sqlite' },
|
|
131
|
-
messagebox,
|
|
132
|
-
chain,
|
|
133
|
-
)
|
|
134
|
-
|
|
135
|
-
return {
|
|
136
|
-
chain,
|
|
137
|
-
port,
|
|
138
|
-
websockets,
|
|
139
|
-
walletStorageUrl,
|
|
140
|
-
knexClient,
|
|
141
|
-
knexConnection,
|
|
142
|
-
identityHex,
|
|
143
|
-
identityPub,
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function resolvePort(configured: number | undefined): number {
|
|
148
|
-
const fromEnv = process.env.ONESAT_MESSAGEBOX_PORT
|
|
149
|
-
if (fromEnv && fromEnv.trim() !== '') {
|
|
150
|
-
const parsed = Number(fromEnv)
|
|
151
|
-
if (!Number.isFinite(parsed) || parsed <= 0 || parsed > 65535) {
|
|
152
|
-
fatal(
|
|
153
|
-
`ONESAT_MESSAGEBOX_PORT must be a valid port number, got: ${fromEnv}`,
|
|
154
|
-
)
|
|
155
|
-
}
|
|
156
|
-
return parsed
|
|
157
|
-
}
|
|
158
|
-
return configured ?? DEFAULT_MESSAGEBOX_PORT
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function resolveStorage(
|
|
162
|
-
storage: ServerStorageConfig,
|
|
163
|
-
messagebox: ServerMessageboxConfig,
|
|
164
|
-
chain: 'main' | 'test',
|
|
165
|
-
): { knexClient: string; knexConnection: Record<string, unknown> } {
|
|
166
|
-
if (storage.provider === 'bun-sqlite') {
|
|
167
|
-
const dataDir = ensureDataDir()
|
|
168
|
-
const filename =
|
|
169
|
-
messagebox.dbPath ?? join(dataDir, `messagebox-${chain}.db`)
|
|
170
|
-
return {
|
|
171
|
-
knexClient: 'sqlite3',
|
|
172
|
-
knexConnection: { filename },
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
if (storage.provider === 'pg') {
|
|
177
|
-
// Prefer a dedicated messagebox database when configured (recommended:
|
|
178
|
-
// avoids `knex_migrations` table collisions with wallet-toolbox).
|
|
179
|
-
// Fall back to schema isolation against the wallet's database — note
|
|
180
|
-
// the `messagebox` schema must exist in that database for table
|
|
181
|
-
// creation to land there instead of `public`.
|
|
182
|
-
if (messagebox.dbUrl) {
|
|
183
|
-
return {
|
|
184
|
-
knexClient: 'pg',
|
|
185
|
-
knexConnection: { connectionString: messagebox.dbUrl },
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
const schema = messagebox.pgSchema ?? DEFAULT_PG_SCHEMA
|
|
189
|
-
return {
|
|
190
|
-
knexClient: 'pg',
|
|
191
|
-
knexConnection: {
|
|
192
|
-
connectionString: storage.dbUrl,
|
|
193
|
-
searchPath: [schema, 'public'],
|
|
194
|
-
},
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
fatal(
|
|
199
|
-
`server.storage.provider '${(storage as { provider: string }).provider}' is not supported by 'serve messagebox'.`,
|
|
200
|
-
)
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
function applyEnv(resolved: ResolvedMessagebox): void {
|
|
204
|
-
// messagebox-server reads env at module-load time, so we set everything
|
|
205
|
-
// before the dynamic import. dotenv.config() inside messagebox-server
|
|
206
|
-
// does not overwrite values already set on process.env.
|
|
207
|
-
process.env.SERVER_PRIVATE_KEY = resolved.identityHex
|
|
208
|
-
process.env.BSV_NETWORK = resolved.chain === 'main' ? 'mainnet' : 'testnet'
|
|
209
|
-
process.env.PORT = String(resolved.port)
|
|
210
|
-
// messagebox-server's index.ts forces HTTP_PORT=3000 when NODE_ENV
|
|
211
|
-
// is anything other than 'development', ignoring our PORT entirely.
|
|
212
|
-
// Use 'development' so PORT is honored.
|
|
213
|
-
process.env.NODE_ENV = 'development'
|
|
214
|
-
process.env.ENABLE_WEBSOCKETS = resolved.websockets ? 'true' : 'false'
|
|
215
|
-
process.env.WALLET_STORAGE_URL = resolved.walletStorageUrl
|
|
216
|
-
process.env.KNEX_DB_CLIENT = resolved.knexClient
|
|
217
|
-
process.env.KNEX_DB_CONNECTION = JSON.stringify(resolved.knexConnection)
|
|
218
|
-
if (process.env.ENABLE_FIREBASE === undefined) {
|
|
219
|
-
process.env.ENABLE_FIREBASE = 'false'
|
|
220
|
-
}
|
|
221
|
-
}
|