xyqon 0.3.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 +18 -6
- package/src/index.js +120 -20
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
|
@@ -42,7 +42,7 @@ ${color('bold', 'Usage')}
|
|
|
42
42
|
xyqon wallet new --name <NAME> [--out <FILE>]
|
|
43
43
|
xyqon wallet show [--wallet <FILE>] [--private]
|
|
44
44
|
xyqon balance [--wallet <FILE>] [--address <PUBLIC_KEY>]
|
|
45
|
-
xyqon send --to <PUBLIC_KEY> --amount <AMOUNT> [--wallet <FILE>]
|
|
45
|
+
xyqon send --to <PUBLIC_KEY> --amount <AMOUNT> [--fee <AMOUNT>] [--wallet <FILE>]
|
|
46
46
|
xyqon coin create --symbol <SYMBOL> --name <NAME> --supply <AMOUNT> [--wallet <FILE>]
|
|
47
47
|
xyqon coin send --symbol <SYMBOL> --to <PUBLIC_KEY> --amount <AMOUNT> [--wallet <FILE>]
|
|
48
48
|
xyqon nft mint --collection <SYMBOL> --token-id <ID> --name <NAME> [--image-url <URL>] [--wallet <FILE>]
|
|
@@ -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) {
|
|
@@ -152,15 +162,17 @@ async function run() {
|
|
|
152
162
|
wallet,
|
|
153
163
|
recipient: options.to,
|
|
154
164
|
amount: options.amount,
|
|
165
|
+
fee: options.fee,
|
|
155
166
|
peers: peersFromOptions(options)
|
|
156
167
|
});
|
|
157
168
|
|
|
158
169
|
printHero('Transaction Sent');
|
|
159
170
|
console.log(`${color('bold', 'Amount:')} ${result.transaction.amount} XYQON`);
|
|
171
|
+
console.log(`${color('bold', 'Fee:')} ${result.transaction.fee} XYQON`);
|
|
160
172
|
console.log(`${color('bold', 'To:')} ${result.transaction.recipient}`);
|
|
161
173
|
console.log(`${color('bold', 'Transaction ID:')} ${result.transactionId}`);
|
|
162
174
|
console.log(`${color('dim', 'Checked balance:')} ${result.preflight.balance} XYQON at block ${result.preflight.blockHeight}`);
|
|
163
|
-
|
|
175
|
+
printDelivery(result);
|
|
164
176
|
console.log(color('yellow', 'The receiver balance updates after a miner includes this transaction in a block.'));
|
|
165
177
|
return;
|
|
166
178
|
}
|
|
@@ -189,7 +201,7 @@ async function run() {
|
|
|
189
201
|
console.log(`${color('bold', 'Symbol:')} ${result.transaction.asset_operation.CreateCoin.symbol}`);
|
|
190
202
|
console.log(`${color('bold', 'Supply:')} ${result.transaction.asset_operation.CreateCoin.supply}`);
|
|
191
203
|
console.log(`${color('bold', 'Transaction ID:')} ${result.transactionId}`);
|
|
192
|
-
|
|
204
|
+
printDelivery(result, 'Broadcast');
|
|
193
205
|
console.log(color('yellow', 'The coin exists after a miner includes this transaction in a block.'));
|
|
194
206
|
return;
|
|
195
207
|
}
|
|
@@ -219,7 +231,7 @@ async function run() {
|
|
|
219
231
|
console.log(`${color('bold', 'To:')} ${result.transaction.recipient}`);
|
|
220
232
|
console.log(`${color('bold', 'Transaction ID:')} ${result.transactionId}`);
|
|
221
233
|
console.log(`${color('dim', 'Checked balance:')} ${result.preflight.balance} ${result.preflight.symbol} at block ${result.preflight.blockHeight}`);
|
|
222
|
-
|
|
234
|
+
printDelivery(result);
|
|
223
235
|
return;
|
|
224
236
|
}
|
|
225
237
|
|
|
@@ -247,7 +259,7 @@ async function run() {
|
|
|
247
259
|
printHero('NFT Minted');
|
|
248
260
|
console.log(`${color('bold', 'NFT:')} ${result.transaction.asset_operation.MintNft.collection}:${result.transaction.asset_operation.MintNft.token_id}`);
|
|
249
261
|
console.log(`${color('bold', 'Transaction ID:')} ${result.transactionId}`);
|
|
250
|
-
|
|
262
|
+
printDelivery(result, 'Broadcast');
|
|
251
263
|
console.log(color('yellow', 'The NFT exists after a miner includes this transaction in a block.'));
|
|
252
264
|
return;
|
|
253
265
|
}
|
|
@@ -276,7 +288,7 @@ async function run() {
|
|
|
276
288
|
console.log(`${color('bold', 'NFT:')} ${result.transaction.asset_operation.TransferNft.collection}:${result.transaction.asset_operation.TransferNft.token_id}`);
|
|
277
289
|
console.log(`${color('bold', 'To:')} ${result.transaction.recipient}`);
|
|
278
290
|
console.log(`${color('bold', 'Transaction ID:')} ${result.transactionId}`);
|
|
279
|
-
|
|
291
|
+
printDelivery(result, 'Broadcast');
|
|
280
292
|
return;
|
|
281
293
|
}
|
|
282
294
|
|
package/src/index.js
CHANGED
|
@@ -9,6 +9,7 @@ export const DEFAULT_PEERS = [
|
|
|
9
9
|
'147.182.138.183:7101'
|
|
10
10
|
];
|
|
11
11
|
const XYQON_EPSILON = 0.00000001;
|
|
12
|
+
export const DEFAULT_XYQON_TRANSACTION_FEE = 0.001;
|
|
12
13
|
|
|
13
14
|
export const DEFAULT_WALLET_PATH = 'xyqon.wallet.json';
|
|
14
15
|
|
|
@@ -103,8 +104,11 @@ function stringifyAssetOperationForSigning(assetOperation) {
|
|
|
103
104
|
throw new Error('unknown asset operation');
|
|
104
105
|
}
|
|
105
106
|
|
|
106
|
-
export function transactionPayload(sender, recipient, amount, senderPublicKey, assetOperation = null) {
|
|
107
|
-
|
|
107
|
+
export function transactionPayload(sender, recipient, amount, senderPublicKey, assetOperation = null, fee = 0) {
|
|
108
|
+
let base = `${sender}|${recipient}|${Number(amount).toFixed(8)}|${senderPublicKey}`;
|
|
109
|
+
if (Math.abs(Number(fee)) > XYQON_EPSILON) {
|
|
110
|
+
base = `${base}|fee:${Number(fee).toFixed(8)}`;
|
|
111
|
+
}
|
|
108
112
|
const serializedAsset = stringifyAssetOperationForSigning(assetOperation);
|
|
109
113
|
return serializedAsset ? `${base}|${serializedAsset}` : base;
|
|
110
114
|
}
|
|
@@ -174,22 +178,27 @@ export function normalizeTokenAmount(amount, label = 'amount') {
|
|
|
174
178
|
return numeric;
|
|
175
179
|
}
|
|
176
180
|
|
|
177
|
-
export function createSignedTransaction(wallet, recipient, amount) {
|
|
181
|
+
export function createSignedTransaction(wallet, recipient, amount, fee = DEFAULT_XYQON_TRANSACTION_FEE) {
|
|
178
182
|
const numericAmount = Number(amount);
|
|
179
183
|
if (!Number.isFinite(numericAmount) || numericAmount <= 0) {
|
|
180
184
|
throw new Error('amount must be a positive number');
|
|
181
185
|
}
|
|
186
|
+
const numericFee = Number(fee);
|
|
187
|
+
if (!Number.isFinite(numericFee) || numericFee < 0) {
|
|
188
|
+
throw new Error('fee must be a non-negative number');
|
|
189
|
+
}
|
|
182
190
|
|
|
183
191
|
const privateKey = hexToBytes(wallet.private_key);
|
|
184
192
|
const senderPublicKey = assertWalletMatchesPrivateKey(wallet, privateKey);
|
|
185
193
|
|
|
186
|
-
const payload = transactionPayload(wallet.name, recipient, numericAmount, senderPublicKey);
|
|
194
|
+
const payload = transactionPayload(wallet.name, recipient, numericAmount, senderPublicKey, null, numericFee);
|
|
187
195
|
const signature = ed25519.sign(new TextEncoder().encode(payload), privateKey);
|
|
188
196
|
|
|
189
197
|
return {
|
|
190
198
|
sender: wallet.name,
|
|
191
199
|
recipient,
|
|
192
200
|
amount: numericAmount,
|
|
201
|
+
fee: numericFee,
|
|
193
202
|
sender_public_key: senderPublicKey,
|
|
194
203
|
signature: bytesToHex(signature)
|
|
195
204
|
};
|
|
@@ -325,6 +334,62 @@ export function sendNetworkMessage(peer, message, timeoutMs = 5000) {
|
|
|
325
334
|
});
|
|
326
335
|
}
|
|
327
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
|
+
|
|
328
393
|
export async function requestChain(peer) {
|
|
329
394
|
const result = await requestMessage(peer, 'RequestChain', 'ChainResponse');
|
|
330
395
|
return { ...result, chain: result.payload };
|
|
@@ -338,6 +403,16 @@ export async function requestPeers(peer) {
|
|
|
338
403
|
};
|
|
339
404
|
}
|
|
340
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
|
+
|
|
341
416
|
export async function discoverPeers(seedPeers = DEFAULT_PEERS) {
|
|
342
417
|
const discovered = new Set(normalizePeers(seedPeers));
|
|
343
418
|
let frontier = [...discovered];
|
|
@@ -402,7 +477,7 @@ export function calculateBalances(chain) {
|
|
|
402
477
|
if (transaction.asset_operation) {
|
|
403
478
|
continue;
|
|
404
479
|
}
|
|
405
|
-
debit(transaction.sender_public_key, transaction.amount);
|
|
480
|
+
debit(transaction.sender_public_key, transaction.amount + (transaction.fee ?? 0));
|
|
406
481
|
credit(transaction.recipient, transaction.amount);
|
|
407
482
|
}
|
|
408
483
|
}
|
|
@@ -525,20 +600,24 @@ export async function getBalance(addressOrWallet, seedPeers = DEFAULT_PEERS) {
|
|
|
525
600
|
};
|
|
526
601
|
}
|
|
527
602
|
|
|
528
|
-
export async function assertSpendableXyqonBalance(wallet, amount, seedPeers = DEFAULT_PEERS) {
|
|
603
|
+
export async function assertSpendableXyqonBalance(wallet, amount, seedPeers = DEFAULT_PEERS, fee = 0) {
|
|
529
604
|
const numericAmount = Number(amount);
|
|
605
|
+
const numericFee = Number(fee);
|
|
530
606
|
const { chain, sourcePeer, peers } = await getBestChain(seedPeers);
|
|
531
607
|
const balances = calculateBalances(chain);
|
|
532
608
|
const balance = balances.get(wallet.public_key) ?? 0;
|
|
609
|
+
const totalDebit = numericAmount + numericFee;
|
|
533
610
|
|
|
534
|
-
if (balance + XYQON_EPSILON <
|
|
611
|
+
if (balance + XYQON_EPSILON < totalDebit) {
|
|
535
612
|
throw new Error(
|
|
536
|
-
`insufficient confirmed XYQON balance; balance is ${balance}, attempted to spend ${numericAmount}`
|
|
613
|
+
`insufficient confirmed XYQON balance; balance is ${balance}, attempted to spend ${numericAmount} plus fee ${numericFee}`
|
|
537
614
|
);
|
|
538
615
|
}
|
|
539
616
|
|
|
540
617
|
return {
|
|
541
618
|
balance,
|
|
619
|
+
fee: numericFee,
|
|
620
|
+
totalDebit,
|
|
542
621
|
blockHeight: chain.chain.at(-1)?.index ?? 0,
|
|
543
622
|
sourcePeer,
|
|
544
623
|
peers
|
|
@@ -613,32 +692,50 @@ export async function getCoinHoldings(addressOrWallet, seedPeers = DEFAULT_PEERS
|
|
|
613
692
|
|
|
614
693
|
export async function broadcastTransaction(transaction, seedPeers = DEFAULT_PEERS) {
|
|
615
694
|
const peers = await discoverPeers(seedPeers);
|
|
616
|
-
const
|
|
617
|
-
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');
|
|
618
699
|
|
|
619
700
|
return {
|
|
620
|
-
transactionId:
|
|
701
|
+
transactionId: id,
|
|
621
702
|
peers,
|
|
622
703
|
results,
|
|
623
|
-
acceptedBy:
|
|
704
|
+
acceptedBy: verifiedResults.length,
|
|
705
|
+
verifiedBy: verifiedResults.length,
|
|
706
|
+
rejectedBy: rejectedResults.length,
|
|
707
|
+
unverifiedBy: results.filter((result) => result.submitted && !result.verified).length
|
|
624
708
|
};
|
|
625
709
|
}
|
|
626
710
|
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
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
|
+
|
|
722
|
+
export async function sendTransaction({ wallet, recipient, amount, fee = DEFAULT_XYQON_TRANSACTION_FEE, peers = DEFAULT_PEERS }) {
|
|
723
|
+
const transaction = createSignedTransaction(wallet, recipient, amount, fee);
|
|
724
|
+
const preflight = await assertSpendableXyqonBalance(wallet, transaction.amount, peers, transaction.fee);
|
|
725
|
+
const broadcast = assertBroadcastVerified(await broadcastTransaction(transaction, peers));
|
|
630
726
|
return {
|
|
631
727
|
transaction,
|
|
632
728
|
preflight,
|
|
633
|
-
...
|
|
729
|
+
...broadcast
|
|
634
730
|
};
|
|
635
731
|
}
|
|
636
732
|
|
|
637
733
|
export async function createCoin({ wallet, symbol, name, supply, peers = DEFAULT_PEERS }) {
|
|
638
734
|
const transaction = createCoinTransaction(wallet, { symbol, name, supply });
|
|
735
|
+
const broadcast = assertBroadcastVerified(await broadcastTransaction(transaction, peers));
|
|
639
736
|
return {
|
|
640
737
|
transaction,
|
|
641
|
-
...
|
|
738
|
+
...broadcast
|
|
642
739
|
};
|
|
643
740
|
}
|
|
644
741
|
|
|
@@ -650,25 +747,28 @@ export async function sendCoin({ wallet, recipient, symbol, amount, peers = DEFA
|
|
|
650
747
|
transaction.asset_operation.TransferCoin.amount,
|
|
651
748
|
peers
|
|
652
749
|
);
|
|
750
|
+
const broadcast = assertBroadcastVerified(await broadcastTransaction(transaction, peers));
|
|
653
751
|
return {
|
|
654
752
|
transaction,
|
|
655
753
|
preflight,
|
|
656
|
-
...
|
|
754
|
+
...broadcast
|
|
657
755
|
};
|
|
658
756
|
}
|
|
659
757
|
|
|
660
758
|
export async function mintNft({ wallet, collection, tokenId, name, imageUrl = null, peers = DEFAULT_PEERS }) {
|
|
661
759
|
const transaction = createNftMintTransaction(wallet, { collection, tokenId, name, imageUrl });
|
|
760
|
+
const broadcast = assertBroadcastVerified(await broadcastTransaction(transaction, peers));
|
|
662
761
|
return {
|
|
663
762
|
transaction,
|
|
664
|
-
...
|
|
763
|
+
...broadcast
|
|
665
764
|
};
|
|
666
765
|
}
|
|
667
766
|
|
|
668
767
|
export async function transferNft({ wallet, recipient, collection, tokenId, peers = DEFAULT_PEERS }) {
|
|
669
768
|
const transaction = createNftTransferTransaction(wallet, { recipient, collection, tokenId });
|
|
769
|
+
const broadcast = assertBroadcastVerified(await broadcastTransaction(transaction, peers));
|
|
670
770
|
return {
|
|
671
771
|
transaction,
|
|
672
|
-
...
|
|
772
|
+
...broadcast
|
|
673
773
|
};
|
|
674
774
|
}
|