@btc-vision/cli 1.0.7 → 1.0.9
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/build/commands/DomainCommand.d.ts +3 -0
- package/build/commands/DomainCommand.js +241 -0
- package/build/commands/InstallCommand.d.ts +2 -0
- package/build/commands/InstallCommand.js +29 -1
- package/build/commands/LoginCommand.js +4 -23
- package/build/commands/WebsiteDeployCommand.d.ts +9 -0
- package/build/commands/WebsiteDeployCommand.js +214 -0
- package/build/commands/WebsitePublishCommand.d.ts +7 -0
- package/build/commands/WebsitePublishCommand.js +233 -0
- package/build/index.js +6 -0
- package/build/lib/BtcResolver.abi.d.ts +2 -0
- package/build/lib/BtcResolver.abi.js +280 -0
- package/build/lib/config.js +11 -0
- package/build/lib/ipfs.d.ts +7 -0
- package/build/lib/ipfs.js +141 -7
- package/build/lib/resolver.d.ts +43 -0
- package/build/lib/resolver.js +197 -0
- package/build/types/BtcResolver.d.ts +139 -0
- package/build/types/BtcResolver.js +7 -0
- package/build/types/index.d.ts +1 -0
- package/package.json +1 -1
|
@@ -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 { getResolverContract, getDomain, getContenthash, getContenthashTypeName, validateDomainName, getTreasuryAddress, getDomainPrice, parseDomainName, } 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('');
|
|
71
|
+
if (options.dryRun) {
|
|
72
|
+
logger.warn('Dry run - no changes made.');
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (!options.yes) {
|
|
76
|
+
const confirmed = await confirm({
|
|
77
|
+
message: `Register ${displayName} for ${formatSats(price)}?`,
|
|
78
|
+
default: true,
|
|
79
|
+
});
|
|
80
|
+
if (!confirmed) {
|
|
81
|
+
logger.warn('Registration cancelled.');
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
logger.info('Preparing transaction...');
|
|
86
|
+
const sender = getWalletAddress(wallet);
|
|
87
|
+
const contract = getResolverContract(network, sender);
|
|
88
|
+
const extraUtxo = {
|
|
89
|
+
address: treasuryAddr,
|
|
90
|
+
value: Number(price),
|
|
91
|
+
};
|
|
92
|
+
const outSimulation = [
|
|
93
|
+
{
|
|
94
|
+
index: 1,
|
|
95
|
+
to: treasuryAddr,
|
|
96
|
+
value: price,
|
|
97
|
+
flags: TransactionOutputFlags.hasTo,
|
|
98
|
+
scriptPubKey: undefined,
|
|
99
|
+
},
|
|
100
|
+
];
|
|
101
|
+
contract.setTransactionDetails({
|
|
102
|
+
inputs: [],
|
|
103
|
+
outputs: outSimulation,
|
|
104
|
+
});
|
|
105
|
+
const registerResult = await contract.registerDomain(name);
|
|
106
|
+
if (registerResult.revert) {
|
|
107
|
+
logger.fail('Registration would fail');
|
|
108
|
+
logger.error(`Reason: ${registerResult.revert}`);
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
if (registerResult.estimatedGas) {
|
|
112
|
+
logger.info(`Estimated gas: ${registerResult.estimatedGas} sats`);
|
|
113
|
+
}
|
|
114
|
+
const txParams = buildTransactionParams(wallet, network, DEFAULT_MAX_SAT_TO_SPEND + price, DEFAULT_FEE_RATE, extraUtxo);
|
|
115
|
+
const receipt = await registerResult.sendTransaction(txParams);
|
|
116
|
+
logger.log('');
|
|
117
|
+
logger.success('Domain registration submitted!');
|
|
118
|
+
logger.log('');
|
|
119
|
+
logger.log(`Domain: ${displayName}`);
|
|
120
|
+
logger.log(`Transaction ID: ${receipt.transactionId}`);
|
|
121
|
+
logger.log(`Fees paid: ${formatSats(receipt.estimatedFees)}`);
|
|
122
|
+
logger.log('');
|
|
123
|
+
const confirmationResult = await waitForTransactionConfirmation(receipt.transactionId, network, {
|
|
124
|
+
message: 'Waiting for registration confirmation',
|
|
125
|
+
});
|
|
126
|
+
if (!confirmationResult.confirmed) {
|
|
127
|
+
if (confirmationResult.revert) {
|
|
128
|
+
logger.fail('Registration failed');
|
|
129
|
+
logger.error(`Reason: ${confirmationResult.revert}`);
|
|
130
|
+
}
|
|
131
|
+
else if (confirmationResult.error) {
|
|
132
|
+
logger.warn('Registration not yet confirmed');
|
|
133
|
+
logger.warn(confirmationResult.error);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
logger.log('');
|
|
138
|
+
logger.success(`You now own ${displayName}!`);
|
|
139
|
+
logger.info('Next step: publish your website with:');
|
|
140
|
+
logger.log(` opnet website ${name} <ipfs-cid> -n ${network}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
logger.fail('Domain registration failed');
|
|
145
|
+
if (error instanceof Error && error.message.includes('User force closed')) {
|
|
146
|
+
logger.warn('Registration cancelled.');
|
|
147
|
+
process.exit(0);
|
|
148
|
+
}
|
|
149
|
+
logger.error(error instanceof Error ? error.message : String(error));
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
async function domainInfo(domain, options) {
|
|
154
|
+
try {
|
|
155
|
+
const network = (options.network || 'mainnet');
|
|
156
|
+
const name = parseDomainName(domain.toLowerCase());
|
|
157
|
+
const displayName = `${name}.btc`;
|
|
158
|
+
logger.info(`Looking up: ${displayName}`);
|
|
159
|
+
const domainData = await getDomain(name, network);
|
|
160
|
+
if (!domainData) {
|
|
161
|
+
logger.warn(`Domain ${displayName} is not registered`);
|
|
162
|
+
const price = await getDomainPrice(name, network);
|
|
163
|
+
logger.log('');
|
|
164
|
+
logger.info('Registration Info');
|
|
165
|
+
logger.log('-'.repeat(50));
|
|
166
|
+
logger.log(`Domain: ${displayName}`);
|
|
167
|
+
logger.log(`Status: Available`);
|
|
168
|
+
logger.log(`Price: ${formatSats(price)}`);
|
|
169
|
+
if (name.length === 3) {
|
|
170
|
+
logger.log(`Pricing tier: Premium (3-character)`);
|
|
171
|
+
}
|
|
172
|
+
else if (name.length === 4) {
|
|
173
|
+
logger.log(`Pricing tier: Premium (4-character)`);
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
logger.log(`Pricing tier: Standard`);
|
|
177
|
+
}
|
|
178
|
+
logger.log('');
|
|
179
|
+
logger.info('Register with:');
|
|
180
|
+
logger.log(` opnet domain register ${name} -n ${network}`);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
const contenthash = await getContenthash(name, network);
|
|
184
|
+
logger.log('');
|
|
185
|
+
logger.info('Domain Information');
|
|
186
|
+
logger.log('-'.repeat(50));
|
|
187
|
+
logger.log(`Domain: ${displayName}`);
|
|
188
|
+
logger.log(`Status: Registered`);
|
|
189
|
+
logger.log(`Owner: ${domainData.owner.toHex()}`);
|
|
190
|
+
logger.log(`Created at: Block #${domainData.createdAt}`);
|
|
191
|
+
logger.log(`TTL: ${domainData.ttl} seconds`);
|
|
192
|
+
if (contenthash.hashType !== 0) {
|
|
193
|
+
logger.log('');
|
|
194
|
+
logger.info('Website (Contenthash)');
|
|
195
|
+
logger.log('-'.repeat(50));
|
|
196
|
+
logger.log(`Type: ${getContenthashTypeName(contenthash.hashType)}`);
|
|
197
|
+
if (contenthash.hashString) {
|
|
198
|
+
logger.log(`Value: ${contenthash.hashString}`);
|
|
199
|
+
if (contenthash.hashType === 1 || contenthash.hashType === 2) {
|
|
200
|
+
logger.log(`Gateway URL: https://ipfs.opnet.org/ipfs/${contenthash.hashString}`);
|
|
201
|
+
}
|
|
202
|
+
else if (contenthash.hashType === 3) {
|
|
203
|
+
logger.log(`Gateway URL: https://ipfs.opnet.org/ipns/${contenthash.hashString}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
const hashHex = Buffer.from(contenthash.hashData).toString('hex');
|
|
208
|
+
logger.log(`Value: ${hashHex}`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
logger.log('');
|
|
213
|
+
logger.warn('No website published');
|
|
214
|
+
logger.info('Publish with:');
|
|
215
|
+
logger.log(` opnet website ${name} <ipfs-cid> -n ${network}`);
|
|
216
|
+
}
|
|
217
|
+
logger.log('');
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
logger.fail('Failed to get domain info');
|
|
221
|
+
logger.error(error instanceof Error ? error.message : String(error));
|
|
222
|
+
process.exit(1);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
const domainCommand = new Command('domain')
|
|
226
|
+
.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 };
|
|
@@ -106,8 +106,10 @@ export class InstallCommand extends BaseCommand {
|
|
|
106
106
|
}
|
|
107
107
|
const outputDir = options?.output || path.join(process.cwd(), 'plugins');
|
|
108
108
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
109
|
-
const
|
|
109
|
+
const packageBaseName = packageName.replace(/^@/, '').replace(/\//g, '-');
|
|
110
|
+
const fileName = `${packageBaseName}-${version}.opnet`;
|
|
110
111
|
const outputPath = path.join(outputDir, fileName);
|
|
112
|
+
this.removeOldVersions(outputDir, packageBaseName, version);
|
|
111
113
|
this.logger.info('Saving plugin...');
|
|
112
114
|
fs.writeFileSync(outputPath, result.data);
|
|
113
115
|
this.logger.success('Plugin installed');
|
|
@@ -126,5 +128,31 @@ export class InstallCommand extends BaseCommand {
|
|
|
126
128
|
this.exitWithError(this.formatError(error));
|
|
127
129
|
}
|
|
128
130
|
}
|
|
131
|
+
removeOldVersions(outputDir, packageBaseName, newVersion) {
|
|
132
|
+
try {
|
|
133
|
+
const entries = fs.readdirSync(outputDir, { withFileTypes: true });
|
|
134
|
+
for (const entry of entries) {
|
|
135
|
+
if (!entry.isFile() || !entry.name.endsWith('.opnet')) {
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
const pattern = new RegExp(`^${this.escapeRegex(packageBaseName)}-(.+)\\.opnet$`);
|
|
139
|
+
const match = entry.name.match(pattern);
|
|
140
|
+
if (match) {
|
|
141
|
+
const oldVersion = match[1];
|
|
142
|
+
if (oldVersion !== newVersion) {
|
|
143
|
+
const oldFilePath = path.join(outputDir, entry.name);
|
|
144
|
+
this.logger.info(`Removing old version: ${entry.name}`);
|
|
145
|
+
fs.unlinkSync(oldFilePath);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
this.logger.warn(`Could not clean up old versions: ${this.formatError(error)}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
escapeRegex(str) {
|
|
155
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
156
|
+
}
|
|
129
157
|
}
|
|
130
158
|
export const installCommand = new InstallCommand().getCommand();
|
|
@@ -76,9 +76,9 @@ export class LoginCommand extends BaseCommand {
|
|
|
76
76
|
network: options.network,
|
|
77
77
|
};
|
|
78
78
|
}
|
|
79
|
-
return this.interactiveLogin(options.network
|
|
79
|
+
return this.interactiveLogin(options.network);
|
|
80
80
|
}
|
|
81
|
-
async interactiveLogin(defaultNetwork
|
|
81
|
+
async interactiveLogin(defaultNetwork) {
|
|
82
82
|
this.logger.info('OPNet Wallet Configuration\n');
|
|
83
83
|
const loginMethod = await select({
|
|
84
84
|
message: 'How would you like to authenticate?',
|
|
@@ -104,27 +104,8 @@ export class LoginCommand extends BaseCommand {
|
|
|
104
104
|
],
|
|
105
105
|
default: defaultNetwork,
|
|
106
106
|
}));
|
|
107
|
-
const selectedLevel =
|
|
108
|
-
|
|
109
|
-
choices: [
|
|
110
|
-
{
|
|
111
|
-
name: 'MLDSA-44 (Level 2, fastest)',
|
|
112
|
-
value: 44,
|
|
113
|
-
description: '1312 byte public key',
|
|
114
|
-
},
|
|
115
|
-
{
|
|
116
|
-
name: 'MLDSA-65 (Level 3, balanced)',
|
|
117
|
-
value: 65,
|
|
118
|
-
description: '1952 byte public key',
|
|
119
|
-
},
|
|
120
|
-
{
|
|
121
|
-
name: 'MLDSA-87 (Level 5, most secure)',
|
|
122
|
-
value: 87,
|
|
123
|
-
description: '2592 byte public key',
|
|
124
|
-
},
|
|
125
|
-
],
|
|
126
|
-
default: defaultLevel,
|
|
127
|
-
}));
|
|
107
|
+
const selectedLevel = 44;
|
|
108
|
+
this.logger.info('Using MLDSA-44 (only supported level on OPNet)');
|
|
128
109
|
if (loginMethod === 'mnemonic') {
|
|
129
110
|
const mnemonic = await password({
|
|
130
111
|
message: 'Enter your mnemonic phrase (12 or 24 words):',
|
|
@@ -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 { getResolverContract, getDomain, getSubdomain, getContenthash, getContenthashTypeName, 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.address.toHex()}`);
|
|
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();
|