@1sat/cli 0.0.30 → 0.0.32
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 +8 -4
- package/src/cli.ts +5 -0
- package/src/commands/config.ts +72 -51
- package/src/commands/serve.ts +297 -0
- package/src/config.ts +105 -27
- package/src/context.ts +5 -45
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@1sat/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.32",
|
|
4
4
|
"description": "CLI for 1Sat Ordinals SDK",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/cli.ts",
|
|
@@ -16,16 +16,20 @@
|
|
|
16
16
|
"keywords": ["1sat", "bsv", "ordinals", "cli"],
|
|
17
17
|
"license": "MIT",
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@1sat/actions": "0.0.
|
|
19
|
+
"@1sat/actions": "0.0.99",
|
|
20
20
|
"@1sat/client": "0.0.21",
|
|
21
21
|
"@1sat/types": "0.0.17",
|
|
22
|
-
"@1sat/wallet-node": "0.0.
|
|
22
|
+
"@1sat/wallet-node": "0.0.29",
|
|
23
|
+
"@1sat/wallet-server": "0.0.1",
|
|
23
24
|
"@bsv/sdk": "^2.0.13",
|
|
25
|
+
"@bsv/wallet-toolbox": "^2.1.21",
|
|
24
26
|
"chalk": "^5.0.0",
|
|
25
27
|
"@clack/prompts": "^0.8.0",
|
|
26
28
|
"bitcoin-backup": "^0.0.11",
|
|
27
29
|
"dotenv": "^17.0.0",
|
|
28
|
-
"evlog": "^2.10.0"
|
|
30
|
+
"evlog": "^2.10.0",
|
|
31
|
+
"knex": "^3.1.0",
|
|
32
|
+
"pg": "^8.11.3"
|
|
29
33
|
},
|
|
30
34
|
"devDependencies": {
|
|
31
35
|
"@types/bun": "^1.3.9",
|
package/src/cli.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { handleMcpProxyCommand } from './commands/mcp-proxy'
|
|
|
16
16
|
import { handleOpnsCommand } from './commands/opns'
|
|
17
17
|
import { handleOrdinalsCommand } from './commands/ordinals'
|
|
18
18
|
import { handleRemoteCommand } from './commands/remote'
|
|
19
|
+
import { handleServeCommand } from './commands/serve'
|
|
19
20
|
import { handleSocialCommand } from './commands/social'
|
|
20
21
|
import { handleSweepCommand } from './commands/sweep'
|
|
21
22
|
import { handleTokensCommand } from './commands/tokens'
|
|
@@ -98,6 +99,10 @@ async function main(): Promise<void> {
|
|
|
98
99
|
await handleMcpProxyCommand()
|
|
99
100
|
break
|
|
100
101
|
|
|
102
|
+
case 'serve':
|
|
103
|
+
await handleServeCommand(rest, flags)
|
|
104
|
+
break
|
|
105
|
+
|
|
101
106
|
case 'help':
|
|
102
107
|
printHelp()
|
|
103
108
|
break
|
package/src/commands/config.ts
CHANGED
|
@@ -10,11 +10,12 @@
|
|
|
10
10
|
|
|
11
11
|
import type { GlobalFlags } from '../args'
|
|
12
12
|
import {
|
|
13
|
-
type OneSatCliConfig,
|
|
14
13
|
getConfigDir,
|
|
15
14
|
getConfigFile,
|
|
16
15
|
loadConfig,
|
|
17
|
-
|
|
16
|
+
parseConfigValue,
|
|
17
|
+
setConfigPath,
|
|
18
|
+
unsetConfigPath,
|
|
18
19
|
} from '../config'
|
|
19
20
|
import { printCommandHelp } from '../help'
|
|
20
21
|
import {
|
|
@@ -23,16 +24,8 @@ import {
|
|
|
23
24
|
formatSuccess,
|
|
24
25
|
formatValue,
|
|
25
26
|
output,
|
|
26
|
-
printKeyValue,
|
|
27
27
|
} from '../output'
|
|
28
28
|
|
|
29
|
-
const SETTABLE_KEYS: Array<keyof OneSatCliConfig> = [
|
|
30
|
-
'chain',
|
|
31
|
-
'dataDir',
|
|
32
|
-
'storageIdentityKey',
|
|
33
|
-
'monitorIntervalMinutes',
|
|
34
|
-
]
|
|
35
|
-
|
|
36
29
|
export async function handleConfigCommand(
|
|
37
30
|
args: string[],
|
|
38
31
|
opts: GlobalFlags,
|
|
@@ -70,12 +63,7 @@ function configShow(opts: GlobalFlags): void {
|
|
|
70
63
|
}
|
|
71
64
|
|
|
72
65
|
console.log()
|
|
73
|
-
|
|
74
|
-
chain: config.chain,
|
|
75
|
-
dataDir: config.dataDir,
|
|
76
|
-
storageIdentityKey: config.storageIdentityKey ?? '(not set)',
|
|
77
|
-
monitorIntervalMinutes: config.monitorIntervalMinutes,
|
|
78
|
-
})
|
|
66
|
+
printNested(config, '')
|
|
79
67
|
console.log()
|
|
80
68
|
console.log(
|
|
81
69
|
` ${formatLabel('config file:')} ${formatValue(getConfigFile())}`,
|
|
@@ -83,56 +71,89 @@ function configShow(opts: GlobalFlags): void {
|
|
|
83
71
|
console.log()
|
|
84
72
|
}
|
|
85
73
|
|
|
86
|
-
function
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
fatal(
|
|
91
|
-
`Usage: 1sat config set <key> <value>\n\nSettable keys: ${SETTABLE_KEYS.join(', ')}`,
|
|
74
|
+
function printNested(value: unknown, prefix: string): void {
|
|
75
|
+
if (value == null || typeof value !== 'object' || Array.isArray(value)) {
|
|
76
|
+
console.log(
|
|
77
|
+
` ${formatLabel(`${prefix || '(value)'}:`)} ${formatValue(String(value))}`,
|
|
92
78
|
)
|
|
79
|
+
return
|
|
93
80
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
81
|
+
const obj = value as Record<string, unknown>
|
|
82
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
83
|
+
const path = prefix ? `${prefix}.${k}` : k
|
|
84
|
+
if (v !== null && typeof v === 'object' && !Array.isArray(v)) {
|
|
85
|
+
printNested(v, path)
|
|
86
|
+
} else {
|
|
87
|
+
const display = Array.isArray(v)
|
|
88
|
+
? JSON.stringify(v)
|
|
89
|
+
: String(v ?? '(not set)')
|
|
90
|
+
console.log(` ${formatLabel(`${path}:`)} ${formatValue(display)}`)
|
|
91
|
+
}
|
|
99
92
|
}
|
|
93
|
+
}
|
|
100
94
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
if (key ===
|
|
106
|
-
|
|
107
|
-
if (!Number.isFinite(n) || n < 0) {
|
|
108
|
-
fatal(
|
|
109
|
-
'monitorIntervalMinutes must be a non-negative number (0 to disable)',
|
|
110
|
-
)
|
|
111
|
-
}
|
|
95
|
+
function configSet(args: string[], opts: GlobalFlags): void {
|
|
96
|
+
const [key, ...valueArgs] = args
|
|
97
|
+
const value = valueArgs.join(' ')
|
|
98
|
+
|
|
99
|
+
if (!key || valueArgs.length === 0) {
|
|
100
|
+
fatal('Usage: 1sat config set <dotted.path> <value>')
|
|
112
101
|
}
|
|
113
102
|
|
|
114
|
-
const
|
|
115
|
-
|
|
103
|
+
const parsed = parseConfigValue(value)
|
|
104
|
+
validateKnownPath(key, parsed)
|
|
105
|
+
|
|
106
|
+
setConfigPath(key, parsed)
|
|
107
|
+
output(
|
|
108
|
+
opts.json ? { [key]: parsed } : formatSuccess(`Set ${key} = ${value}`),
|
|
109
|
+
opts,
|
|
110
|
+
)
|
|
116
111
|
}
|
|
117
112
|
|
|
118
113
|
function configUnset(args: string[], opts: GlobalFlags): void {
|
|
119
114
|
const [key] = args
|
|
120
115
|
|
|
121
116
|
if (!key) {
|
|
122
|
-
fatal(
|
|
123
|
-
`Usage: 1sat config unset <key>\n\nKeys that can be unset: ${SETTABLE_KEYS.join(', ')}`,
|
|
124
|
-
)
|
|
117
|
+
fatal('Usage: 1sat config unset <dotted.path>')
|
|
125
118
|
}
|
|
126
119
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
)
|
|
131
|
-
}
|
|
120
|
+
unsetConfigPath(key)
|
|
121
|
+
output(opts.json ? { [key]: undefined } : formatSuccess(`Unset ${key}`), opts)
|
|
122
|
+
}
|
|
132
123
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
124
|
+
/**
|
|
125
|
+
* Enforce the few semantic constraints we care about. Unknown paths are
|
|
126
|
+
* allowed — callers that read them can validate at consumption time.
|
|
127
|
+
*/
|
|
128
|
+
function validateKnownPath(key: string, value: unknown): void {
|
|
129
|
+
switch (key) {
|
|
130
|
+
case 'chain':
|
|
131
|
+
if (value !== 'main' && value !== 'test') {
|
|
132
|
+
fatal("chain must be 'main' or 'test'")
|
|
133
|
+
}
|
|
134
|
+
break
|
|
135
|
+
case 'server.storage.provider':
|
|
136
|
+
if (
|
|
137
|
+
value !== 'bun-sqlite' &&
|
|
138
|
+
value !== 'knex-sqlite' &&
|
|
139
|
+
value !== 'knex-pg'
|
|
140
|
+
) {
|
|
141
|
+
fatal(
|
|
142
|
+
"server.storage.provider must be 'bun-sqlite' | 'knex-sqlite' | 'knex-pg'",
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
break
|
|
146
|
+
case 'server.port':
|
|
147
|
+
if (typeof value !== 'number' || value <= 0 || !Number.isInteger(value)) {
|
|
148
|
+
fatal('server.port must be a positive integer')
|
|
149
|
+
}
|
|
150
|
+
break
|
|
151
|
+
case 'server.accounts.enabled':
|
|
152
|
+
if (typeof value !== 'boolean') {
|
|
153
|
+
fatal('server.accounts.enabled must be true or false')
|
|
154
|
+
}
|
|
155
|
+
break
|
|
156
|
+
}
|
|
136
157
|
}
|
|
137
158
|
|
|
138
159
|
function configPath(opts: GlobalFlags): void {
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `1sat serve` command — launch wallet server and/or monitor.
|
|
3
|
+
*
|
|
4
|
+
* 1sat serve Wallet server + monitor daemon
|
|
5
|
+
* 1sat serve wallet Wallet server only
|
|
6
|
+
* 1sat serve monitor Monitor daemon only
|
|
7
|
+
*
|
|
8
|
+
* The server wraps the same wallet instance the CLI uses. Storage, active
|
|
9
|
+
* remote, and backups all come from `~/.1sat/cli/config.json` via the same
|
|
10
|
+
* `createNodeWallet` factory `1sat wallet` commands use.
|
|
11
|
+
*
|
|
12
|
+
* Server-only settings live under `server.*` in the config:
|
|
13
|
+
* 1sat config set server.port 8100
|
|
14
|
+
* 1sat config set server.host 0.0.0.0
|
|
15
|
+
* 1sat config set server.accounts.enabled true
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { join } from 'node:path'
|
|
19
|
+
import { OneSatServices } from '@1sat/client'
|
|
20
|
+
import { type NodeWalletResult, createNodeWallet } from '@1sat/wallet-node'
|
|
21
|
+
import {
|
|
22
|
+
createWalletServer,
|
|
23
|
+
runMigrations as runAccountsMigrations,
|
|
24
|
+
} from '@1sat/wallet-server'
|
|
25
|
+
import type { PrivateKey } from '@bsv/sdk'
|
|
26
|
+
import { type Knex, knex } from 'knex'
|
|
27
|
+
import type { GlobalFlags } from '../args'
|
|
28
|
+
import {
|
|
29
|
+
type ServerAccountsConfig,
|
|
30
|
+
type ServerStorageConfig,
|
|
31
|
+
loadConfig,
|
|
32
|
+
} from '../config'
|
|
33
|
+
import { ensureDataDir } from '../config'
|
|
34
|
+
import { printCommandHelp } from '../help'
|
|
35
|
+
import { loadKey, resolvePassword } from '../keys'
|
|
36
|
+
import { fatal } from '../output'
|
|
37
|
+
|
|
38
|
+
const DEFAULT_HOST = '127.0.0.1'
|
|
39
|
+
const DEFAULT_PORT = 8100
|
|
40
|
+
const DEFAULT_ONESAT_URL = 'https://api.1sat.app/1sat'
|
|
41
|
+
const DEFAULT_BASELINE_BYTES = 1024 * 1024 * 1024 // 1 GB
|
|
42
|
+
const DEFAULT_SATS_PER_GB = 1_000_000
|
|
43
|
+
const DEFAULT_DURATION_BLOCKS = 4383
|
|
44
|
+
const DEFAULT_STORAGE_IDENTITY_KEY = '1sat-cli-default'
|
|
45
|
+
|
|
46
|
+
type ServeMode = 'all' | 'wallet' | 'monitor'
|
|
47
|
+
|
|
48
|
+
interface ResolvedServe {
|
|
49
|
+
chain: 'main' | 'test'
|
|
50
|
+
host: string
|
|
51
|
+
port: number
|
|
52
|
+
onesatURL: string
|
|
53
|
+
storage: ServerStorageConfig
|
|
54
|
+
dataDir: string
|
|
55
|
+
sqliteFilename: string
|
|
56
|
+
storageIdentityKey: string
|
|
57
|
+
activeRemote?: string
|
|
58
|
+
backups?: string[]
|
|
59
|
+
accounts: ResolvedAccounts
|
|
60
|
+
privateKey: PrivateKey
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
interface ResolvedAccounts {
|
|
64
|
+
enabled: boolean
|
|
65
|
+
baselineBytes: number
|
|
66
|
+
satsPerGb: number
|
|
67
|
+
durationBlocks: number
|
|
68
|
+
freeIdentityKeys: string[]
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function handleServeCommand(
|
|
72
|
+
args: string[],
|
|
73
|
+
opts: GlobalFlags,
|
|
74
|
+
): Promise<void> {
|
|
75
|
+
const [subcommand] = args
|
|
76
|
+
const mode = resolveMode(subcommand)
|
|
77
|
+
|
|
78
|
+
if (mode === null) {
|
|
79
|
+
printCommandHelp('serve', {
|
|
80
|
+
'(no subcommand)': 'Wallet server plus monitor daemon',
|
|
81
|
+
wallet: 'Wallet server only',
|
|
82
|
+
monitor: 'Monitor daemon only',
|
|
83
|
+
})
|
|
84
|
+
if (subcommand && subcommand !== 'help') process.exit(1)
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const resolved = await resolveServe(opts)
|
|
89
|
+
const handles: Stoppable[] = []
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
if (resolved.storage.provider === 'bun-sqlite') {
|
|
93
|
+
handles.push(await runBunSqlite(resolved, mode))
|
|
94
|
+
} else {
|
|
95
|
+
fatal(
|
|
96
|
+
`server.storage.provider '${resolved.storage.provider}' is not yet wired through the shared wallet factory. Only bun-sqlite is supported at the moment.`,
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
await waitForShutdown()
|
|
101
|
+
} finally {
|
|
102
|
+
for (const h of handles.reverse()) {
|
|
103
|
+
try {
|
|
104
|
+
await h.stop()
|
|
105
|
+
} catch (err) {
|
|
106
|
+
console.error(`Error during shutdown: ${(err as Error).message}`)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function resolveMode(subcommand: string | undefined): ServeMode | null {
|
|
113
|
+
if (!subcommand) return 'all'
|
|
114
|
+
switch (subcommand) {
|
|
115
|
+
case 'wallet':
|
|
116
|
+
case 'monitor':
|
|
117
|
+
return subcommand
|
|
118
|
+
default:
|
|
119
|
+
return null
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Load the CLI config, apply serve defaults, and resolve the server identity
|
|
125
|
+
* key via the existing keyring mechanism.
|
|
126
|
+
*/
|
|
127
|
+
async function resolveServe(opts: GlobalFlags): Promise<ResolvedServe> {
|
|
128
|
+
const config = loadConfig()
|
|
129
|
+
const server = config.server ?? {}
|
|
130
|
+
|
|
131
|
+
const storage: ServerStorageConfig = server.storage ?? {
|
|
132
|
+
provider: 'bun-sqlite',
|
|
133
|
+
}
|
|
134
|
+
if (storage.provider === 'knex-pg' && !storage.dbUrl) {
|
|
135
|
+
fatal(
|
|
136
|
+
'server.storage.provider is knex-pg but server.storage.dbUrl is not set. ' +
|
|
137
|
+
'Set it with: 1sat config set server.storage.dbUrl postgres://…',
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const dataDir = ensureDataDir()
|
|
142
|
+
const chain = opts.chain ?? config.chain ?? 'main'
|
|
143
|
+
|
|
144
|
+
let privateKey: PrivateKey
|
|
145
|
+
try {
|
|
146
|
+
privateKey = await loadKey(resolvePassword())
|
|
147
|
+
} catch (err) {
|
|
148
|
+
fatal((err as Error).message)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
chain,
|
|
153
|
+
host: server.host ?? DEFAULT_HOST,
|
|
154
|
+
port: server.port ?? DEFAULT_PORT,
|
|
155
|
+
onesatURL: DEFAULT_ONESAT_URL,
|
|
156
|
+
storage,
|
|
157
|
+
dataDir,
|
|
158
|
+
sqliteFilename: deriveSqliteFilename(dataDir, chain),
|
|
159
|
+
storageIdentityKey:
|
|
160
|
+
config.storageIdentityKey ?? DEFAULT_STORAGE_IDENTITY_KEY,
|
|
161
|
+
activeRemote: config.activeRemote,
|
|
162
|
+
backups: config.backups,
|
|
163
|
+
accounts: resolveAccounts(server.accounts),
|
|
164
|
+
privateKey,
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function deriveSqliteFilename(dataDir: string, chain: string): string {
|
|
169
|
+
return join(dataDir, `wallet-${chain}.db`)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function resolveAccounts(accounts?: ServerAccountsConfig): ResolvedAccounts {
|
|
173
|
+
return {
|
|
174
|
+
enabled: accounts?.enabled ?? false,
|
|
175
|
+
baselineBytes: accounts?.baselineBytes ?? DEFAULT_BASELINE_BYTES,
|
|
176
|
+
satsPerGb: accounts?.satsPerGb ?? DEFAULT_SATS_PER_GB,
|
|
177
|
+
durationBlocks: accounts?.durationBlocks ?? DEFAULT_DURATION_BLOCKS,
|
|
178
|
+
freeIdentityKeys: accounts?.freeIdentityKeys ?? [],
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
interface Stoppable {
|
|
183
|
+
stop(): Promise<void>
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* bun-sqlite path: construct the wallet via the same `createNodeWallet`
|
|
188
|
+
* factory the CLI uses. Server + monitor operate on that single wallet
|
|
189
|
+
* instance, so `activeRemote`, `backups`, and `storageIdentityKey` behave
|
|
190
|
+
* identically to `1sat wallet <command>`.
|
|
191
|
+
*/
|
|
192
|
+
async function runBunSqlite(
|
|
193
|
+
resolved: ResolvedServe,
|
|
194
|
+
mode: ServeMode,
|
|
195
|
+
): Promise<Stoppable> {
|
|
196
|
+
const walletResult = await createNodeWallet({
|
|
197
|
+
privateKey: resolved.privateKey,
|
|
198
|
+
chain: resolved.chain,
|
|
199
|
+
storageIdentityKey: resolved.storageIdentityKey,
|
|
200
|
+
filename: resolved.sqliteFilename,
|
|
201
|
+
activeRemote: resolved.activeRemote,
|
|
202
|
+
backups: resolved.backups,
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
const accounts = await buildAccountsForServer(resolved)
|
|
206
|
+
|
|
207
|
+
const serverHandle =
|
|
208
|
+
mode === 'monitor'
|
|
209
|
+
? undefined
|
|
210
|
+
: await startWalletServer(resolved, walletResult, accounts)
|
|
211
|
+
|
|
212
|
+
if (mode !== 'wallet') {
|
|
213
|
+
await walletResult.monitor.startTasks()
|
|
214
|
+
console.log('[monitor] started')
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
async stop() {
|
|
219
|
+
if (mode !== 'wallet') {
|
|
220
|
+
walletResult.monitor.stopTasks()
|
|
221
|
+
}
|
|
222
|
+
if (serverHandle) await serverHandle.stop()
|
|
223
|
+
if (accounts) await accounts.knex.destroy()
|
|
224
|
+
await walletResult.destroy()
|
|
225
|
+
},
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async function startWalletServer(
|
|
230
|
+
resolved: ResolvedServe,
|
|
231
|
+
walletResult: NodeWalletResult,
|
|
232
|
+
accounts: AccountsRuntime | undefined,
|
|
233
|
+
): Promise<{ stop(): Promise<void> }> {
|
|
234
|
+
const handle = createWalletServer({
|
|
235
|
+
storage: walletResult.storage,
|
|
236
|
+
serverPrivateKey: resolved.privateKey.toHex(),
|
|
237
|
+
listen: { port: resolved.port, host: resolved.host },
|
|
238
|
+
publicPath: '/',
|
|
239
|
+
internalPath: null,
|
|
240
|
+
accounts: accounts?.walletServerAccounts,
|
|
241
|
+
})
|
|
242
|
+
const port = await handle.start()
|
|
243
|
+
const accountsNote = resolved.accounts.enabled ? ' (accounts: on)' : ''
|
|
244
|
+
console.log(`[wallet] listening on ${resolved.host}:${port}${accountsNote}`)
|
|
245
|
+
return { stop: () => handle.stop() }
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
interface AccountsRuntime {
|
|
249
|
+
walletServerAccounts: NonNullable<
|
|
250
|
+
Parameters<typeof createWalletServer>[0]['accounts']
|
|
251
|
+
>
|
|
252
|
+
knex: Knex
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async function buildAccountsForServer(
|
|
256
|
+
resolved: ResolvedServe,
|
|
257
|
+
): Promise<AccountsRuntime | undefined> {
|
|
258
|
+
if (!resolved.accounts.enabled) return undefined
|
|
259
|
+
|
|
260
|
+
// Accounts uses its own knex pool. For bun-sqlite wallets that means
|
|
261
|
+
// opening the same file via better-sqlite3; for future pg wallets it
|
|
262
|
+
// shares the pg connection. Always a file path for now.
|
|
263
|
+
const accountsKnex = knex({
|
|
264
|
+
client: 'better-sqlite3',
|
|
265
|
+
connection: { filename: resolved.sqliteFilename },
|
|
266
|
+
useNullAsDefault: true,
|
|
267
|
+
})
|
|
268
|
+
await runAccountsMigrations(accountsKnex)
|
|
269
|
+
|
|
270
|
+
const services = new OneSatServices(resolved.chain, resolved.onesatURL)
|
|
271
|
+
|
|
272
|
+
return {
|
|
273
|
+
knex: accountsKnex,
|
|
274
|
+
walletServerAccounts: {
|
|
275
|
+
config: {
|
|
276
|
+
enabled: true,
|
|
277
|
+
baselineBytes: resolved.accounts.baselineBytes,
|
|
278
|
+
satsPerGb: resolved.accounts.satsPerGb,
|
|
279
|
+
durationBlocks: resolved.accounts.durationBlocks,
|
|
280
|
+
freeIdentityKeys: resolved.accounts.freeIdentityKeys,
|
|
281
|
+
},
|
|
282
|
+
knex: accountsKnex,
|
|
283
|
+
currentBlock: () => services.chaintracks.currentHeight(),
|
|
284
|
+
},
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function waitForShutdown(): Promise<void> {
|
|
289
|
+
return new Promise((resolve) => {
|
|
290
|
+
const handler = (sig: string) => {
|
|
291
|
+
console.log(`Received ${sig}, shutting down...`)
|
|
292
|
+
resolve()
|
|
293
|
+
}
|
|
294
|
+
process.once('SIGINT', () => handler('SIGINT'))
|
|
295
|
+
process.once('SIGTERM', () => handler('SIGTERM'))
|
|
296
|
+
})
|
|
297
|
+
}
|
package/src/config.ts
CHANGED
|
@@ -11,6 +11,49 @@ import { join } from 'node:path'
|
|
|
11
11
|
const CONFIG_DIR = join(homedir(), '.1sat', 'cli')
|
|
12
12
|
const CONFIG_FILE = join(CONFIG_DIR, 'config.json')
|
|
13
13
|
|
|
14
|
+
export interface ServerStorageBunSqliteConfig {
|
|
15
|
+
provider: 'bun-sqlite'
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ServerStorageKnexSqliteConfig {
|
|
19
|
+
provider: 'knex-sqlite'
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ServerStorageKnexPgConfig {
|
|
23
|
+
provider: 'knex-pg'
|
|
24
|
+
/** Postgres connection URL (required for knex-pg). */
|
|
25
|
+
dbUrl: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type ServerStorageConfig =
|
|
29
|
+
| ServerStorageBunSqliteConfig
|
|
30
|
+
| ServerStorageKnexSqliteConfig
|
|
31
|
+
| ServerStorageKnexPgConfig
|
|
32
|
+
|
|
33
|
+
export interface ServerAccountsConfig {
|
|
34
|
+
/** Master toggle. Defaults to false when omitted. */
|
|
35
|
+
enabled?: boolean
|
|
36
|
+
/** Free baseline per identity key, in bytes. */
|
|
37
|
+
baselineBytes?: number
|
|
38
|
+
/** Sats charged per GB of paid capacity per `durationBlocks`. */
|
|
39
|
+
satsPerGb?: number
|
|
40
|
+
/** Block window a payment remains valid for. */
|
|
41
|
+
durationBlocks?: number
|
|
42
|
+
/** Identity keys that bypass metering (server's own key is auto-added). */
|
|
43
|
+
freeIdentityKeys?: string[]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface ServerConfig {
|
|
47
|
+
/** Hostname to bind. Defaults to `127.0.0.1`. */
|
|
48
|
+
host?: string
|
|
49
|
+
/** Port to bind. Defaults to `8100`. */
|
|
50
|
+
port?: number
|
|
51
|
+
/** Storage backend. Defaults to `{ provider: 'bun-sqlite' }`. */
|
|
52
|
+
storage?: ServerStorageConfig
|
|
53
|
+
/** Optional account/metering layer (opt-in per-deployment). */
|
|
54
|
+
accounts?: ServerAccountsConfig
|
|
55
|
+
}
|
|
56
|
+
|
|
14
57
|
export interface OneSatCliConfig {
|
|
15
58
|
/** Network: mainnet or testnet */
|
|
16
59
|
chain: 'main' | 'test'
|
|
@@ -22,38 +65,13 @@ export interface OneSatCliConfig {
|
|
|
22
65
|
backups?: string[]
|
|
23
66
|
/** Storage identity key for wallet persistence */
|
|
24
67
|
storageIdentityKey?: string
|
|
25
|
-
/**
|
|
26
|
-
|
|
68
|
+
/** Settings read by `1sat serve` subcommands. Absent for client-only installs. */
|
|
69
|
+
server?: ServerConfig
|
|
27
70
|
}
|
|
28
71
|
|
|
29
72
|
const DEFAULT_CONFIG: OneSatCliConfig = {
|
|
30
73
|
chain: 'main',
|
|
31
74
|
dataDir: join(CONFIG_DIR, 'data'),
|
|
32
|
-
monitorIntervalMinutes: 5,
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Monitor state — stored separately so we don't rewrite config.json on every command
|
|
36
|
-
const MONITOR_STATE_FILE = join(CONFIG_DIR, 'monitor-state.json')
|
|
37
|
-
|
|
38
|
-
export interface MonitorState {
|
|
39
|
-
lastMonitorRun: number // unix ms timestamp
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function loadMonitorState(): MonitorState {
|
|
43
|
-
if (!existsSync(MONITOR_STATE_FILE)) return { lastMonitorRun: 0 }
|
|
44
|
-
try {
|
|
45
|
-
const raw = readFileSync(MONITOR_STATE_FILE, 'utf8')
|
|
46
|
-
return JSON.parse(raw)
|
|
47
|
-
} catch {
|
|
48
|
-
return { lastMonitorRun: 0 }
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export function saveMonitorState(state: MonitorState): void {
|
|
53
|
-
ensureConfigDir()
|
|
54
|
-
writeFileSync(MONITOR_STATE_FILE, JSON.stringify(state, null, 2), {
|
|
55
|
-
mode: 0o600,
|
|
56
|
-
})
|
|
57
75
|
}
|
|
58
76
|
|
|
59
77
|
/**
|
|
@@ -99,6 +117,66 @@ export function updateConfig(patch: Partial<OneSatCliConfig>): OneSatCliConfig {
|
|
|
99
117
|
return next
|
|
100
118
|
}
|
|
101
119
|
|
|
120
|
+
/**
|
|
121
|
+
* Set a value at a dotted path inside the config, creating intermediate
|
|
122
|
+
* objects as needed. `value` is stored as-is; callers are responsible for
|
|
123
|
+
* type coercion (typically via `parseConfigValue`).
|
|
124
|
+
*/
|
|
125
|
+
export function setConfigPath(path: string, value: unknown): OneSatCliConfig {
|
|
126
|
+
if (!path) throw new Error('setConfigPath requires a non-empty path')
|
|
127
|
+
const config = loadConfig() as Record<string, unknown>
|
|
128
|
+
const segments = path.split('.')
|
|
129
|
+
let cursor: Record<string, unknown> = config
|
|
130
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
131
|
+
const key = segments[i]
|
|
132
|
+
const existing = cursor[key]
|
|
133
|
+
if (
|
|
134
|
+
existing == null ||
|
|
135
|
+
typeof existing !== 'object' ||
|
|
136
|
+
Array.isArray(existing)
|
|
137
|
+
) {
|
|
138
|
+
cursor[key] = {}
|
|
139
|
+
}
|
|
140
|
+
cursor = cursor[key] as Record<string, unknown>
|
|
141
|
+
}
|
|
142
|
+
cursor[segments[segments.length - 1]] = value
|
|
143
|
+
saveConfig(config as OneSatCliConfig)
|
|
144
|
+
return config as OneSatCliConfig
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Remove a value at a dotted path. Leaves empty parent objects in place to
|
|
149
|
+
* keep the file shape explicit.
|
|
150
|
+
*/
|
|
151
|
+
export function unsetConfigPath(path: string): OneSatCliConfig {
|
|
152
|
+
if (!path) throw new Error('unsetConfigPath requires a non-empty path')
|
|
153
|
+
const config = loadConfig() as Record<string, unknown>
|
|
154
|
+
const segments = path.split('.')
|
|
155
|
+
let cursor: Record<string, unknown> | undefined = config
|
|
156
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
157
|
+
const next = cursor?.[segments[i]]
|
|
158
|
+
if (next == null || typeof next !== 'object')
|
|
159
|
+
return config as OneSatCliConfig
|
|
160
|
+
cursor = next as Record<string, unknown>
|
|
161
|
+
}
|
|
162
|
+
if (cursor) delete cursor[segments[segments.length - 1]]
|
|
163
|
+
saveConfig(config as OneSatCliConfig)
|
|
164
|
+
return config as OneSatCliConfig
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Parse a raw CLI argument into a typed JSON value. Tries `JSON.parse`
|
|
169
|
+
* first so numbers, booleans, objects and arrays round-trip naturally;
|
|
170
|
+
* falls back to the raw string for bare words that aren't valid JSON.
|
|
171
|
+
*/
|
|
172
|
+
export function parseConfigValue(raw: string): unknown {
|
|
173
|
+
try {
|
|
174
|
+
return JSON.parse(raw)
|
|
175
|
+
} catch {
|
|
176
|
+
return raw
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
102
180
|
/**
|
|
103
181
|
* Get the config directory path.
|
|
104
182
|
*/
|
package/src/context.ts
CHANGED
|
@@ -7,12 +7,7 @@
|
|
|
7
7
|
import { type OneSatContext, createContext } from '@1sat/actions'
|
|
8
8
|
import { type NodeWalletResult, createNodeWallet } from '@1sat/wallet-node'
|
|
9
9
|
import type { PrivateKey } from '@bsv/sdk'
|
|
10
|
-
import {
|
|
11
|
-
ensureDataDir,
|
|
12
|
-
loadConfig,
|
|
13
|
-
loadMonitorState,
|
|
14
|
-
saveMonitorState,
|
|
15
|
-
} from './config'
|
|
10
|
+
import { ensureDataDir, loadConfig } from './config'
|
|
16
11
|
|
|
17
12
|
/** Extended context that includes cleanup */
|
|
18
13
|
export interface CliContext {
|
|
@@ -21,48 +16,16 @@ export interface CliContext {
|
|
|
21
16
|
destroy: () => Promise<void>
|
|
22
17
|
}
|
|
23
18
|
|
|
24
|
-
/**
|
|
25
|
-
* Run the monitor once if the interval has elapsed.
|
|
26
|
-
* This is called lazily by commands that need current state.
|
|
27
|
-
*/
|
|
28
|
-
async function runMonitorIfStale(
|
|
29
|
-
walletResult: NodeWalletResult,
|
|
30
|
-
intervalMinutes: number,
|
|
31
|
-
): Promise<void> {
|
|
32
|
-
if (!walletResult.monitor) return // remote active, monitor runs remotely
|
|
33
|
-
if (intervalMinutes === 0) return // disabled by user
|
|
34
|
-
|
|
35
|
-
const state = loadMonitorState()
|
|
36
|
-
const now = Date.now()
|
|
37
|
-
const elapsed = now - state.lastMonitorRun
|
|
38
|
-
const intervalMs = intervalMinutes * 60 * 1000
|
|
39
|
-
|
|
40
|
-
if (elapsed >= intervalMs) {
|
|
41
|
-
const originalLog = console.log
|
|
42
|
-
const originalInfo = console.info
|
|
43
|
-
const originalWarn = console.warn
|
|
44
|
-
console.log = () => {}
|
|
45
|
-
console.info = () => {}
|
|
46
|
-
console.warn = () => {}
|
|
47
|
-
|
|
48
|
-
try {
|
|
49
|
-
await walletResult.monitor.runOnce()
|
|
50
|
-
} finally {
|
|
51
|
-
console.log = originalLog
|
|
52
|
-
console.info = originalInfo
|
|
53
|
-
console.warn = originalWarn
|
|
54
|
-
}
|
|
55
|
-
saveMonitorState({ lastMonitorRun: Date.now() })
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
19
|
/**
|
|
60
20
|
* Create a fully initialized OneSatContext for CLI use.
|
|
61
21
|
*
|
|
62
22
|
* Sets up:
|
|
63
23
|
* - Node wallet with SQLite storage
|
|
64
24
|
* - 1Sat services for API access
|
|
65
|
-
* - Monitor for transaction lifecycle
|
|
25
|
+
* - Monitor for transaction lifecycle. When local storage is the active
|
|
26
|
+
* store, the wallet factory fires `monitor.runOnce()` internally on
|
|
27
|
+
* creation; individual tasks self-throttle via their own intervals, so
|
|
28
|
+
* repeated CLI invocations are cheap.
|
|
66
29
|
*/
|
|
67
30
|
export async function loadContext(
|
|
68
31
|
privateKey: PrivateKey,
|
|
@@ -82,9 +45,6 @@ export async function loadContext(
|
|
|
82
45
|
backups: config.backups,
|
|
83
46
|
})
|
|
84
47
|
|
|
85
|
-
// Run monitor once if interval has elapsed (lazy refresh)
|
|
86
|
-
await runMonitorIfStale(walletResult, config.monitorIntervalMinutes)
|
|
87
|
-
|
|
88
48
|
const ctx = createContext(walletResult.wallet, {
|
|
89
49
|
services: walletResult.services,
|
|
90
50
|
chain: opts.chain,
|