xyqon 0.4.0 → 0.5.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/README.md CHANGED
@@ -20,12 +20,30 @@ You do not need to mine and you do not need to run a full node.
20
20
  npm install -g xyqon
21
21
  ```
22
22
 
23
+ Update an existing global install:
24
+
25
+ ```bash
26
+ npm update -g xyqon
27
+ ```
28
+
29
+ Or install this exact release:
30
+
31
+ ```bash
32
+ npm install -g xyqon@0.5.0
33
+ ```
34
+
23
35
  Or run without installing:
24
36
 
25
37
  ```bash
26
38
  npx xyqon help
27
39
  ```
28
40
 
41
+ Run this exact release without installing:
42
+
43
+ ```bash
44
+ npx xyqon@0.5.0 help
45
+ ```
46
+
29
47
  ## Create A Wallet
30
48
 
31
49
  ```bash
@@ -67,7 +85,8 @@ xyqon send \
67
85
  ```
68
86
 
69
87
  The transaction is broadcast to reachable public nodes. A miner must include it in a block before the receiver sees the confirmed balance.
70
- The client checks the best reachable public chain before broadcasting and refuses sends that exceed the wallet's confirmed XYQON balance.
88
+ The client checks the best reachable public chain before broadcasting, refuses sends that exceed the wallet's confirmed XYQON balance, and now requires at least one node to verify the transaction as pending or confirmed before returning success.
89
+ If every reachable node rejects the transaction or cannot return a transaction status, the send fails with the node's rejection reason when available.
71
90
 
72
91
  ## Create And Send Coins
73
92
 
@@ -107,6 +126,13 @@ xyqon nft send \
107
126
  xyqon nodes
108
127
  ```
109
128
 
129
+ ## Show Version
130
+
131
+ ```bash
132
+ xyqon version
133
+ xyqon -v
134
+ ```
135
+
110
136
  ## Seed Nodes
111
137
 
112
138
  The package starts with these seed nodes:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xyqon",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Friendly wallet CLI and JavaScript client for the XYQON network.",
