@btc-vision/cli 1.0.6 → 1.0.8

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.
@@ -1,6 +1,6 @@
1
1
  import { confirm, input } from '@inquirer/prompts';
2
2
  import { BaseCommand } from './BaseCommand.js';
3
- import { getPackage, getRegistryContract, getVersion, isVersionImmutable } from '../lib/registry.js';
3
+ import { getPackage, getRegistryContract, getVersion, isVersionImmutable, } from '../lib/registry.js';
4
4
  import { canSign, loadCredentials } from '../lib/credentials.js';
5
5
  import { CLIWallet } from '../lib/wallet.js';
6
6
  import { buildTransactionParams, checkBalance, formatSats, getWalletAddress, } from '../lib/transaction.js';
@@ -0,0 +1,3 @@
1
+ import { Command } from 'commander';
2
+ declare const domainCommand: Command;
3
+ export { domainCommand };
@@ -0,0 +1,241 @@
1
+ import { Command } from 'commander';
2
+ import { confirm } from '@inquirer/prompts';
3
+ import { Logger } from '@btc-vision/logger';
4
+ import { CLIWallet } from '../lib/wallet.js';
5
+ import { canSign, loadCredentials } from '../lib/credentials.js';
6
+ import { getContenthash, getContenthashTypeName, getDomain, getDomainPrice, getResolverContract, getTreasuryAddress, parseDomainName, validateDomainName, } from '../lib/resolver.js';
7
+ import { buildTransactionParams, checkBalance, DEFAULT_FEE_RATE, DEFAULT_MAX_SAT_TO_SPEND, formatSats, getWalletAddress, waitForTransactionConfirmation, } from '../lib/transaction.js';
8
+ import { TransactionOutputFlags } from 'opnet';
9
+ const logger = new Logger();
10
+ async function registerDomain(domain, options) {
11
+ try {
12
+ const network = (options.network || 'mainnet');
13
+ const name = parseDomainName(domain.toLowerCase());
14
+ const displayName = `${name}.btc`;
15
+ if (name.includes('.')) {
16
+ logger.fail('Cannot register subdomains directly');
17
+ logger.info('Register the parent domain first, then create subdomains');
18
+ process.exit(1);
19
+ }
20
+ const validationError = validateDomainName(name);
21
+ if (validationError) {
22
+ logger.fail(`Invalid domain name: ${validationError}`);
23
+ process.exit(1);
24
+ }
25
+ logger.info(`Registering domain: ${displayName}`);
26
+ logger.info('Checking domain availability...');
27
+ const existingDomain = await getDomain(name, network);
28
+ if (existingDomain) {
29
+ logger.fail(`Domain ${displayName} is already registered`);
30
+ logger.log(`Owner: ${existingDomain.owner.toHex()}`);
31
+ process.exit(1);
32
+ }
33
+ logger.success(`Domain ${displayName} is available`);
34
+ const price = await getDomainPrice(name, network);
35
+ const treasuryAddr = await getTreasuryAddress(network);
36
+ logger.info(`Registration price: ${formatSats(price)}`);
37
+ if (name.length === 3) {
38
+ logger.warn('Premium pricing applied (3-character domain)');
39
+ }
40
+ else if (name.length === 4) {
41
+ logger.warn('Premium pricing applied (4-character domain)');
42
+ }
43
+ logger.info('Loading wallet...');
44
+ const credentials = loadCredentials();
45
+ if (!credentials || !canSign(credentials)) {
46
+ logger.fail('No credentials configured');
47
+ logger.warn('Run `opnet login` to configure your wallet.');
48
+ process.exit(1);
49
+ }
50
+ const wallet = CLIWallet.fromCredentials(credentials);
51
+ logger.success('Wallet loaded');
52
+ logger.info('Checking wallet balance...');
53
+ const minRequired = price + 10000n;
54
+ const { sufficient, balance } = await checkBalance(wallet, network, minRequired);
55
+ if (!sufficient) {
56
+ logger.fail('Insufficient balance');
57
+ logger.error(`Required: ~${formatSats(minRequired)}`);
58
+ logger.error(`Available: ${formatSats(balance)}`);
59
+ process.exit(1);
60
+ }
61
+ logger.success(`Wallet balance: ${formatSats(balance)}`);
62
+ logger.log('');
63
+ logger.info('Registration Summary');
64
+ logger.log('-'.repeat(50));
65
+ logger.log(`Domain: ${displayName}`);
66
+ logger.log(`Price: ${formatSats(price)}`);
67
+ logger.log(`Treasury: ${treasuryAddr}`);
68
+ logger.log(`Network: ${network}`);
69
+ logger.log(`Your wallet: ${wallet.p2trAddress}`);
70
+ logger.log(`MLDSA Public Key Hash: ${wallet.address.toHex()}`);
71
+ logger.log('');
72
+ if (options.dryRun) {
73
+ logger.warn('Dry run - no changes made.');
74
+ return;
75
+ }
76
+ if (!options.yes) {
77
+ const confirmed = await confirm({
78
+ message: `Register ${displayName} for ${formatSats(price)}?`,
79
+ default: true,
80
+ });
81
+ if (!confirmed) {
82
+ logger.warn('Registration cancelled.');
83
+ return;
84
+ }
85
+ }
86
+ logger.info('Preparing transaction...');
87
+ const sender = getWalletAddress(wallet);
88
+ const contract = getResolverContract(network, sender);
89
+ const extraUtxo = {
90
+ address: treasuryAddr,
91
+ value: Number(price),
92
+ };
93
+ const outSimulation = [
94
+ {
95
+ index: 1,
96
+ to: treasuryAddr,
97
+ value: price,
98
+ flags: TransactionOutputFlags.hasTo,
99
+ scriptPubKey: undefined,
100
+ },
101
+ ];
102
+ contract.setTransactionDetails({
103
+ inputs: [],
104
+ outputs: outSimulation,
105
+ });
106
+ const registerResult = await contract.registerDomain(name);
107
+ if (registerResult.revert) {
108
+ logger.fail('Registration would fail');
109
+ logger.error(`Reason: ${registerResult.revert}`);
110
+ process.exit(1);
111
+ }
112
+ if (registerResult.estimatedGas) {
113
+ logger.info(`Estimated gas: ${registerResult.estimatedGas} sats`);
114
+ }
115
+ const txParams = buildTransactionParams(wallet, network, DEFAULT_MAX_SAT_TO_SPEND + price, DEFAULT_FEE_RATE, extraUtxo);
116
+ const receipt = await registerResult.sendTransaction(txParams);
117
+ logger.log('');
118
+ logger.success('Domain registration submitted!');
119
+ logger.log('');
120
+ logger.log(`Domain: ${displayName}`);
121
+ logger.log(`Transaction ID: ${receipt.transactionId}`);
122
+ logger.log(`Fees paid: ${formatSats(receipt.estimatedFees)}`);
123
+ logger.log('');
124
+ const confirmationResult = await waitForTransactionConfirmation(receipt.transactionId, network, {
125
+ message: 'Waiting for registration confirmation',
126
+ });
127
+ if (!confirmationResult.confirmed) {
128
+ if (confirmationResult.revert) {
129
+ logger.fail('Registration failed');
130
+ logger.error(`Reason: ${confirmationResult.revert}`);
131
+ }
132
+ else if (confirmationResult.error) {
133
+ logger.warn('Registration not yet confirmed');
134
+ logger.warn(confirmationResult.error);
135
+ }
136
+ }
137
+ else {
138
+ logger.log('');
139
+ logger.success(`You now own ${displayName}!`);
140
+ logger.info('Next step: publish your website with:');
141
+ logger.log(` opnet website ${name} <ipfs-cid> -n ${network}`);
142
+ }
143
+ }
144
+ catch (error) {
145
+ logger.fail('Domain registration failed');
146
+ if (error instanceof Error && error.message.includes('User force closed')) {
147
+ logger.warn('Registration cancelled.');
148
+ process.exit(0);
149
+ }
150
+ logger.error(error instanceof Error ? error.message : String(error));
151
+ process.exit(1);
152
+ }
153
+ }
154
+ async function domainInfo(domain, options) {
155
+ try {
156
+ const network = (options.network || 'mainnet');
157
+ const name = parseDomainName(domain.toLowerCase());
158
+ const displayName = `${name}.btc`;
159
+ logger.info(`Looking up: ${displayName}`);
160
+ const domainData = await getDomain(name, network);
161
+ if (!domainData) {
162
+ logger.warn(`Domain ${displayName} is not registered`);
163
+ const price = await getDomainPrice(name, network);
164
+ logger.log('');
165
+ logger.info('Registration Info');
166
+ logger.log('-'.repeat(50));
167
+ logger.log(`Domain: ${displayName}`);
168
+ logger.log(`Status: Available`);
169
+ logger.log(`Price: ${formatSats(price)}`);
170
+ if (name.length === 3) {
171
+ logger.log(`Pricing tier: Premium (3-character)`);
172
+ }
173
+ else if (name.length === 4) {
174
+ logger.log(`Pricing tier: Premium (4-character)`);
175
+ }
176
+ else {
177
+ logger.log(`Pricing tier: Standard`);
178
+ }
179
+ logger.log('');
180
+ logger.info('Register with:');
181
+ logger.log(` opnet domain register ${name} -n ${network}`);
182
+ return;
183
+ }
184
+ const contenthash = await getContenthash(name, network);
185
+ logger.log('');
186
+ logger.info('Domain Information');
187
+ logger.log('-'.repeat(50));
188
+ logger.log(`Domain: ${displayName}`);
189
+ logger.log(`Status: Registered`);
190
+ logger.log(`Owner: ${domainData.owner.toHex()}`);
191
+ logger.log(`Created at: Block #${domainData.createdAt}`);
192
+ logger.log(`TTL: ${domainData.ttl} seconds`);
193
+ if (contenthash.hashType !== 0) {
194
+ logger.log('');
195
+ logger.info('Website (Contenthash)');
196
+ logger.log('-'.repeat(50));
197
+ logger.log(`Type: ${getContenthashTypeName(contenthash.hashType)}`);
198
+ if (contenthash.hashString) {
199
+ logger.log(`Value: ${contenthash.hashString}`);
200
+ if (contenthash.hashType === 1 || contenthash.hashType === 2) {
201
+ logger.log(`Gateway URL: https://ipfs.opnet.org/ipfs/${contenthash.hashString}`);
202
+ }
203
+ else if (contenthash.hashType === 3) {
204
+ logger.log(`Gateway URL: https://ipfs.opnet.org/ipns/${contenthash.hashString}`);
205
+ }
206
+ }
207
+ else {
208
+ const hashHex = Buffer.from(contenthash.hashData).toString('hex');
209
+ logger.log(`Value: ${hashHex}`);
210
+ }
211
+ }
212
+ else {
213
+ logger.log('');
214
+ logger.warn('No website published');
215
+ logger.info('Publish with:');
216
+ logger.log(` opnet website ${name} <ipfs-cid> -n ${network}`);
217
+ }
218
+ logger.log('');
219
+ }
220
+ catch (error) {
221
+ logger.fail('Failed to get domain info');
222
+ logger.error(error instanceof Error ? error.message : String(error));
223
+ process.exit(1);
224
+ }
225
+ }
226
+ const domainCommand = new Command('domain').description('Manage .btc domains');
227
+ domainCommand
228
+ .command('register')
229
+ .description('Register a new .btc domain')
230
+ .argument('<name>', 'Domain name to register (without .btc suffix)')
231
+ .option('-n, --network <network>', 'Network to use', 'mainnet')
232
+ .option('--dry-run', 'Show what would happen without registering')
233
+ .option('-y, --yes', 'Skip confirmation prompts')
234
+ .action((name, options) => registerDomain(name, options));
235
+ domainCommand
236
+ .command('info')
237
+ .description('Get information about a .btc domain')
238
+ .argument('<name>', 'Domain name to look up (with or without .btc suffix)')
239
+ .option('-n, --network <network>', 'Network to use', 'mainnet')
240
+ .action((name, options) => domainInfo(name, options));
241
+ export { domainCommand };
@@ -1,6 +1,6 @@
1
1
  import { confirm } from '@inquirer/prompts';
