@btc-vision/cli 1.0.2 → 1.0.3

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.
Files changed (37) hide show
  1. package/README.md +55 -23
  2. package/build/commands/AcceptCommand.js +62 -15
  3. package/build/commands/ConfigCommand.js +2 -27
  4. package/build/commands/DeprecateCommand.js +32 -11
  5. package/build/commands/InitCommand.js +0 -6
  6. package/build/commands/PublishCommand.js +82 -27
  7. package/build/commands/ScopeRegisterCommand.d.ts +7 -0
  8. package/build/commands/ScopeRegisterCommand.js +115 -0
  9. package/build/commands/TransferCommand.js +118 -30
  10. package/build/commands/UndeprecateCommand.js +31 -10
  11. package/build/index.js +2 -0
  12. package/build/lib/config.d.ts +1 -0
  13. package/build/lib/config.js +3 -2
  14. package/build/lib/ipfs.js +85 -76
  15. package/build/lib/registry.d.ts +1 -1
  16. package/build/lib/registry.js +3 -3
  17. package/build/lib/transaction.d.ts +27 -0
  18. package/build/lib/transaction.js +91 -0
  19. package/build/lib/wallet.d.ts +7 -7
  20. package/build/lib/wallet.js +3 -6
  21. package/build/types/index.d.ts +1 -0
  22. package/package.json +2 -1
  23. package/src/commands/AcceptCommand.ts +89 -16
  24. package/src/commands/ConfigCommand.ts +2 -29
  25. package/src/commands/DeprecateCommand.ts +48 -11
  26. package/src/commands/InitCommand.ts +0 -6
  27. package/src/commands/PublishCommand.ts +138 -28
  28. package/src/commands/ScopeRegisterCommand.ts +164 -0
  29. package/src/commands/TransferCommand.ts +159 -31
  30. package/src/commands/UndeprecateCommand.ts +43 -10
  31. package/src/index.ts +2 -0
  32. package/src/lib/config.ts +3 -2
  33. package/src/lib/ipfs.ts +113 -99
  34. package/src/lib/registry.ts +5 -2
  35. package/src/lib/transaction.ts +205 -0
  36. package/src/lib/wallet.ts +10 -19
  37. package/src/types/index.ts +3 -1
@@ -6,9 +6,15 @@
6
6
 
7
7
  import { confirm } from '@inquirer/prompts';
8
8
  import { BaseCommand } from './BaseCommand.js';
9
- import { getPackage, getVersion, isVersionImmutable } from '../lib/registry.js';
9
+ import { getPackage, getRegistryContract, getVersion, isVersionImmutable } from '../lib/registry.js';
10
10
  import { canSign, loadCredentials } from '../lib/credentials.js';
11
11
  import { CLIWallet } from '../lib/wallet.js';
12
+ import {
13
+ buildTransactionParams,
14
+ checkBalance,
15
+ formatSats,
16
+ getWalletAddress,
17
+ } from '../lib/transaction.js';
12
18
  import { NetworkName } from '../types/index.js';
13
19
 
14
20
  interface UndeprecateOptions {
@@ -46,7 +52,7 @@ export class UndeprecateCommand extends BaseCommand {
46
52
  this.logger.warn('Run `opnet login` to configure your wallet.');
47
53
  process.exit(1);
48
54
  }
49
- CLIWallet.fromCredentials(credentials);
55
+ const wallet = CLIWallet.fromCredentials(credentials);
50
56
  this.logger.success('Wallet loaded');
51
57
 
52
58
  // Get package info
@@ -107,18 +113,45 @@ export class UndeprecateCommand extends BaseCommand {
107
113
  }
108
114
  }
109
115
 
