@neuraiproject/neurai-assets 1.0.1 → 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/README.md +18 -13
- package/examples/10-depin-post-quantum.js +83 -0
- package/examples/README.md +20 -2
- package/package.json +7 -4
- package/src/NeuraiAssets.js +38 -2
- package/src/builders/BaseAssetTransactionBuilder.js +3 -0
- package/src/builders/FreezeAddressBuilder.js +5 -22
- package/src/builders/IssueDepinBuilder.js +144 -0
- package/src/builders/IssueQualifierBuilder.js +31 -46
- package/src/builders/IssueRestrictedBuilder.js +59 -25
- package/src/builders/IssueUniqueBuilder.js +6 -13
- package/src/builders/ReissueBuilder.js +3 -1
- package/src/builders/ReissueRestrictedBuilder.js +5 -15
- package/src/builders/TagAddressBuilder.js +38 -51
- package/src/builders/index.js +2 -0
- package/src/constants/burnAddresses.js +22 -4
- package/src/constants/fees.js +2 -2
- package/src/constants/index.js +2 -0
- package/src/constants/networks.js +46 -7
- 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 +25 -1
- package/src/utils/networkDetector.js +23 -7
- package/src/utils/outputFormatter.js +65 -19
- 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 +38 -1
- package/tests/unit/utils/networkDetector.test.js +89 -0
- package/tests/unit/validators/assetNameValidator.test.js +29 -0
package/README.md
CHANGED
|
@@ -96,23 +96,23 @@ const result = await assets.reissueAsset({
|
|
|
96
96
|
### Create UNIQUE Assets (NFTs)
|
|
97
97
|
|
|
98
98
|
```javascript
|
|
99
|
+
// Without IPFS metadata
|
|
99
100
|
const result = await assets.createUniqueAssets({
|
|
100
|
-
|
|
101
|
-
assetTags: [
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
hasIpfs: true,
|
|
110
|
-
ipfsHash: 'QmNFT2...'
|
|
111
|
-
}
|
|
112
|
-
]
|
|
101
|
+
rootName: 'MYTOKEN',
|
|
102
|
+
assetTags: ['NFT001', 'NFT002', 'NFT003']
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// With IPFS metadata (ipfsHashes must be same length as assetTags)
|
|
106
|
+
const result = await assets.createUniqueAssets({
|
|
107
|
+
rootName: 'MYTOKEN',
|
|
108
|
+
assetTags: ['NFT001', 'NFT002'],
|
|
109
|
+
ipfsHashes: ['QmNFT1...', 'QmNFT2...']
|
|
113
110
|
});
|
|
114
111
|
```
|
|
115
112
|
|
|
113
|
+
> **Note**: UNIQUE asset properties (`units`, `reissuable`) are always `0` and are
|
|
114
|
+
> set automatically by the node — they cannot be configured per asset.
|
|
115
|
+
|
|
116
116
|
### Create QUALIFIER (KYC Tags)
|
|
117
117
|
|
|
118
118
|
```javascript
|
|
@@ -363,6 +363,11 @@ When you create an asset, an **owner token** is automatically generated (e.g., `
|
|
|
363
363
|
|
|
364
364
|
The library automatically validates that the owner token is returned in each operation to prevent accidental loss.
|
|
365
365
|
|
|
366
|
+
> **UNIQUE assets exception**: When issuing UNIQUE assets (`ROOT#TAG`), the Neurai node
|
|
367
|
+
> returns the owner token automatically as part of processing the `issue_unique` operation.
|
|
368
|
+
> The library does not add a manual return output for this case — doing so would duplicate
|
|
369
|
+
> the owner token in the outputs and cause the transaction to fail with "Assets would be burnt".
|
|
370
|
+
|
|
366
371
|
## Operation Costs
|
|
367
372
|
|
|
368
373
|
| Operation | Cost (XNA burned) |
|
|
@@ -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
|
|
@@ -167,7 +183,8 @@ class NeuraiAssets {
|
|
|
167
183
|
* @returns {Promise<object>} Transaction data
|
|
168
184
|
*/
|
|
169
185
|
async createQualifier(params) {
|
|
170
|
-
const
|
|
186
|
+
const normalized = { ...params, assetName: params.assetName || params.qualifierName };
|
|
187
|
+
const builder = new IssueQualifierBuilder(this.rpc, this._buildParams(normalized));
|
|
171
188
|
return await builder.build();
|
|
172
189
|
}
|
|
173
190
|
|
|
@@ -438,6 +455,25 @@ class NeuraiAssets {
|
|
|
438
455
|
return await this.queries.cancelSnapshotRequest(assetName, blockHeight);
|
|
439
456
|
}
|
|
440
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
|
+
|
|
441
477
|
/**
|
|
442
478
|
* Check if asset exists
|
|
443
479
|
* @param {string} assetName - Asset name
|
|
@@ -450,7 +486,7 @@ class NeuraiAssets {
|
|
|
450
486
|
/**
|
|
451
487
|
* Get asset type from name
|
|
452
488
|
* @param {string} assetName - Asset name
|
|
453
|
-
* @returns {string} Asset type ('ROOT', 'SUB', 'UNIQUE', 'QUALIFIER', 'RESTRICTED', 'OWNER')
|
|
489
|
+
* @returns {string} Asset type ('ROOT', 'SUB', 'UNIQUE', 'QUALIFIER', 'RESTRICTED', 'DEPIN', 'OWNER')
|
|
454
490
|
*/
|
|
455
491
|
getAssetType(assetName) {
|
|
456
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
|
}
|
|
@@ -47,14 +47,7 @@ class FreezeAddressBuilder extends BaseAssetTransactionBuilder {
|
|
|
47
47
|
);
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
//
|
|
51
|
-
const validPrefixes = this.network === 'xna' ? ['N'] : ['m', 'n'];
|
|
52
|
-
if (!validPrefixes.some(prefix => address.startsWith(prefix))) {
|
|
53
|
-
throw new InvalidAddressError(
|
|
54
|
-
`addresses[${index}] has invalid prefix for network ${this.network}`,
|
|
55
|
-
address
|
|
56
|
-
);
|
|
57
|
-
}
|
|
50
|
+
// Address prefix validation is left to the node (varies by network)
|
|
58
51
|
});
|
|
59
52
|
}
|
|
60
53
|
|
|
@@ -162,13 +155,6 @@ class FreezeAddressBuilder extends BaseAssetTransactionBuilder {
|
|
|
162
155
|
outputs.push({ [changeAddress]: parseFloat(xnaChange.toFixed(8)) });
|
|
163
156
|
}
|
|
164
157
|
|
|
165
|
-
// Second: Owner token return (CRITICAL - must return or lost forever!)
|
|
166
|
-
const ownerTokenReturn = this.ownerTokenManager.createOwnerTokenReturnOutput(
|
|
167
|
-
ownerTokenName,
|
|
168
|
-
changeAddress
|
|
169
|
-
);
|
|
170
|
-
outputs.push(ownerTokenReturn);
|
|
171
|
-
|
|
172
158
|
// Last: Freeze/Unfreeze operation
|
|
173
159
|
let operationOutput;
|
|
174
160
|
let targetAddresses = [];
|
|
@@ -180,7 +166,7 @@ class FreezeAddressBuilder extends BaseAssetTransactionBuilder {
|
|
|
180
166
|
asset_name: assetName,
|
|
181
167
|
addresses: targetAddresses
|
|
182
168
|
});
|
|
183
|
-
outputs.push({ [
|
|
169
|
+
outputs.push({ [changeAddress]: operationOutput });
|
|
184
170
|
break;
|
|
185
171
|
|
|
186
172
|
case 'UNFREEZE_ADDRESSES':
|
|
@@ -189,7 +175,7 @@ class FreezeAddressBuilder extends BaseAssetTransactionBuilder {
|
|
|
189
175
|
asset_name: assetName,
|
|
190
176
|
addresses: targetAddresses
|
|
191
177
|
});
|
|
192
|
-
outputs.push({ [
|
|
178
|
+
outputs.push({ [changeAddress]: operationOutput });
|
|
193
179
|
break;
|
|
194
180
|
|
|
195
181
|
case 'FREEZE_ASSET':
|
|
@@ -209,13 +195,10 @@ class FreezeAddressBuilder extends BaseAssetTransactionBuilder {
|
|
|
209
195
|
// 13. Order outputs (protocol requirement)
|
|
210
196
|
const orderedOutputs = this.outputOrderer.order(outputs);
|
|
211
197
|
|
|
212
|
-
// 14.
|
|
213
|
-
this.ownerTokenManager.validateOwnerTokenReturn(inputs, orderedOutputs);
|
|
214
|
-
|
|
215
|
-
// 15. Create raw transaction
|
|
198
|
+
// 14. Create raw transaction
|
|
216
199
|
const rawTx = await this.buildRawTransaction(inputs, orderedOutputs);
|
|
217
200
|
|
|
218
|
-
//
|
|
201
|
+
// 15. Format and return result
|
|
219
202
|
const allUTXOs = [...baseCurrencyUTXOs, ownerTokenUTXO];
|
|
220
203
|
|
|
221
204
|
return this.formatResult(
|
|
@@ -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;
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
* - Quantity: 1-10 units only
|
|
10
10
|
* - Units: Always 0 (non-divisible)
|
|
11
11
|
* - Used to tag addresses for restricted asset compliance
|
|
12
|
-
* -
|
|
12
|
+
* - Root qualifiers do not create owner tokens
|
|
13
|
+
* - Sub-qualifiers consume and return the parent qualifier asset itself
|
|
13
14
|
*/
|
|
14
15
|
|
|
15
16
|
const BaseAssetTransactionBuilder = require('./BaseAssetTransactionBuilder');
|
|
@@ -75,13 +76,14 @@ class IssueQualifierBuilder extends BaseAssetTransactionBuilder {
|
|
|
75
76
|
const isSub = this.isSubQualifier(assetName);
|
|
76
77
|
const parsed = AssetNameParser.parse(assetName);
|
|
77
78
|
|
|
78
|
-
// 3. If sub-qualifier, check parent exists and get
|
|
79
|
-
let
|
|
80
|
-
let
|
|
79
|
+
// 3. If sub-qualifier, check parent exists and get parent qualifier input
|
|
80
|
+
let parentQualifierUTXOs = [];
|
|
81
|
+
let parentQualifierQuantity = null;
|
|
82
|
+
let parentQualifierName = null;
|
|
81
83
|
const addresses = await this._getAddresses();
|
|
82
84
|
|
|
83
85
|
if (isSub) {
|
|
84
|
-
|
|
86
|
+
parentQualifierName = parsed.parent;
|
|
85
87
|
|
|
86
88
|
// Check parent qualifier exists
|
|
87
89
|
const parentExists = await this.assetExists(parentQualifierName);
|
|
@@ -92,18 +94,16 @@ class IssueQualifierBuilder extends BaseAssetTransactionBuilder {
|
|
|
92
94
|
);
|
|
93
95
|
}
|
|
94
96
|
|
|
95
|
-
// Find parent
|
|
96
|
-
ownerTokenName = AssetNameParser.getOwnerTokenName(parentQualifierName);
|
|
97
|
+
// Find parent qualifier balance to spend and return as change
|
|
97
98
|
try {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
);
|
|
99
|
+
const selection = await this.utxoSelector.selectAssetUTXOs(addresses, parentQualifierName, 1);
|
|
100
|
+
parentQualifierUTXOs = selection.utxos;
|
|
101
|
+
parentQualifierQuantity = selection.totalAmount;
|
|
102
102
|
} catch (error) {
|
|
103
|
-
if (error
|
|
103
|
+
if (error.name === 'InsufficientFundsError') {
|
|
104
104
|
throw new OwnerTokenNotFoundError(
|
|
105
|
-
`You must own the parent qualifier
|
|
106
|
-
|
|
105
|
+
`You must own the parent qualifier asset (${parentQualifierName}) to create a sub-qualifier.`,
|
|
106
|
+
parentQualifierName
|
|
107
107
|
);
|
|
108
108
|
}
|
|
109
109
|
throw error;
|
|
@@ -129,7 +129,7 @@ class IssueQualifierBuilder extends BaseAssetTransactionBuilder {
|
|
|
129
129
|
const changeAddress = await this.getChangeAddress();
|
|
130
130
|
|
|
131
131
|
// 7. Estimate fee
|
|
132
|
-
const outputCount =
|
|
132
|
+
const outputCount = 3;
|
|
133
133
|
const estimatedFee = await this.estimateFee(2, outputCount);
|
|
134
134
|
|
|
135
135
|
// 8. Calculate total XNA needed
|
|
@@ -141,7 +141,7 @@ class IssueQualifierBuilder extends BaseAssetTransactionBuilder {
|
|
|
141
141
|
const totalXNAInput = utxoSelection.totalXNA;
|
|
142
142
|
|
|
143
143
|
// 10. Recalculate fee with actual input count
|
|
144
|
-
const actualInputCount = baseCurrencyUTXOs.length +
|
|
144
|
+
const actualInputCount = baseCurrencyUTXOs.length + parentQualifierUTXOs.length;
|
|
145
145
|
const actualFee = await this.estimateFee(actualInputCount, outputCount);
|
|
146
146
|
|
|
147
147
|
// 11. Verify we have enough XNA
|
|
@@ -172,16 +172,16 @@ class IssueQualifierBuilder extends BaseAssetTransactionBuilder {
|
|
|
172
172
|
});
|
|
173
173
|
});
|
|
174
174
|
|
|
175
|
-
// Add
|
|
176
|
-
|
|
175
|
+
// Add parent qualifier inputs if sub-qualifier
|
|
176
|
+
parentQualifierUTXOs.forEach(parentUTXO => {
|
|
177
177
|
inputs.push({
|
|
178
|
-
txid:
|
|
179
|
-
vout:
|
|
180
|
-
address:
|
|
181
|
-
assetName:
|
|
182
|
-
satoshis:
|
|
178
|
+
txid: parentUTXO.txid,
|
|
179
|
+
vout: parentUTXO.outputIndex,
|
|
180
|
+
address: parentUTXO.address,
|
|
181
|
+
assetName: parentUTXO.assetName,
|
|
182
|
+
satoshis: parentUTXO.satoshis
|
|
183
183
|
});
|
|
184
|
-
}
|
|
184
|
+
});
|
|
185
185
|
|
|
186
186
|
// 14. Build outputs (ORDER CRITICAL!)
|
|
187
187
|
const outputs = [];
|
|
@@ -194,21 +194,14 @@ class IssueQualifierBuilder extends BaseAssetTransactionBuilder {
|
|
|
194
194
|
outputs.push({ [changeAddress]: parseFloat(xnaChange.toFixed(8)) });
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
-
// Third: Owner token return (if sub-qualifier)
|
|
198
|
-
if (ownerTokenUTXO && ownerTokenName) {
|
|
199
|
-
const ownerTokenReturn = this.ownerTokenManager.createOwnerTokenReturnOutput(
|
|
200
|
-
ownerTokenName,
|
|
201
|
-
changeAddress
|
|
202
|
-
);
|
|
203
|
-
outputs.push(ownerTokenReturn);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
197
|
// Last: Issue qualifier operation
|
|
207
198
|
const issueQualifierOutput = OutputFormatter.formatIssueQualifierOutput({
|
|
208
199
|
asset_name: assetName,
|
|
209
200
|
asset_quantity: quantity,
|
|
210
201
|
has_ipfs: hasIpfs,
|
|
211
|
-
ipfs_hash: ipfsHash
|
|
202
|
+
ipfs_hash: ipfsHash,
|
|
203
|
+
root_change_address: isSub ? changeAddress : undefined,
|
|
204
|
+
change_quantity: isSub ? parentQualifierQuantity : undefined
|
|
212
205
|
});
|
|
213
206
|
|
|
214
207
|
outputs.push({ [toAddress]: issueQualifierOutput });
|
|
@@ -216,18 +209,11 @@ class IssueQualifierBuilder extends BaseAssetTransactionBuilder {
|
|
|
216
209
|
// 15. Order outputs (protocol requirement)
|
|
217
210
|
const orderedOutputs = this.outputOrderer.order(outputs);
|
|
218
211
|
|
|
219
|
-
// 16.
|
|
220
|
-
if (ownerTokenUTXO) {
|
|
221
|
-
this.ownerTokenManager.validateOwnerTokenReturn(inputs, orderedOutputs);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// 17. Create raw transaction
|
|
212
|
+
// 16. Create raw transaction
|
|
225
213
|
const rawTx = await this.buildRawTransaction(inputs, orderedOutputs);
|
|
226
214
|
|
|
227
|
-
//
|
|
228
|
-
const allUTXOs =
|
|
229
|
-
? [...baseCurrencyUTXOs, ownerTokenUTXO]
|
|
230
|
-
: baseCurrencyUTXOs;
|
|
215
|
+
// 17. Format and return result
|
|
216
|
+
const allUTXOs = [...baseCurrencyUTXOs, ...parentQualifierUTXOs];
|
|
231
217
|
|
|
232
218
|
return this.formatResult(
|
|
233
219
|
rawTx,
|
|
@@ -240,8 +226,7 @@ class IssueQualifierBuilder extends BaseAssetTransactionBuilder {
|
|
|
240
226
|
assetName,
|
|
241
227
|
qualifierType: isSub ? 'SUB_QUALIFIER' : 'QUALIFIER',
|
|
242
228
|
parentQualifier: isSub ? parsed.parent : null,
|
|
243
|
-
|
|
244
|
-
parentOwnerTokenUsed: ownerTokenName,
|
|
229
|
+
parentQualifierUsed: parentQualifierName,
|
|
245
230
|
operationType: isSub ? 'ISSUE_SUB_QUALIFIER' : 'ISSUE_QUALIFIER'
|
|
246
231
|
}
|
|
247
232
|
);
|