@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.
@@ -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();
@@ -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.2",
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": "^10.2.0"
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.6"
41
+ "@neuraiproject/neurai-rpc": "^0.4.7"
39
42
  },
40
43
  "engines": {
41
44
  "node": ">=14.0.0"
@@ -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 });
@@ -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 addresses = network === 'xna' ? MAINNET_BURN_ADDRESSES : TESTNET_BURN_ADDRESSES;
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 addresses = network === 'xna' ? MAINNET_BURN_ADDRESSES : TESTNET_BURN_ADDRESSES;
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
  };
@@ -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('N')) {
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
@@ -9,6 +9,7 @@
9
9
  * - UNIQUE assets (NFTs)
10
10
  * - QUALIFIER assets (KYC/compliance tags)
11
11
  * - RESTRICTED assets (security tokens with compliance)
12
+ * - DEPIN assets (soulbound assets)
12
13
  *
13
14
  * @example
14
15
  * const NeuraiAssets = require('@neuraiproject/neurai-assets');
@@ -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' || network === 'testnet' || network === 'regtest';
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 RESTRICTED)
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', () => {
@@ -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');