116
+ // Check wallet balance
117
+ this.logger.info('Checking wallet balance...');
118
+ const { sufficient, balance } = await checkBalance(wallet, network);
119
+ if (!sufficient) {
120
+ this.logger.fail('Insufficient balance');
121
+ this.logger.error(`Wallet balance: ${formatSats(balance)}`);
122
+ this.logger.error('Please fund your wallet before undeprecating.');
123
+ process.exit(1);
124
+ }
125
+ this.logger.success(`Wallet balance: ${formatSats(balance)}`);
126
+
110
127
  // Execute undeprecation
111
128
  this.logger.info('Removing deprecation...');
112
- this.logger.warn('Undeprecation transaction required.');
113
- this.logger.log('Transaction would call: undeprecateVersion(');
114
- this.logger.log(` packageName: "${packageName}",`);
115
- this.logger.log(` version: "${version}"`);
116
- this.logger.log(')');
117
- this.logger.info('Undeprecation (transaction pending)');
118
129
 
130
+ const sender = getWalletAddress(wallet);
131
+ const contract = getRegistryContract(network, sender);
132
+ const txParams = buildTransactionParams(wallet, network);
133
+
134
+ const undeprecateResult = await contract.undeprecateVersion(packageName, version);
135
+
136
+ if (undeprecateResult.revert) {
137
+ this.logger.fail('Undeprecation would fail');
138
+ this.logger.error(`Reason: ${undeprecateResult.revert}`);
139
+ process.exit(1);
140
+ }
141
+
142
+ if (undeprecateResult.estimatedGas) {
143
+ this.logger.info(`Estimated gas: ${undeprecateResult.estimatedGas} sats`);
144
+ }
145
+
146
+ const receipt = await undeprecateResult.sendTransaction(txParams);
147
+
148
+ this.logger.log('');
149
+ this.logger.success('Deprecation removed successfully!');
119
150
  this.logger.log('');
120
- this.logger.success('Undeprecation submitted!');
121
- this.logger.warn('Note: Registry transaction support is coming soon.');
151
+ this.logger.log(`Package: ${packageName}`);
152
+ this.logger.log(`Version: ${version}`);
153
+ this.logger.log(`Transaction ID: ${receipt.transactionId}`);
154
+ this.logger.log(`Fees paid: ${formatSats(receipt.estimatedFees)}`);
122
155
  this.logger.log('');
