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 +2 -1
- package/package.json +1 -1
- package/src/cli.js +15 -5
- package/src/index.js +96 -9
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
|
|
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
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
}
|