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 +21 -0
- package/README.md +112 -0
- package/package.json +47 -0
- package/src/cli.js +180 -0
- package/src/index.js +275 -0
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
|
+
}
|