5
5
  "keywords": [
6
6
  "xyqon",
package/src/cli.js CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { createRequire } from 'node:module';
2
3
  import {
3
4
  DEFAULT_PEERS,
4
5
  DEFAULT_WALLET_PATH,
@@ -15,6 +16,9 @@ import {
15
16
  sendTransaction
16
17
  } from './index.js';
17
18
 
19
+ const require = createRequire(import.meta.url);
20
+ const { version: VERSION } = require('../package.json');
21
+
18
22
  const colors = {
19
23
  cyan: '\x1b[36m',
20
24
  green: '\x1b[32m',
@@ -48,6 +52,8 @@ ${color('bold', 'Usage')}
48
52
  xyqon nft mint --collection <SYMBOL> --token-id <ID> --name <NAME> [--image-url <URL>] [--wallet <FILE>]
49
53
  xyqon nft send --collection <SYMBOL> --token-id <ID> --to <PUBLIC_KEY> [--wallet <FILE>]
50
54
  xyqon nodes
55
+ xyqon version
56
+ xyqon -v
51
57
 
52
58
  ${color('bold', 'Network options')}
53
59
  --peer <IP[:PORT]> Add a seed peer. Can be repeated.
@@ -58,6 +64,16 @@ ${color('bold', 'Defaults')}
58
64
  `);
59
65
  }
60
66
 
67
+ function printDelivery(result, label = 'Delivered') {
68
+ console.log(`${color('green', `${label}:`)} ${result.verifiedBy}/${result.peers.length} nodes verified pending or confirmed`);
69
+ if (result.rejectedBy > 0) {
70
+ console.log(`${color('red', 'Rejected:')} ${result.rejectedBy}/${result.peers.length} nodes`);
71
+ }
72
+ if (result.unverifiedBy > 0) {
73
+ console.log(`${color('yellow', 'Unverified:')} ${result.unverifiedBy}/${result.peers.length} nodes did not return a transaction status`);
74
+ }
75
+ }
76
+
61
77
  function parseArgs(argv) {
62
78
  const values = { _: [] };
63
79
  for (let index = 0; index < argv.length; index += 1) {
@@ -105,6 +121,11 @@ async function run() {
105
121
  return;
106
122
  }
107
123
 
124
+ if (command === 'version' || command === '--version' || command === '-v') {
125
+ console.log(VERSION);
126
+ return;
127
+ }
128
+
108
129
  if (command === 'wallet' && subcommand === 'new') {
109
130
  const path = options.out ?? DEFAULT_WALLET_PATH;
110
131
  const wallet = createWallet(options.name ?? 'XYQON User');
@@ -162,7 +183,7 @@ async function run() {
162
183
  console.log(`${color('bold', 'To:')} ${result.transaction.recipient}`);
163
184
  console.log(`${color('bold', 'Transaction ID:')} ${result.transactionId}`);
164
185
  console.log(`${color('dim', 'Checked balance:')} ${result.preflight.balance} XYQON at block ${result.preflight.blockHeight}`);
165
- console.log(`${color('green', 'Delivered:')} ${result.acceptedBy}/${result.peers.length} reachable nodes`);
186
+ printDelivery(result);
166
187
  console.log(color('yellow', 'The receiver balance updates after a miner includes this transaction in a block.'));
167
188
  return;
168
189
  }
@@ -191,7 +212,7 @@ async function run() {
191
212
  console.log(`${color('bold', 'Symbol:')} ${result.transaction.asset_operation.CreateCoin.symbol}`);
192
213
  console.log(`${color('bold', 'Supply:')} ${result.transaction.asset_operation.CreateCoin.supply}`);
193
214
  console.log(`${color('bold', 'Transaction ID:')} ${result.transactionId}`);
194
- console.log(`${color('green', 'Broadcast:')} ${result.acceptedBy}/${result.peers.length} reachable nodes`);
215
+ printDelivery(result, 'Broadcast');
195
216
  console.log(color('yellow', 'The coin exists after a miner includes this transaction in a block.'));
196
217
  return;
197
218
  }
@@ -221,7 +242,7 @@ async function run() {
221
242
  console.log(`${color('bold', 'To:')} ${result.transaction.recipient}`);
222
243
  console.log(`${color('bold', 'Transaction ID:')} ${result.transactionId}`);
223
244
  console.log(`${color('dim', 'Checked balance:')} ${result.preflight.balance} ${result.preflight.symbol} at block ${result.preflight.blockHeight}`);
224
- console.log(`${color('green', 'Delivered:')} ${result.acceptedBy}/${result.peers.length} reachable nodes`);
245
+ printDelivery(result);
225
246
  return;
226
247
  }
227
248
 
@@ -249,7 +270,7 @@ async function run() {
249
270
  printHero('NFT Minted');
250
271
  console.log(`${color('bold', 'NFT:')} ${result.transaction.asset_operation.MintNft.collection}:${result.transaction.asset_operation.MintNft.token_id}`);
251
272
  console.log(`${color('bold', 'Transaction ID:')} ${result.transactionId}`);
252
- console.log(`${color('green', 'Broadcast:')} ${result.acceptedBy}/${result.peers.length} reachable nodes`);
273
+ printDelivery(result, 'Broadcast');
253
274
  console.log(color('yellow', 'The NFT exists after a miner includes this transaction in a block.'));
254
275
  return;
255
276
  }
@@ -278,7 +299,7 @@ async function run() {
278
299
  console.log(`${color('bold', 'NFT:')} ${result.transaction.asset_operation.TransferNft.collection}:${result.transaction.asset_operation.TransferNft.token_id}`);
279
300
  console.log(`${color('bold', 'To:')} ${result.transaction.recipient}`);
280
301
  console.log(`${color('bold', 'Transaction ID:')} ${result.transactionId}`);
281
- console.log(`${color('green', 'Broadcast:')} ${result.acceptedBy}/${result.peers.length} reachable nodes`);
302
+ printDelivery(result, 'Broadcast');
282
303
  return;
283
304
  }
284
305
 
package/src/index.js CHANGED
@@ -334,6 +334,62 @@ export function sendNetworkMessage(peer, message, timeoutMs = 5000) {
334
334
  });
335
335
  }
336
336
 
337
+ export function sendTransactionMessage(peer, transaction, timeoutMs = 5000) {
338
+ return new Promise((resolve) => {
339
+ const normalizedPeer = normalizePeer(peer);
340
+ const [host, portText] = normalizedPeer.split(':');
341
+ const socket = net.createConnection({ host, port: Number(portText) });
342
+ const startedAt = Date.now();
343
+ const id = transactionId(transaction);
344
+ let data = '';
345
+ let wrote = false;
346
+ let settled = false;
347
+
348
+ const finish = (result) => {
349
+ if (settled) {
350
+ return;
351
+ }
352
+ settled = true;
353
+ socket.destroy();
354
+ resolve({ peer: normalizedPeer, latencyMs: Date.now() - startedAt, ...result });
355
+ };
356
+
357
+ socket.setTimeout(timeoutMs);
358
+ socket.on('connect', () => {
359
+ socket.write(`${JSON.stringify({ NewTransaction: transaction })}\n`, () => {
360
+ wrote = true;
361
+ });
362
+ });
363
+ socket.on('data', (chunk) => {
364
+ data += chunk.toString('utf8');
365
+ if (!data.includes('\n')) {
366
+ return;
367
+ }
368
+
369
+ try {
370
+ const parsed = JSON.parse(data.trim());
371
+ const status = parsed.TransactionResponse;
372
+ if (!status || status.id !== id) {
373
+ finish({ ok: false, submitted: wrote, error: 'peer returned an unexpected transaction response' });
374
+ return;
375
+ }
376
+
377
+ finish({
378
+ ok: status.status === 'pending' || status.status === 'confirmed',
379
+ submitted: status.status === 'pending' || status.status === 'confirmed',
380
+ verified: true,
381
+ status: status.status,
382
+ error: status.error ?? undefined
383
+ });
384
+ } catch (error) {
385
+ finish({ ok: false, submitted: wrote, error: error.message });
386
+ }
387
+ });
388
+ socket.on('timeout', () => finish({ ok: false, submitted: wrote, verified: false, error: 'timeout waiting for transaction response' }));
389
+ socket.on('error', (error) => finish({ ok: false, submitted: wrote, error: error.message }));
390
+ });
391
+ }
392
+
337
393
  export async function requestChain(peer) {
338
394
  const result = await requestMessage(peer, 'RequestChain', 'ChainResponse');
339
395
  return { ...result, chain: result.payload };
@@ -347,6 +403,16 @@ export async function requestPeers(peer) {
347
403
  };
348
404
  }
349
405
 
406
+ export async function requestTransactionStatus(peer, id) {
407
+ const result = await requestMessage(peer, { RequestTransaction: id }, 'TransactionResponse');
408
+ return {
409
+ ...result,
410
+ transaction: result.payload,
411
+ status: result.payload?.status,
412
+ verified: result.ok && (result.payload?.status === 'pending' || result.payload?.status === 'confirmed')
413
+ };
414
+ }
415
+
350
416
  export async function discoverPeers(seedPeers = DEFAULT_PEERS) {
351
417
  const discovered = new Set(normalizePeers(seedPeers));
352
418
  let frontier = [...discovered];
@@ -626,32 +692,50 @@ export async function getCoinHoldings(addressOrWallet, seedPeers = DEFAULT_PEERS
626
692
 
627
693
  export async function broadcastTransaction(transaction, seedPeers = DEFAULT_PEERS) {
628
694
  const peers = await discoverPeers(seedPeers);
629
- const message = { NewTransaction: transaction };
630
- const results = await Promise.all(peers.map((peer) => sendNetworkMessage(peer, message)));
695
+ const id = transactionId(transaction);
696
+ const results = await Promise.all(peers.map((peer) => sendTransactionMessage(peer, transaction)));
697
+ const verifiedResults = results.filter((result) => result.verified);
698
+ const rejectedResults = results.filter((result) => result.status === 'rejected');
631
699
 
632
700
  return {
633
- transactionId: transactionId(transaction),
701
+ transactionId: id,
634
702
  peers,
635
703
  results,
636
- acceptedBy: results.filter((result) => result.ok).length
704
+ acceptedBy: verifiedResults.length,
705
+ verifiedBy: verifiedResults.length,
706
+ rejectedBy: rejectedResults.length,
707
+ unverifiedBy: results.filter((result) => result.submitted && !result.verified).length
637
708
  };
638
709
  }
639
710
 
711
+ function assertBroadcastVerified(broadcast) {
712
+ if (broadcast.verifiedBy > 0) {
713
+ return broadcast;
714
+ }
715
+
716
+ const rejection = broadcast.results.find((result) => result.status === 'rejected' && result.error);
717
+ const failure = rejection ?? broadcast.results.find((result) => result.error);
718
+ const detail = failure ? ` ${failure.peer}: ${failure.error}` : '';
719
+ throw new Error(`transaction was not accepted by any reachable XYQON node.${detail}`);
720
+ }
721
+
640
722
  export async function sendTransaction({ wallet, recipient, amount, fee = DEFAULT_XYQON_TRANSACTION_FEE, peers = DEFAULT_PEERS }) {
641
723
  const transaction = createSignedTransaction(wallet, recipient, amount, fee);
642
724
  const preflight = await assertSpendableXyqonBalance(wallet, transaction.amount, peers, transaction.fee);
725
+ const broadcast = assertBroadcastVerified(await broadcastTransaction(transaction, peers));
643
726
  return {
644
727
  transaction,
645
728
  preflight,
646
- ...(await broadcastTransaction(transaction, peers))
729
+ ...broadcast
647
730
  };
648
731
  }
649
732
 
650
733
  export async function createCoin({ wallet, symbol, name, supply, peers = DEFAULT_PEERS }) {
651
734
  const transaction = createCoinTransaction(wallet, { symbol, name, supply });
735
+ const broadcast = assertBroadcastVerified(await broadcastTransaction(transaction, peers));
652
736
  return {
653
737
  transaction,
654
- ...(await broadcastTransaction(transaction, peers))
738
+ ...broadcast
655
739
  };
656
740
  }
657
741
 
@@ -663,25 +747,28 @@ export async function sendCoin({ wallet, recipient, symbol, amount, peers = DEFA
663
747
  transaction.asset_operation.TransferCoin.amount,
664
748
  peers
665
749
  );
750
+ const broadcast = assertBroadcastVerified(await broadcastTransaction(transaction, peers));
666
751
  return {
667
752
  transaction,
668
753
  preflight,
669
- ...(await broadcastTransaction(transaction, peers))
754
+ ...broadcast
670
755
  };
671
756
  }
672
757
 
673
758
  export async function mintNft({ wallet, collection, tokenId, name, imageUrl = null, peers = DEFAULT_PEERS }) {
674
759
  const transaction = createNftMintTransaction(wallet, { collection, tokenId, name, imageUrl });
760
+ const broadcast = assertBroadcastVerified(await broadcastTransaction(transaction, peers));
675
761
  return {
676
762
  transaction,
677
- ...(await broadcastTransaction(transaction, peers))
763
+ ...broadcast
678
764
  };
679
765
  }
680
766
 
681
767
  export async function transferNft({ wallet, recipient, collection, tokenId, peers = DEFAULT_PEERS }) {
682
768
  const transaction = createNftTransferTransaction(wallet, { recipient, collection, tokenId });
769
+ const broadcast = assertBroadcastVerified(await broadcastTransaction(transaction, peers));
683
770
  return {
684
771
  transaction,
685
- ...(await broadcastTransaction(transaction, peers))
772
+ ...broadcast
686
773
  };
687
774
  }