bitgit 0.1.0
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/LICENSE +21 -0
- package/README.md +126 -0
- package/package.json +54 -0
- package/src/bsv/broadcast.ts +56 -0
- package/src/bsv/script.ts +103 -0
- package/src/bsv/tx.ts +90 -0
- package/src/bsv/utxo.ts +90 -0
- package/src/cli.ts +101 -0
- package/src/commands/init.ts +125 -0
- package/src/commands/push.ts +267 -0
- package/src/commands/register.ts +151 -0
- package/src/commands/status.ts +148 -0
- package/src/config.ts +101 -0
- package/src/db.ts +34 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bit init — scaffold .bit.yaml + .well-known/path402.json
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { writeFileSync, mkdirSync, existsSync, readFileSync } from 'fs';
|
|
6
|
+
import { execSync } from 'child_process';
|
|
7
|
+
import { resolve, basename } from 'path';
|
|
8
|
+
import { stringify as toYaml } from 'yaml';
|
|
9
|
+
import { configPath, type BitConfig } from '../config.js';
|
|
10
|
+
import * as readline from 'readline';
|
|
11
|
+
|
|
12
|
+
function ask(prompt: string, defaultValue?: string): Promise<string> {
|
|
13
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
14
|
+
const suffix = defaultValue ? ` [${defaultValue}]` : '';
|
|
15
|
+
return new Promise((resolve) => {
|
|
16
|
+
rl.question(`${prompt}${suffix}: `, (answer) => {
|
|
17
|
+
rl.close();
|
|
18
|
+
resolve(answer.trim() || defaultValue || '');
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function detectProjectName(): string {
|
|
24
|
+
const cwd = process.cwd();
|
|
25
|
+
|
|
26
|
+
// Try package.json
|
|
27
|
+
const pkgPath = resolve(cwd, 'package.json');
|
|
28
|
+
if (existsSync(pkgPath)) {
|
|
29
|
+
try {
|
|
30
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
31
|
+
if (pkg.name) return pkg.name.replace(/^@[^/]+\//, '');
|
|
32
|
+
} catch {}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Try git remote
|
|
36
|
+
try {
|
|
37
|
+
const remote = execSync('git remote get-url origin', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
38
|
+
const match = remote.match(/\/([^/]+?)(?:\.git)?$/);
|
|
39
|
+
if (match) return match[1];
|
|
40
|
+
} catch {}
|
|
41
|
+
|
|
42
|
+
// Fall back to directory name
|
|
43
|
+
return basename(cwd);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function init(): Promise<void> {
|
|
47
|
+
const cfgPath = configPath();
|
|
48
|
+
if (existsSync(cfgPath)) {
|
|
49
|
+
console.log('.bit.yaml already exists. Delete it first to re-initialize.');
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
console.log('bit init — setting up Bitcoin config for this project\n');
|
|
54
|
+
|
|
55
|
+
const detectedName = detectProjectName();
|
|
56
|
+
const name = await ask('Project name', detectedName);
|
|
57
|
+
const domain = await ask('Domain (optional)');
|
|
58
|
+
const token = await ask('Token symbol (optional)');
|
|
59
|
+
const contentType = await ask('Content type (blog/repo/domain/custom)', 'blog') as BitConfig['content']['type'];
|
|
60
|
+
const source = await ask(
|
|
61
|
+
'Content source directory',
|
|
62
|
+
contentType === 'blog' ? 'content/blog/' : contentType === 'repo' ? '.' : '',
|
|
63
|
+
);
|
|
64
|
+
const format = await ask('Inscription format (bitcoin_schema/op_return)', 'bitcoin_schema') as BitConfig['content']['format'];
|
|
65
|
+
|
|
66
|
+
const config: BitConfig = {
|
|
67
|
+
project: {
|
|
68
|
+
name,
|
|
69
|
+
...(domain ? { domain } : {}),
|
|
70
|
+
...(token ? { token } : {}),
|
|
71
|
+
},
|
|
72
|
+
wallet: {
|
|
73
|
+
key_env: 'BOASE_TREASURY_PRIVATE_KEY',
|
|
74
|
+
},
|
|
75
|
+
content: {
|
|
76
|
+
type: contentType,
|
|
77
|
+
source,
|
|
78
|
+
format,
|
|
79
|
+
protocol: `${name}-${contentType}`,
|
|
80
|
+
},
|
|
81
|
+
db: {
|
|
82
|
+
supabase_url_env: 'NEXT_PUBLIC_SUPABASE_URL',
|
|
83
|
+
supabase_key_env: 'SUPABASE_SERVICE_ROLE_KEY',
|
|
84
|
+
version_table: 'blog_post_versions',
|
|
85
|
+
},
|
|
86
|
+
...(domain
|
|
87
|
+
? {
|
|
88
|
+
dns_dex: {
|
|
89
|
+
token_symbol: token ? `$${token}` : `$${domain}`,
|
|
90
|
+
},
|
|
91
|
+
}
|
|
92
|
+
: {}),
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
writeFileSync(cfgPath, toYaml(config), 'utf8');
|
|
96
|
+
console.log(`\nCreated ${cfgPath}`);
|
|
97
|
+
|
|
98
|
+
// Generate .well-known/path402.json
|
|
99
|
+
if (domain) {
|
|
100
|
+
const wellKnownDir = resolve(process.cwd(), 'public', '.well-known');
|
|
101
|
+
mkdirSync(wellKnownDir, { recursive: true });
|
|
102
|
+
|
|
103
|
+
const path402 = {
|
|
104
|
+
name,
|
|
105
|
+
domain,
|
|
106
|
+
token: token || undefined,
|
|
107
|
+
protocol: '$402',
|
|
108
|
+
endpoints: {
|
|
109
|
+
press: `https://${domain}/api/path402/press`,
|
|
110
|
+
discover: `https://${domain}/api/path402/discover`,
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const wellKnownPath = resolve(wellKnownDir, 'path402.json');
|
|
115
|
+
writeFileSync(wellKnownPath, JSON.stringify(path402, null, 2), 'utf8');
|
|
116
|
+
console.log(`Created ${wellKnownPath}`);
|
|
117
|
+
|
|
118
|
+
// Print DNS records
|
|
119
|
+
console.log(`\nAdd these DNS TXT records for ${domain}:`);
|
|
120
|
+
console.log(` _path402.${domain} → v=path402; endpoint=https://${domain}/api/path402`);
|
|
121
|
+
console.log(` _dnsdex.${domain} → dnsdex-verify=pending`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
console.log('\nDone. Run `bit status` to verify, `bit push` to inscribe.');
|
|
125
|
+
}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bit push — git push + inscribe changed content on BSV
|
|
3
|
+
*
|
|
4
|
+
* 1. git push to remote (if commits to push)
|
|
5
|
+
* 2. Detect changes since last inscription (git diff)
|
|
6
|
+
* 3. Inscribe changed content on BSV
|
|
7
|
+
* 4. Update version chain in DB
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { execSync } from 'child_process';
|
|
11
|
+
import { readFileSync, existsSync } from 'fs';
|
|
12
|
+
import { resolve, relative } from 'path';
|
|
13
|
+
import { createHash } from 'crypto';
|
|
14
|
+
import { PrivateKey } from '@bsv/sdk';
|
|
15
|
+
import { loadConfig, resolvePrivateKey } from '../config.js';
|
|
16
|
+
import { fetchUtxos, type UTXO } from '../bsv/utxo.js';
|
|
17
|
+
import { buildInscriptionTx } from '../bsv/tx.js';
|
|
18
|
+
import { buildOpReturn, buildBitcoinSchema } from '../bsv/script.js';
|
|
19
|
+
import { broadcast } from '../bsv/broadcast.js';
|
|
20
|
+
import { getDb } from '../db.js';
|
|
21
|
+
|
|
22
|
+
export async function push(args: string[]): Promise<void> {
|
|
23
|
+
const dryRun = args.includes('--dry-run');
|
|
24
|
+
const skipGit = args.includes('--skip-git');
|
|
25
|
+
const config = loadConfig();
|
|
26
|
+
const cwd = process.cwd();
|
|
27
|
+
|
|
28
|
+
console.log(`bit push — ${config.project.name}`);
|
|
29
|
+
if (dryRun) console.log(' (dry run — no transactions will be broadcast)\n');
|
|
30
|
+
|
|
31
|
+
// 1. Git push
|
|
32
|
+
if (!skipGit) {
|
|
33
|
+
try {
|
|
34
|
+
const status = execSync('git status --porcelain', { cwd, encoding: 'utf8' }).trim();
|
|
35
|
+
if (status) {
|
|
36
|
+
console.log('Warning: uncommitted changes exist. Pushing what is committed.\n');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Check if there are commits to push
|
|
40
|
+
try {
|
|
41
|
+
const ahead = execSync('git rev-list --count @{upstream}..HEAD', {
|
|
42
|
+
cwd,
|
|
43
|
+
encoding: 'utf8',
|
|
44
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
45
|
+
}).trim();
|
|
46
|
+
|
|
47
|
+
if (parseInt(ahead) > 0) {
|
|
48
|
+
console.log(`Pushing ${ahead} commit(s) to remote...`);
|
|
49
|
+
if (!dryRun) {
|
|
50
|
+
execSync('git push', { cwd, stdio: 'inherit' });
|
|
51
|
+
}
|
|
52
|
+
console.log();
|
|
53
|
+
} else {
|
|
54
|
+
console.log('Git: up to date with remote.\n');
|
|
55
|
+
}
|
|
56
|
+
} catch {
|
|
57
|
+
// No upstream tracking — skip git push
|
|
58
|
+
console.log('Git: no upstream branch. Skipping git push.\n');
|
|
59
|
+
}
|
|
60
|
+
} catch (err: any) {
|
|
61
|
+
console.log(`Git push failed: ${err.message}. Continuing with inscription.\n`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 2. Find changed content
|
|
66
|
+
const sourceDir = resolve(cwd, config.content.source);
|
|
67
|
+
if (!existsSync(sourceDir)) {
|
|
68
|
+
console.error(`Content source not found: ${config.content.source}`);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Get last inscribed commit from DB (if configured)
|
|
73
|
+
let lastInscribedCommit: string | null = null;
|
|
74
|
+
let db;
|
|
75
|
+
try {
|
|
76
|
+
db = getDb(config);
|
|
77
|
+
const versionTable = config.db?.version_table || 'blog_post_versions';
|
|
78
|
+
const { data } = await db
|
|
79
|
+
.from(versionTable)
|
|
80
|
+
.select('content_hash, inscription_txid')
|
|
81
|
+
.eq('project', config.project.name)
|
|
82
|
+
.order('created_at', { ascending: false })
|
|
83
|
+
.limit(1)
|
|
84
|
+
.maybeSingle();
|
|
85
|
+
|
|
86
|
+
if (data?.inscription_txid) {
|
|
87
|
+
console.log(`Last inscription: ${data.inscription_txid}`);
|
|
88
|
+
}
|
|
89
|
+
} catch {
|
|
90
|
+
// DB not available — inscribe everything in source
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Find files to inscribe
|
|
94
|
+
let filesToInscribe: string[] = [];
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
// Try git diff to find changes
|
|
98
|
+
const lastTag = execSync('git describe --tags --abbrev=0 2>/dev/null || echo ""', {
|
|
99
|
+
cwd,
|
|
100
|
+
encoding: 'utf8',
|
|
101
|
+
}).trim();
|
|
102
|
+
|
|
103
|
+
const diffBase = lastTag || 'HEAD~1';
|
|
104
|
+
const diff = execSync(`git diff --name-only ${diffBase} -- "${config.content.source}"`, {
|
|
105
|
+
cwd,
|
|
106
|
+
encoding: 'utf8',
|
|
107
|
+
}).trim();
|
|
108
|
+
|
|
109
|
+
if (diff) {
|
|
110
|
+
filesToInscribe = diff.split('\n').filter((f) => existsSync(resolve(cwd, f)));
|
|
111
|
+
}
|
|
112
|
+
} catch {
|
|
113
|
+
// No git history — fall through
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// If no git diff results, use all files in source directory
|
|
117
|
+
if (filesToInscribe.length === 0) {
|
|
118
|
+
try {
|
|
119
|
+
const allFiles = execSync(`find "${sourceDir}" -type f -name "*.md" -o -name "*.json" | sort`, {
|
|
120
|
+
cwd,
|
|
121
|
+
encoding: 'utf8',
|
|
122
|
+
}).trim();
|
|
123
|
+
if (allFiles) {
|
|
124
|
+
filesToInscribe = allFiles.split('\n').map((f) => relative(cwd, f));
|
|
125
|
+
}
|
|
126
|
+
} catch {}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (filesToInscribe.length === 0) {
|
|
130
|
+
console.log('No content files found to inscribe.');
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
console.log(`Found ${filesToInscribe.length} file(s) to inscribe:`);
|
|
135
|
+
filesToInscribe.forEach((f) => console.log(` ${f}`));
|
|
136
|
+
console.log();
|
|
137
|
+
|
|
138
|
+
if (dryRun) {
|
|
139
|
+
console.log('Dry run — skipping inscription.');
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// 3. Inscribe on BSV
|
|
144
|
+
const wif = resolvePrivateKey(config);
|
|
145
|
+
const privateKey = PrivateKey.fromWif(wif);
|
|
146
|
+
const address = privateKey.toPublicKey().toAddress();
|
|
147
|
+
|
|
148
|
+
console.log(`Treasury: ${address}`);
|
|
149
|
+
|
|
150
|
+
const utxos = await fetchUtxos(address);
|
|
151
|
+
if (utxos.length === 0) {
|
|
152
|
+
console.error('No UTXOs found. Fund the treasury address first.');
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
let currentUtxo = utxos[0];
|
|
157
|
+
let totalFees = 0;
|
|
158
|
+
const results: { file: string; txid: string }[] = [];
|
|
159
|
+
|
|
160
|
+
for (let i = 0; i < filesToInscribe.length; i++) {
|
|
161
|
+
const filePath = resolve(cwd, filesToInscribe[i]);
|
|
162
|
+
const content = readFileSync(filePath, 'utf8');
|
|
163
|
+
const contentHash = createHash('sha256').update(content).digest('hex');
|
|
164
|
+
|
|
165
|
+
console.log(`[${i + 1}/${filesToInscribe.length}] ${filesToInscribe[i]} (${content.length} bytes)`);
|
|
166
|
+
|
|
167
|
+
// Build OP_RETURN script based on format
|
|
168
|
+
let opReturnScript;
|
|
169
|
+
const payloadSize = content.length;
|
|
170
|
+
|
|
171
|
+
if (config.content.format === 'bitcoin_schema') {
|
|
172
|
+
const mapData: Record<string, string> = {
|
|
173
|
+
app: config.project.domain || config.project.name,
|
|
174
|
+
type: config.content.type,
|
|
175
|
+
file: filesToInscribe[i],
|
|
176
|
+
hash: contentHash,
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
// Extract slug from filename for blog posts
|
|
180
|
+
if (config.content.type === 'blog') {
|
|
181
|
+
const slug = filesToInscribe[i]
|
|
182
|
+
.replace(/^.*\//, '')
|
|
183
|
+
.replace(/\.md$/, '');
|
|
184
|
+
mapData.slug = slug;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
opReturnScript = buildBitcoinSchema(
|
|
188
|
+
content,
|
|
189
|
+
content.endsWith('.json') ? 'application/json' : 'text/markdown',
|
|
190
|
+
mapData,
|
|
191
|
+
privateKey,
|
|
192
|
+
);
|
|
193
|
+
} else {
|
|
194
|
+
const payload = JSON.stringify({
|
|
195
|
+
protocol: config.content.protocol,
|
|
196
|
+
file: filesToInscribe[i],
|
|
197
|
+
hash: contentHash,
|
|
198
|
+
content,
|
|
199
|
+
timestamp: new Date().toISOString(),
|
|
200
|
+
});
|
|
201
|
+
opReturnScript = buildOpReturn(
|
|
202
|
+
config.content.protocol,
|
|
203
|
+
'application/json',
|
|
204
|
+
payload,
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
const { tx, fee, changeSats } = await buildInscriptionTx({
|
|
210
|
+
privateKey,
|
|
211
|
+
utxo: currentUtxo,
|
|
212
|
+
opReturnScript,
|
|
213
|
+
payloadSize,
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
const rawTx = tx.toHex();
|
|
217
|
+
const txid = await broadcast(rawTx);
|
|
218
|
+
|
|
219
|
+
totalFees += fee;
|
|
220
|
+
results.push({ file: filesToInscribe[i], txid });
|
|
221
|
+
|
|
222
|
+
console.log(` TXID: ${txid}`);
|
|
223
|
+
console.log(` Fee: ${fee} sats | Remaining: ${changeSats} sats`);
|
|
224
|
+
console.log(` https://whatsonchain.com/tx/${txid}`);
|
|
225
|
+
|
|
226
|
+
// 4. Update version chain in DB
|
|
227
|
+
if (db && config.db?.version_table) {
|
|
228
|
+
try {
|
|
229
|
+
await db.from(config.db.version_table).insert({
|
|
230
|
+
project: config.project.name,
|
|
231
|
+
file_path: filesToInscribe[i],
|
|
232
|
+
content_hash: contentHash,
|
|
233
|
+
inscription_txid: txid,
|
|
234
|
+
created_at: new Date().toISOString(),
|
|
235
|
+
});
|
|
236
|
+
} catch (dbErr: any) {
|
|
237
|
+
console.log(` DB update failed: ${dbErr.message}`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Chain: use change output as next input
|
|
242
|
+
currentUtxo = {
|
|
243
|
+
txid,
|
|
244
|
+
vout: 1, // change is output index 1 (after OP_RETURN at 0)
|
|
245
|
+
satoshis: changeSats,
|
|
246
|
+
script: currentUtxo.script,
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
// Small delay for mempool propagation
|
|
250
|
+
if (i < filesToInscribe.length - 1) {
|
|
251
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
252
|
+
}
|
|
253
|
+
} catch (err: any) {
|
|
254
|
+
console.log(` FAILED: ${err.message}`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Summary
|
|
259
|
+
console.log('\n' + '='.repeat(50));
|
|
260
|
+
console.log(`Inscribed: ${results.length}/${filesToInscribe.length} files`);
|
|
261
|
+
console.log(`Total fees: ${totalFees} sats`);
|
|
262
|
+
|
|
263
|
+
if (results.length > 0) {
|
|
264
|
+
console.log('\nInscriptions:');
|
|
265
|
+
results.forEach((r) => console.log(` ${r.file} → ${r.txid}`));
|
|
266
|
+
}
|
|
267
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bit register <domain> — inscribe a domain on DNS-DEX
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { createHash } from 'crypto';
|
|
6
|
+
import { PrivateKey } from '@bsv/sdk';
|
|
7
|
+
import { loadConfig, resolvePrivateKey } from '../config.js';
|
|
8
|
+
import { selectUtxo } from '../bsv/utxo.js';
|
|
9
|
+
import { buildInscriptionTx } from '../bsv/tx.js';
|
|
10
|
+
import { buildOpReturn } from '../bsv/script.js';
|
|
11
|
+
import { broadcast } from '../bsv/broadcast.js';
|
|
12
|
+
import { getDb } from '../db.js';
|
|
13
|
+
|
|
14
|
+
export async function register(domain: string, args: string[]): Promise<void> {
|
|
15
|
+
const dryRun = args.includes('--dry-run');
|
|
16
|
+
const supply = parseInt(args.find((a) => a.startsWith('--supply='))?.split('=')[1] || '1000000000');
|
|
17
|
+
const category = args.find((a) => a.startsWith('--category='))?.split('=')[1] || 'other';
|
|
18
|
+
|
|
19
|
+
let config;
|
|
20
|
+
try {
|
|
21
|
+
config = loadConfig();
|
|
22
|
+
} catch {
|
|
23
|
+
// No config — use env vars directly
|
|
24
|
+
config = null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
console.log(`bit register — inscribing ${domain} on DNS-DEX`);
|
|
28
|
+
if (dryRun) console.log(' (dry run)\n');
|
|
29
|
+
|
|
30
|
+
const ts = new Date().toISOString();
|
|
31
|
+
const tick = `$${domain}`;
|
|
32
|
+
|
|
33
|
+
// In dry-run mode, skip key resolution if no key available
|
|
34
|
+
let wif: string | undefined;
|
|
35
|
+
let address = '(no key configured)';
|
|
36
|
+
try {
|
|
37
|
+
wif = config ? resolvePrivateKey(config) : (
|
|
38
|
+
process.env.BOASE_TREASURY_PRIVATE_KEY ||
|
|
39
|
+
process.env.BSV_PRIVATE_KEY ||
|
|
40
|
+
undefined
|
|
41
|
+
);
|
|
42
|
+
if (wif) {
|
|
43
|
+
const privateKey = PrivateKey.fromWif(wif);
|
|
44
|
+
address = privateKey.toPublicKey().toAddress();
|
|
45
|
+
}
|
|
46
|
+
} catch {}
|
|
47
|
+
|
|
48
|
+
if (!wif && !dryRun) {
|
|
49
|
+
console.error('No private key found. Set BOASE_TREASURY_PRIVATE_KEY.');
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Build DNS-DEX inscription document
|
|
54
|
+
const doc = {
|
|
55
|
+
p: 'dnsdex-domain',
|
|
56
|
+
v: '1.0',
|
|
57
|
+
op: 'mint',
|
|
58
|
+
tick,
|
|
59
|
+
domain,
|
|
60
|
+
supply,
|
|
61
|
+
owner: {
|
|
62
|
+
wallet: config?.dns_dex?.token_symbol || tick,
|
|
63
|
+
type: 'treasury_key',
|
|
64
|
+
},
|
|
65
|
+
x402: {
|
|
66
|
+
fee_cents: 1,
|
|
67
|
+
initial_price: 0,
|
|
68
|
+
},
|
|
69
|
+
meta: {
|
|
70
|
+
category,
|
|
71
|
+
description: `https://${domain}`,
|
|
72
|
+
timestamp: ts,
|
|
73
|
+
},
|
|
74
|
+
verification: {
|
|
75
|
+
method: 'treasury_key',
|
|
76
|
+
proof: createHash('sha256')
|
|
77
|
+
.update(`${domain}:${address}:${ts}`)
|
|
78
|
+
.digest('hex')
|
|
79
|
+
.slice(0, 32),
|
|
80
|
+
verified_at: ts,
|
|
81
|
+
},
|
|
82
|
+
platform: 'dns-dex.com',
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const payload = JSON.stringify(doc);
|
|
86
|
+
const contentHash = createHash('sha256').update(payload).digest('hex');
|
|
87
|
+
|
|
88
|
+
console.log(`Treasury: ${address}`);
|
|
89
|
+
console.log(`Tick: ${tick} | Supply: ${supply.toLocaleString()} | Category: ${category}`);
|
|
90
|
+
console.log(`Payload: ${payload.length} bytes\n`);
|
|
91
|
+
|
|
92
|
+
if (dryRun) {
|
|
93
|
+
console.log('Dry run — would inscribe:');
|
|
94
|
+
console.log(JSON.stringify(doc, null, 2));
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const privateKey = PrivateKey.fromWif(wif!);
|
|
99
|
+
|
|
100
|
+
const utxo = await selectUtxo(address);
|
|
101
|
+
const opReturnScript = buildOpReturn('dnsdex-domain', 'application/json', payload);
|
|
102
|
+
|
|
103
|
+
const { tx, fee, changeSats } = await buildInscriptionTx({
|
|
104
|
+
privateKey,
|
|
105
|
+
utxo,
|
|
106
|
+
opReturnScript,
|
|
107
|
+
payloadSize: payload.length,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const txid = await broadcast(tx.toHex());
|
|
111
|
+
const origin = `${txid}_0`;
|
|
112
|
+
const inscriptionId = `${txid}i0`;
|
|
113
|
+
|
|
114
|
+
console.log('Domain inscribed on BSV!');
|
|
115
|
+
console.log(` TXID: ${txid}`);
|
|
116
|
+
console.log(` Origin: ${origin}`);
|
|
117
|
+
console.log(` Fee: ${fee} sats`);
|
|
118
|
+
console.log(` Explorer: https://whatsonchain.com/tx/${txid}`);
|
|
119
|
+
console.log(` Ordinal: https://1satordinals.com/inscription/${inscriptionId}`);
|
|
120
|
+
|
|
121
|
+
// Update DB
|
|
122
|
+
try {
|
|
123
|
+
const db = getDb(config || undefined);
|
|
124
|
+
const { error } = await db
|
|
125
|
+
.from('dnsdex_domains')
|
|
126
|
+
.upsert({
|
|
127
|
+
domain,
|
|
128
|
+
status: 'active',
|
|
129
|
+
bsv_txid: txid,
|
|
130
|
+
bsv_origin: origin,
|
|
131
|
+
inscription_id: inscriptionId,
|
|
132
|
+
content_hash: contentHash,
|
|
133
|
+
verification_method: 'treasury_key',
|
|
134
|
+
verified_at: ts,
|
|
135
|
+
updated_at: ts,
|
|
136
|
+
}, { onConflict: 'domain' });
|
|
137
|
+
|
|
138
|
+
if (error) {
|
|
139
|
+
console.log(`\nDB update failed: ${error.message}`);
|
|
140
|
+
} else {
|
|
141
|
+
console.log(`\nDB: ${domain} → active`);
|
|
142
|
+
}
|
|
143
|
+
} catch (err: any) {
|
|
144
|
+
console.log(`\nDB not available: ${err.message}`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Print DNS records to add
|
|
148
|
+
console.log(`\nAdd these DNS TXT records for ${domain}:`);
|
|
149
|
+
console.log(` _dnsdex.${domain} → dnsdex-verify=${txid}`);
|
|
150
|
+
console.log(` _dnsdex-verification.${domain} → ${doc.verification.proof}`);
|
|
151
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bit status — show Bitcoin state for this project
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { PrivateKey } from '@bsv/sdk';
|
|
6
|
+
import { loadConfig, resolvePrivateKey } from '../config.js';
|
|
7
|
+
import { fetchUtxos } from '../bsv/utxo.js';
|
|
8
|
+
import { getDb } from '../db.js';
|
|
9
|
+
|
|
10
|
+
export async function status(): Promise<void> {
|
|
11
|
+
const config = loadConfig();
|
|
12
|
+
|
|
13
|
+
console.log(`bit status — ${config.project.name}\n`);
|
|
14
|
+
|
|
15
|
+
// Wallet info
|
|
16
|
+
let address: string;
|
|
17
|
+
try {
|
|
18
|
+
const wif = resolvePrivateKey(config);
|
|
19
|
+
const privateKey = PrivateKey.fromWif(wif);
|
|
20
|
+
address = privateKey.toPublicKey().toAddress();
|
|
21
|
+
|
|
22
|
+
const utxos = await fetchUtxos(address);
|
|
23
|
+
const totalSats = utxos.reduce((sum, u) => sum + u.satoshis, 0);
|
|
24
|
+
|
|
25
|
+
console.log('Wallet:');
|
|
26
|
+
console.log(` Address: ${address}`);
|
|
27
|
+
console.log(` UTXOs: ${utxos.length}`);
|
|
28
|
+
console.log(` Balance: ${totalSats.toLocaleString()} sats`);
|
|
29
|
+
console.log();
|
|
30
|
+
} catch (err: any) {
|
|
31
|
+
console.log(`Wallet: not configured (${err.message})\n`);
|
|
32
|
+
address = '';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Domain info
|
|
36
|
+
if (config.project.domain) {
|
|
37
|
+
console.log('Domain:');
|
|
38
|
+
console.log(` Name: ${config.project.domain}`);
|
|
39
|
+
|
|
40
|
+
// Check DNS-DEX registration
|
|
41
|
+
try {
|
|
42
|
+
const db = getDb(config);
|
|
43
|
+
const { data } = await db
|
|
44
|
+
.from('dnsdex_domains')
|
|
45
|
+
.select('status, bsv_txid, verified_at')
|
|
46
|
+
.eq('domain', config.project.domain)
|
|
47
|
+
.maybeSingle();
|
|
48
|
+
|
|
49
|
+
if (data) {
|
|
50
|
+
console.log(` DNS-DEX: ${data.status}`);
|
|
51
|
+
if (data.bsv_txid) {
|
|
52
|
+
console.log(` TXID: ${data.bsv_txid}`);
|
|
53
|
+
console.log(` Explorer: https://whatsonchain.com/tx/${data.bsv_txid}`);
|
|
54
|
+
}
|
|
55
|
+
} else {
|
|
56
|
+
console.log(' DNS-DEX: not registered');
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
console.log(' DNS-DEX: (DB not available)');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Check DNS TXT records
|
|
63
|
+
try {
|
|
64
|
+
const { execSync } = await import('child_process');
|
|
65
|
+
const txt = execSync(`dig +short TXT _dnsdex.${config.project.domain} 2>/dev/null`, {
|
|
66
|
+
encoding: 'utf8',
|
|
67
|
+
}).trim();
|
|
68
|
+
console.log(` DNS TXT: ${txt || '(not set)'}`);
|
|
69
|
+
} catch {
|
|
70
|
+
console.log(' DNS TXT: (could not resolve)');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
console.log();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Token info
|
|
77
|
+
if (config.project.token) {
|
|
78
|
+
console.log('Token:');
|
|
79
|
+
console.log(` Symbol: ${config.project.token}`);
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const db = getDb(config);
|
|
83
|
+
const { data } = await db
|
|
84
|
+
.from('path402_tokens')
|
|
85
|
+
.select('id, name, total_supply, current_supply, base_price_sats, deploy_txid')
|
|
86
|
+
.eq('symbol', config.project.token)
|
|
87
|
+
.maybeSingle();
|
|
88
|
+
|
|
89
|
+
if (data) {
|
|
90
|
+
console.log(` Name: ${data.name}`);
|
|
91
|
+
console.log(` Supply: ${data.current_supply}/${data.total_supply}`);
|
|
92
|
+
console.log(` Price: ${data.base_price_sats} sats`);
|
|
93
|
+
if (data.deploy_txid) {
|
|
94
|
+
console.log(` On-chain: https://whatsonchain.com/tx/${data.deploy_txid}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Holder count
|
|
98
|
+
const { count } = await db
|
|
99
|
+
.from('path402_holdings')
|
|
100
|
+
.select('*', { count: 'exact', head: true })
|
|
101
|
+
.eq('token_id', data.id)
|
|
102
|
+
.gt('balance', 0);
|
|
103
|
+
|
|
104
|
+
console.log(` Holders: ${count || 0}`);
|
|
105
|
+
} else {
|
|
106
|
+
console.log(' (not found in path402_tokens)');
|
|
107
|
+
}
|
|
108
|
+
} catch {
|
|
109
|
+
console.log(' (DB not available)');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
console.log();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Version chain
|
|
116
|
+
console.log('Version chain:');
|
|
117
|
+
try {
|
|
118
|
+
const db = getDb(config);
|
|
119
|
+
const versionTable = config.db?.version_table || 'blog_post_versions';
|
|
120
|
+
const { data, error } = await db
|
|
121
|
+
.from(versionTable)
|
|
122
|
+
.select('file_path, content_hash, inscription_txid, created_at')
|
|
123
|
+
.eq('project', config.project.name)
|
|
124
|
+
.order('created_at', { ascending: false })
|
|
125
|
+
.limit(5);
|
|
126
|
+
|
|
127
|
+
if (error) {
|
|
128
|
+
console.log(` (query failed: ${error.message})`);
|
|
129
|
+
} else if (!data || data.length === 0) {
|
|
130
|
+
console.log(' No inscriptions yet. Run `bit push` to inscribe content.');
|
|
131
|
+
} else {
|
|
132
|
+
console.log(` Latest ${data.length} inscription(s):`);
|
|
133
|
+
for (const row of data) {
|
|
134
|
+
const date = new Date(row.created_at).toISOString().split('T')[0];
|
|
135
|
+
console.log(` ${date} ${row.file_path || '(unknown)'}`);
|
|
136
|
+
console.log(` ${row.inscription_txid}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
} catch {
|
|
140
|
+
console.log(' (DB not available)');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Content source
|
|
144
|
+
console.log(`\nConfig:`);
|
|
145
|
+
console.log(` Source: ${config.content.source}`);
|
|
146
|
+
console.log(` Format: ${config.content.format}`);
|
|
147
|
+
console.log(` Protocol: ${config.content.protocol}`);
|
|
148
|
+
}
|