xyqon 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 XYQON
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,112 @@
1
+ # XYQON Client
2
+
3
+ Friendly wallet CLI and JavaScript client for the live XYQON network.
4
+
5
+ It can:
6
+
7
+ - Generate a new XYQON wallet
8
+ - Show your public key
9
+ - Check your balance from live public nodes
10
+ - Send a signed transaction to the network
11
+ - Show currently reachable nodes
12
+
13
+ You do not need to mine and you do not need to run a full node.
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ npm install -g xyqon-client
19
+ ```
20
+
21
+ Or run without installing:
22
+
23
+ ```bash
24
+ npx xyqon-client help
25
+ ```
26
+
27
+ ## Create A Wallet
28
+
29
+ ```bash
30
+ xyqon wallet new --name Cathy
31
+ ```
32
+
33
+ This creates:
34
+
35
+ ```text
36
+ xyqon.wallet.json
37
+ ```
38
+
39
+ Keep this file private. It contains your private key.
40
+
41
+ ## Show Your Address
42
+
43
+ ```bash
44
+ xyqon wallet show
45
+ ```
46
+
47
+ ## Check Balance
48
+
49
+ ```bash
50
+ xyqon balance
51
+ ```
52
+
53
+ Use a custom wallet path:
54
+
55
+ ```bash
56
+ xyqon balance --wallet ./cathy.wallet.json
57
+ ```
58
+
59
+ ## Send XYQON
60
+
61
+ ```bash
62
+ xyqon send \
63
+ --to RECIPIENT_PUBLIC_KEY \
64
+ --amount 1
65
+ ```
66
+
67
+ The transaction is broadcast to reachable public nodes. A miner must include it in a block before the receiver sees the confirmed balance.
68
+
69
+ ## Show Nodes
70
+
71
+ ```bash
72
+ xyqon nodes
73
+ ```
74
+
75
+ ## Seed Nodes
76
+
77
+ The package starts with these seed nodes:
78
+
79
+ ```text
80
+ 68.183.98.134:7101
81
+ 143.244.149.8:7101
82
+ 147.182.138.183:7101
83
+ ```
84
+
85
+ You can override them:
86
+
87
+ ```bash
88
+ xyqon balance --peer 68.183.98.134:7101 --peer 143.244.149.8:7101
89
+ ```
90
+
91
+ ## JavaScript Usage
92
+
93
+ ```js
94
+ import {
95
+ createWallet,
96
+ loadWallet,
97
+ getBalance,
98
+ sendTransaction
99
+ } from 'xyqon-client';
100
+
101
+ const wallet = createWallet('Cathy');
102
+ console.log(wallet.public_key);
103
+
104
+ const balance = await getBalance(wallet.public_key);
105
+ console.log(balance);
106
+
107
+ await sendTransaction({
108
+ wallet,
109
+ recipient: 'RECIPIENT_PUBLIC_KEY',
110
+ amount: 1
111
+ });
112
+ ```
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "xyqon",
3
+ "version": "0.1.0",
4
+ "description": "Friendly wallet CLI and JavaScript client for the XYQON network.",
5
+ "keywords": [
6
+ "xyqon",
7
+ "wallet",
8
+ "blockchain",
9
+ "ed25519",
10
+ "cli"
11
+ ],
12
+ "homepage": "https://github.com/gideonlouw/xyqon#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/gideonlouw/xyqon/issues"
15
+ },
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/gideonlouw/xyqon.git"
19
+ },
20
+ "license": "MIT",
21
+ "author": "xyqon",
22
+ "type": "module",
23
+ "exports": {
24
+ ".": "./src/index.js"
25
+ },
26
+ "main": "index.js",
27
+ "bin": {
28
+ "xyqon": "src/cli.js",
29
+ "xyqon-client": "src/cli.js"
30
+ },
31
+ "files": [
32
+ "src",
33
+ "README.md",
34
+ "LICENSE"
35
+ ],
36
+ "scripts": {
37
+ "start": "node src/cli.js",
38
+ "check": "node src/cli.js help && node src/cli.js nodes"
39
+ },
40
+ "dependencies": {
41
+ "@noble/curves": "^1.8.2"
42
+ },
43
+ "devDependencies": {},
44
+ "engines": {
45
+ "node": ">=20"
46
+ }
47
+ }
package/src/cli.js ADDED
@@ -0,0 +1,180 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ DEFAULT_PEERS,
4
+ DEFAULT_WALLET_PATH,
5
+ createWallet,
6
+ discoverPeers,
7
+ getBalance,
8
+ getBestChain,
9
+ loadWallet,
10
+ saveWallet,
11
+ sendTransaction
12
+ } from './index.js';
13
+
14
+ const colors = {
15
+ cyan: '\x1b[36m',
16
+ green: '\x1b[32m',
17
+ red: '\x1b[31m',
18
+ yellow: '\x1b[33m',
19
+ bold: '\x1b[1m',
20
+ dim: '\x1b[2m',
21
+ reset: '\x1b[0m'
22
+ };
23
+
24
+ function color(name, text) {
25
+ return `${colors[name]}${text}${colors.reset}`;
26
+ }
27
+
28
+ function printHero(title = 'XYQON Wallet') {
29
+ console.log(color('cyan', '╔════════════════════════════════════════════════════╗'));
30
+ console.log(color('cyan', `║ ${title.padEnd(50)} ║`));
31
+ console.log(color('cyan', '╚════════════════════════════════════════════════════╝'));
32
+ }
33
+
34
+ function printHelp() {
35
+ printHero('XYQON Client');
36
+ console.log(`
37
+ ${color('bold', 'Usage')}
38
+ xyqon wallet new --name <NAME> [--out <FILE>]
39
+ xyqon wallet show [--wallet <FILE>] [--private]
40
+ xyqon balance [--wallet <FILE>] [--address <PUBLIC_KEY>]
41
+ xyqon send --to <PUBLIC_KEY> --amount <AMOUNT> [--wallet <FILE>]
42
+ xyqon nodes
43
+
44
+ ${color('bold', 'Network options')}
45
+ --peer <IP[:PORT]> Add a seed peer. Can be repeated.
46
+
47
+ ${color('bold', 'Defaults')}
48
+ Wallet: ${DEFAULT_WALLET_PATH}
49
+ Peers: ${DEFAULT_PEERS.join(', ')}
50
+ `);
51
+ }
52
+
53
+ function parseArgs(argv) {
54
+ const values = { _: [] };
55
+ for (let index = 0; index < argv.length; index += 1) {
56
+ const item = argv[index];
57
+ if (!item.startsWith('--')) {
58
+ values._.push(item);
59
+ continue;
60
+ }
61
+
62
+ const key = item.slice(2);
63
+ if (key === 'private' || key === 'force') {
64
+ values[key] = true;
65
+ continue;
66
+ }
67
+
68
+ const value = argv[index + 1];
69
+ if (!value || value.startsWith('--')) {
70
+ throw new Error(`--${key} requires a value`);
71
+ }
72
+ index += 1;
73
+
74
+ if (key === 'peer') {
75
+ values.peer = [...(values.peer ?? []), value];
76
+ } else {
77
+ values[key] = value;
78
+ }
79
+ }
80
+
81
+ return values;
82
+ }
83
+
84
+ function peersFromOptions(options) {
85
+ return options.peer?.length ? options.peer : DEFAULT_PEERS;
86
+ }
87
+
88
+ async function run() {
89
+ const argv = process.argv.slice(2);
90
+ const command = argv[0];
91
+ const subcommand = command === 'wallet' ? argv[1] : undefined;
92
+ const rest = command === 'wallet' ? argv.slice(2) : argv.slice(1);
93
+ const options = parseArgs(rest);
94
+
95
+ if (!command || command === 'help' || command === '--help' || command === '-h') {
96
+ printHelp();
97
+ return;
98
+ }
99
+
100
+ if (command === 'wallet' && subcommand === 'new') {
101
+ const path = options.out ?? DEFAULT_WALLET_PATH;
102
+ const wallet = createWallet(options.name ?? 'XYQON User');
103
+ await saveWallet(wallet, path);
104
+ printHero('Wallet Created');
105
+ console.log(`${color('green', 'Saved:')} ${path}`);
106
+ console.log(`${color('green', 'Public key:')} ${wallet.public_key}`);
107
+ console.log(color('yellow', 'Keep the wallet file private. It contains your private key.'));
108
+ return;
109
+ }
110
+
111
+ if (command === 'wallet' && subcommand === 'show') {
112
+ const wallet = await loadWallet(options.wallet ?? DEFAULT_WALLET_PATH);
113
+ printHero('Wallet');
114
+ console.log(`${color('bold', 'Name:')} ${wallet.name}`);
115
+ console.log(`${color('bold', 'Public key:')} ${wallet.public_key}`);
116
+ if (options.private) {
117
+ console.log(`${color('red', 'Private key:')} ${wallet.private_key}`);
118
+ }
119
+ return;
120
+ }
121
+
122
+ if (command === 'balance') {
123
+ const wallet = options.address ? null : await loadWallet(options.wallet ?? DEFAULT_WALLET_PATH);
124
+ const address = options.address ?? wallet.public_key;
125
+ const balance = await getBalance(address, peersFromOptions(options));
126
+ printHero('Balance');
127
+ console.log(`${color('bold', 'Address:')} ${balance.address}`);
128
+ console.log(`${color('green', 'Balance:')} ${balance.balance} XYQON`);
129
+ console.log(`${color('dim', 'Block height:')} ${balance.blockHeight}`);
130
+ console.log(`${color('dim', 'Source node:')} ${balance.sourcePeer}`);
131
+ return;
132
+ }
133
+
134
+ if (command === 'send') {
135
+ if (!options.to) {
136
+ throw new Error('send requires --to <PUBLIC_KEY>');
137
+ }
138
+ if (!options.amount) {
139
+ throw new Error('send requires --amount <AMOUNT>');
140
+ }
141
+
142
+ const wallet = await loadWallet(options.wallet ?? DEFAULT_WALLET_PATH);
143
+ const result = await sendTransaction({
144
+ wallet,
145
+ recipient: options.to,
146
+ amount: options.amount,
147
+ peers: peersFromOptions(options)
148
+ });
149
+
150
+ printHero('Transaction Sent');
151
+ console.log(`${color('bold', 'Amount:')} ${result.transaction.amount} XYQON`);
152
+ console.log(`${color('bold', 'To:')} ${result.transaction.recipient}`);
153
+ console.log(`${color('bold', 'Transaction ID:')} ${result.transactionId}`);
154
+ console.log(`${color('green', 'Broadcast:')} ${result.acceptedBy}/${result.peers.length} reachable nodes`);
155
+ console.log(color('yellow', 'The receiver balance updates after a miner includes this transaction in a block.'));
156
+ return;
157
+ }
158
+
159
+ if (command === 'nodes') {
160
+ const peers = await discoverPeers(peersFromOptions(options));
161
+ const { nodes, chain, sourcePeer } = await getBestChain(peers);
162
+ printHero('Live Nodes');
163
+ for (const node of nodes) {
164
+ const icon = node.ok ? color('green', '●') : color('red', '●');
165
+ console.log(`${icon} ${node.peer.padEnd(22)} ${node.ok ? `${node.latencyMs} ms` : node.error}`);
166
+ }
167
+ console.log('');
168
+ console.log(`${color('bold', 'Height:')} ${chain.chain.at(-1)?.index ?? 0}`);
169
+ console.log(`${color('bold', 'Supply:')} ${chain.circulating_supply} XYQON`);
170
+ console.log(`${color('dim', 'Best source:')} ${sourcePeer}`);
171
+ return;
172
+ }
173
+
174
+ throw new Error(`unknown command: ${[command, subcommand].filter(Boolean).join(' ')}`);
175
+ }
176
+
177
+ run().catch((error) => {
178
+ console.error(color('red', `Error: ${error.message}`));
179
+ process.exit(1);
180
+ });
package/src/index.js ADDED
@@ -0,0 +1,275 @@
1
+ import { ed25519 } from '@noble/curves/ed25519';
2
+ import { createHash } from 'node:crypto';
3
+ import { readFile, writeFile } from 'node:fs/promises';
4
+ import net from 'node:net';
5
+
6
+ export const DEFAULT_PEERS = [
7
+ '68.183.98.134:7101',
8
+ '143.244.149.8:7101',
9
+ '147.182.138.183:7101'
10
+ ];
11
+
12
+ export const DEFAULT_WALLET_PATH = 'xyqon.wallet.json';
13
+
14
+ export function bytesToHex(bytes) {
15
+ return [...bytes].map((byte) => byte.toString(16).padStart(2, '0')).join('');
16
+ }
17
+
18
+ export function hexToBytes(hex) {
19
+ if (!/^[0-9a-fA-F]+$/.test(hex) || hex.length % 2 !== 0) {
20
+ throw new Error('expected a valid hex string');
21
+ }
22
+
23
+ return Uint8Array.from(hex.match(/.{2}/g).map((part) => Number.parseInt(part, 16)));
24
+ }
25
+
26
+ export function normalizePeer(peer) {
27
+ const value = `${peer}`.split('#')[0].trim();
28
+ if (!value) {
29
+ return null;
30
+ }
31
+
32
+ return value.includes(':') ? value : `${value}:7101`;
33
+ }
34
+
35
+ export function normalizePeers(peers = DEFAULT_PEERS) {
36
+ return [...new Set(peers.map(normalizePeer).filter(Boolean))];
37
+ }
38
+
39
+ export function createWallet(name = 'XYQON User') {
40
+ const privateKey = ed25519.utils.randomPrivateKey();
41
+ const publicKey = ed25519.getPublicKey(privateKey);
42
+
43
+ return {
44
+ name,
45
+ public_key: bytesToHex(publicKey),
46
+ private_key: bytesToHex(privateKey)
47
+ };
48
+ }
49
+
50
+ export async function saveWallet(wallet, path = DEFAULT_WALLET_PATH) {
51
+ await writeFile(path, `${JSON.stringify(wallet, null, 2)}\n`, { mode: 0o600 });
52
+ }
53
+
54
+ export async function loadWallet(path = DEFAULT_WALLET_PATH) {
55
+ const wallet = JSON.parse(await readFile(path, 'utf8'));
56
+ if (!wallet.name || !wallet.public_key || !wallet.private_key) {
57
+ throw new Error(`wallet file is missing required fields: ${path}`);
58
+ }
59
+
60
+ return wallet;
61
+ }
62
+
63
+ export function transactionPayload(sender, recipient, amount, senderPublicKey) {
64
+ return `${sender}|${recipient}|${Number(amount).toFixed(8)}|${senderPublicKey}`;
65
+ }
66
+
67
+ export function createSignedTransaction(wallet, recipient, amount) {
68
+ const numericAmount = Number(amount);
69
+ if (!Number.isFinite(numericAmount) || numericAmount <= 0) {
70
+ throw new Error('amount must be a positive number');
71
+ }
72
+
73
+ 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
+ }
78
+
79
+ const payload = transactionPayload(wallet.name, recipient, numericAmount, senderPublicKey);
80
+ const signature = ed25519.sign(new TextEncoder().encode(payload), privateKey);
81
+
82
+ return {
83
+ sender: wallet.name,
84
+ recipient,
85
+ amount: numericAmount,
86
+ sender_public_key: senderPublicKey,
87
+ signature: bytesToHex(signature)
88
+ };
89
+ }
90
+
91
+ export function transactionId(transaction) {
92
+ return createHash('sha256').update(JSON.stringify(transaction)).digest('hex');
93
+ }
94
+
95
+ export function requestMessage(peer, message, responseKey, timeoutMs = 5000) {
96
+ return new Promise((resolve) => {
97
+ const normalizedPeer = normalizePeer(peer);
98
+ const [host, portText] = normalizedPeer.split(':');
99
+ const socket = net.createConnection({ host, port: Number(portText) });
100
+ const startedAt = Date.now();
101
+ let data = '';
102
+ let settled = false;
103
+
104
+ const finish = (result) => {
105
+ if (settled) {
106
+ return;
107
+ }
108
+ settled = true;
109
+ socket.destroy();
110
+ resolve({ peer: normalizedPeer, latencyMs: Date.now() - startedAt, ...result });
111
+ };
112
+
113
+ socket.setTimeout(timeoutMs);
114
+ socket.on('connect', () => socket.write(`${JSON.stringify(message)}\n`));
115
+ socket.on('data', (chunk) => {
116
+ data += chunk.toString('utf8');
117
+ if (!data.includes('\n')) {
118
+ return;
119
+ }
120
+
121
+ try {
122
+ const parsed = JSON.parse(data.trim());
123
+ finish({ ok: true, payload: parsed[responseKey] });
124
+ } catch (error) {
125
+ finish({ ok: false, error: error.message });
126
+ }
127
+ });
128
+ socket.on('timeout', () => finish({ ok: false, error: 'timeout' }));
129
+ socket.on('error', (error) => finish({ ok: false, error: error.message }));
130
+ });
131
+ }
132
+
133
+ export function sendNetworkMessage(peer, message, timeoutMs = 5000) {
134
+ return new Promise((resolve) => {
135
+ const normalizedPeer = normalizePeer(peer);
136
+ const [host, portText] = normalizedPeer.split(':');
137
+ const socket = net.createConnection({ host, port: Number(portText) });
138
+ const startedAt = Date.now();
139
+ let settled = false;
140
+
141
+ const finish = (result) => {
142
+ if (settled) {
143
+ return;
144
+ }
145
+ settled = true;
146
+ socket.destroy();
147
+ resolve({ peer: normalizedPeer, latencyMs: Date.now() - startedAt, ...result });
148
+ };
149
+
150
+ socket.setTimeout(timeoutMs);
151
+ socket.on('connect', () => {
152
+ socket.write(`${JSON.stringify(message)}\n`, () => finish({ ok: true }));
153
+ });
154
+ socket.on('timeout', () => finish({ ok: false, error: 'timeout' }));
155
+ socket.on('error', (error) => finish({ ok: false, error: error.message }));
156
+ });
157
+ }
158
+
159
+ export async function requestChain(peer) {
160
+ const result = await requestMessage(peer, 'RequestChain', 'ChainResponse');
161
+ return { ...result, chain: result.payload };
162
+ }
163
+
164
+ export async function requestPeers(peer) {
165
+ const result = await requestMessage(peer, 'RequestPeers', 'PeerResponse');
166
+ return {
167
+ ...result,
168
+ peers: Array.isArray(result.payload) ? normalizePeers(result.payload) : []
169
+ };
170
+ }
171
+
172
+ export async function discoverPeers(seedPeers = DEFAULT_PEERS) {
173
+ const discovered = new Set(normalizePeers(seedPeers));
174
+ let frontier = [...discovered];
175
+
176
+ for (let depth = 0; depth < 2 && frontier.length > 0; depth += 1) {
177
+ const responses = await Promise.all(frontier.map((peer) => requestPeers(peer)));
178
+ const next = [];
179
+
180
+ for (const response of responses) {
181
+ if (!response.ok) {
182
+ continue;
183
+ }
184
+
185
+ for (const peer of response.peers) {
186
+ if (!discovered.has(peer)) {
187
+ discovered.add(peer);
188
+ next.push(peer);
189
+ }
190
+ }
191
+ }
192
+
193
+ frontier = next;
194
+ }
195
+
196
+ return [...discovered].sort();
197
+ }
198
+
199
+ export async function getBestChain(seedPeers = DEFAULT_PEERS) {
200
+ const peers = await discoverPeers(seedPeers);
201
+ const responses = await Promise.all(peers.map((peer) => requestChain(peer)));
202
+ const online = responses.filter((response) => response.ok && response.chain);
203
+
204
+ if (online.length === 0) {
205
+ throw new Error('no reachable XYQON nodes returned a chain');
206
+ }
207
+
208
+ online.sort((left, right) => chainScore(right.chain) - chainScore(left.chain));
209
+ return {
210
+ sourcePeer: online[0].peer,
211
+ peers,
212
+ nodes: responses,
213
+ chain: online[0].chain
214
+ };
215
+ }
216
+
217
+ export function chainScore(chain) {
218
+ return chain.chain.slice(1).reduce((score, block) => score + 2 ** Math.min(block.difficulty, 52), 0);
219
+ }
220
+
221
+ export function calculateBalances(chain) {
222
+ const balances = new Map();
223
+ const credit = (address, amount) => balances.set(address, (balances.get(address) ?? 0) + amount);
224
+ const debit = (address, amount) => balances.set(address, (balances.get(address) ?? 0) - amount);
225
+
226
+ for (const block of chain.chain.slice(1)) {
227
+ const [coinbase, ...transactions] = block.transactions;
228
+ if (coinbase?.recipient) {
229
+ credit(coinbase.recipient, coinbase.amount);
230
+ }
231
+
232
+ for (const transaction of transactions) {
233
+ debit(transaction.sender_public_key, transaction.amount);
234
+ credit(transaction.recipient, transaction.amount);
235
+ }
236
+ }
237
+
238
+ return balances;
239
+ }
240
+
241
+ export async function getBalance(addressOrWallet, seedPeers = DEFAULT_PEERS) {
242
+ const address = typeof addressOrWallet === 'string' ? addressOrWallet : addressOrWallet.public_key;
243
+ const { chain, sourcePeer, peers } = await getBestChain(seedPeers);
244
+ const balances = calculateBalances(chain);
245
+
246
+ return {
247
+ address,
248
+ balance: balances.get(address) ?? 0,
249
+ blockHeight: chain.chain.at(-1)?.index ?? 0,
250
+ circulatingSupply: chain.circulating_supply,
251
+ sourcePeer,
252
+ peers
253
+ };
254
+ }
255
+
256
+ export async function broadcastTransaction(transaction, seedPeers = DEFAULT_PEERS) {
257
+ const peers = await discoverPeers(seedPeers);
258
+ const message = { NewTransaction: transaction };
259
+ const results = await Promise.all(peers.map((peer) => sendNetworkMessage(peer, message)));
260
+
261
+ return {
262
+ transactionId: transactionId(transaction),
263
+ peers,
264
+ results,
265
+ acceptedBy: results.filter((result) => result.ok).length
266
+ };
267
+ }
268
+
269
+ export async function sendTransaction({ wallet, recipient, amount, peers = DEFAULT_PEERS }) {
270
+ const transaction = createSignedTransaction(wallet, recipient, amount);
271
+ return {
272
+ transaction,
273
+ ...(await broadcastTransaction(transaction, peers))
274
+ };
275
+ }