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 +27 -1
- package/package.json +1 -1
- package/src/cli.js +26 -5
- package/src/index.js +96 -9
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
|
|
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
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
630
|
-
const results = await Promise.all(peers.map((peer) =>
|
|
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:
|
|
701
|
+
transactionId: id,
|
|
634
702
|
peers,
|
|
635
703
|
results,
|
|
636
|
-
acceptedBy:
|
|
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
|
-
...
|
|
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
|
-
...
|
|
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
|
-
...
|
|
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
|
-
...
|
|
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
|
-
...
|
|
772
|
+
...broadcast
|
|
686
773
|
};
|
|
687
774
|
}
|