123
156
  } catch (error) {
124
157
  this.logger.fail('Undeprecation failed');
package/src/index.ts CHANGED
@@ -24,6 +24,7 @@ import { deprecateCommand } from './commands/DeprecateCommand.js';
24
24
  import { undeprecateCommand } from './commands/UndeprecateCommand.js';
25
25
  import { transferCommand } from './commands/TransferCommand.js';
26
26
  import { acceptCommand } from './commands/AcceptCommand.js';
27
+ import { scopeRegisterCommand } from './commands/ScopeRegisterCommand.js';
27
28
  import { installCommand } from './commands/InstallCommand.js';
28
29
  import { updateCommand } from './commands/UpdateCommand.js';
29
30
  import { listCommand } from './commands/ListCommand.js';
@@ -57,6 +58,7 @@ program.addCommand(deprecateCommand);
57
58
  program.addCommand(undeprecateCommand);
58
59
  program.addCommand(transferCommand);
59
60
  program.addCommand(acceptCommand);
61
+ program.addCommand(scopeRegisterCommand);
60
62
  program.addCommand(installCommand);
61
63
  program.addCommand(updateCommand);
62
64
  program.addCommand(listCommand);
package/src/lib/config.ts CHANGED
@@ -19,7 +19,7 @@ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
19
19
  /**
20
20
  * Default CLI configuration
21
21
  */
22
- const DEFAULT_CONFIG: CLIConfig = {
22
+ export const DEFAULT_CONFIG: CLIConfig = {
23
23
  defaultNetwork: 'regtest',
24
24
  rpcUrls: {
25
25
  mainnet: 'https://api.opnet.org',
@@ -35,11 +35,12 @@ const DEFAULT_CONFIG: CLIConfig = {
35
35
  ],
36
36
  ipfsPinningEndpoint: 'https://ipfs.opnet.org/api/v0/add',
37
37
  ipfsPinningApiKey: '',
38
+ ipfsPinningSecret: '',
38
39
  ipfsPinningAuthHeader: 'Authorization',
39
40
  registryAddresses: {
40
41
  mainnet: '', // TODO: Set once deployed
41
42
  testnet: '', // TODO: Set once deployed
42
- regtest: '', // TODO: Set once deployed
43
+ regtest: '0x0737d17d0eff9915208f3c20ed7659587889bc94d25972672b3a6c03ff4ddbcc', // TODO: Set once deployed
43
44
  },
44
45
  defaultMldsaLevel: 44,
45
46
  indexerUrl: 'https://indexer.opnet.org',
package/src/lib/ipfs.ts CHANGED
@@ -103,117 +103,131 @@ async function httpRequest(url: string, options: RequestOptions): Promise<Buffer
103
103
  * @returns The IPFS CID
104
104
  */
105
105
  export async function pinToIPFS(data: Buffer, name?: string): Promise<PinResult> {
106
- const config = loadConfig();
107
- const endpoint = config.ipfsPinningEndpoint;
106
+ try {
107
+ const config = loadConfig();
108
+ const endpoint = config.ipfsPinningEndpoint;
109
+
110
+ if (!endpoint) {
111
+ throw new Error(
112
+ 'IPFS pinning endpoint not configured. Run `opnet config set ipfsPinningEndpoint <url>`',
113
+ );
114
+ }
115
+
116
+ // Build multipart form data
117
+ const boundary = '----FormBoundary' + Math.random().toString(36).substring(2);
118
+ const fileName = name || 'plugin.opnet';
119
+
120
+ const formParts: Buffer[] = [];
108
121
 
109
- if (!endpoint) {
110
- throw new Error(
111
- 'IPFS pinning endpoint not configured. Run `opnet config set ipfsPinningEndpoint <url>`',
122
+ // File part
123
+ formParts.push(
124
+ Buffer.from(
125
+ `--${boundary}\r\n` +
126
+ `Content-Disposition: form-data; name="file"; filename="${fileName}"\r\n` +
127
+ `Content-Type: application/octet-stream\r\n\r\n`,
128
+ ),
112
129
  );
113
- }
130
+ formParts.push(data);
131
+ formParts.push(Buffer.from('\r\n'));
114
132
 
115
- // Build multipart form data
116
- const boundary = '----FormBoundary' + Math.random().toString(36).substring(2);
117
- const fileName = name || 'plugin.opnet';
133
+ // End boundary
134
+ formParts.push(Buffer.from(`--${boundary}--\r\n`));
118
135
 
119
- const formParts: Buffer[] = [];
136
+ const body = Buffer.concat(formParts);
120
137
 
121
- // File part
122
- formParts.push(
123
- Buffer.from(
124
- `--${boundary}\r\n` +
125
- `Content-Disposition: form-data; name="file"; filename="${fileName}"\r\n` +
126
- `Content-Type: application/octet-stream\r\n\r\n`,
127
- ),
128
- );
129
- formParts.push(data);
130
- formParts.push(Buffer.from('\r\n'));
131
-
132
- // End boundary
133
- formParts.push(Buffer.from(`--${boundary}--\r\n`));
134
-
135
- const body = Buffer.concat(formParts);
136
-
137
- // Build headers
138
- const headers: Record<string, string> = {
139
- 'Content-Type': `multipart/form-data; boundary=${boundary}`,
140
- 'Content-Length': body.length.toString(),
141
- };
142
-
143
- // Add authorization if configured
144
- if (config.ipfsPinningAuthHeader) {
145
- const [headerName, headerValue] = config.ipfsPinningAuthHeader
146
- .split(':')
147
- .map((s) => s.trim());
148
- if (headerName && headerValue) {
149
- headers[headerName] = headerValue;
138
+ // Build headers
139
+ const headers: Record<string, string> = {
140
+ 'Content-Type': `multipart/form-data; boundary=${boundary}`,
141
+ 'Content-Length': body.length.toString(),
142
+ };
143
+
144
+ // Add authorization if configured
145
+ if (config.ipfsPinningAuthHeader) {
146
+ const [headerName, headerValue] = config.ipfsPinningAuthHeader
147
+ .split(':')
148
+ .map((s) => s.trim());
149
+ if (headerName && headerValue) {
150
+ headers[headerName] = headerValue;
151
+ }
152
+ } else if (config.ipfsPinningApiKey) {
153
+ headers['Authorization'] = `Bearer ${config.ipfsPinningApiKey}`;
150
154
  }
151
- } else if (config.ipfsPinningApiKey) {
152
- headers['Authorization'] = `Bearer ${config.ipfsPinningApiKey}`;
153
- }
154
155
 
155
- // Detect pinning service type from URL and adjust request
156
- const url = new URL(endpoint);
157
-
158
- let requestUrl: string;
159
- if (url.hostname.includes('ipfs.opnet.org')) {
160
- // OPNet IPFS gateway - uses standard IPFS API
161
- requestUrl = endpoint;
162
- } else if (url.hostname.includes('pinata')) {
163
- // Pinata-specific endpoint
164
- requestUrl = 'https://api.pinata.cloud/pinning/pinFileToIPFS';
165
- if (config.ipfsPinningApiKey) {
166
- headers['pinata_api_key'] = config.ipfsPinningApiKey;
156
+ // Detect pinning service type from URL and adjust request
157
+ const url = new URL(endpoint);
158
+
159
+ let requestUrl: string;
160
+ if (url.hostname.includes('ipfs.opnet.org')) {
161
+ // OPNet IPFS gateway - uses standard IPFS API
162
+ requestUrl = endpoint;
163
+ } else if (url.hostname.includes('pinata')) {
164
+ // Pinata-specific endpoint
165
+ requestUrl = 'https://api.pinata.cloud/pinning/pinFileToIPFS';
166
+ // Only set pinata_api_key header if using API key (not JWT)
167
+ // JWT tokens start with "eyJ", API keys don't
168
+ if (config.ipfsPinningApiKey && !config.ipfsPinningApiKey.startsWith('eyJ')) {
169
+ headers['pinata_api_key'] = config.ipfsPinningApiKey;
170
+ if (config.ipfsPinningSecret) {
171
+ headers['pinata_secret_api_key'] = config.ipfsPinningSecret;
172
+ }
173
+ }
174
+ } else if (url.hostname.includes('web3.storage') || url.hostname.includes('w3s.link')) {
175
+ // web3.storage endpoint
176
+ requestUrl = endpoint.endsWith('/') ? endpoint + 'upload' : endpoint + '/upload';
177
+ } else if (url.hostname.includes('nft.storage')) {
178
+ // nft.storage endpoint
179
+ requestUrl = 'https://api.nft.storage/upload';
180
+ } else if (url.pathname.includes('/api/v0/')) {
181
+ // Standard IPFS API endpoint
182
+ requestUrl = endpoint;
183
+ } else {
184
+ // Generic IPFS pinning service (assumed to follow IPFS Pinning Services API)
185
+ requestUrl = endpoint.endsWith('/') ? endpoint + 'pins' : endpoint + '/pins';
167
186
  }
168
- } else if (url.hostname.includes('web3.storage') || url.hostname.includes('w3s.link')) {
169
- // web3.storage endpoint
170
- requestUrl = endpoint.endsWith('/') ? endpoint + 'upload' : endpoint + '/upload';
171
- } else if (url.hostname.includes('nft.storage')) {
172
- // nft.storage endpoint
173
- requestUrl = 'https://api.nft.storage/upload';
174
- } else if (url.pathname.includes('/api/v0/')) {
175
- // Standard IPFS API endpoint
176
- requestUrl = endpoint;
177
- } else {
178
- // Generic IPFS pinning service (assumed to follow IPFS Pinning Services API)
179
- requestUrl = endpoint.endsWith('/') ? endpoint + 'pins' : endpoint + '/pins';
180
- }
181
187
 
182
- const response = await httpRequest(requestUrl, {
183
- method: 'POST',
184
- headers,
185
- body,
186
- timeout: 120000, // 2 minutes for upload
187
- });
188
+ const response = await httpRequest(requestUrl, {
189
+ method: 'POST',
190
+ headers,
191
+ body,
192
+ timeout: 120000, // 2 minutes for upload
193
+ });
188
194
 
189
- // Parse response to extract CID
190
- const result = JSON.parse(response.toString()) as Record<string, unknown>;
191
-
192
- // Handle different response formats
193
- let cid: string | undefined;
194
-
195
- if (typeof result.IpfsHash === 'string') {
196
- // Pinata format
197
- cid = result.IpfsHash;
198
- } else if (typeof result.cid === 'string') {
199
- // web3.storage / nft.storage format
200
- cid = result.cid;
201
- } else if (typeof result.Hash === 'string') {
202
- // IPFS API format
203
- cid = result.Hash;
204
- } else if (result.value && typeof (result.value as Record<string, unknown>).cid === 'string') {
205
- // NFT.storage wrapped format
206
- cid = (result.value as Record<string, unknown>).cid as string;
207
- }
195
+ // Parse response to extract CID
196
+ const result = JSON.parse(response.toString()) as Record<string, unknown>;
197
+
198
+ // Handle different response formats
199
+ let cid: string | undefined;
200
+
201
+ if (typeof result.IpfsHash === 'string') {
202
+ // Pinata format
203
+ cid = result.IpfsHash;
204
+ } else if (typeof result.cid === 'string') {
205
+ // web3.storage / nft.storage format
206
+ cid = result.cid;
207
+ } else if (typeof result.Hash === 'string') {
208
+ // IPFS API format
209
+ cid = result.Hash;
210
+ } else if (
211
+ result.value &&
212
+ typeof (result.value as Record<string, unknown>).cid === 'string'
213
+ ) {
214
+ // NFT.storage wrapped format
215
+ cid = (result.value as Record<string, unknown>).cid as string;
216
+ }
208
217
 
209
- if (!cid) {
210
- throw new Error(`Failed to extract CID from pinning response: ${JSON.stringify(result)}`);
211
- }
218
+ if (!cid) {
219
+ throw new Error(
220
+ `Failed to extract CID from pinning response: ${JSON.stringify(result)}`,
221
+ );
222
+ }
212
223
 
213
- return {
214
- cid,
215
- size: data.length,
216
- };
224
+ return {
225
+ cid,
226
+ size: data.length,
227
+ };
228
+ } catch (e) {
229
+ throw new Error(`IPFS pinning failed: ${e instanceof Error ? e.message : String(e)}`);
230
+ }
217
231
  }
218
232
 
219
233
  /**
@@ -73,12 +73,14 @@ const registryCache = new Map<string, IPackageRegistry>();
73
73
  * Get the registry contract instance
74
74
  *
75
75
  * @param network - Network name (defaults to configured default)
76
+ * @param sender - Optional sender address for write operations
76
77
  * @returns Contract instance
77
78
  */
78
- export function getRegistryContract(network?: NetworkName): IPackageRegistry {
79
+ export function getRegistryContract(network?: NetworkName, sender?: Address): IPackageRegistry {
79
80
  const config = loadConfig();
80
81
  const targetNetwork = network || config.defaultNetwork;
81
- const cacheKey = targetNetwork;
82
+ // Include sender in cache key to handle both read-only and write cases
83
+ const cacheKey = sender ? `${targetNetwork}:${sender.toHex()}` : targetNetwork;
82
84
 
83
85
  const cached = registryCache.get(cacheKey);
84
86
  if (cached) {
@@ -94,6 +96,7 @@ export function getRegistryContract(network?: NetworkName): IPackageRegistry {
94
96
  PACKAGE_REGISTRY_ABI,
95
97
  provider,
96
98
  bitcoinNetwork,
99
+ sender,
97
100
  );
98
101
 
99
102
  registryCache.set(cacheKey, contract);
@@ -0,0 +1,205 @@
1
+ /**
2
+ * Transaction helper for OPNet CLI
3
+ *
4
+ * Provides utilities for building and executing registry transactions.
5
+ *
6
+ * @module lib/transaction
7
+ */
8
+
9
+ import { Address } from '@btc-vision/transaction';
10
+ import { TransactionParameters } from 'opnet';
11
+ import ora, { Ora } from 'ora';
12
+
13
+ import { CLIWallet, getNetwork } from './wallet.js';
14
+ import { NetworkName } from '../types/index.js';
15
+ import { getProvider } from './provider.js';
16
+ import { PsbtOutputExtended } from '@btc-vision/bitcoin';
17
+
18
+ /** Default maximum satoshis to spend per transaction */
19
+ export const DEFAULT_MAX_SAT_TO_SPEND = 100_000n;
20
+
21
+ /** Default fee rate in sat/vbyte */
22
+ export const DEFAULT_FEE_RATE = 6;
23
+
24
+ /**
25
+ * Build TransactionParameters from a CLIWallet
26
+ *
27
+ * @param wallet - The CLI wallet instance
28
+ * @param network - Target network name
29
+ * @param maxSatToSpend - Maximum satoshis to spend (optional)
30
+ * @param feeRate - Fee rate in sat/vbyte (optional)
31
+ * @param extra
32
+ * @returns TransactionParameters for contract calls
33
+ */
34
+ export function buildTransactionParams(
35
+ wallet: CLIWallet,
36
+ network: NetworkName,
37
+ maxSatToSpend: bigint = DEFAULT_MAX_SAT_TO_SPEND,
38
+ feeRate: number = DEFAULT_FEE_RATE,
39
+ extra?: PsbtOutputExtended,
40
+ ): TransactionParameters {
41
+ const bitcoinNetwork = getNetwork(network);
42
+
43
+ return {
44
+ signer: wallet.keypair,
45
+ mldsaSigner: wallet.mldsaKeypair,
46
+ refundTo: wallet.p2trAddress,
47
+ maximumAllowedSatToSpend: maxSatToSpend,
48
+ network: bitcoinNetwork,
49
+ feeRate,
50
+ extraOutputs: extra ? [extra] : undefined,
51
+ };
52
+ }
53
+
54
+ /**
55
+ * Get the sender Address from a wallet
56
+ *
57
+ * @param wallet - The CLI wallet
58
+ * @returns Address instance for the wallet
59
+ */
60
+ export function getWalletAddress(wallet: CLIWallet): Address {
61
+ return wallet.address;
62
+ }
63
+
64
+ /**
65
+ * Format satoshis for display
66
+ *
67
+ * @param sats - Amount in satoshis
68
+ * @returns Formatted string with BTC equivalent
69
+ */
70
+ export function formatSats(sats: bigint): string {
71
+ const btc = Number(sats) / 100_000_000;
72
+ if (btc >= 0.001) {
73
+ return `${sats} sats (${btc.toFixed(8)} BTC)`;
74
+ }
75
+ return `${sats} sats`;
76
+ }
77
+
78
+ /**
79
+ * Check if wallet has sufficient balance for transaction
80
+ *
81
+ * @param wallet - The CLI wallet
82
+ * @param network - Target network
83
+ * @param minBalance - Minimum required balance in satoshis
84
+ * @returns True if balance is sufficient
85
+ */
86
+ export async function checkBalance(
87
+ wallet: CLIWallet,
88
+ network: NetworkName,
89
+ minBalance: bigint = 10_000n,
90
+ ): Promise<{ sufficient: boolean; balance: bigint }> {
91
+ const provider = getProvider(network);
92
+ const balance = await provider.getBalance(wallet.p2trAddress);
93
+
94
+ return {
95
+ sufficient: balance >= minBalance,
96
+ balance,
97
+ };
98
+ }
99
+
100
+ /** Default polling interval in milliseconds */
101
+ export const DEFAULT_POLL_INTERVAL = 10_000;
102
+
103
+ /** Default maximum wait time in milliseconds (10 minutes) */
104
+ export const DEFAULT_MAX_WAIT_TIME = 600_000;
105
+
106
+ export interface TransactionConfirmationResult {
107
+ confirmed: boolean;
108
+ blockNumber?: bigint;
109
+ revert?: string;
110
+ error?: string;
111
+ }
112
+
113
+ /**
114
+ * Wait for a transaction to be confirmed on-chain
115
+ *
116
+ * Polls the transaction status every pollInterval ms until:
117
+ * - The transaction is confirmed (has a blockNumber)
118
+ * - The transaction fails (has a revert)
119
+ * - The maximum wait time is exceeded
120
+ *
121
+ * @param txHash - The transaction hash to wait for
122
+ * @param network - Target network
123
+ * @param options - Optional configuration
124
+ * @returns Transaction confirmation result
125
+ */
126
+ export async function waitForTransactionConfirmation(
127
+ txHash: string,
128
+ network: NetworkName,
129
+ options?: {
130
+ pollInterval?: number;
131
+ maxWaitTime?: number;
132
+ message?: string;
133
+ },
134
+ ): Promise<TransactionConfirmationResult> {
135
+ const pollInterval = options?.pollInterval ?? DEFAULT_POLL_INTERVAL;
136
+ const maxWaitTime = options?.maxWaitTime ?? DEFAULT_MAX_WAIT_TIME;
137
+ const message = options?.message ?? 'Waiting for transaction confirmation';
138
+
139
+ const provider = getProvider(network);
140
+ const startTime = Date.now();
141
+
142
+ const spinner: Ora = ora({
143
+ text: `${message} (0s elapsed)`,
144
+ spinner: 'dots',
145
+ }).start();
146
+
147
+ const updateSpinnerText = (): void => {
148
+ const elapsed = Math.floor((Date.now() - startTime) / 1000);
149
+ spinner.text = `${message} (${elapsed}s elapsed)`;
150
+ };
151
+
152
+ // Update spinner text every second
153
+ const textUpdateInterval = setInterval(updateSpinnerText, 1000);
154
+
155
+ try {
156
+ while (Date.now() - startTime < maxWaitTime) {
157
+ try {
158
+ const tx = await provider.getTransaction(txHash);
159
+
160
+ // Check if transaction has a block number (confirmed)
161
+ if (tx.blockNumber !== undefined && tx.blockNumber !== null) {
162
+ const blockNum =
163
+ typeof tx.blockNumber === 'bigint'
164
+ ? tx.blockNumber
165
+ : BigInt(tx.blockNumber);
166
+ spinner.succeed(`Transaction confirmed in block ${blockNum}`);
167
+ return {
168
+ confirmed: true,
169
+ blockNumber: blockNum,
170
+ };
171
+ }
172
+
173
+ // Check if transaction reverted
174
+ if (tx.revert) {
175
+ spinner.fail(`Transaction reverted: ${tx.revert}`);
176
+ return {
177
+ confirmed: false,
178
+ revert: tx.revert,
179
+ };
180
+ }
181
+ } catch {
182
+ // Transaction not found yet, continue polling
183
+ }
184
+
185
+ // Wait before next poll
186
+ await sleep(pollInterval);
187
+ }
188
+
189
+ // Timeout reached
190
+ spinner.fail(`Timeout waiting for transaction confirmation`);
191
+ return {
192
+ confirmed: false,
193
+ error: `Transaction not confirmed within ${maxWaitTime / 1000} seconds`,
194
+ };
195
+ } finally {
196
+ clearInterval(textUpdateInterval);
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Sleep for a given number of milliseconds
202
+ */
203
+ function sleep(ms: number): Promise<void> {
204
+ return new Promise((resolve) => setTimeout(resolve, ms));
205
+ }
package/src/lib/wallet.ts CHANGED
@@ -6,12 +6,13 @@
6
6
  * @module lib/wallet
7
7
  */
8
8
 
9
- import { Network, networks } from '@btc-vision/bitcoin';
10
- import { MLDSASecurityLevel, QuantumBIP32Factory } from '@btc-vision/bip32';
11
- import { EcKeyPair, MessageSigner, Mnemonic, Wallet } from '@btc-vision/transaction';
9
+ import { Network, networks, Signer } from '@btc-vision/bitcoin';
10
+ import { MLDSASecurityLevel, QuantumBIP32Factory, QuantumBIP32Interface } from '@btc-vision/bip32';
11
+ import { Address, EcKeyPair, MessageSigner, Mnemonic, Wallet } from '@btc-vision/transaction';
12
12
  import * as crypto from 'crypto';
13
13
  import { CLICredentials, CLIMldsaLevel, NetworkName } from '../types/index.js';
14
14
  import { canSign, loadCredentials } from './credentials.js';
15
+ import { ECPairInterface } from 'ecpair';
15
16
 
16
17
  /**
17
18
  * Convert CLI network name to bitcoin-js Network object
@@ -55,6 +56,10 @@ export class CLIWallet {
55
56
  this.mldsaLevel = mldsaLevel;
56
57
  }
57
58
 
59
+ get address(): Address {
60
+ return this.wallet.address;
61
+ }
62
+
58
63
  /**
59
64
  * Get the P2TR (taproot) address
60
65
  */
@@ -62,24 +67,17 @@ export class CLIWallet {
62
67
  return this.wallet.p2tr;
63
68
  }
64
69
 
65
- /**
66
- * Get the underlying Wallet instance
67
- */
68
- get underlyingWallet(): Wallet {
69
- return this.wallet;
70
- }
71
-
72
70
  /**
73
71
  * Get the Bitcoin keypair for classical signing
74
72
  */
75
- get keypair(): EcKeyPair {
73
+ get keypair(): Signer | ECPairInterface | null {
76
74
  return this.wallet.keypair;
77
75
  }
78
76
 
79
77
  /**
80
78
  * Get the MLDSA keypair for quantum-resistant signing
81
79
  */
82
- get mldsaKeypair(): EcKeyPair {
80
+ get mldsaKeypair(): QuantumBIP32Interface {
83
81
  return this.wallet.mldsaKeypair;
84
82
  }
85
83
 
@@ -105,13 +103,6 @@ export class CLIWallet {
105
103
  return this.mldsaLevel;
106
104
  }
107
105
 
108
- /**
109
- * Get the network this wallet is configured for
110
- */
111
- get walletNetwork(): Network {
112
- return this.network;
113
- }
114
-
115
106
  /**
116
107
  * Create a wallet from credentials
117
108
  *
@@ -32,8 +32,10 @@ export interface CLIConfig {
32
32
  ipfsGateways: string[];
33
33
  /** IPFS pinning service endpoint */
34
34
  ipfsPinningEndpoint: string;
35
- /** IPFS pinning API key */
35
+ /** IPFS pinning API key (or JWT token) */
36
36
  ipfsPinningApiKey: string;
37
+ /** IPFS pinning API secret (for services requiring key+secret) */
38
+ ipfsPinningSecret: string;
37
39
  /** Authorization header name for pinning service */
38
40
  ipfsPinningAuthHeader: string;
39
41
  /** Registry contract addresses per network */