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