2
2
  import { BaseCommand } from './BaseCommand.js';
3
- import { getPackage, getRegistryContract, getVersion, isVersionImmutable } from '../lib/registry.js';
3
+ import { getPackage, getRegistryContract, getVersion, isVersionImmutable, } from '../lib/registry.js';
4
4
  import { canSign, loadCredentials } from '../lib/credentials.js';
5
5
  import { CLIWallet } from '../lib/wallet.js';
6
6
  import { buildTransactionParams, checkBalance, formatSats, getWalletAddress, } from '../lib/transaction.js';
@@ -0,0 +1,9 @@
1
+ import { BaseCommand } from './BaseCommand.js';
2
+ export declare class WebsiteDeployCommand extends BaseCommand {
3
+ constructor();
4
+ protected configure(): void;
5
+ private execute;
6
+ private countFiles;
7
+ private getDirectorySize;
8
+ }
9
+ export declare const websiteDeployCommand: import("commander").Command;
@@ -0,0 +1,214 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { confirm } from '@inquirer/prompts';
4
+ import { BaseCommand } from './BaseCommand.js';
5
+ import { CLIWallet } from '../lib/wallet.js';
6
+ import { canSign, loadCredentials } from '../lib/credentials.js';
7
+ import { uploadDirectory, uploadFile } from '../lib/ipfs.js';
8
+ import { formatFileSize } from '../lib/binary.js';
9
+ import { getContenthash, getContenthashTypeName, getDomain, getResolverContract, getSubdomain, isSubdomain, parseDomainName, } from '../lib/resolver.js';
10
+ import { buildTransactionParams, checkBalance, DEFAULT_FEE_RATE, DEFAULT_MAX_SAT_TO_SPEND, formatSats, getWalletAddress, waitForTransactionConfirmation, } from '../lib/transaction.js';
11
+ export class WebsiteDeployCommand extends BaseCommand {
12
+ constructor() {
13
+ super('deploy', 'Upload website to IPFS and publish to .btc domain');
14
+ }
15
+ configure() {
16
+ this.command
17
+ .argument('<domain>', 'Domain name (e.g., mysite or mysite.btc)')
18
+ .argument('<path>', 'Path to website directory or HTML file')
19
+ .option('-n, --network <network>', 'Network to use', 'mainnet')
20
+ .option('--dry-run', "Upload to IPFS but don't update on-chain")
21
+ .option('-y, --yes', 'Skip confirmation prompts')
22
+ .action((domain, websitePath, options) => this.execute(domain, websitePath, options || { network: 'mainnet' }));
23
+ }
24
+ async execute(domain, websitePath, options) {
25
+ try {
26
+ const network = (options.network || 'mainnet');
27
+ const name = parseDomainName(domain.toLowerCase());
28
+ const isSubdomainName = isSubdomain(name);
29
+ const displayName = `${name}.btc`;
30
+ const resolvedPath = path.resolve(websitePath);
31
+ if (!fs.existsSync(resolvedPath)) {
32
+ this.logger.fail(`Path not found: ${resolvedPath}`);
33
+ process.exit(1);
34
+ }
35
+ const stats = fs.statSync(resolvedPath);
36
+ const isDirectory = stats.isDirectory();
37
+ this.logger.info(`Deploying to ${displayName}...`);
38
+ this.logger.log('');
39
+ if (isDirectory) {
40
+ const files = this.countFiles(resolvedPath);
41
+ const totalSize = this.getDirectorySize(resolvedPath);
42
+ this.logger.info('Website Directory');
43
+ this.logger.log('-'.repeat(50));
44
+ this.logger.log(`Path: ${resolvedPath}`);
45
+ this.logger.log(`Files: ${files}`);
46
+ this.logger.log(`Total size: ${formatFileSize(totalSize)}`);
47
+ }
48
+ else {
49
+ this.logger.info('Website File');
50
+ this.logger.log('-'.repeat(50));
51
+ this.logger.log(`Path: ${resolvedPath}`);
52
+ this.logger.log(`Size: ${formatFileSize(stats.size)}`);
53
+ }
54
+ this.logger.log('');
55
+ this.logger.info('Loading wallet...');
56
+ const credentials = loadCredentials();
57
+ if (!credentials || !canSign(credentials)) {
58
+ this.logger.fail('No credentials configured');
59
+ this.logger.warn('Run `opnet login` to configure your wallet.');
60
+ process.exit(1);
61
+ }
62
+ const wallet = CLIWallet.fromCredentials(credentials);
63
+ this.logger.success('Wallet loaded');
64
+ this.logger.info('Checking domain ownership...');
65
+ let ownerAddress;
66
+ if (isSubdomainName) {
67
+ const subdomainInfo = await getSubdomain(name, network);
68
+ if (!subdomainInfo) {
69
+ this.logger.fail(`Subdomain ${displayName} does not exist`);
70
+ process.exit(1);
71
+ }
72
+ ownerAddress = subdomainInfo.owner.toHex();
73
+ }
74
+ else {
75
+ const domainInfo = await getDomain(name, network);
76
+ if (!domainInfo) {
77
+ this.logger.fail(`Domain ${displayName} does not exist`);
78
+ this.logger.info('Register the domain first with:');
79
+ this.logger.log(` opnet domain register ${name} -n ${network}`);
80
+ process.exit(1);
81
+ }
82
+ ownerAddress = domainInfo.owner.toHex();
83
+ }
84
+ if (wallet.address.toHex() !== ownerAddress) {
85
+ this.logger.fail('You are not the owner of this domain');
86
+ this.logger.log(`Domain owner: ${ownerAddress}`);
87
+ this.logger.log(`Your address: ${wallet.p2trAddress}`);
88
+ process.exit(1);
89
+ }
90
+ this.logger.success('Ownership verified');
91
+ const currentContenthash = await getContenthash(name, network);
92
+ if (currentContenthash.hashType !== 0) {
93
+ this.logger.warn(`Current website: ${currentContenthash.hashString || 'SHA256 hash'} (${getContenthashTypeName(currentContenthash.hashType)})`);
94
+ }
95
+ this.logger.info('Checking wallet balance...');
96
+ const { sufficient, balance } = await checkBalance(wallet, network);
97
+ if (!sufficient) {
98
+ this.logger.fail('Insufficient balance');
99
+ this.logger.error(`Wallet balance: ${formatSats(balance)}`);
100
+ this.logger.error('Please fund your wallet to pay for gas fees.');
101
+ process.exit(1);
102
+ }
103
+ this.logger.success(`Wallet balance: ${formatSats(balance)}`);
104
+ if (!options.yes) {
105
+ const confirmed = await confirm({
106
+ message: `Upload and deploy to ${displayName}?`,
107
+ default: true,
108
+ });
109
+ if (!confirmed) {
110
+ this.logger.warn('Deployment cancelled.');
111
+ return;
112
+ }
113
+ }
114
+ this.logger.log('');
115
+ this.logger.info('Uploading to IPFS...');
116
+ let cid;
117
+ if (isDirectory) {
118
+ const result = await uploadDirectory(resolvedPath);
119
+ cid = result.cid;
120
+ this.logger.success(`Uploaded ${result.files} files (${formatFileSize(result.totalSize)})`);
121
+ }
122
+ else {
123
+ const result = await uploadFile(resolvedPath);
124
+ cid = result.cid;
125
+ this.logger.success(`Uploaded file (${formatFileSize(result.size)})`);
126
+ }
127
+ this.logger.success(`IPFS CID: ${cid}`);
128
+ this.logger.log(`Gateway URL: https://ipfs.opnet.org/ipfs/${cid}`);
129
+ this.logger.log('');
130
+ if (options.dryRun) {
131
+ this.logger.warn('Dry run - website uploaded to IPFS but not published on-chain.');
132
+ this.logger.info('To publish on-chain, run:');
133
+ this.logger.log(` opnet website ${name} ${cid} -n ${network}`);
134
+ return;
135
+ }
136
+ this.logger.info('Publishing to blockchain...');
137
+ const sender = getWalletAddress(wallet);
138
+ const contract = getResolverContract(network, sender);
139
+ const txParams = buildTransactionParams(wallet, network, DEFAULT_MAX_SAT_TO_SPEND, DEFAULT_FEE_RATE);
140
+ const result = await contract.setContenthashCIDv1(name, cid);
141
+ if (result.revert) {
142
+ this.logger.fail('Transaction would fail');
143
+ this.logger.error(`Reason: ${result.revert}`);
144
+ process.exit(1);
145
+ }
146
+ if (result.estimatedGas) {
147
+ this.logger.info(`Estimated gas: ${result.estimatedGas} sats`);
148
+ }
149
+ const receipt = await result.sendTransaction(txParams);
150
+ this.logger.log('');
151
+ this.logger.success('Website deployed successfully!');
152
+ this.logger.log('');
153
+ this.logger.log(`Domain: ${displayName}`);
154
+ this.logger.log(`IPFS CID: ${cid}`);
155
+ this.logger.log(`Transaction ID: ${receipt.transactionId}`);
156
+ this.logger.log(`Fees paid: ${formatSats(receipt.estimatedFees)}`);
157
+ this.logger.log(`Gateway URL: https://ipfs.opnet.org/ipfs/${cid}`);
158
+ this.logger.log('');
159
+ const confirmationResult = await waitForTransactionConfirmation(receipt.transactionId, network, {
160
+ message: 'Waiting for transaction confirmation',
161
+ });
162
+ if (!confirmationResult.confirmed) {
163
+ if (confirmationResult.revert) {
164
+ this.logger.fail('Transaction failed');
165
+ this.logger.error(`Reason: ${confirmationResult.revert}`);
166
+ }
167
+ else if (confirmationResult.error) {
168
+ this.logger.warn('Transaction not yet confirmed');
169
+ this.logger.warn(confirmationResult.error);
170
+ }
171
+ }
172
+ else {
173
+ this.logger.log('');
174
+ this.logger.success(`${displayName} is now live!`);
175
+ }
176
+ }
177
+ catch (error) {
178
+ this.logger.fail('Deployment failed');
179
+ if (this.isUserCancelled(error)) {
180
+ this.logger.warn('Deployment cancelled.');
181
+ process.exit(0);
182
+ }
183
+ this.exitWithError(this.formatError(error));
184
+ }
185
+ }
186
+ countFiles(dirPath) {
187
+ let count = 0;
188
+ const entries = fs.readdirSync(dirPath, { withFileTypes: true });
189
+ for (const entry of entries) {
190
+ if (entry.isDirectory()) {
191
+ count += this.countFiles(path.join(dirPath, entry.name));
192
+ }
193
+ else if (entry.isFile()) {
194
+ count++;
195
+ }
196
+ }
197
+ return count;
198
+ }
199
+ getDirectorySize(dirPath) {
200
+ let size = 0;
201
+ const entries = fs.readdirSync(dirPath, { withFileTypes: true });
202
+ for (const entry of entries) {
203
+ const fullPath = path.join(dirPath, entry.name);
204
+ if (entry.isDirectory()) {
205
+ size += this.getDirectorySize(fullPath);
206
+ }
207
+ else if (entry.isFile()) {
208
+ size += fs.statSync(fullPath).size;
209
+ }
210
+ }
211
+ return size;
212
+ }
213
+ }
214
+ export const websiteDeployCommand = new WebsiteDeployCommand().getCommand();
@@ -0,0 +1,7 @@
1
+ import { BaseCommand } from './BaseCommand.js';
2
+ export declare class WebsitePublishCommand extends BaseCommand {
3
+ constructor();
4
+ protected configure(): void;
5
+ private execute;
6
+ }
7
+ export declare const websitePublishCommand: import("commander").Command;