xyqon 0.2.0 → 0.4.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
@@ -67,6 +67,7 @@ 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
71
 
71
72
  ## Create And Send Coins
72
73
 
@@ -83,6 +84,7 @@ xyqon coin send \
83
84
  ```
84
85
 
85
86
  Coin creation and coin sends are signed 0 XYQON transactions. A miner must include them before the new coin or transfer appears on-chain.
87
+ Before broadcasting a coin transfer, the client checks the best reachable public chain and refuses sends that exceed the wallet's confirmed coin balance.
86
88
 
87
89
  ## Mint And Send NFTs
88
90
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xyqon",
3
- "version": "0.2.0",
3
+ "version": "0.4.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
@@ -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>]
@@ -152,14 +152,17 @@ async function run() {
152
152
  wallet,
153
153
  recipient: options.to,
154
154
  amount: options.amount,
155
+ fee: options.fee,
155
156
  peers: peersFromOptions(options)
156
157
  });
157
158
 
158
159
  printHero('Transaction Sent');
159
160
  console.log(`${color('bold', 'Amount:')} ${result.transaction.amount} XYQON`);
161
+ console.log(`${color('bold', 'Fee:')} ${result.transaction.fee} XYQON`);
160
162
  console.log(`${color('bold', 'To:')} ${result.transaction.recipient}`);
161
163
  console.log(`${color('bold', 'Transaction ID:')} ${result.transactionId}`);
162
- console.log(`${color('green', 'Broadcast:')} ${result.acceptedBy}/${result.peers.length} reachable nodes`);
164
+ 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`);
163
166
  console.log(color('yellow', 'The receiver balance updates after a miner includes this transaction in a block.'));
164
167
  return;
165
168
  }
