xyqon 0.1.0 → 0.3.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
@@ -8,6 +8,8 @@ It can:
8
8
  - Show your public key
9
9
  - Check your balance from live public nodes
10
10
  - Send a signed transaction to the network
11
+ - Create and send coins on the XYQON network
12
+ - Mint and transfer NFTs
11
13
  - Show currently reachable nodes
12
14
 
13
15
  You do not need to mine and you do not need to run a full node.
@@ -15,13 +17,13 @@ You do not need to mine and you do not need to run a full node.
15
17
  ## Install
16
18
 
17
19
  ```bash
18
- npm install -g xyqon-client
20
+ npm install -g xyqon
19
21
  ```
20
22
 
21
23
  Or run without installing:
22
24
 
23
25
  ```bash
24
- npx xyqon-client help
26
+ npx xyqon help
25
27
  ```
26
28
 
27
29
  ## Create A Wallet
@@ -65,6 +67,39 @@ xyqon send \
65
67
  ```
66
68
 
67
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.
71
+
72
+ ## Create And Send Coins
73
+
74
+ ```bash
75
+ xyqon coin create \
76
+ --symbol GAME \
77
+ --name "Game Coin" \
78
+ --supply 1000000
79
+
80
+ xyqon coin send \
81
+ --symbol GAME \
82
+ --to RECIPIENT_PUBLIC_KEY \
83
+ --amount 25
84
+ ```
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.
88
+
89
+ ## Mint And Send NFTs
90
+
91
+ ```bash
92
+ xyqon nft mint \
93
+ --collection ITEMS \
94
+ --token-id sword-001 \
95
+ --name "Iron Sword" \
96
+ --image-url https://example.com/iron-sword.png
97
+
98
+ xyqon nft send \
99
+ --collection ITEMS \
100
+ --token-id sword-001 \
101
+ --to RECIPIENT_PUBLIC_KEY
102
+ ```
68
103
 
69
104
  ## Show Nodes
70
105
 
@@ -95,8 +130,16 @@ import {
95
130
  createWallet,
96
131
  loadWallet,
97
132
  getBalance,
98
- sendTransaction
99
- } from 'xyqon-client';
133
+ sendTransaction,
134
+ createCoin,
135
+ sendCoin,
136
+ mintNft,
137
+ transferNft,
138
+ getCreatedCoins,
139
+ getCreatedNfts,
140
+ getOwnedNfts,
141
+ getCoinHoldings
142
+ } from 'xyqon';
100
143
 
101
144
  const wallet = createWallet('Cathy');
102
145
  console.log(wallet.public_key);
@@ -109,4 +152,38 @@ await sendTransaction({
109
152
  recipient: 'RECIPIENT_PUBLIC_KEY',
110
153
  amount: 1
111
154
  });
155
+
156
+ await createCoin({
157
+ wallet,
158
+ symbol: 'GAME',
159
+ name: 'Game Coin',
160
+ supply: 1000000
161
+ });
162
+
163
+ await sendCoin({
164
+ wallet,
165
+ recipient: 'RECIPIENT_PUBLIC_KEY',
166
+ symbol: 'GAME',
167
+ amount: 25
168
+ });
169
+
170
+ await mintNft({
171
+ wallet,
172
+ collection: 'ITEMS',
173
+ tokenId: 'sword-001',
174
+ name: 'Iron Sword',
175
+ imageUrl: 'https://example.com/iron-sword.png'
176
+ });
177
+
178
+ await transferNft({
179
+ wallet,
180
+ recipient: 'RECIPIENT_PUBLIC_KEY',
181
+ collection: 'ITEMS',
182
+ tokenId: 'sword-001'
183
+ });
184
+
185
+ const createdCoins = await getCreatedCoins(wallet);
186
+ const createdNfts = await getCreatedNfts(wallet);
187
+ const ownedNfts = await getOwnedNfts(wallet);
188
+ const coinHoldings = await getCoinHoldings(wallet);
112
189
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xyqon",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Friendly wallet CLI and JavaScript client for the XYQON network.",
5
5
  "keywords": [
6
6
  "xyqon",
@@ -23,7 +23,7 @@
23
23
  "exports": {
24
24
  ".": "./src/index.js"
25
25
  },
26
- "main": "index.js",
26
+ "main": "./src/index.js",
27
27
  "bin": {
28
28
  "xyqon": "src/cli.js",
29
29
  "xyqon-client": "src/cli.js"
package/src/cli.js CHANGED
@@ -3,11 +3,15 @@ import {
3
3
  DEFAULT_PEERS,
4
4
  DEFAULT_WALLET_PATH,
5
5
  createWallet,
6
+ createCoin,
6
7
  discoverPeers,
7
8
  getBalance,
8
9
  getBestChain,
9
10
  loadWallet,
11
+ mintNft,
10
12
  saveWallet,
13
+ sendCoin,
14
+ transferNft,
11
15
  sendTransaction
12
16
  } from './index.js';
13
17
 
@@ -39,6 +43,10 @@ ${color('bold', 'Usage')}
39
43
  xyqon wallet show [--wallet <FILE>] [--private]
40
44
  xyqon balance [--wallet <FILE>] [--address <PUBLIC_KEY>]
41
45
  xyqon send --to <PUBLIC_KEY> --amount <AMOUNT> [--wallet <FILE>]
46
+ xyqon coin create --symbol <SYMBOL> --name <NAME> --supply <AMOUNT> [--wallet <FILE>]
47
+ xyqon coin send --symbol <SYMBOL> --to <PUBLIC_KEY> --amount <AMOUNT> [--wallet <FILE>]
48
+ xyqon nft mint --collection <SYMBOL> --token-id <ID> --name <NAME> [--image-url <URL>] [--wallet <FILE>]
49
+ xyqon nft send --collection <SYMBOL> --token-id <ID> --to <PUBLIC_KEY> [--wallet <FILE>]
42
50
  xyqon nodes
43
51
 
44
52
  ${color('bold', 'Network options')}
@@ -88,8 +96,8 @@ function peersFromOptions(options) {
88
96
  async function run() {
89
97
  const argv = process.argv.slice(2);
90
98
  const command = argv[0];
91
- const subcommand = command === 'wallet' ? argv[1] : undefined;
92
- const rest = command === 'wallet' ? argv.slice(2) : argv.slice(1);
99
+ const subcommand = command === 'wallet' || command === 'coin' || command === 'nft' ? argv[1] : undefined;
100
+ const rest = command === 'wallet' || command === 'coin' || command === 'nft' ? argv.slice(2) : argv.slice(1);
93
101
  const options = parseArgs(rest);
94
102
 
95
103
  if (!command || command === 'help' || command === '--help' || command === '-h') {
@@ -151,11 +159,127 @@ async function run() {
151
159
  console.log(`${color('bold', 'Amount:')} ${result.transaction.amount} XYQON`);
152
160
  console.log(`${color('bold', 'To:')} ${result.transaction.recipient}`);
153
161
  console.log(`${color('bold', 'Transaction ID:')} ${result.transactionId}`);
154
- console.log(`${color('green', 'Broadcast:')} ${result.acceptedBy}/${result.peers.length} reachable nodes`);
162
+ console.log(`${color('dim', 'Checked balance:')} ${result.preflight.balance} XYQON at block ${result.preflight.blockHeight}`);
163
+ console.log(`${color('green', 'Delivered:')} ${result.acceptedBy}/${result.peers.length} reachable nodes`);
155
164
  console.log(color('yellow', 'The receiver balance updates after a miner includes this transaction in a block.'));
156
165
  return;
157
166
  }
158
167
 
168
+ if (command === 'coin' && subcommand === 'create') {
169
+ if (!options.symbol) {
170
+ throw new Error('coin create requires --symbol <SYMBOL>');
171
+ }
172
+ if (!options.name) {
173
+ throw new Error('coin create requires --name <NAME>');
174
+ }
175
+ if (!options.supply) {
176
+ throw new Error('coin create requires --supply <AMOUNT>');
177
+ }
178
+
179
+ const wallet = await loadWallet(options.wallet ?? DEFAULT_WALLET_PATH);
180
+ const result = await createCoin({
181
+ wallet,
182
+ symbol: options.symbol,
183
+ name: options.name,
184
+ supply: options.supply,
185
+ peers: peersFromOptions(options)
186
+ });
187
+
188
+ printHero('Coin Created');
189
+ console.log(`${color('bold', 'Symbol:')} ${result.transaction.asset_operation.CreateCoin.symbol}`);
190
+ console.log(`${color('bold', 'Supply:')} ${result.transaction.asset_operation.CreateCoin.supply}`);
191
+ console.log(`${color('bold', 'Transaction ID:')} ${result.transactionId}`);
192
+ console.log(`${color('green', 'Broadcast:')} ${result.acceptedBy}/${result.peers.length} reachable nodes`);
193
+ console.log(color('yellow', 'The coin exists after a miner includes this transaction in a block.'));
194
+ return;
195
+ }
196
+
197
+ if (command === 'coin' && subcommand === 'send') {
198
+ if (!options.symbol) {
199
+ throw new Error('coin send requires --symbol <SYMBOL>');
200
+ }
201
+ if (!options.to) {
202
+ throw new Error('coin send requires --to <PUBLIC_KEY>');
203
+ }
204
+ if (!options.amount) {
205
+ throw new Error('coin send requires --amount <AMOUNT>');
206
+ }
207
+
208
+ const wallet = await loadWallet(options.wallet ?? DEFAULT_WALLET_PATH);
209
+ const result = await sendCoin({
210
+ wallet,
211
+ recipient: options.to,
212
+ symbol: options.symbol,
213
+ amount: options.amount,
214
+ peers: peersFromOptions(options)
215
+ });
216
+
217
+ printHero('Coin Sent');
218
+ console.log(`${color('bold', 'Amount:')} ${result.transaction.asset_operation.TransferCoin.amount} ${result.transaction.asset_operation.TransferCoin.symbol}`);
219
+ console.log(`${color('bold', 'To:')} ${result.transaction.recipient}`);
220
+ console.log(`${color('bold', 'Transaction ID:')} ${result.transactionId}`);
221
+ console.log(`${color('dim', 'Checked balance:')} ${result.preflight.balance} ${result.preflight.symbol} at block ${result.preflight.blockHeight}`);
222
+ console.log(`${color('green', 'Delivered:')} ${result.acceptedBy}/${result.peers.length} reachable nodes`);
223
+ return;
224
+ }
225
+
226
+ if (command === 'nft' && subcommand === 'mint') {
227
+ if (!options.collection) {
228
+ throw new Error('nft mint requires --collection <SYMBOL>');
229
+ }
230
+ if (!options['token-id']) {
231
+ throw new Error('nft mint requires --token-id <ID>');
232
+ }
233
+ if (!options.name) {
234
+ throw new Error('nft mint requires --name <NAME>');
235
+ }
236
+
237
+ const wallet = await loadWallet(options.wallet ?? DEFAULT_WALLET_PATH);
238
+ const result = await mintNft({
239
+ wallet,
240
+ collection: options.collection,
241
+ tokenId: options['token-id'],
242
+ name: options.name,
243
+ imageUrl: options['image-url'],
244
+ peers: peersFromOptions(options)
245
+ });
246
+
247
+ printHero('NFT Minted');
248
+ console.log(`${color('bold', 'NFT:')} ${result.transaction.asset_operation.MintNft.collection}:${result.transaction.asset_operation.MintNft.token_id}`);
249
+ console.log(`${color('bold', 'Transaction ID:')} ${result.transactionId}`);
250
+ console.log(`${color('green', 'Broadcast:')} ${result.acceptedBy}/${result.peers.length} reachable nodes`);
251
+ console.log(color('yellow', 'The NFT exists after a miner includes this transaction in a block.'));
252
+ return;
253
+ }
254
+
255
+ if (command === 'nft' && subcommand === 'send') {
256
+ if (!options.collection) {
257
+ throw new Error('nft send requires --collection <SYMBOL>');
258
+ }
259
+ if (!options['token-id']) {
260
+ throw new Error('nft send requires --token-id <ID>');
261
+ }
262
+ if (!options.to) {
263
+ throw new Error('nft send requires --to <PUBLIC_KEY>');
264
+ }
265
+
266
+ const wallet = await loadWallet(options.wallet ?? DEFAULT_WALLET_PATH);
267
+ const result = await transferNft({
268
+ wallet,
269
+ recipient: options.to,
270
+ collection: options.collection,
271
+ tokenId: options['token-id'],
272
+ peers: peersFromOptions(options)
273
+ });
274
+
275
+ printHero('NFT Sent');
276
+ console.log(`${color('bold', 'NFT:')} ${result.transaction.asset_operation.TransferNft.collection}:${result.transaction.asset_operation.TransferNft.token_id}`);
277
+ console.log(`${color('bold', 'To:')} ${result.transaction.recipient}`);
278
+ console.log(`${color('bold', 'Transaction ID:')} ${result.transactionId}`);
279
+ console.log(`${color('green', 'Broadcast:')} ${result.acceptedBy}/${result.peers.length} reachable nodes`);
280
+ return;
281
+ }
282
+
159
283
  if (command === 'nodes') {
160
284
  const peers = await discoverPeers(peersFromOptions(options));
161
285
  const { nodes, chain, sourcePeer } = await getBestChain(peers);
package/src/index.js CHANGED
@@ -8,6 +8,7 @@ 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;
11
12
 
12
13
  export const DEFAULT_WALLET_PATH = 'xyqon.wallet.json';
13
14
 
@@ -60,8 +61,117 @@ export async function loadWallet(path = DEFAULT_WALLET_PATH) {
60
61
  return wallet;
61
62
  }
62
63
 
63
- export function transactionPayload(sender, recipient, amount, senderPublicKey) {
64
- return `${sender}|${recipient}|${Number(amount).toFixed(8)}|${senderPublicKey}`;
64
+ function rustJsonNumber(value) {
65
+ const numeric = Number(value);
66
+ if (!Number.isFinite(numeric)) {
67
+ throw new Error('amount must be a finite number');
68
+ }
69
+
70
+ return Number.isInteger(numeric) ? `${numeric}.0` : `${numeric}`;
71
+ }
72
+
73
+ function jsonString(value) {
74
+ return JSON.stringify(`${value}`);
75
+ }
76
+
77
+ function stringifyAssetOperationForSigning(assetOperation) {
78
+ if (!assetOperation) {
79
+ return null;
80
+ }
81
+
82
+ if (assetOperation.CreateCoin) {
83
+ const coin = assetOperation.CreateCoin;
84
+ return `{"CreateCoin":{"symbol":${jsonString(coin.symbol)},"name":${jsonString(coin.name)},"supply":${rustJsonNumber(coin.supply)}}}`;
85
+ }
86
+
87
+ if (assetOperation.TransferCoin) {
88
+ const transfer = assetOperation.TransferCoin;
89
+ return `{"TransferCoin":{"symbol":${jsonString(transfer.symbol)},"amount":${rustJsonNumber(transfer.amount)}}}`;
90
+ }
91
+
92
+ if (assetOperation.MintNft) {
93
+ const nft = assetOperation.MintNft;
94
+ const imageUrl = nft.image_url ? `,"image_url":${jsonString(nft.image_url)}` : '';
95
+ return `{"MintNft":{"collection":${jsonString(nft.collection)},"token_id":${jsonString(nft.token_id)},"name":${jsonString(nft.name)}${imageUrl}}}`;
96
+ }
97
+
98
+ if (assetOperation.TransferNft) {
99
+ const transfer = assetOperation.TransferNft;
100
+ return `{"TransferNft":{"collection":${jsonString(transfer.collection)},"token_id":${jsonString(transfer.token_id)}}}`;
101
+ }
102
+
103
+ throw new Error('unknown asset operation');
104
+ }
105
+
106
+ export function transactionPayload(sender, recipient, amount, senderPublicKey, assetOperation = null) {
107
+ const base = `${sender}|${recipient}|${Number(amount).toFixed(8)}|${senderPublicKey}`;
108
+ const serializedAsset = stringifyAssetOperationForSigning(assetOperation);
109
+ return serializedAsset ? `${base}|${serializedAsset}` : base;
110
+ }
111
+
112
+ function assertWalletMatchesPrivateKey(wallet, privateKey) {
113
+ const senderPublicKey = bytesToHex(ed25519.getPublicKey(privateKey));
114
+ if (senderPublicKey !== wallet.public_key) {
115
+ throw new Error('wallet public key does not match its private key');
116
+ }
117
+
118
+ return senderPublicKey;
119
+ }
120
+
121
+ function createSignedAssetTransaction(wallet, recipient, assetOperation) {
122
+ const privateKey = hexToBytes(wallet.private_key);
123
+ const senderPublicKey = assertWalletMatchesPrivateKey(wallet, privateKey);
124
+ const payload = transactionPayload(wallet.name, recipient, 0, senderPublicKey, assetOperation);
125
+ const signature = ed25519.sign(new TextEncoder().encode(payload), privateKey);
126
+
127
+ return {
128
+ sender: wallet.name,
129
+ recipient,
130
+ amount: 0,
131
+ sender_public_key: senderPublicKey,
132
+ signature: bytesToHex(signature),
133
+ asset_operation: assetOperation
134
+ };
135
+ }
136
+
137
+ export function normalizeCoinSymbol(symbol) {
138
+ const value = `${symbol}`.trim().toUpperCase();
139
+ if (!/^[A-Z0-9]{2,12}$/.test(value)) {
140
+ throw new Error('coin symbol must be 2 to 12 letters or numbers');
141
+ }
142
+
143
+ return value;
144
+ }
145
+
146
+ export function normalizeAssetName(name) {
147
+ const value = `${name}`.trim();
148
+ if (!value || value.length > 64) {
149
+ throw new Error('asset name must be 1 to 64 characters');
150
+ }
151
+
152
+ return value;
153
+ }
154
+
155
+ export function normalizeTokenId(tokenId) {
156
+ const value = `${tokenId}`.trim().toLowerCase();
157
+ if (!/^[a-z0-9_-]{1,64}$/.test(value)) {
158
+ throw new Error('NFT token id must be 1 to 64 letters, numbers, hyphens, or underscores');
159
+ }
160
+
161
+ return value;
162
+ }
163
+
164
+ export function normalizeTokenAmount(amount, label = 'amount') {
165
+ const numeric = Number(amount);
166
+ if (!Number.isFinite(numeric) || numeric <= 0) {
167
+ throw new Error(`${label} must be a positive finite number`);
168
+ }
169
+
170
+ if (Math.round(numeric * 100_000_000) / 100_000_000 !== numeric) {
171
+ throw new Error(`${label} can have at most 8 decimal places`);
172
+ }
173
+
174
+ return numeric;
65
175
  }
66
176
 
67
177
  export function createSignedTransaction(wallet, recipient, amount) {
@@ -71,10 +181,7 @@ export function createSignedTransaction(wallet, recipient, amount) {
71
181
  }
72
182
 
73
183
  const privateKey = hexToBytes(wallet.private_key);
74
- const senderPublicKey = bytesToHex(ed25519.getPublicKey(privateKey));
75
- if (senderPublicKey !== wallet.public_key) {
76
- throw new Error('wallet public key does not match its private key');
77
- }
184
+ const senderPublicKey = assertWalletMatchesPrivateKey(wallet, privateKey);
78
185
 
79
186
  const payload = transactionPayload(wallet.name, recipient, numericAmount, senderPublicKey);
80
187
  const signature = ed25519.sign(new TextEncoder().encode(payload), privateKey);
@@ -88,6 +195,68 @@ export function createSignedTransaction(wallet, recipient, amount) {
88
195
  };
89
196
  }
90
197
 
198
+ export function createCoinTransaction(wallet, { symbol, name, supply }) {
199
+ const senderPublicKey = wallet.public_key;
200
+ const assetOperation = {
201
+ CreateCoin: {
202
+ symbol: normalizeCoinSymbol(symbol),
203
+ name: normalizeAssetName(name),
204
+ supply: normalizeTokenAmount(supply, 'coin supply')
205
+ }
206
+ };
207
+
208
+ return createSignedAssetTransaction(wallet, senderPublicKey, assetOperation);
209
+ }
210
+
211
+ export function createCoinTransferTransaction(wallet, { recipient, symbol, amount }) {
212
+ if (!recipient) {
213
+ throw new Error('coin transfer requires a recipient public key');
214
+ }
215
+
216
+ const assetOperation = {
217
+ TransferCoin: {
218
+ symbol: normalizeCoinSymbol(symbol),
219
+ amount: normalizeTokenAmount(amount, 'coin transfer amount')
220
+ }
221
+ };
222
+
223
+ return createSignedAssetTransaction(wallet, recipient, assetOperation);
224
+ }
225
+
226
+ export function createNftMintTransaction(wallet, { collection, tokenId, name, imageUrl = null }) {
227
+ const senderPublicKey = wallet.public_key;
228
+ const mint = {
229
+ collection: normalizeCoinSymbol(collection),
230
+ token_id: normalizeTokenId(tokenId),
231
+ name: normalizeAssetName(name)
232
+ };
233
+
234
+ if (imageUrl) {
235
+ const value = `${imageUrl}`.trim();
236
+ if (value.length > 512 || !/^https?:\/\//i.test(value)) {
237
+ throw new Error('NFT image URL must start with http:// or https:// and be at most 512 characters');
238
+ }
239
+ mint.image_url = value;
240
+ }
241
+
242
+ return createSignedAssetTransaction(wallet, senderPublicKey, { MintNft: mint });
243
+ }
244
+
245
+ export function createNftTransferTransaction(wallet, { recipient, collection, tokenId }) {
246
+ if (!recipient) {
247
+ throw new Error('NFT transfer requires a recipient public key');
248
+ }
249
+
250
+ const assetOperation = {
251
+ TransferNft: {
252
+ collection: normalizeCoinSymbol(collection),
253
+ token_id: normalizeTokenId(tokenId)
254
+ }
255
+ };
256
+
257
+ return createSignedAssetTransaction(wallet, recipient, assetOperation);
258
+ }
259
+
91
260
  export function transactionId(transaction) {
92
261
  return createHash('sha256').update(JSON.stringify(transaction)).digest('hex');
93
262
  }
@@ -230,6 +399,9 @@ export function calculateBalances(chain) {
230
399
  }
231
400
 
232
401
  for (const transaction of transactions) {
402
+ if (transaction.asset_operation) {
403
+ continue;
404
+ }
233
405
  debit(transaction.sender_public_key, transaction.amount);
234
406
  credit(transaction.recipient, transaction.amount);
235
407
  }
@@ -238,6 +410,106 @@ export function calculateBalances(chain) {
238
410
  return balances;
239
411
  }
240
412
 
413
+ export function calculateAssets(chain) {
414
+ const coins = new Map();
415
+ const nfts = new Map();
416
+
417
+ const ensureCoin = (symbol, name = symbol, supply = 0) => {
418
+ if (!coins.has(symbol)) {
419
+ coins.set(symbol, { symbol, name, supply, holders: new Map() });
420
+ }
421
+ return coins.get(symbol);
422
+ };
423
+
424
+ const addBalance = (symbol, address, amount) => {
425
+ const coin = ensureCoin(symbol);
426
+ coin.holders.set(address, (coin.holders.get(address) ?? 0) + amount);
427
+ };
428
+
429
+ for (const block of chain.chain.slice(1)) {
430
+ for (const transaction of block.transactions.slice(1)) {
431
+ const operation = transaction.asset_operation;
432
+ if (!operation) {
433
+ continue;
434
+ }
435
+
436
+ if (operation.CreateCoin) {
437
+ const coin = operation.CreateCoin;
438
+ const symbol = normalizeCoinSymbol(coin.symbol);
439
+ const record = ensureCoin(symbol, coin.name, coin.supply);
440
+ record.creator = transaction.sender_public_key;
441
+ addBalance(symbol, transaction.sender_public_key, coin.supply);
442
+ } else if (operation.TransferCoin) {
443
+ const transfer = operation.TransferCoin;
444
+ const symbol = normalizeCoinSymbol(transfer.symbol);
445
+ addBalance(symbol, transaction.sender_public_key, -transfer.amount);
446
+ addBalance(symbol, transaction.recipient, transfer.amount);
447
+ } else if (operation.MintNft) {
448
+ const nft = operation.MintNft;
449
+ const collection = normalizeCoinSymbol(nft.collection);
450
+ const tokenId = normalizeTokenId(nft.token_id);
451
+ nfts.set(`${collection}:${tokenId}`, {
452
+ collection,
453
+ tokenId,
454
+ name: nft.name,
455
+ imageUrl: nft.image_url ?? null,
456
+ creator: transaction.sender_public_key,
457
+ owner: transaction.sender_public_key
458
+ });
459
+ } else if (operation.TransferNft) {
460
+ const transfer = operation.TransferNft;
461
+ const key = `${normalizeCoinSymbol(transfer.collection)}:${normalizeTokenId(transfer.token_id)}`;
462
+ const nft = nfts.get(key);
463
+ if (nft) {
464
+ nft.owner = transaction.recipient;
465
+ }
466
+ }
467
+ }
468
+ }
469
+
470
+ return {
471
+ coins: [...coins.values()].map((coin) => ({
472
+ ...coin,
473
+ creator: coin.creator ?? null,
474
+ holders: [...coin.holders.entries()]
475
+ .map(([address, balance]) => ({ address, balance }))
476
+ .filter((holder) => Math.abs(holder.balance) > 0.00000001)
477
+ .sort((left, right) => right.balance - left.balance)
478
+ })),
479
+ nfts: [...nfts.values()].sort((left, right) =>
480
+ `${left.collection}:${left.tokenId}`.localeCompare(`${right.collection}:${right.tokenId}`)
481
+ )
482
+ };
483
+ }
484
+
485
+ export function listCoinsCreatedBy(chain, addressOrWallet) {
486
+ const address = typeof addressOrWallet === 'string' ? addressOrWallet : addressOrWallet.public_key;
487
+ return calculateAssets(chain).coins.filter((coin) => coin.creator === address);
488
+ }
489
+
490
+ export function listNftsCreatedBy(chain, addressOrWallet) {
491
+ const address = typeof addressOrWallet === 'string' ? addressOrWallet : addressOrWallet.public_key;
492
+ return calculateAssets(chain).nfts.filter((nft) => nft.creator === address);
493
+ }
494
+
495
+ export function listNftsOwnedBy(chain, addressOrWallet) {
496
+ const address = typeof addressOrWallet === 'string' ? addressOrWallet : addressOrWallet.public_key;
497
+ return calculateAssets(chain).nfts.filter((nft) => nft.owner === address);
498
+ }
499
+
500
+ export function listCoinHoldings(chain, addressOrWallet) {
501
+ const address = typeof addressOrWallet === 'string' ? addressOrWallet : addressOrWallet.public_key;
502
+ return calculateAssets(chain).coins
503
+ .map((coin) => ({
504
+ symbol: coin.symbol,
505
+ name: coin.name,
506
+ supply: coin.supply,
507
+ creator: coin.creator,
508
+ balance: coin.holders.find((holder) => holder.address === address)?.balance ?? 0
509
+ }))
510
+ .filter((coin) => Math.abs(coin.balance) > 0.00000001);
511
+ }
512
+
241
513
  export async function getBalance(addressOrWallet, seedPeers = DEFAULT_PEERS) {
242
514
  const address = typeof addressOrWallet === 'string' ? addressOrWallet : addressOrWallet.public_key;
243
515
  const { chain, sourcePeer, peers } = await getBestChain(seedPeers);
@@ -253,6 +525,92 @@ export async function getBalance(addressOrWallet, seedPeers = DEFAULT_PEERS) {
253
525
  };
254
526
  }
255
527
 
528
+ export async function assertSpendableXyqonBalance(wallet, amount, seedPeers = DEFAULT_PEERS) {
529
+ const numericAmount = Number(amount);
530
+ const { chain, sourcePeer, peers } = await getBestChain(seedPeers);
531
+ const balances = calculateBalances(chain);
532
+ const balance = balances.get(wallet.public_key) ?? 0;
533
+
534
+ if (balance + XYQON_EPSILON < numericAmount) {
535
+ throw new Error(
536
+ `insufficient confirmed XYQON balance; balance is ${balance}, attempted to spend ${numericAmount}`
537
+ );
538
+ }
539
+
540
+ return {
541
+ balance,
542
+ blockHeight: chain.chain.at(-1)?.index ?? 0,
543
+ sourcePeer,
544
+ peers
545
+ };
546
+ }
547
+
548
+ export async function assertSpendableCoinBalance(wallet, symbol, amount, seedPeers = DEFAULT_PEERS) {
549
+ const normalizedSymbol = normalizeCoinSymbol(symbol);
550
+ const numericAmount = Number(amount);
551
+ const { chain, sourcePeer, peers } = await getBestChain(seedPeers);
552
+ const holding = listCoinHoldings(chain, wallet).find((coin) => coin.symbol === normalizedSymbol);
553
+ const balance = holding?.balance ?? 0;
554
+
555
+ if (balance + XYQON_EPSILON < numericAmount) {
556
+ throw new Error(
557
+ `insufficient confirmed ${normalizedSymbol} balance; balance is ${balance}, attempted to spend ${numericAmount}`
558
+ );
559
+ }
560
+
561
+ return {
562
+ symbol: normalizedSymbol,
563
+ balance,
564
+ blockHeight: chain.chain.at(-1)?.index ?? 0,
565
+ sourcePeer,
566
+ peers
567
+ };
568
+ }
569
+
570
+ export async function getCreatedCoins(addressOrWallet, seedPeers = DEFAULT_PEERS) {
571
+ const { chain, sourcePeer, peers } = await getBestChain(seedPeers);
572
+ return {
573
+ address: typeof addressOrWallet === 'string' ? addressOrWallet : addressOrWallet.public_key,
574
+ coins: listCoinsCreatedBy(chain, addressOrWallet),
575
+ blockHeight: chain.chain.at(-1)?.index ?? 0,
576
+ sourcePeer,
577
+ peers
578
+ };
579
+ }
580
+
581
+ export async function getCreatedNfts(addressOrWallet, seedPeers = DEFAULT_PEERS) {
582
+ const { chain, sourcePeer, peers } = await getBestChain(seedPeers);
583
+ return {
584
+ address: typeof addressOrWallet === 'string' ? addressOrWallet : addressOrWallet.public_key,
585
+ nfts: listNftsCreatedBy(chain, addressOrWallet),
586
+ blockHeight: chain.chain.at(-1)?.index ?? 0,
587
+ sourcePeer,
588
+ peers
589
+ };
590
+ }
591
+
592
+ export async function getOwnedNfts(addressOrWallet, seedPeers = DEFAULT_PEERS) {
593
+ const { chain, sourcePeer, peers } = await getBestChain(seedPeers);
594
+ return {
595
+ address: typeof addressOrWallet === 'string' ? addressOrWallet : addressOrWallet.public_key,
596
+ nfts: listNftsOwnedBy(chain, addressOrWallet),
597
+ blockHeight: chain.chain.at(-1)?.index ?? 0,
598
+ sourcePeer,
599
+ peers
600
+ };
601
+ }
602
+
603
+ export async function getCoinHoldings(addressOrWallet, seedPeers = DEFAULT_PEERS) {
604
+ const { chain, sourcePeer, peers } = await getBestChain(seedPeers);
605
+ return {
606
+ address: typeof addressOrWallet === 'string' ? addressOrWallet : addressOrWallet.public_key,
607
+ coins: listCoinHoldings(chain, addressOrWallet),
608
+ blockHeight: chain.chain.at(-1)?.index ?? 0,
609
+ sourcePeer,
610
+ peers
611
+ };
612
+ }
613
+
256
614
  export async function broadcastTransaction(transaction, seedPeers = DEFAULT_PEERS) {
257
615
  const peers = await discoverPeers(seedPeers);
258
616
  const message = { NewTransaction: transaction };
@@ -268,6 +626,47 @@ export async function broadcastTransaction(transaction, seedPeers = DEFAULT_PEER
268
626
 
269
627
  export async function sendTransaction({ wallet, recipient, amount, peers = DEFAULT_PEERS }) {
270
628
  const transaction = createSignedTransaction(wallet, recipient, amount);
629
+ const preflight = await assertSpendableXyqonBalance(wallet, transaction.amount, peers);
630
+ return {
631
+ transaction,
632
+ preflight,
633
+ ...(await broadcastTransaction(transaction, peers))
634
+ };
635
+ }
636
+
637
+ export async function createCoin({ wallet, symbol, name, supply, peers = DEFAULT_PEERS }) {
638
+ const transaction = createCoinTransaction(wallet, { symbol, name, supply });
639
+ return {
640
+ transaction,
641
+ ...(await broadcastTransaction(transaction, peers))
642
+ };
643
+ }
644
+
645
+ export async function sendCoin({ wallet, recipient, symbol, amount, peers = DEFAULT_PEERS }) {
646
+ const transaction = createCoinTransferTransaction(wallet, { recipient, symbol, amount });
647
+ const preflight = await assertSpendableCoinBalance(
648
+ wallet,
649
+ transaction.asset_operation.TransferCoin.symbol,
650
+ transaction.asset_operation.TransferCoin.amount,
651
+ peers
652
+ );
653
+ return {
654
+ transaction,
655
+ preflight,
656
+ ...(await broadcastTransaction(transaction, peers))
657
+ };
658
+ }
659
+
660
+ export async function mintNft({ wallet, collection, tokenId, name, imageUrl = null, peers = DEFAULT_PEERS }) {
661
+ const transaction = createNftMintTransaction(wallet, { collection, tokenId, name, imageUrl });
662
+ return {
663
+ transaction,
664
+ ...(await broadcastTransaction(transaction, peers))
665
+ };
666
+ }
667
+
668
+ export async function transferNft({ wallet, recipient, collection, tokenId, peers = DEFAULT_PEERS }) {
669
+ const transaction = createNftTransferTransaction(wallet, { recipient, collection, tokenId });
271
670
  return {
272
671
  transaction,
273
672
  ...(await broadcastTransaction(transaction, peers))