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 +81 -4
- package/package.json +2 -2
- package/src/cli.js +127 -3
- package/src/index.js +405 -6
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
|
|
20
|
+
npm install -g xyqon
|
|
19
21
|
```
|
|
20
22
|
|
|
21
23
|
Or run without installing:
|
|
22
24
|
|
|
23
25
|
```bash
|
|
24
|
-
npx xyqon
|
|
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
|
-
|
|
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.
|
|
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('
|
|
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
|
-
|
|
64
|
-
|
|
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 =
|
|
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))
|