@@ -217,7 +220,8 @@ async function run() {
217
220
  console.log(`${color('bold', 'Amount:')} ${result.transaction.asset_operation.TransferCoin.amount} ${result.transaction.asset_operation.TransferCoin.symbol}`);
218
221
  console.log(`${color('bold', 'To:')} ${result.transaction.recipient}`);
219
222
  console.log(`${color('bold', 'Transaction ID:')} ${result.transactionId}`);
220
- console.log(`${color('green', 'Broadcast:')} ${result.acceptedBy}/${result.peers.length} reachable nodes`);
223
+ 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`);
221
225
  return;
222
226
  }
223
227
 
package/src/index.js CHANGED
@@ -8,6 +8,8 @@ export const DEFAULT_PEERS = [
8
8
  '143.244.149.8:7101',
9
9
  '147.182.138.183:7101'
10
10
  ];
11
+ const XYQON_EPSILON = 0.00000001;
12
+ export const DEFAULT_XYQON_TRANSACTION_FEE = 0.001;
11
13
 
12
14
  export const DEFAULT_WALLET_PATH = 'xyqon.wallet.json';
13
15
 
@@ -102,8 +104,11 @@ function stringifyAssetOperationForSigning(assetOperation) {
102
104
  throw new Error('unknown asset operation');
103
105
  }
104
106
 
105
- export function transactionPayload(sender, recipient, amount, senderPublicKey, assetOperation = null) {
106
- const base = `${sender}|${recipient}|${Number(amount).toFixed(8)}|${senderPublicKey}`;
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
+ }
107
112
  const serializedAsset = stringifyAssetOperationForSigning(assetOperation);
108
113
  return serializedAsset ? `${base}|${serializedAsset}` : base;
109
114
  }
@@ -173,22 +178,27 @@ export function normalizeTokenAmount(amount, label = 'amount') {
173
178
  return numeric;
174
179
  }
175
180
 
176
- export function createSignedTransaction(wallet, recipient, amount) {
181
+ export function createSignedTransaction(wallet, recipient, amount, fee = DEFAULT_XYQON_TRANSACTION_FEE) {
177
182
  const numericAmount = Number(amount);
178
183
  if (!Number.isFinite(numericAmount) || numericAmount <= 0) {
179
184
  throw new Error('amount must be a positive number');
180
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
+ }
181
190
 
182
191
  const privateKey = hexToBytes(wallet.private_key);
183
192
  const senderPublicKey = assertWalletMatchesPrivateKey(wallet, privateKey);
184
193
 
185
- const payload = transactionPayload(wallet.name, recipient, numericAmount, senderPublicKey);
194
+ const payload = transactionPayload(wallet.name, recipient, numericAmount, senderPublicKey, null, numericFee);
186
195
  const signature = ed25519.sign(new TextEncoder().encode(payload), privateKey);
187
196
 
188
197
  return {
189
198
  sender: wallet.name,
190
199
  recipient,
191
200
  amount: numericAmount,
201
+ fee: numericFee,
192
202
  sender_public_key: senderPublicKey,
193
203
  signature: bytesToHex(signature)
194
204
  };
@@ -401,7 +411,7 @@ export function calculateBalances(chain) {
401
411
  if (transaction.asset_operation) {
402
412
  continue;
403
413
  }
404
- debit(transaction.sender_public_key, transaction.amount);
414
+ debit(transaction.sender_public_key, transaction.amount + (transaction.fee ?? 0));
405
415
  credit(transaction.recipient, transaction.amount);
406
416
  }
407
417
  }
@@ -524,6 +534,52 @@ export async function getBalance(addressOrWallet, seedPeers = DEFAULT_PEERS) {
524
534
  };
525
535
  }
526
536
 
537
+ export async function assertSpendableXyqonBalance(wallet, amount, seedPeers = DEFAULT_PEERS, fee = 0) {
538
+ const numericAmount = Number(amount);
539
+ const numericFee = Number(fee);
540
+ const { chain, sourcePeer, peers } = await getBestChain(seedPeers);
541
+ const balances = calculateBalances(chain);
542
+ const balance = balances.get(wallet.public_key) ?? 0;
543
+ const totalDebit = numericAmount + numericFee;
544
+
545
+ if (balance + XYQON_EPSILON < totalDebit) {
546
+ throw new Error(
547
+ `insufficient confirmed XYQON balance; balance is ${balance}, attempted to spend ${numericAmount} plus fee ${numericFee}`
548
+ );
549
+ }
550
+
551
+ return {
552
+ balance,
553
+ fee: numericFee,
554
+ totalDebit,
555
+ blockHeight: chain.chain.at(-1)?.index ?? 0,
556
+ sourcePeer,
557
+ peers
558
+ };
559
+ }
560
+
561
+ export async function assertSpendableCoinBalance(wallet, symbol, amount, seedPeers = DEFAULT_PEERS) {
562
+ const normalizedSymbol = normalizeCoinSymbol(symbol);
563
+ const numericAmount = Number(amount);
564
+ const { chain, sourcePeer, peers } = await getBestChain(seedPeers);
565
+ const holding = listCoinHoldings(chain, wallet).find((coin) => coin.symbol === normalizedSymbol);
566
+ const balance = holding?.balance ?? 0;
567
+
568
+ if (balance + XYQON_EPSILON < numericAmount) {
569
+ throw new Error(
570
+ `insufficient confirmed ${normalizedSymbol} balance; balance is ${balance}, attempted to spend ${numericAmount}`
571
+ );
572
+ }
573
+
574
+ return {
575
+ symbol: normalizedSymbol,
576
+ balance,
577
+ blockHeight: chain.chain.at(-1)?.index ?? 0,
578
+ sourcePeer,
579
+ peers
580
+ };
581
+ }
582
+
527
583
  export async function getCreatedCoins(addressOrWallet, seedPeers = DEFAULT_PEERS) {
528
584
  const { chain, sourcePeer, peers } = await getBestChain(seedPeers);
529
585
  return {
@@ -581,10 +637,12 @@ export async function broadcastTransaction(transaction, seedPeers = DEFAULT_PEER
581
637
  };
582
638
  }
583
639
 
584
- export async function sendTransaction({ wallet, recipient, amount, peers = DEFAULT_PEERS }) {
585
- const transaction = createSignedTransaction(wallet, recipient, amount);
640
+ export async function sendTransaction({ wallet, recipient, amount, fee = DEFAULT_XYQON_TRANSACTION_FEE, peers = DEFAULT_PEERS }) {
641
+ const transaction = createSignedTransaction(wallet, recipient, amount, fee);
642
+ const preflight = await assertSpendableXyqonBalance(wallet, transaction.amount, peers, transaction.fee);
586
643
  return {
587
644
  transaction,
645
+ preflight,
588
646
  ...(await broadcastTransaction(transaction, peers))
589
647
  };
590
648
  }
@@ -599,8 +657,15 @@ export async function createCoin({ wallet, symbol, name, supply, peers = DEFAULT
599
657
 
600
658
  export async function sendCoin({ wallet, recipient, symbol, amount, peers = DEFAULT_PEERS }) {
601
659
  const transaction = createCoinTransferTransaction(wallet, { recipient, symbol, amount });
660
+ const preflight = await assertSpendableCoinBalance(
661
+ wallet,
662
+ transaction.asset_operation.TransferCoin.symbol,
663
+ transaction.asset_operation.TransferCoin.amount,
664
+ peers
665
+ );
602
666
  return {
603
667
  transaction,
668
+ preflight,
604
669
  ...(await broadcastTransaction(transaction, peers))
605
670
  };
606
671
  }