xyqon 0.4.0 → 0.4.1

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
@@ -67,7 +67,8 @@ xyqon send \
67
67
  ```
68
68
 
69
69
  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.
70
+ 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.
71
+ 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
72
 
72
73
  ## Create And Send Coins
73
74
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xyqon",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Friendly wallet CLI and JavaScript client for the XYQON network.",
5
5
  "keywords": [
6
6
  "xyqon",
package/src/cli.js CHANGED
@@ -58,6 +58,16 @@ ${color('bold', 'Defaults')}
58
58
  `);
59
59
  }
60
60
 
61
+ function printDelivery(result, label = 'Delivered') {
62
+ console.log(`${color('green', `${label}:`)} ${result.verifiedBy}/${result.peers.length} nodes verified pending or confirmed`);
63
+ if (result.rejectedBy > 0) {
64
+ console.log(`${color('red', 'Rejected:')} ${result.rejectedBy}/${result.peers.length} nodes`);
65
+ }
66
+ if (result.unverifiedBy > 0) {
67
+ console.log(`${color('yellow', 'Unverified:')} ${result.unverifiedBy}/${result.peers.length} nodes did not return a transaction status`);
68
+ }
69
+ }
70
+
61
71
  function parseArgs(argv) {
62
72
  const values = { _: [] };
63
73
  for (let index = 0; index < argv.length; index += 1) {
@@ -162,7 +172,7 @@ async function run() {
162
172
  console.log(`${color('bold', 'To:')} ${result.transaction.recipient}`);
163
173
  console.log(`${color('bold', 'Transaction ID:')} ${result.transactionId}`);
164
174
  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`);
175
+ printDelivery(result);
166
176
  console.log(color('yellow', 'The receiver balance updates after a miner includes this transaction in a block.'));
167
177
  return;
168
178
  }
@@ -191,7 +201,7 @@ async function run() {
191
201
  console.log(`${color('bold', 'Symbol:')} ${result.transaction.asset_operation.CreateCoin.symbol}`);
192
202
  console.log(`${color('bold', 'Supply:')} ${result.transaction.asset_operation.CreateCoin.supply}`);
193
203
  console.log(`${color('bold', 'Transaction ID:')} ${result.transactionId}`);
194
- console.log(`${color('green', 'Broadcast:')} ${result.acceptedBy}/${result.peers.length} reachable nodes`);
204
+ printDelivery(result, 'Broadcast');
195
205
  console.log(color('yellow', 'The coin exists after a miner includes this transaction in a block.'));
196
206
  return;
197
207
  }
@@ -221,7 +231,7 @@ async function run() {
221
231
  console.log(`${color('bold', 'To:')} ${result.transaction.recipient}`);
222
232
  console.log(`${color('bold', 'Transaction ID:')} ${result.transactionId}`);
223
233
  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`);
234
+ printDelivery(result);
225
235
  return;
226
236
  }
227
237
 
@@ -249,7 +259,7 @@ async function run() {
249
259
  printHero('NFT Minted');
250
260
  console.log(`${color('bold', 'NFT:')} ${result.transaction.asset_operation.MintNft.collection}:${result.transaction.asset_operation.MintNft.token_id}`);
251
261
  console.log(`${color('bold', 'Transaction ID:')} ${result.transactionId}`);
252
- console.log(`${color('green', 'Broadcast:')} ${result.acceptedBy}/${result.peers.length} reachable nodes`);
262
+ printDelivery(result, 'Broadcast');
253
263
  console.log(color('yellow', 'The NFT exists after a miner includes this transaction in a block.'));
254
264
  return;
255
265
  }
@@ -278,7 +288,7 @@ async function run() {
278
288
  console.log(`${color('bold', 'NFT:')} ${result.transaction.asset_operation.TransferNft.collection}:${result.transaction.asset_operation.TransferNft.token_id}`);
279
289
  console.log(`${color('bold', 'To:')} ${result.transaction.recipient}`);
280
290
  console.log(`${color('bold', 'Transaction ID:')} ${result.transactionId}`);
281
- console.log(`${color('green', 'Broadcast:')} ${result.acceptedBy}/${result.peers.length} reachable nodes`);
291
+ printDelivery(result, 'Broadcast');
282
292
  return;
283
293
  }
284
294
 
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
  }