@neuraiproject/neurai-assets 1.0.2 → 1.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/examples/10-depin-post-quantum.js +83 -0
- package/examples/README.md +20 -2
- package/package.json +7 -4
- package/src/NeuraiAssets.js +36 -1
- package/src/builders/BaseAssetTransactionBuilder.js +3 -0
- package/src/builders/IssueDepinBuilder.js +144 -0
- package/src/builders/ReissueBuilder.js +2 -1
- package/src/builders/index.js +2 -0
- package/src/constants/burnAddresses.js +22 -4
- package/src/constants/index.js +2 -0
- package/src/constants/networks.js +44 -5
- package/src/index.js +1 -0
- package/src/managers/BurnManager.js +9 -0
- package/src/queries/AssetQueries.js +50 -1
- package/src/utils/assetNameParser.js +22 -1
- package/src/utils/networkDetector.js +21 -5
- package/src/validators/assetNameValidator.js +78 -2
- package/tests/integration/assetLifecycle.test.js +27 -0
- package/tests/mocks/rpcMock.js +3 -1
- package/tests/unit/NeuraiAssets.test.js +26 -0
- package/tests/unit/utils/assetNameParser.test.js +37 -0
- package/tests/unit/utils/networkDetector.test.js +89 -0
- package/tests/unit/validators/assetNameValidator.test.js +29 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example: DEPIN Assets with Post-Quantum Addresses
|
|
3
|
+
*
|
|
4
|
+
* This example demonstrates how to use DEPIN assets with PQ addresses
|
|
5
|
+
* generated by neurai-key using the xna-pq / xna-pq-test networks.
|
|
6
|
+
*
|
|
7
|
+
* PQ address examples:
|
|
8
|
+
* - Mainnet: nq1...
|
|
9
|
+
* - Testnet: tnq1...
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const NeuraiAssets = require('@neuraiproject/neurai-assets');
|
|
13
|
+
|
|
14
|
+
async function createDepinForPQAddress() {
|
|
15
|
+
const rpc = async (method, params) => {
|
|
16
|
+
console.log(`RPC Call: ${method}`, params);
|
|
17
|
+
// Replace with your actual RPC client
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const assets = new NeuraiAssets(rpc, {
|
|
21
|
+
network: 'xna-pq', // Or 'xna-pq-test'
|
|
22
|
+
addresses: ['nq1yourpqwalletaddress...'], // PQ addresses from neurai-key
|
|
23
|
+
changeAddress: 'nq1yourpqchangeaddress...',
|
|
24
|
+
toAddress: 'nq1recipientpqaddress...'
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const result = await assets.createDepinAsset({
|
|
29
|
+
assetName: '&DEVICE/ROUTER001',
|
|
30
|
+
quantity: 1,
|
|
31
|
+
reissuable: false
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
console.log('DEPIN transaction created');
|
|
35
|
+
console.log('Raw transaction:', result.rawTx);
|
|
36
|
+
console.log('Burn:', result.burnAmount, 'XNA');
|
|
37
|
+
console.log('Owner token:', result.ownerTokenName);
|
|
38
|
+
} catch (error) {
|
|
39
|
+
console.error('Error creating DEPIN asset for PQ address:', error.message);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function queryDepinValidityForPQAddress() {
|
|
44
|
+
const rpc = async (method, params) => {
|
|
45
|
+
console.log(`RPC Call: ${method}`, params);
|
|
46
|
+
|
|
47
|
+
if (method === 'checkdepinvalidity') {
|
|
48
|
+
return {
|
|
49
|
+
has_asset: true,
|
|
50
|
+
amount: 1,
|
|
51
|
+
valid: 1,
|
|
52
|
+
blocked: false
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (method === 'listdepinholders') {
|
|
57
|
+
return [
|
|
58
|
+
{ address: 'nq1firstholder...', amount: 1, valid: 1 },
|
|
59
|
+
{ address: 'nq1secondholder...', amount: 1, valid: 0 }
|
|
60
|
+
];
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const assets = new NeuraiAssets(rpc, {
|
|
65
|
+
network: 'xna-pq',
|
|
66
|
+
addresses: ['nq1yourpqwalletaddress...'],
|
|
67
|
+
changeAddress: 'nq1yourpqchangeaddress...',
|
|
68
|
+
toAddress: 'nq1recipientpqaddress...'
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
const validity = await assets.checkDepinValidity('&DEVICE/ROUTER001', 'nq1recipientpqaddress...');
|
|
73
|
+
console.log('DEPIN validity for PQ address:', validity);
|
|
74
|
+
|
|
75
|
+
const holders = await assets.listDepinHolders('&DEVICE/ROUTER001');
|
|
76
|
+
console.log('DEPIN holders:', holders);
|
|
77
|
+
} catch (error) {
|
|
78
|
+
console.error('Error querying DEPIN PQ data:', error.message);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
createDepinForPQAddress();
|
|
83
|
+
queryDepinValidityForPQAddress();
|
package/examples/README.md
CHANGED
|
@@ -163,6 +163,23 @@ Learn complete integration with `@neuraiproject/neurai-jswallet`.
|
|
|
163
163
|
|
|
164
164
|
---
|
|
165
165
|
|
|
166
|
+
### 10. DEPIN with Post-Quantum Addresses
|
|
167
|
+
**File:** [10-depin-post-quantum.js](10-depin-post-quantum.js)
|
|
168
|
+
|
|
169
|
+
Learn how to use DEPIN assets with PQ addresses generated by `neurai-key`.
|
|
170
|
+
|
|
171
|
+
- PQ mainnet network: `xna-pq`
|
|
172
|
+
- PQ testnet network: `xna-pq-test`
|
|
173
|
+
- PQ address formats: `nq1...` and `tnq1...`
|
|
174
|
+
|
|
175
|
+
**Topics covered:**
|
|
176
|
+
- Initializing `NeuraiAssets` with PQ networks
|
|
177
|
+
- Creating DEPIN assets sent to PQ addresses
|
|
178
|
+
- Querying DEPIN validity for PQ holders
|
|
179
|
+
- Listing DEPIN holders with PQ addresses
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
166
183
|
## Running the Examples
|
|
167
184
|
|
|
168
185
|
### Prerequisites
|
|
@@ -184,9 +201,9 @@ node 01-create-root-asset.js
|
|
|
184
201
|
|
|
185
202
|
1. **Mock RPC**: All examples use mock RPC functions. Replace with your actual RPC client in production.
|
|
186
203
|
|
|
187
|
-
2. **Network**: Examples use mainnet (`'xna'`). Change to `'xna-test'` for testnet.
|
|
204
|
+
2. **Network**: Examples use mainnet (`'xna'`). Change to `'xna-test'` for testnet. PQ networks are `xna-pq` and `xna-pq-test`.
|
|
188
205
|
|
|
189
|
-
3. **Addresses**: Replace placeholder addresses with your actual wallet addresses.
|
|
206
|
+
3. **Addresses**: Replace placeholder addresses with your actual wallet addresses. PQ examples use `nq1...` / `tnq1...` addresses.
|
|
190
207
|
|
|
191
208
|
4. **Testing**: Test on testnet first before using mainnet.
|
|
192
209
|
|
|
@@ -232,6 +249,7 @@ const txid = await wallet.broadcastTransaction(signedTx);
|
|
|
232
249
|
| QUALIFIER | `#NAME` | 2000 XNA | `#KYC_VERIFIED` |
|
|
233
250
|
| SUB_QUALIFIER | `#ROOT/#SUB` | 200 XNA | `#KYC/#LEVEL2` |
|
|
234
251
|
| RESTRICTED | `$NAME` | 3000 XNA | `$SECURITY` |
|
|
252
|
+
| DEPIN | `&NAME` or `&ROOT/SUB` | 10 XNA | `&DEVICE/ROUTER001` |
|
|
235
253
|
| OWNER | `NAME!` | N/A | `MYTOKEN!` |
|
|
236
254
|
|
|
237
255
|
## Common Patterns
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neuraiproject/neurai-assets",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Non-custodial Neurai asset management library for JavaScript",
|
|
5
5
|
"main": "./src/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -29,13 +29,16 @@
|
|
|
29
29
|
"publishConfig": {
|
|
30
30
|
"access": "public"
|
|
31
31
|
},
|
|
32
|
-
"dependencies": {},
|
|
33
32
|
"devDependencies": {
|
|
34
33
|
"chai": "^4.3.10",
|
|
35
|
-
"mocha": "^
|
|
34
|
+
"mocha": "^11.7.5"
|
|
35
|
+
},
|
|
36
|
+
"overrides": {
|
|
37
|
+
"diff": "8.0.4",
|
|
38
|
+
"serialize-javascript": "7.0.5"
|
|
36
39
|
},
|
|
37
40
|
"peerDependencies": {
|
|
38
|
-
"@neuraiproject/neurai-rpc": "^0.4.
|
|
41
|
+
"@neuraiproject/neurai-rpc": "^0.4.7"
|
|
39
42
|
},
|
|
40
43
|
"engines": {
|
|
41
44
|
"node": ">=14.0.0"
|
package/src/NeuraiAssets.js
CHANGED
|
@@ -25,6 +25,7 @@ const { AssetQueries } = require('./queries');
|
|
|
25
25
|
const {
|
|
26
26
|
IssueRootBuilder,
|
|
27
27
|
IssueSubBuilder,
|
|
28
|
+
IssueDepinBuilder,
|
|
28
29
|
IssueUniqueBuilder,
|
|
29
30
|
IssueQualifierBuilder,
|
|
30
31
|
IssueRestrictedBuilder,
|
|
@@ -119,6 +120,21 @@ class NeuraiAssets {
|
|
|
119
120
|
return await builder.build();
|
|
120
121
|
}
|
|
121
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Create a DEPIN asset
|
|
125
|
+
* @param {object} params - DEPIN creation parameters
|
|
126
|
+
* @param {string} params.assetName - Asset name (&NAME or &NAME/SUB)
|
|
127
|
+
* @param {number} params.quantity - Total supply
|
|
128
|
+
* @param {boolean} [params.reissuable=true] - Can mint more later
|
|
129
|
+
* @param {boolean} [params.hasIpfs=false] - Has IPFS metadata
|
|
130
|
+
* @param {string} [params.ipfsHash] - IPFS hash
|
|
131
|
+
* @returns {Promise<object>} Transaction data
|
|
132
|
+
*/
|
|
133
|
+
async createDepinAsset(params) {
|
|
134
|
+
const builder = new IssueDepinBuilder(this.rpc, this._buildParams(params));
|
|
135
|
+
return await builder.build();
|
|
136
|
+
}
|
|
137
|
+
|
|
122
138
|
/**
|
|
123
139
|
* Reissue (mint more) of a ROOT or SUB asset
|
|
124
140
|
* @param {object} params - Reissue parameters
|
|
@@ -439,6 +455,25 @@ class NeuraiAssets {
|
|
|
439
455
|
return await this.queries.cancelSnapshotRequest(assetName, blockHeight);
|
|
440
456
|
}
|
|
441
457
|
|
|
458
|
+
/**
|
|
459
|
+
* List DEPIN holders with validity status
|
|
460
|
+
* @param {string} assetName - DEPIN asset name
|
|
461
|
+
* @returns {Promise<Array>} Holder entries
|
|
462
|
+
*/
|
|
463
|
+
async listDepinHolders(assetName) {
|
|
464
|
+
return await this.queries.listDepinHolders(assetName);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Check DEPIN validity for an address
|
|
469
|
+
* @param {string} assetName - DEPIN asset name
|
|
470
|
+
* @param {string} address - Address to query
|
|
471
|
+
* @returns {Promise<object>} Validity details
|
|
472
|
+
*/
|
|
473
|
+
async checkDepinValidity(assetName, address) {
|
|
474
|
+
return await this.queries.checkDepinValidity(assetName, address);
|
|
475
|
+
}
|
|
476
|
+
|
|
442
477
|
/**
|
|
443
478
|
* Check if asset exists
|
|
444
479
|
* @param {string} assetName - Asset name
|
|
@@ -451,7 +486,7 @@ class NeuraiAssets {
|
|
|
451
486
|
/**
|
|
452
487
|
* Get asset type from name
|
|
453
488
|
* @param {string} assetName - Asset name
|
|
454
|
-
* @returns {string} Asset type ('ROOT', 'SUB', 'UNIQUE', 'QUALIFIER', 'RESTRICTED', 'OWNER')
|
|
489
|
+
* @returns {string} Asset type ('ROOT', 'SUB', 'UNIQUE', 'QUALIFIER', 'RESTRICTED', 'DEPIN', 'OWNER')
|
|
455
490
|
*/
|
|
456
491
|
getAssetType(assetName) {
|
|
457
492
|
return this.queries.getAssetType(assetName);
|
|
@@ -225,6 +225,9 @@ class BaseAssetTransactionBuilder {
|
|
|
225
225
|
case 'RESTRICTED':
|
|
226
226
|
AssetNameValidator.validateRestricted(assetName);
|
|
227
227
|
break;
|
|
228
|
+
case 'DEPIN':
|
|
229
|
+
AssetNameValidator.validateDepin(assetName);
|
|
230
|
+
break;
|
|
228
231
|
default:
|
|
229
232
|
throw new Error(`Unknown asset type: ${type}`);
|
|
230
233
|
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Issue DePIN Builder
|
|
3
|
+
* Builds transactions for creating DEPIN assets.
|
|
4
|
+
*
|
|
5
|
+
* DEPIN assets:
|
|
6
|
+
* - Soulbound assets
|
|
7
|
+
* - Format: &NAME or &NAME/SUB
|
|
8
|
+
* - Cost: 10 XNA (same burn as UNIQUE assets)
|
|
9
|
+
* - Units: Always 0
|
|
10
|
+
* - Owner token is auto-created by the node
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const BaseAssetTransactionBuilder = require('./BaseAssetTransactionBuilder');
|
|
14
|
+
const { OutputFormatter } = require('../utils');
|
|
15
|
+
const { AssetExistsError } = require('../errors');
|
|
16
|
+
const { IpfsValidator } = require('../validators');
|
|
17
|
+
|
|
18
|
+
class IssueDepinBuilder extends BaseAssetTransactionBuilder {
|
|
19
|
+
/**
|
|
20
|
+
* Validate issue DEPIN parameters
|
|
21
|
+
* @param {object} params - Issue parameters
|
|
22
|
+
* @throws {Error} If validation fails
|
|
23
|
+
*/
|
|
24
|
+
validateParams(params) {
|
|
25
|
+
if (!params.assetName) {
|
|
26
|
+
throw new Error('assetName is required');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (params.quantity === undefined || params.quantity === null) {
|
|
30
|
+
throw new Error('quantity is required');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
this.validateAssetName(params.assetName, 'DEPIN');
|
|
34
|
+
this.validateAmount(params.quantity, 0);
|
|
35
|
+
|
|
36
|
+
if (params.units !== undefined && params.units !== 0) {
|
|
37
|
+
throw new Error('DEPIN assets must use units=0');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (params.hasIpfs && params.ipfsHash) {
|
|
41
|
+
IpfsValidator.validate(params.ipfsHash);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (params.reissuable !== undefined && typeof params.reissuable !== 'boolean') {
|
|
45
|
+
throw new Error('reissuable must be a boolean');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Build DEPIN asset issuance transaction
|
|
53
|
+
* @returns {Promise<object>} Transaction result
|
|
54
|
+
*/
|
|
55
|
+
async build() {
|
|
56
|
+
await this.validateParams(this.params);
|
|
57
|
+
|
|
58
|
+
const {
|
|
59
|
+
assetName,
|
|
60
|
+
quantity,
|
|
61
|
+
reissuable = true,
|
|
62
|
+
hasIpfs = false,
|
|
63
|
+
ipfsHash = ''
|
|
64
|
+
} = this.params;
|
|
65
|
+
|
|
66
|
+
const exists = await this.assetExists(assetName);
|
|
67
|
+
if (exists) {
|
|
68
|
+
throw new AssetExistsError(
|
|
69
|
+
`Asset ${assetName} already exists on the blockchain`,
|
|
70
|
+
assetName
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const burnInfo = this.burnManager.getIssueDepinBurn();
|
|
75
|
+
const toAddress = await this.getToAddress();
|
|
76
|
+
const changeAddress = await this.getChangeAddress();
|
|
77
|
+
|
|
78
|
+
const estimatedFee = await this.estimateFee(1, 3);
|
|
79
|
+
const totalXNANeeded = burnInfo.amount + estimatedFee;
|
|
80
|
+
|
|
81
|
+
const utxoSelection = await this.selectUTXOs(totalXNANeeded, null, 0);
|
|
82
|
+
const baseCurrencyUTXOs = utxoSelection.xnaUTXOs;
|
|
83
|
+
const totalXNAInput = utxoSelection.totalXNA;
|
|
84
|
+
|
|
85
|
+
const actualFee = await this.estimateFee(baseCurrencyUTXOs.length, 3);
|
|
86
|
+
const totalRequired = burnInfo.amount + actualFee;
|
|
87
|
+
|
|
88
|
+
if (totalXNAInput < totalRequired) {
|
|
89
|
+
const additionalNeeded = totalRequired - totalXNAInput + 0.001;
|
|
90
|
+
const additionalSelection = await this.selectUTXOs(additionalNeeded, null, 0);
|
|
91
|
+
baseCurrencyUTXOs.push(...additionalSelection.xnaUTXOs);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const finalTotalInput = baseCurrencyUTXOs.reduce(
|
|
95
|
+
(sum, utxo) => sum + utxo.satoshis / 100000000,
|
|
96
|
+
0
|
|
97
|
+
);
|
|
98
|
+
const xnaChange = finalTotalInput - burnInfo.amount - actualFee;
|
|
99
|
+
|
|
100
|
+
const inputs = baseCurrencyUTXOs.map(utxo => ({
|
|
101
|
+
txid: utxo.txid,
|
|
102
|
+
vout: utxo.outputIndex,
|
|
103
|
+
address: utxo.address,
|
|
104
|
+
satoshis: utxo.satoshis
|
|
105
|
+
}));
|
|
106
|
+
|
|
107
|
+
const outputs = [];
|
|
108
|
+
outputs.push({ [burnInfo.address]: burnInfo.amount });
|
|
109
|
+
|
|
110
|
+
if (xnaChange > 0.00000001) {
|
|
111
|
+
outputs.push({ [changeAddress]: parseFloat(xnaChange.toFixed(8)) });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const issueOutput = OutputFormatter.formatIssueOutput({
|
|
115
|
+
asset_name: assetName,
|
|
116
|
+
asset_quantity: this.toSatoshis(quantity, 0),
|
|
117
|
+
units: 0,
|
|
118
|
+
reissuable,
|
|
119
|
+
has_ipfs: hasIpfs,
|
|
120
|
+
ipfs_hash: ipfsHash
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
outputs.push({ [toAddress]: issueOutput });
|
|
124
|
+
|
|
125
|
+
const orderedOutputs = this.outputOrderer.order(outputs);
|
|
126
|
+
const rawTx = await this.buildRawTransaction(inputs, orderedOutputs);
|
|
127
|
+
|
|
128
|
+
return this.formatResult(
|
|
129
|
+
rawTx,
|
|
130
|
+
baseCurrencyUTXOs,
|
|
131
|
+
inputs,
|
|
132
|
+
orderedOutputs,
|
|
133
|
+
actualFee,
|
|
134
|
+
burnInfo.amount,
|
|
135
|
+
{
|
|
136
|
+
assetName,
|
|
137
|
+
ownerTokenName: `${assetName}!`,
|
|
138
|
+
operationType: 'ISSUE_DEPIN'
|
|
139
|
+
}
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
module.exports = IssueDepinBuilder;
|
|
@@ -103,6 +103,7 @@ class ReissueBuilder extends BaseAssetTransactionBuilder {
|
|
|
103
103
|
const addresses = await this._getAddresses();
|
|
104
104
|
const toAddress = await this.getToAddress();
|
|
105
105
|
const changeAddress = await this.getChangeAddress();
|
|
106
|
+
const isDepinAsset = AssetNameParser.isDepin(assetName);
|
|
106
107
|
|
|
107
108
|
// 6. Find owner token (CRITICAL: must have this)
|
|
108
109
|
const ownerTokenName = AssetNameParser.getOwnerTokenName(assetName);
|
|
@@ -201,7 +202,7 @@ class ReissueBuilder extends BaseAssetTransactionBuilder {
|
|
|
201
202
|
asset_quantity: this.toSatoshis(quantity, units),
|
|
202
203
|
reissuable: reissuable !== undefined ? reissuable : undefined,
|
|
203
204
|
new_ipfs: newIpfs || undefined,
|
|
204
|
-
owner_change_address: changeAddress
|
|
205
|
+
owner_change_address: isDepinAsset ? toAddress : changeAddress
|
|
205
206
|
});
|
|
206
207
|
|
|
207
208
|
outputs.push({ [toAddress]: reissueOutput });
|
package/src/builders/index.js
CHANGED
|
@@ -9,6 +9,7 @@ const BaseAssetTransactionBuilder = require('./BaseAssetTransactionBuilder');
|
|
|
9
9
|
// Basic Builders
|
|
10
10
|
const IssueRootBuilder = require('./IssueRootBuilder');
|
|
11
11
|
const IssueSubBuilder = require('./IssueSubBuilder');
|
|
12
|
+
const IssueDepinBuilder = require('./IssueDepinBuilder');
|
|
12
13
|
const ReissueBuilder = require('./ReissueBuilder');
|
|
13
14
|
|
|
14
15
|
// Advanced Builders
|
|
@@ -26,6 +27,7 @@ module.exports = {
|
|
|
26
27
|
// Basic Builders
|
|
27
28
|
IssueRootBuilder,
|
|
28
29
|
IssueSubBuilder,
|
|
30
|
+
IssueDepinBuilder,
|
|
29
31
|
ReissueBuilder,
|
|
30
32
|
|
|
31
33
|
// Advanced Builders
|
|
@@ -3,6 +3,21 @@
|
|
|
3
3
|
* Different addresses for mainnet and testnet
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
const MAINNET_NETWORKS = ['xna', 'mainnet', 'xna-pq', 'mainnet-pq'];
|
|
7
|
+
const TESTNET_NETWORKS = ['xna-test', 'testnet', 'regtest', 'xna-pq-test', 'testnet-pq'];
|
|
8
|
+
|
|
9
|
+
function resolveNetworkFamily(network) {
|
|
10
|
+
if (MAINNET_NETWORKS.includes(network)) {
|
|
11
|
+
return 'mainnet';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (TESTNET_NETWORKS.includes(network)) {
|
|
15
|
+
return 'testnet';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
throw new Error(`Unknown network: ${network}`);
|
|
19
|
+
}
|
|
20
|
+
|
|
6
21
|
const MAINNET_BURN_ADDRESSES = {
|
|
7
22
|
ISSUE_ROOT: 'NbURNXXXXXXXXXXXXXXXXXXXXXXXT65Gdr',
|
|
8
23
|
ISSUE_SUB: 'NXissueSubAssetXXXXXXXXXXXXXX6B2JF',
|
|
@@ -32,11 +47,12 @@ const TESTNET_BURN_ADDRESSES = {
|
|
|
32
47
|
/**
|
|
33
48
|
* Get burn address for an operation and network
|
|
34
49
|
* @param {string} operationType - Operation type (e.g., 'ISSUE_ROOT')
|
|
35
|
-
* @param {string} network - Network type ('xna' or 'xna-test')
|
|
50
|
+
* @param {string} network - Network type ('xna', 'xna-test', 'xna-pq', or 'xna-pq-test')
|
|
36
51
|
* @returns {string} Burn address
|
|
37
52
|
*/
|
|
38
53
|
function getBurnAddress(operationType, network) {
|
|
39
|
-
const
|
|
54
|
+
const family = resolveNetworkFamily(network);
|
|
55
|
+
const addresses = family === 'mainnet' ? MAINNET_BURN_ADDRESSES : TESTNET_BURN_ADDRESSES;
|
|
40
56
|
|
|
41
57
|
const address = addresses[operationType];
|
|
42
58
|
if (!address) {
|
|
@@ -49,17 +65,19 @@ function getBurnAddress(operationType, network) {
|
|
|
49
65
|
/**
|
|
50
66
|
* Check if an address is a burn address
|
|
51
67
|
* @param {string} address - Address to check
|
|
52
|
-
* @param {string} network - Network type ('xna' or 'xna-test')
|
|
68
|
+
* @param {string} network - Network type ('xna', 'xna-test', 'xna-pq', or 'xna-pq-test')
|
|
53
69
|
* @returns {boolean} True if it's a burn address
|
|
54
70
|
*/
|
|
55
71
|
function isBurnAddress(address, network) {
|
|
56
|
-
const
|
|
72
|
+
const family = resolveNetworkFamily(network);
|
|
73
|
+
const addresses = family === 'mainnet' ? MAINNET_BURN_ADDRESSES : TESTNET_BURN_ADDRESSES;
|
|
57
74
|
return Object.values(addresses).includes(address);
|
|
58
75
|
}
|
|
59
76
|
|
|
60
77
|
module.exports = {
|
|
61
78
|
MAINNET_BURN_ADDRESSES,
|
|
62
79
|
TESTNET_BURN_ADDRESSES,
|
|
80
|
+
resolveNetworkFamily,
|
|
63
81
|
getBurnAddress,
|
|
64
82
|
isBurnAddress
|
|
65
83
|
};
|
package/src/constants/index.js
CHANGED
|
@@ -8,6 +8,7 @@ const { ASSET_COSTS, getAssetCost, getUniqueAssetCost, getTaggingCost } = requir
|
|
|
8
8
|
const {
|
|
9
9
|
MAINNET_BURN_ADDRESSES,
|
|
10
10
|
TESTNET_BURN_ADDRESSES,
|
|
11
|
+
resolveNetworkFamily,
|
|
11
12
|
getBurnAddress,
|
|
12
13
|
isBurnAddress
|
|
13
14
|
} = require('./burnAddresses');
|
|
@@ -32,6 +33,7 @@ module.exports = {
|
|
|
32
33
|
// Burn Addresses
|
|
33
34
|
MAINNET_BURN_ADDRESSES,
|
|
34
35
|
TESTNET_BURN_ADDRESSES,
|
|
36
|
+
resolveNetworkFamily,
|
|
35
37
|
getBurnAddress,
|
|
36
38
|
isBurnAddress,
|
|
37
39
|
|
|
@@ -7,17 +7,41 @@ const NETWORKS = {
|
|
|
7
7
|
name: 'xna',
|
|
8
8
|
displayName: 'Neurai Mainnet',
|
|
9
9
|
addressPrefix: 'N',
|
|
10
|
+
pqAddressPrefix: 'nq1',
|
|
10
11
|
assetNameMaxLength: 32,
|
|
11
12
|
defaultRPCPort: 19001,
|
|
12
|
-
coin: 'XNA'
|
|
13
|
+
coin: 'XNA',
|
|
14
|
+
baseNetwork: 'xna'
|
|
13
15
|
},
|
|
14
16
|
TESTNET: {
|
|
15
17
|
name: 'xna-test',
|
|
16
18
|
displayName: 'Neurai Testnet',
|
|
17
19
|
addressPrefix: 't',
|
|
20
|
+
pqAddressPrefix: 'tnq1',
|
|
18
21
|
assetNameMaxLength: 32, // Same as mainnet
|
|
19
22
|
defaultRPCPort: 19101,
|
|
20
|
-
coin: 'TXNA'
|
|
23
|
+
coin: 'TXNA',
|
|
24
|
+
baseNetwork: 'xna-test'
|
|
25
|
+
},
|
|
26
|
+
MAINNET_PQ: {
|
|
27
|
+
name: 'xna-pq',
|
|
28
|
+
displayName: 'Neurai Mainnet PQ',
|
|
29
|
+
addressPrefix: 'N',
|
|
30
|
+
pqAddressPrefix: 'nq1',
|
|
31
|
+
assetNameMaxLength: 32,
|
|
32
|
+
defaultRPCPort: 19001,
|
|
33
|
+
coin: 'XNA',
|
|
34
|
+
baseNetwork: 'xna'
|
|
35
|
+
},
|
|
36
|
+
TESTNET_PQ: {
|
|
37
|
+
name: 'xna-pq-test',
|
|
38
|
+
displayName: 'Neurai Testnet PQ',
|
|
39
|
+
addressPrefix: 't',
|
|
40
|
+
pqAddressPrefix: 'tnq1',
|
|
41
|
+
assetNameMaxLength: 32,
|
|
42
|
+
defaultRPCPort: 19101,
|
|
43
|
+
coin: 'TXNA',
|
|
44
|
+
baseNetwork: 'xna-test'
|
|
21
45
|
}
|
|
22
46
|
};
|
|
23
47
|
|
|
@@ -57,6 +81,13 @@ const ASSET_NAME_RULES = {
|
|
|
57
81
|
maxLength: 30,
|
|
58
82
|
pattern: /^[A-Z0-9_.]+$/,
|
|
59
83
|
prefix: '$'
|
|
84
|
+
},
|
|
85
|
+
DEPIN: {
|
|
86
|
+
minLength: 3,
|
|
87
|
+
maxLength: 120,
|
|
88
|
+
pattern: /^[A-Z0-9_.]+$/,
|
|
89
|
+
prefix: '&',
|
|
90
|
+
separator: '/'
|
|
60
91
|
}
|
|
61
92
|
};
|
|
62
93
|
|
|
@@ -75,7 +106,7 @@ const ASSET_LIMITS = {
|
|
|
75
106
|
|
|
76
107
|
/**
|
|
77
108
|
* Get network configuration
|
|
78
|
-
* @param {string} networkName - Network name ('xna' or 'xna-test')
|
|
109
|
+
* @param {string} networkName - Network name ('xna', 'xna-test', 'xna-pq', or 'xna-pq-test')
|
|
79
110
|
* @returns {object} Network configuration
|
|
80
111
|
*/
|
|
81
112
|
function getNetworkConfig(networkName) {
|
|
@@ -83,6 +114,10 @@ function getNetworkConfig(networkName) {
|
|
|
83
114
|
return NETWORKS.MAINNET;
|
|
84
115
|
} else if (networkName === 'xna-test' || networkName === 'testnet') {
|
|
85
116
|
return NETWORKS.TESTNET;
|
|
117
|
+
} else if (networkName === 'xna-pq' || networkName === 'mainnet-pq') {
|
|
118
|
+
return NETWORKS.MAINNET_PQ;
|
|
119
|
+
} else if (networkName === 'xna-pq-test' || networkName === 'testnet-pq') {
|
|
120
|
+
return NETWORKS.TESTNET_PQ;
|
|
86
121
|
} else {
|
|
87
122
|
throw new Error(`Unknown network: ${networkName}`);
|
|
88
123
|
}
|
|
@@ -91,10 +126,14 @@ function getNetworkConfig(networkName) {
|
|
|
91
126
|
/**
|
|
92
127
|
* Detect network from address prefix
|
|
93
128
|
* @param {string} address - Neurai address
|
|
94
|
-
* @returns {string} Network name ('xna' or 'xna-test')
|
|
129
|
+
* @returns {string} Network name ('xna', 'xna-test', 'xna-pq', or 'xna-pq-test')
|
|
95
130
|
*/
|
|
96
131
|
function detectNetworkFromAddress(address) {
|
|
97
|
-
if (address.startsWith(
|
|
132
|
+
if (address.startsWith(NETWORKS.MAINNET_PQ.pqAddressPrefix)) {
|
|
133
|
+
return 'xna-pq';
|
|
134
|
+
} else if (address.startsWith(NETWORKS.TESTNET_PQ.pqAddressPrefix)) {
|
|
135
|
+
return 'xna-pq-test';
|
|
136
|
+
} else if (address.startsWith('N')) {
|
|
98
137
|
return 'xna';
|
|
99
138
|
} else if (address.startsWith('t')) {
|
|
100
139
|
return 'xna-test';
|
package/src/index.js
CHANGED
|
@@ -79,6 +79,15 @@ class BurnManager {
|
|
|
79
79
|
return this.getBurnInfo('ISSUE_UNIQUE', count);
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Get burn info for DEPIN asset issuance
|
|
84
|
+
* DEPIN assets reuse the UNIQUE burn amount/address.
|
|
85
|
+
* @returns {object} { address, amount }
|
|
86
|
+
*/
|
|
87
|
+
getIssueDepinBurn() {
|
|
88
|
+
return this.getIssueUniqueBurn(1);
|
|
89
|
+
}
|
|
90
|
+
|
|
82
91
|
/**
|
|
83
92
|
* Get burn info for QUALIFIER asset issuance
|
|
84
93
|
* @returns {object} { address, amount }
|
|
@@ -387,6 +387,53 @@ class AssetQueries {
|
|
|
387
387
|
}
|
|
388
388
|
}
|
|
389
389
|
|
|
390
|
+
/**
|
|
391
|
+
* List DEPIN holders with validity status
|
|
392
|
+
* @param {string} assetName - DEPIN asset name
|
|
393
|
+
* @returns {Promise<Array>} Array of holder objects
|
|
394
|
+
*/
|
|
395
|
+
async listDepinHolders(assetName) {
|
|
396
|
+
if (!assetName) {
|
|
397
|
+
throw new Error('DEPIN asset name is required');
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
try {
|
|
401
|
+
const result = await this.rpc('listdepinholders', [assetName]);
|
|
402
|
+
return result || [];
|
|
403
|
+
} catch (error) {
|
|
404
|
+
if (error.message && error.message.includes('not found')) {
|
|
405
|
+
throw new AssetNotFoundError(
|
|
406
|
+
`DEPIN asset ${assetName} not found on blockchain`,
|
|
407
|
+
assetName
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
throw new Error(`Failed to list DEPIN holders: ${error.message}`);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Check DEPIN validity for a specific address
|
|
416
|
+
* @param {string} assetName - DEPIN asset name
|
|
417
|
+
* @param {string} address - Address to query
|
|
418
|
+
* @returns {Promise<object>} Validity information
|
|
419
|
+
*/
|
|
420
|
+
async checkDepinValidity(assetName, address) {
|
|
421
|
+
if (!assetName) {
|
|
422
|
+
throw new Error('DEPIN asset name is required');
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if (!address) {
|
|
426
|
+
throw new Error('Address is required');
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
try {
|
|
430
|
+
const result = await this.rpc('checkdepinvalidity', [assetName, address]);
|
|
431
|
+
return result || { has_asset: false };
|
|
432
|
+
} catch (error) {
|
|
433
|
+
throw new Error(`Failed to check DEPIN validity: ${error.message}`);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
390
437
|
/**
|
|
391
438
|
* Get total count of assets on blockchain
|
|
392
439
|
* @returns {Promise<number>} Total asset count
|
|
@@ -421,7 +468,7 @@ class AssetQueries {
|
|
|
421
468
|
/**
|
|
422
469
|
* Get asset type from name
|
|
423
470
|
* @param {string} assetName - Asset name
|
|
424
|
-
* @returns {string} Asset type ('ROOT', 'SUB', 'UNIQUE', 'QUALIFIER', 'RESTRICTED', 'OWNER')
|
|
471
|
+
* @returns {string} Asset type ('ROOT', 'SUB', 'UNIQUE', 'QUALIFIER', 'RESTRICTED', 'DEPIN', 'OWNER')
|
|
425
472
|
*/
|
|
426
473
|
getAssetType(assetName) {
|
|
427
474
|
if (!assetName) {
|
|
@@ -434,6 +481,8 @@ class AssetQueries {
|
|
|
434
481
|
return assetName.includes('/') ? 'SUB_QUALIFIER' : 'QUALIFIER';
|
|
435
482
|
} else if (assetName.startsWith('$')) {
|
|
436
483
|
return 'RESTRICTED';
|
|
484
|
+
} else if (assetName.startsWith('&')) {
|
|
485
|
+
return 'DEPIN';
|
|
437
486
|
} else if (assetName.includes('#')) {
|
|
438
487
|
return 'UNIQUE';
|
|
439
488
|
} else if (assetName.includes('/')) {
|
|
@@ -15,6 +15,7 @@ class AssetNameParser {
|
|
|
15
15
|
const isOwner = name.endsWith('!');
|
|
16
16
|
const isRestricted = name.startsWith('$');
|
|
17
17
|
const isQualifier = name.startsWith('#');
|
|
18
|
+
const isDepin = name.startsWith('&');
|
|
18
19
|
const cleanName = isOwner ? name.slice(0, -1) : name;
|
|
19
20
|
|
|
20
21
|
let type;
|
|
@@ -40,6 +41,15 @@ class AssetNameParser {
|
|
|
40
41
|
// RESTRICTED: $NAME
|
|
41
42
|
type = AssetType.RESTRICTED;
|
|
42
43
|
prefix = '$';
|
|
44
|
+
} else if (isDepin) {
|
|
45
|
+
// DEPIN: &NAME or &NAME/SUB
|
|
46
|
+
type = AssetType.DEPIN;
|
|
47
|
+
prefix = '&';
|
|
48
|
+
if (cleanName.includes('/')) {
|
|
49
|
+
const parts = cleanName.split('/');
|
|
50
|
+
parent = parts[0];
|
|
51
|
+
subName = parts.slice(1).join('/');
|
|
52
|
+
}
|
|
43
53
|
} else if (cleanName.includes('#')) {
|
|
44
54
|
// UNIQUE: ROOT#TAG
|
|
45
55
|
type = AssetType.UNIQUE;
|
|
@@ -71,6 +81,7 @@ class AssetNameParser {
|
|
|
71
81
|
prefix,
|
|
72
82
|
isOwner: true,
|
|
73
83
|
isRestricted: cleanName.startsWith('$'),
|
|
84
|
+
isDepin: cleanName.startsWith('&'),
|
|
74
85
|
isQualifier: false,
|
|
75
86
|
fullName: name,
|
|
76
87
|
baseName: cleanName
|
|
@@ -86,6 +97,7 @@ class AssetNameParser {
|
|
|
86
97
|
prefix,
|
|
87
98
|
isOwner,
|
|
88
99
|
isRestricted,
|
|
100
|
+
isDepin,
|
|
89
101
|
isQualifier,
|
|
90
102
|
fullName: name,
|
|
91
103
|
baseName: cleanName
|
|
@@ -179,7 +191,16 @@ class AssetNameParser {
|
|
|
179
191
|
* @returns {boolean} True if sub-asset
|
|
180
192
|
*/
|
|
181
193
|
static isSub(name) {
|
|
182
|
-
return name.includes('/') && !name.startsWith('#');
|
|
194
|
+
return name.includes('/') && !name.startsWith('#') && !name.startsWith('&');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Check if asset is a DEPIN asset
|
|
199
|
+
* @param {string} name - Asset name
|
|
200
|
+
* @returns {boolean} True if DEPIN
|
|
201
|
+
*/
|
|
202
|
+
static isDepin(name) {
|
|
203
|
+
return name.startsWith('&');
|
|
183
204
|
}
|
|
184
205
|
|
|
185
206
|
/**
|
|
@@ -40,13 +40,21 @@ class NetworkDetector {
|
|
|
40
40
|
/**
|
|
41
41
|
* Detect network from address
|
|
42
42
|
* @param {string} address - Neurai address
|
|
43
|
-
* @returns {string} Network name ('xna' or 'xna-test')
|
|
43
|
+
* @returns {string} Network name ('xna', 'xna-test', 'xna-pq', or 'xna-pq-test')
|
|
44
44
|
*/
|
|
45
45
|
static detectFromAddress(address) {
|
|
46
46
|
if (!address || typeof address !== 'string') {
|
|
47
47
|
throw new Error('Address must be a non-empty string');
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
if (address.startsWith(NETWORKS.MAINNET_PQ.pqAddressPrefix)) {
|
|
51
|
+
return 'xna-pq';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (address.startsWith(NETWORKS.TESTNET_PQ.pqAddressPrefix)) {
|
|
55
|
+
return 'xna-pq-test';
|
|
56
|
+
}
|
|
57
|
+
|
|
50
58
|
// Mainnet addresses start with 'N'
|
|
51
59
|
if (address.startsWith(NETWORKS.MAINNET.addressPrefix)) {
|
|
52
60
|
return 'xna';
|
|
@@ -63,7 +71,7 @@ class NetworkDetector {
|
|
|
63
71
|
/**
|
|
64
72
|
* Detect network from multiple addresses
|
|
65
73
|
* @param {string[]} addresses - Array of addresses
|
|
66
|
-
* @returns {string} Network name ('xna' or 'xna-test')
|
|
74
|
+
* @returns {string} Network name ('xna', 'xna-test', 'xna-pq', or 'xna-pq-test')
|
|
67
75
|
*/
|
|
68
76
|
static detectFromAddresses(addresses) {
|
|
69
77
|
if (!Array.isArray(addresses) || addresses.length === 0) {
|
|
@@ -87,7 +95,7 @@ class NetworkDetector {
|
|
|
87
95
|
/**
|
|
88
96
|
* Validate that addresses match expected network
|
|
89
97
|
* @param {string[]} addresses - Array of addresses
|
|
90
|
-
* @param {string} expectedNetwork - Expected network ('xna' or 'xna-test')
|
|
98
|
+
* @param {string} expectedNetwork - Expected network ('xna', 'xna-test', 'xna-pq', or 'xna-pq-test')
|
|
91
99
|
* @returns {boolean} True if all addresses match network
|
|
92
100
|
*/
|
|
93
101
|
static validateAddressesNetwork(addresses, expectedNetwork) {
|
|
@@ -117,6 +125,10 @@ class NetworkDetector {
|
|
|
117
125
|
return NETWORKS.MAINNET;
|
|
118
126
|
} else if (network === 'xna-test' || network === 'testnet') {
|
|
119
127
|
return NETWORKS.TESTNET;
|
|
128
|
+
} else if (network === 'xna-pq' || network === 'mainnet-pq') {
|
|
129
|
+
return NETWORKS.MAINNET_PQ;
|
|
130
|
+
} else if (network === 'xna-pq-test' || network === 'testnet-pq') {
|
|
131
|
+
return NETWORKS.TESTNET_PQ;
|
|
120
132
|
} else {
|
|
121
133
|
throw new Error(`Unknown network: ${network}`);
|
|
122
134
|
}
|
|
@@ -128,7 +140,7 @@ class NetworkDetector {
|
|
|
128
140
|
* @returns {boolean} True if mainnet
|
|
129
141
|
*/
|
|
130
142
|
static isMainnet(network) {
|
|
131
|
-
return network === 'xna' || network === 'mainnet';
|
|
143
|
+
return network === 'xna' || network === 'mainnet' || network === 'xna-pq' || network === 'mainnet-pq';
|
|
132
144
|
}
|
|
133
145
|
|
|
134
146
|
/**
|
|
@@ -137,7 +149,11 @@ class NetworkDetector {
|
|
|
137
149
|
* @returns {boolean} True if testnet
|
|
138
150
|
*/
|
|
139
151
|
static isTestnet(network) {
|
|
140
|
-
return network === 'xna-test' ||
|
|
152
|
+
return network === 'xna-test' ||
|
|
153
|
+
network === 'testnet' ||
|
|
154
|
+
network === 'regtest' ||
|
|
155
|
+
network === 'xna-pq-test' ||
|
|
156
|
+
network === 'testnet-pq';
|
|
141
157
|
}
|
|
142
158
|
}
|
|
143
159
|
|
|
@@ -240,6 +240,77 @@ class AssetNameValidator {
|
|
|
240
240
|
return true;
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
+
/**
|
|
244
|
+
* Validate DEPIN asset name
|
|
245
|
+
* Format: &NAME or &NAME/SUB[/...]
|
|
246
|
+
*/
|
|
247
|
+
static validateDepin(name) {
|
|
248
|
+
if (!name || typeof name !== 'string') {
|
|
249
|
+
throw new InvalidAssetNameError('DEPIN asset name must be a non-empty string', name);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (!name.startsWith(ASSET_NAME_RULES.DEPIN.prefix)) {
|
|
253
|
+
throw new InvalidAssetNameError(
|
|
254
|
+
`DEPIN asset must start with ${ASSET_NAME_RULES.DEPIN.prefix}`,
|
|
255
|
+
name
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (name.length > ASSET_NAME_RULES.DEPIN.maxLength) {
|
|
260
|
+
throw new InvalidAssetNameError(
|
|
261
|
+
`DEPIN asset name cannot exceed ${ASSET_NAME_RULES.DEPIN.maxLength} characters`,
|
|
262
|
+
name
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const parts = name.split(ASSET_NAME_RULES.DEPIN.separator);
|
|
267
|
+
if (parts.length === 0) {
|
|
268
|
+
throw new InvalidAssetNameError('DEPIN asset name is invalid', name);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const rootPart = parts[0].substring(1);
|
|
272
|
+
if (rootPart.length < ASSET_NAME_RULES.DEPIN.minLength) {
|
|
273
|
+
throw new InvalidAssetNameError(
|
|
274
|
+
`DEPIN root name must be at least ${ASSET_NAME_RULES.DEPIN.minLength} characters`,
|
|
275
|
+
name
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (rootPart !== rootPart.toUpperCase()) {
|
|
280
|
+
throw new InvalidAssetNameError('DEPIN asset name must be uppercase', name);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (!ASSET_NAME_RULES.DEPIN.pattern.test(rootPart)) {
|
|
284
|
+
throw new InvalidAssetNameError(
|
|
285
|
+
'DEPIN asset name can only contain A-Z, 0-9, underscore, and period',
|
|
286
|
+
name
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const subParts = parts.slice(1);
|
|
291
|
+
subParts.forEach(part => {
|
|
292
|
+
if (part.length < ASSET_NAME_RULES.DEPIN.minLength) {
|
|
293
|
+
throw new InvalidAssetNameError(
|
|
294
|
+
`Each DEPIN sub-part must be at least ${ASSET_NAME_RULES.DEPIN.minLength} characters`,
|
|
295
|
+
name
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (part !== part.toUpperCase()) {
|
|
300
|
+
throw new InvalidAssetNameError('DEPIN asset name must be uppercase', name);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (!ASSET_NAME_RULES.DEPIN.pattern.test(part)) {
|
|
304
|
+
throw new InvalidAssetNameError(
|
|
305
|
+
'DEPIN asset name can only contain A-Z, 0-9, underscore, and period',
|
|
306
|
+
name
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
return true;
|
|
312
|
+
}
|
|
313
|
+
|
|
243
314
|
/**
|
|
244
315
|
* Validate owner token name
|
|
245
316
|
* Format: ASSETNAME!
|
|
@@ -255,9 +326,11 @@ class AssetNameValidator {
|
|
|
255
326
|
|
|
256
327
|
const assetName = name.substring(0, name.length - 1);
|
|
257
328
|
|
|
258
|
-
// Validate the asset name part (could be ROOT or
|
|
329
|
+
// Validate the asset name part (could be ROOT, RESTRICTED, or DEPIN)
|
|
259
330
|
if (assetName.startsWith('$')) {
|
|
260
331
|
this.validateRestricted(assetName);
|
|
332
|
+
} else if (assetName.startsWith('&')) {
|
|
333
|
+
this.validateDepin(assetName);
|
|
261
334
|
} else {
|
|
262
335
|
this.validateRoot(assetName);
|
|
263
336
|
}
|
|
@@ -268,7 +341,7 @@ class AssetNameValidator {
|
|
|
268
341
|
/**
|
|
269
342
|
* Auto-detect asset type and validate
|
|
270
343
|
* @param {string} name - Asset name
|
|
271
|
-
* @returns {string} Asset type ('ROOT', 'SUB', 'UNIQUE', 'QUALIFIER', 'RESTRICTED', 'OWNER')
|
|
344
|
+
* @returns {string} Asset type ('ROOT', 'SUB', 'UNIQUE', 'QUALIFIER', 'RESTRICTED', 'DEPIN', 'OWNER')
|
|
272
345
|
*/
|
|
273
346
|
static validateAndDetectType(name) {
|
|
274
347
|
if (name.endsWith('!')) {
|
|
@@ -280,6 +353,9 @@ class AssetNameValidator {
|
|
|
280
353
|
} else if (name.startsWith('$')) {
|
|
281
354
|
this.validateRestricted(name);
|
|
282
355
|
return 'RESTRICTED';
|
|
356
|
+
} else if (name.startsWith('&')) {
|
|
357
|
+
this.validateDepin(name);
|
|
358
|
+
return 'DEPIN';
|
|
283
359
|
} else if (name.includes('#')) {
|
|
284
360
|
this.validateUnique(name);
|
|
285
361
|
return 'UNIQUE';
|
|
@@ -55,6 +55,18 @@ describe('Integration: Asset Lifecycle', () => {
|
|
|
55
55
|
expect(parsed.isRestricted).to.be.true;
|
|
56
56
|
expect(parsed.prefix).to.equal('$');
|
|
57
57
|
});
|
|
58
|
+
|
|
59
|
+
it('should validate and parse DEPIN assets correctly', () => {
|
|
60
|
+
const assetName = '&FRANCE/PARIS';
|
|
61
|
+
|
|
62
|
+
expect(AssetNameValidator.validateDepin(assetName)).to.be.true;
|
|
63
|
+
|
|
64
|
+
const parsed = AssetNameParser.parse(assetName);
|
|
65
|
+
expect(parsed.type).to.equal(12);
|
|
66
|
+
expect(parsed.isDepin).to.be.true;
|
|
67
|
+
expect(parsed.parent).to.equal('&FRANCE');
|
|
68
|
+
expect(parsed.subName).to.equal('PARIS');
|
|
69
|
+
});
|
|
58
70
|
});
|
|
59
71
|
|
|
60
72
|
describe('Amount Conversion Integration', () => {
|
|
@@ -134,6 +146,7 @@ describe('Integration: Asset Lifecycle', () => {
|
|
|
134
146
|
{ name: 'MYTOKEN#NFT', expected: 'UNIQUE' },
|
|
135
147
|
{ name: '#KYC', expected: 'QUALIFIER' },
|
|
136
148
|
{ name: '$SECURITY', expected: 'RESTRICTED' },
|
|
149
|
+
{ name: '&FRANCE/PARIS', expected: 'DEPIN' },
|
|
137
150
|
{ name: 'MYTOKEN!', expected: 'OWNER' }
|
|
138
151
|
];
|
|
139
152
|
|
|
@@ -210,6 +223,20 @@ describe('Integration: Asset Lifecycle', () => {
|
|
|
210
223
|
expect(parsed.type).to.equal(4);
|
|
211
224
|
expect(parsed.isQualifier).to.be.true;
|
|
212
225
|
});
|
|
226
|
+
|
|
227
|
+
it('should handle depin workflow', () => {
|
|
228
|
+
const depinName = '&FRANCE/PARIS';
|
|
229
|
+
|
|
230
|
+
expect(AssetNameValidator.validateDepin(depinName)).to.be.true;
|
|
231
|
+
expect(AssetNameParser.isDepin(depinName)).to.be.true;
|
|
232
|
+
|
|
233
|
+
const ownerToken = AssetNameParser.getOwnerTokenName(depinName);
|
|
234
|
+
expect(ownerToken).to.equal('&FRANCE/PARIS!');
|
|
235
|
+
|
|
236
|
+
const parsed = AssetNameParser.parse(depinName);
|
|
237
|
+
expect(parsed.type).to.equal(12);
|
|
238
|
+
expect(parsed.parent).to.equal('&FRANCE');
|
|
239
|
+
});
|
|
213
240
|
});
|
|
214
241
|
|
|
215
242
|
describe('Error Handling Integration', () => {
|
package/tests/mocks/rpcMock.js
CHANGED
|
@@ -58,7 +58,9 @@ class RPCMock {
|
|
|
58
58
|
'isaddressfrozen': false,
|
|
59
59
|
'checkglobalrestriction': false,
|
|
60
60
|
'getverifierstring': '',
|
|
61
|
-
'isvalidverifierstring': true
|
|
61
|
+
'isvalidverifierstring': true,
|
|
62
|
+
'listdepinholders': [],
|
|
63
|
+
'checkdepinvalidity': { has_asset: false }
|
|
62
64
|
};
|
|
63
65
|
|
|
64
66
|
return defaults[method] || null;
|
|
@@ -116,6 +116,11 @@ describe('NeuraiAssets', () => {
|
|
|
116
116
|
expect(type).to.equal('RESTRICTED');
|
|
117
117
|
});
|
|
118
118
|
|
|
119
|
+
it('should detect DEPIN asset type', () => {
|
|
120
|
+
const type = assets.getAssetType('&FRANCE/PARIS');
|
|
121
|
+
expect(type).to.equal('DEPIN');
|
|
122
|
+
});
|
|
123
|
+
|
|
119
124
|
it('should detect OWNER token type', () => {
|
|
120
125
|
const type = assets.getAssetType('MYTOKEN!');
|
|
121
126
|
expect(type).to.equal('OWNER');
|
|
@@ -165,12 +170,31 @@ describe('NeuraiAssets', () => {
|
|
|
165
170
|
const result = await testAssets.assetExists('MYTOKEN');
|
|
166
171
|
expect(result).to.exist;
|
|
167
172
|
});
|
|
173
|
+
|
|
174
|
+
it('should call listDepinHolders', async () => {
|
|
175
|
+
const holders = [{ address: 't1...', amount: 1, valid: 1 }];
|
|
176
|
+
const rpcMock = createMockRPC({ 'listdepinholders': holders });
|
|
177
|
+
const testAssets = new NeuraiAssets(rpcMock);
|
|
178
|
+
|
|
179
|
+
const result = await testAssets.listDepinHolders('&FRANCE');
|
|
180
|
+
expect(result).to.deep.equal(holders);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('should call checkDepinValidity', async () => {
|
|
184
|
+
const validity = { has_asset: true, amount: 1, valid: 1, blocked: false };
|
|
185
|
+
const rpcMock = createMockRPC({ 'checkdepinvalidity': validity });
|
|
186
|
+
const testAssets = new NeuraiAssets(rpcMock);
|
|
187
|
+
|
|
188
|
+
const result = await testAssets.checkDepinValidity('&FRANCE', 't1...');
|
|
189
|
+
expect(result).to.deep.equal(validity);
|
|
190
|
+
});
|
|
168
191
|
});
|
|
169
192
|
|
|
170
193
|
describe('Method Availability', () => {
|
|
171
194
|
it('should have all ROOT asset methods', () => {
|
|
172
195
|
expect(assets.createRootAsset).to.be.a('function');
|
|
173
196
|
expect(assets.createSubAsset).to.be.a('function');
|
|
197
|
+
expect(assets.createDepinAsset).to.be.a('function');
|
|
174
198
|
expect(assets.reissueAsset).to.be.a('function');
|
|
175
199
|
});
|
|
176
200
|
|
|
@@ -209,6 +233,8 @@ describe('NeuraiAssets', () => {
|
|
|
209
233
|
expect(assets.isValidVerifierString).to.be.a('function');
|
|
210
234
|
expect(assets.getSnapshotRequest).to.be.a('function');
|
|
211
235
|
expect(assets.cancelSnapshotRequest).to.be.a('function');
|
|
236
|
+
expect(assets.listDepinHolders).to.be.a('function');
|
|
237
|
+
expect(assets.checkDepinValidity).to.be.a('function');
|
|
212
238
|
expect(assets.assetExists).to.be.a('function');
|
|
213
239
|
expect(assets.getAssetType).to.be.a('function');
|
|
214
240
|
expect(assets.getAssetCount).to.be.a('function');
|
|
@@ -58,6 +58,15 @@ describe('AssetNameParser', () => {
|
|
|
58
58
|
expect(result.name).to.equal('$SECURITY');
|
|
59
59
|
});
|
|
60
60
|
|
|
61
|
+
it('should parse DEPIN asset names', () => {
|
|
62
|
+
const result = AssetNameParser.parse('&FRANCE/PARIS');
|
|
63
|
+
expect(result.type).to.equal(AssetType.DEPIN);
|
|
64
|
+
expect(result.prefix).to.equal('&');
|
|
65
|
+
expect(result.isDepin).to.be.true;
|
|
66
|
+
expect(result.parent).to.equal('&FRANCE');
|
|
67
|
+
expect(result.subName).to.equal('PARIS');
|
|
68
|
+
});
|
|
69
|
+
|
|
61
70
|
it('should parse OWNER token names', () => {
|
|
62
71
|
const result = AssetNameParser.parse('MYTOKEN!');
|
|
63
72
|
expect(result.type).to.equal(AssetType.OWNER);
|
|
@@ -73,6 +82,15 @@ describe('AssetNameParser', () => {
|
|
|
73
82
|
expect(result.isRestricted).to.be.true;
|
|
74
83
|
expect(result.baseName).to.equal('$SECURITY');
|
|
75
84
|
});
|
|
85
|
+
|
|
86
|
+
it('should parse DEPIN OWNER token names', () => {
|
|
87
|
+
const result = AssetNameParser.parse('&FRANCE!');
|
|
88
|
+
expect(result.type).to.equal(AssetType.OWNER);
|
|
89
|
+
expect(result.isOwner).to.be.true;
|
|
90
|
+
expect(result.isDepin).to.be.true;
|
|
91
|
+
expect(result.baseType).to.equal(AssetType.DEPIN);
|
|
92
|
+
expect(result.baseName).to.equal('&FRANCE');
|
|
93
|
+
});
|
|
76
94
|
});
|
|
77
95
|
|
|
78
96
|
describe('getType', () => {
|
|
@@ -83,6 +101,7 @@ describe('AssetNameParser', () => {
|
|
|
83
101
|
expect(AssetNameParser.getType('#KYC')).to.equal(AssetType.QUALIFIER);
|
|
84
102
|
expect(AssetNameParser.getType('#KYC/TIER1')).to.equal(AssetType.SUB_QUALIFIER);
|
|
85
103
|
expect(AssetNameParser.getType('$SECURITY')).to.equal(AssetType.RESTRICTED);
|
|
104
|
+
expect(AssetNameParser.getType('&FRANCE/PARIS')).to.equal(AssetType.DEPIN);
|
|
86
105
|
expect(AssetNameParser.getType('MYTOKEN!')).to.equal(AssetType.OWNER);
|
|
87
106
|
});
|
|
88
107
|
});
|
|
@@ -103,6 +122,10 @@ describe('AssetNameParser', () => {
|
|
|
103
122
|
it('should return parent for SUB_QUALIFIER', () => {
|
|
104
123
|
expect(AssetNameParser.getParent('#KYC/TIER1')).to.equal('#KYC');
|
|
105
124
|
});
|
|
125
|
+
|
|
126
|
+
it('should return parent for DEPIN sub-assets', () => {
|
|
127
|
+
expect(AssetNameParser.getParent('&FRANCE/PARIS')).to.equal('&FRANCE');
|
|
128
|
+
});
|
|
106
129
|
});
|
|
107
130
|
|
|
108
131
|
describe('isOwnerToken', () => {
|
|
@@ -121,6 +144,7 @@ describe('AssetNameParser', () => {
|
|
|
121
144
|
it('should add ! to get owner token name', () => {
|
|
122
145
|
expect(AssetNameParser.getOwnerTokenName('MYTOKEN')).to.equal('MYTOKEN!');
|
|
123
146
|
expect(AssetNameParser.getOwnerTokenName('$SECURITY')).to.equal('SECURITY!');
|
|
147
|
+
expect(AssetNameParser.getOwnerTokenName('&FRANCE')).to.equal('&FRANCE!');
|
|
124
148
|
});
|
|
125
149
|
|
|
126
150
|
it('should return as-is if already an owner token', () => {
|
|
@@ -184,6 +208,19 @@ describe('AssetNameParser', () => {
|
|
|
184
208
|
it('should return false for non-sub assets', () => {
|
|
185
209
|
expect(AssetNameParser.isSub('MYTOKEN')).to.be.false;
|
|
186
210
|
expect(AssetNameParser.isSub('#KYC/TIER1')).to.be.false;
|
|
211
|
+
expect(AssetNameParser.isSub('&FRANCE/PARIS')).to.be.false;
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
describe('isDepin', () => {
|
|
216
|
+
it('should identify depin assets', () => {
|
|
217
|
+
expect(AssetNameParser.isDepin('&FRANCE')).to.be.true;
|
|
218
|
+
expect(AssetNameParser.isDepin('&FRANCE/PARIS')).to.be.true;
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('should return false for non-depin assets', () => {
|
|
222
|
+
expect(AssetNameParser.isDepin('MYTOKEN')).to.be.false;
|
|
223
|
+
expect(AssetNameParser.isDepin('$SECURITY')).to.be.false;
|
|
187
224
|
});
|
|
188
225
|
});
|
|
189
226
|
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for NetworkDetector and network constants.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const { expect } = require('chai');
|
|
6
|
+
const NetworkDetector = require('../../../src/utils/networkDetector');
|
|
7
|
+
const {
|
|
8
|
+
getNetworkConfig,
|
|
9
|
+
detectNetworkFromAddress,
|
|
10
|
+
getBurnAddress
|
|
11
|
+
} = require('../../../src/constants');
|
|
12
|
+
|
|
13
|
+
describe('NetworkDetector', () => {
|
|
14
|
+
describe('detectFromAddress', () => {
|
|
15
|
+
it('should detect legacy mainnet addresses', () => {
|
|
16
|
+
expect(NetworkDetector.detectFromAddress('NExampleLegacyAddress')).to.equal('xna');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should detect legacy testnet addresses', () => {
|
|
20
|
+
expect(NetworkDetector.detectFromAddress('tExampleLegacyAddress')).to.equal('xna-test');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should detect PQ mainnet addresses', () => {
|
|
24
|
+
expect(NetworkDetector.detectFromAddress('nq1examplepqaddress')).to.equal('xna-pq');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should detect PQ testnet addresses', () => {
|
|
28
|
+
expect(NetworkDetector.detectFromAddress('tnq1examplepqaddress')).to.equal('xna-pq-test');
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('detectFromAddresses', () => {
|
|
33
|
+
it('should accept multiple PQ addresses from the same network', () => {
|
|
34
|
+
const result = NetworkDetector.detectFromAddresses([
|
|
35
|
+
'nq1firstpqaddress',
|
|
36
|
+
'nq1secondpqaddress'
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
expect(result).to.equal('xna-pq');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should reject mixed PQ networks', () => {
|
|
43
|
+
expect(() => NetworkDetector.detectFromAddresses([
|
|
44
|
+
'nq1firstpqaddress',
|
|
45
|
+
'tnq1secondpqaddress'
|
|
46
|
+
])).to.throw('Mixed network addresses detected');
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('validateAddressesNetwork', () => {
|
|
51
|
+
it('should validate PQ addresses against expected network', () => {
|
|
52
|
+
expect(NetworkDetector.validateAddressesNetwork(['nq1examplepqaddress'], 'xna-pq')).to.be.true;
|
|
53
|
+
expect(NetworkDetector.validateAddressesNetwork(['tnq1examplepqaddress'], 'xna-pq-test')).to.be.true;
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('getNetworkConfig', () => {
|
|
58
|
+
it('should return PQ network config for mainnet', () => {
|
|
59
|
+
const config = getNetworkConfig('xna-pq');
|
|
60
|
+
expect(config.name).to.equal('xna-pq');
|
|
61
|
+
expect(config.pqAddressPrefix).to.equal('nq1');
|
|
62
|
+
expect(config.baseNetwork).to.equal('xna');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should return PQ network config for testnet', () => {
|
|
66
|
+
const config = getNetworkConfig('xna-pq-test');
|
|
67
|
+
expect(config.name).to.equal('xna-pq-test');
|
|
68
|
+
expect(config.pqAddressPrefix).to.equal('tnq1');
|
|
69
|
+
expect(config.baseNetwork).to.equal('xna-test');
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe('detectNetworkFromAddress', () => {
|
|
74
|
+
it('should detect PQ addresses through constants helper', () => {
|
|
75
|
+
expect(detectNetworkFromAddress('nq1examplepqaddress')).to.equal('xna-pq');
|
|
76
|
+
expect(detectNetworkFromAddress('tnq1examplepqaddress')).to.equal('xna-pq-test');
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('getBurnAddress', () => {
|
|
81
|
+
it('should use mainnet burn addresses for xna-pq', () => {
|
|
82
|
+
expect(getBurnAddress('ISSUE_ROOT', 'xna-pq')).to.equal('NbURNXXXXXXXXXXXXXXXXXXXXXXXT65Gdr');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should use testnet burn addresses for xna-pq-test', () => {
|
|
86
|
+
expect(getBurnAddress('ISSUE_ROOT', 'xna-pq-test')).to.equal('tBURNXXXXXXXXXXXXXXXXXXXXXXXVZLroy');
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -172,10 +172,34 @@ describe('AssetNameValidator', () => {
|
|
|
172
172
|
});
|
|
173
173
|
});
|
|
174
174
|
|
|
175
|
+
describe('validateDepin', () => {
|
|
176
|
+
it('should validate correct DEPIN asset names', () => {
|
|
177
|
+
expect(AssetNameValidator.validateDepin('&FRANCE')).to.be.true;
|
|
178
|
+
expect(AssetNameValidator.validateDepin('&FRANCE/PARIS')).to.be.true;
|
|
179
|
+
expect(AssetNameValidator.validateDepin('&NODE_1')).to.be.true;
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('should reject names without & prefix', () => {
|
|
183
|
+
expect(() => AssetNameValidator.validateDepin('FRANCE'))
|
|
184
|
+
.to.throw(InvalidAssetNameError, 'must start with &');
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('should reject lowercase depin names', () => {
|
|
188
|
+
expect(() => AssetNameValidator.validateDepin('&france'))
|
|
189
|
+
.to.throw(InvalidAssetNameError, 'must be uppercase');
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('should reject depin sub-parts shorter than 3 characters', () => {
|
|
193
|
+
expect(() => AssetNameValidator.validateDepin('&FRANCE/AB'))
|
|
194
|
+
.to.throw(InvalidAssetNameError, 'Each DEPIN sub-part must be at least 3 characters');
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
175
198
|
describe('validateOwnerToken', () => {
|
|
176
199
|
it('should validate correct owner token names', () => {
|
|
177
200
|
expect(AssetNameValidator.validateOwnerToken('MYTOKEN!')).to.be.true;
|
|
178
201
|
expect(AssetNameValidator.validateOwnerToken('$SECURITY!')).to.be.true;
|
|
202
|
+
expect(AssetNameValidator.validateOwnerToken('&FRANCE!')).to.be.true;
|
|
179
203
|
});
|
|
180
204
|
|
|
181
205
|
it('should reject names without ! suffix', () => {
|
|
@@ -220,6 +244,11 @@ describe('AssetNameValidator', () => {
|
|
|
220
244
|
expect(type).to.equal('RESTRICTED');
|
|
221
245
|
});
|
|
222
246
|
|
|
247
|
+
it('should detect and validate DEPIN assets', () => {
|
|
248
|
+
const type = AssetNameValidator.validateAndDetectType('&FRANCE/PARIS');
|
|
249
|
+
expect(type).to.equal('DEPIN');
|
|
250
|
+
});
|
|
251
|
+
|
|
223
252
|
it('should detect and validate OWNER tokens', () => {
|
|
224
253
|
const type = AssetNameValidator.validateAndDetectType('MYTOKEN!');
|
|
225
254
|
expect(type).to.equal('OWNER');
|