@neuraiproject/neurai-assets 1.0.1 → 1.0.2
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/package.json +1 -1
- package/src/NeuraiAssets.js +2 -1
- package/src/builders/FreezeAddressBuilder.js +5 -22
- 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 +2 -1
- package/src/builders/ReissueRestrictedBuilder.js +5 -15
- package/src/builders/TagAddressBuilder.js +38 -51
- package/src/constants/fees.js +2 -2
- package/src/constants/networks.js +2 -2
- package/src/utils/assetNameParser.js +3 -0
- package/src/utils/networkDetector.js +2 -2
- package/src/utils/outputFormatter.js +65 -19
- package/tests/unit/utils/assetNameParser.test.js +1 -1
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) |
|
package/package.json
CHANGED
package/src/NeuraiAssets.js
CHANGED
|
@@ -167,7 +167,8 @@ class NeuraiAssets {
|
|
|
167
167
|
* @returns {Promise<object>} Transaction data
|
|
168
168
|
*/
|
|
169
169
|
async createQualifier(params) {
|
|
170
|
-
const
|
|
170
|
+
const normalized = { ...params, assetName: params.assetName || params.qualifierName };
|
|
171
|
+
const builder = new IssueQualifierBuilder(this.rpc, this._buildParams(normalized));
|
|
171
172
|
return await builder.build();
|
|
172
173
|
}
|
|
173
174
|
|
|
@@ -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(
|
|
@@ -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
|
);
|
|
@@ -9,12 +9,12 @@
|
|
|
9
9
|
* - Requires verifier string (boolean logic with qualifiers)
|
|
10
10
|
* - Only addresses meeting verifier requirements can receive/hold
|
|
11
11
|
* - Can freeze individual addresses or entire asset
|
|
12
|
-
* - Creates owner token (
|
|
12
|
+
* - Creates owner token (ASSET!)
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
const BaseAssetTransactionBuilder = require('./BaseAssetTransactionBuilder');
|
|
16
|
-
const { OutputFormatter } = require('../utils');
|
|
17
|
-
const { AssetExistsError } = require('../errors');
|
|
16
|
+
const { OutputFormatter, AssetNameParser } = require('../utils');
|
|
17
|
+
const { AssetExistsError, OwnerTokenNotFoundError } = require('../errors');
|
|
18
18
|
const { IpfsValidator, VerifierValidator } = require('../validators');
|
|
19
19
|
|
|
20
20
|
class IssueRestrictedBuilder extends BaseAssetTransactionBuilder {
|
|
@@ -98,21 +98,39 @@ class IssueRestrictedBuilder extends BaseAssetTransactionBuilder {
|
|
|
98
98
|
const toAddress = await this.getToAddress();
|
|
99
99
|
const changeAddress = await this.getChangeAddress();
|
|
100
100
|
|
|
101
|
-
// 6.
|
|
102
|
-
const
|
|
101
|
+
// 6. Find owner token UTXO (CRITICAL: node requires it as input)
|
|
102
|
+
const ownerTokenName = AssetNameParser.getOwnerTokenName(assetName);
|
|
103
|
+
let ownerTokenUTXO;
|
|
104
|
+
try {
|
|
105
|
+
ownerTokenUTXO = await this.ownerTokenManager.findOwnerTokenUTXO(
|
|
106
|
+
ownerTokenName,
|
|
107
|
+
addresses
|
|
108
|
+
);
|
|
109
|
+
} catch (error) {
|
|
110
|
+
if (error instanceof OwnerTokenNotFoundError) {
|
|
111
|
+
throw new OwnerTokenNotFoundError(
|
|
112
|
+
`You must own the owner token (${ownerTokenName}) to issue the restricted asset ${assetName}.`,
|
|
113
|
+
ownerTokenName
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
103
118
|
|
|
104
|
-
// 7.
|
|
119
|
+
// 7. Estimate fee (+1 for owner token input)
|
|
120
|
+
const estimatedFee = await this.estimateFee(2, 4);
|
|
121
|
+
|
|
122
|
+
// 8. Calculate total XNA needed
|
|
105
123
|
const totalXNANeeded = burnInfo.amount + estimatedFee;
|
|
106
124
|
|
|
107
|
-
//
|
|
125
|
+
// 9. Select XNA UTXOs
|
|
108
126
|
const utxoSelection = await this.selectUTXOs(totalXNANeeded, null, 0);
|
|
109
127
|
const baseCurrencyUTXOs = utxoSelection.xnaUTXOs;
|
|
110
128
|
const totalXNAInput = utxoSelection.totalXNA;
|
|
111
129
|
|
|
112
|
-
//
|
|
113
|
-
const actualFee = await this.estimateFee(baseCurrencyUTXOs.length,
|
|
130
|
+
// 10. Recalculate fee with actual input count (+1 for owner token)
|
|
131
|
+
const actualFee = await this.estimateFee(baseCurrencyUTXOs.length + 1, 4);
|
|
114
132
|
|
|
115
|
-
//
|
|
133
|
+
// 11. Verify we have enough XNA
|
|
116
134
|
const totalRequired = burnInfo.amount + actualFee;
|
|
117
135
|
if (totalXNAInput < totalRequired) {
|
|
118
136
|
const additionalNeeded = totalRequired - totalXNAInput + 0.001;
|
|
@@ -120,22 +138,35 @@ class IssueRestrictedBuilder extends BaseAssetTransactionBuilder {
|
|
|
120
138
|
baseCurrencyUTXOs.push(...additionalSelection.xnaUTXOs);
|
|
121
139
|
}
|
|
122
140
|
|
|
123
|
-
//
|
|
141
|
+
// 12. Calculate XNA change
|
|
124
142
|
const finalTotalInput = baseCurrencyUTXOs.reduce(
|
|
125
143
|
(sum, utxo) => sum + utxo.satoshis / 100000000,
|
|
126
144
|
0
|
|
127
145
|
);
|
|
128
146
|
const xnaChange = finalTotalInput - burnInfo.amount - actualFee;
|
|
129
147
|
|
|
130
|
-
//
|
|
131
|
-
const inputs =
|
|
132
|
-
txid: utxo.txid,
|
|
133
|
-
vout: utxo.outputIndex,
|
|
134
|
-
address: utxo.address,
|
|
135
|
-
satoshis: utxo.satoshis
|
|
136
|
-
}));
|
|
148
|
+
// 13. Build inputs (XNA + owner token)
|
|
149
|
+
const inputs = [];
|
|
137
150
|
|
|
138
|
-
|
|
151
|
+
baseCurrencyUTXOs.forEach(utxo => {
|
|
152
|
+
inputs.push({
|
|
153
|
+
txid: utxo.txid,
|
|
154
|
+
vout: utxo.outputIndex,
|
|
155
|
+
address: utxo.address,
|
|
156
|
+
satoshis: utxo.satoshis
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// Add owner token input (node requires it to issue restricted asset)
|
|
161
|
+
inputs.push({
|
|
162
|
+
txid: ownerTokenUTXO.txid,
|
|
163
|
+
vout: ownerTokenUTXO.outputIndex,
|
|
164
|
+
address: ownerTokenUTXO.address,
|
|
165
|
+
assetName: ownerTokenUTXO.assetName,
|
|
166
|
+
satoshis: ownerTokenUTXO.satoshis
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// 14. Build outputs (ORDER CRITICAL!)
|
|
139
170
|
const outputs = [];
|
|
140
171
|
|
|
141
172
|
// First: Burn output
|
|
@@ -154,28 +185,31 @@ class IssueRestrictedBuilder extends BaseAssetTransactionBuilder {
|
|
|
154
185
|
units: units,
|
|
155
186
|
reissuable: reissuable,
|
|
156
187
|
has_ipfs: hasIpfs,
|
|
157
|
-
ipfs_hash: ipfsHash
|
|
188
|
+
ipfs_hash: ipfsHash,
|
|
189
|
+
owner_change_address: changeAddress
|
|
158
190
|
});
|
|
159
191
|
|
|
160
192
|
outputs.push({ [toAddress]: issueRestrictedOutput });
|
|
161
193
|
|
|
162
|
-
//
|
|
194
|
+
// 15. Order outputs (protocol requirement)
|
|
163
195
|
const orderedOutputs = this.outputOrderer.order(outputs);
|
|
164
196
|
|
|
165
|
-
//
|
|
197
|
+
// 16. Create raw transaction
|
|
166
198
|
const rawTx = await this.buildRawTransaction(inputs, orderedOutputs);
|
|
167
199
|
|
|
168
|
-
//
|
|
200
|
+
// 17. Format and return result
|
|
201
|
+
const allUTXOs = [...baseCurrencyUTXOs, ownerTokenUTXO];
|
|
202
|
+
|
|
169
203
|
return this.formatResult(
|
|
170
204
|
rawTx,
|
|
171
|
-
|
|
205
|
+
allUTXOs,
|
|
172
206
|
inputs,
|
|
173
207
|
orderedOutputs,
|
|
174
208
|
actualFee,
|
|
175
209
|
burnInfo.amount,
|
|
176
210
|
{
|
|
177
211
|
assetName,
|
|
178
|
-
ownerTokenName
|
|
212
|
+
ownerTokenName,
|
|
179
213
|
verifierString,
|
|
180
214
|
requiredQualifiers,
|
|
181
215
|
operationType: 'ISSUE_RESTRICTED'
|
|
@@ -202,14 +202,9 @@ class IssueUniqueBuilder extends BaseAssetTransactionBuilder {
|
|
|
202
202
|
outputs.push({ [changeAddress]: parseFloat(xnaChange.toFixed(8)) });
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
-
// Third: Owner token return (CRITICAL - must return or lost forever!)
|
|
206
|
-
const ownerTokenReturn = this.ownerTokenManager.createOwnerTokenReturnOutput(
|
|
207
|
-
ownerTokenName,
|
|
208
|
-
changeAddress
|
|
209
|
-
);
|
|
210
|
-
outputs.push(ownerTokenReturn);
|
|
211
|
-
|
|
212
205
|
// Last: Issue unique operation
|
|
206
|
+
// NOTE: owner token return is handled automatically by the node when processing
|
|
207
|
+
// issue_unique — adding it manually would cause TOKEN! to appear twice in outputs
|
|
213
208
|
const issueUniqueOutput = OutputFormatter.formatIssueUniqueOutput({
|
|
214
209
|
root_name: rootName,
|
|
215
210
|
asset_tags: assetTags,
|
|
@@ -221,16 +216,14 @@ class IssueUniqueBuilder extends BaseAssetTransactionBuilder {
|
|
|
221
216
|
// 15. Order outputs (protocol requirement)
|
|
222
217
|
const orderedOutputs = this.outputOrderer.order(outputs);
|
|
223
218
|
|
|
224
|
-
// 16.
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
// 17. Create raw transaction
|
|
219
|
+
// 16. Create raw transaction
|
|
220
|
+
// NOTE: validateOwnerTokenReturn removed — the node returns TOKEN! automatically
|
|
228
221
|
const rawTx = await this.buildRawTransaction(inputs, orderedOutputs);
|
|
229
222
|
|
|
230
|
-
//
|
|
223
|
+
// 17. Build list of created NFT names
|
|
231
224
|
const createdNFTs = assetTags.map(tag => `${rootName}#${tag}`);
|
|
232
225
|
|
|
233
|
-
//
|
|
226
|
+
// 18. Format and return result
|
|
234
227
|
const allUTXOs = [...baseCurrencyUTXOs, ownerTokenUTXO];
|
|
235
228
|
|
|
236
229
|
return this.formatResult(
|
|
@@ -200,7 +200,8 @@ class ReissueBuilder extends BaseAssetTransactionBuilder {
|
|
|
200
200
|
asset_name: assetName,
|
|
201
201
|
asset_quantity: this.toSatoshis(quantity, units),
|
|
202
202
|
reissuable: reissuable !== undefined ? reissuable : undefined,
|
|
203
|
-
new_ipfs: newIpfs || undefined
|
|
203
|
+
new_ipfs: newIpfs || undefined,
|
|
204
|
+
owner_change_address: changeAddress
|
|
204
205
|
});
|
|
205
206
|
|
|
206
207
|
outputs.push({ [toAddress]: reissueOutput });
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Reissue Restricted:
|
|
6
6
|
* - Mints additional supply of restricted asset
|
|
7
7
|
* - Cost: 200 XNA (burned)
|
|
8
|
-
* - Requires asset's owner token (
|
|
8
|
+
* - Requires asset's owner token (ASSET!)
|
|
9
9
|
* - Can update verifier string
|
|
10
10
|
* - Can lock asset (make it non-reissuable)
|
|
11
11
|
* - Can update IPFS metadata
|
|
@@ -200,14 +200,6 @@ class ReissueRestrictedBuilder extends BaseAssetTransactionBuilder {
|
|
|
200
200
|
outputs.push({ [changeAddress]: parseFloat(xnaChange.toFixed(8)) });
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
-
// Third: Owner token return (CRITICAL - must return or lost forever!)
|
|
204
|
-
const ownerReturnAddress = this.params.ownerChangeAddress || changeAddress;
|
|
205
|
-
const ownerTokenReturn = this.ownerTokenManager.createOwnerTokenReturnOutput(
|
|
206
|
-
ownerTokenName,
|
|
207
|
-
ownerReturnAddress
|
|
208
|
-
);
|
|
209
|
-
outputs.push(ownerTokenReturn);
|
|
210
|
-
|
|
211
203
|
// Last: Reissue restricted operation
|
|
212
204
|
const units = assetData.units || 0;
|
|
213
205
|
const reissueRestrictedOutput = OutputFormatter.formatReissueRestrictedOutput({
|
|
@@ -216,7 +208,8 @@ class ReissueRestrictedBuilder extends BaseAssetTransactionBuilder {
|
|
|
216
208
|
change_verifier: changeVerifier,
|
|
217
209
|
new_verifier: changeVerifier ? newVerifier : undefined,
|
|
218
210
|
reissuable: reissuable !== undefined ? reissuable : undefined,
|
|
219
|
-
new_ipfs: newIpfs || undefined
|
|
211
|
+
new_ipfs: newIpfs || undefined,
|
|
212
|
+
owner_change_address: this.params.ownerChangeAddress || changeAddress
|
|
220
213
|
});
|
|
221
214
|
|
|
222
215
|
outputs.push({ [toAddress]: reissueRestrictedOutput });
|
|
@@ -224,13 +217,10 @@ class ReissueRestrictedBuilder extends BaseAssetTransactionBuilder {
|
|
|
224
217
|
// 16. Order outputs (protocol requirement)
|
|
225
218
|
const orderedOutputs = this.outputOrderer.order(outputs);
|
|
226
219
|
|
|
227
|
-
// 17.
|
|
228
|
-
this.ownerTokenManager.validateOwnerTokenReturn(inputs, orderedOutputs);
|
|
229
|
-
|
|
230
|
-
// 18. Create raw transaction
|
|
220
|
+
// 17. Create raw transaction
|
|
231
221
|
const rawTx = await this.buildRawTransaction(inputs, orderedOutputs);
|
|
232
222
|
|
|
233
|
-
//
|
|
223
|
+
// 18. Format and return result
|
|
234
224
|
const allUTXOs = [...baseCurrencyUTXOs, ownerTokenUTXO];
|
|
235
225
|
|
|
236
226
|
// Extract qualifiers from new verifier if changed
|
|
@@ -6,13 +6,13 @@
|
|
|
6
6
|
* - Assign qualifier tags to addresses (for restricted asset compliance)
|
|
7
7
|
* - Remove qualifier tags from addresses
|
|
8
8
|
* - Cost: 0.1 XNA per address (burned)
|
|
9
|
-
* - Requires qualifier
|
|
9
|
+
* - Requires spending the qualifier asset itself (#QUALIFIER)
|
|
10
10
|
* - Used to mark addresses as KYC'd, accredited, etc.
|
|
11
11
|
* - Owner token must be returned
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
const BaseAssetTransactionBuilder = require('./BaseAssetTransactionBuilder');
|
|
15
|
-
const { OutputFormatter
|
|
15
|
+
const { OutputFormatter } = require('../utils');
|
|
16
16
|
const { AssetNotFoundError, OwnerTokenNotFoundError, InvalidAddressError } = require('../errors');
|
|
17
17
|
|
|
18
18
|
class TagAddressBuilder extends BaseAssetTransactionBuilder {
|
|
@@ -32,6 +32,10 @@ class TagAddressBuilder extends BaseAssetTransactionBuilder {
|
|
|
32
32
|
throw new Error('addresses is required and must be a non-empty array');
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
if (params.addresses.length > 10) {
|
|
36
|
+
throw new Error('addresses array cannot exceed 10 entries per transaction (node limit)');
|
|
37
|
+
}
|
|
38
|
+
|
|
35
39
|
// Validate qualifier name
|
|
36
40
|
this.validateAssetName(params.qualifierName, 'QUALIFIER');
|
|
37
41
|
|
|
@@ -44,14 +48,7 @@ class TagAddressBuilder extends BaseAssetTransactionBuilder {
|
|
|
44
48
|
);
|
|
45
49
|
}
|
|
46
50
|
|
|
47
|
-
//
|
|
48
|
-
const validPrefixes = this.network === 'xna' ? ['N'] : ['m', 'n'];
|
|
49
|
-
if (!validPrefixes.some(prefix => address.startsWith(prefix))) {
|
|
50
|
-
throw new InvalidAddressError(
|
|
51
|
-
`addresses[${index}] has invalid prefix for network ${this.network}`,
|
|
52
|
-
address
|
|
53
|
-
);
|
|
54
|
-
}
|
|
51
|
+
// Address prefix validation is left to the node (varies by network)
|
|
55
52
|
});
|
|
56
53
|
|
|
57
54
|
return true;
|
|
@@ -69,7 +66,6 @@ class TagAddressBuilder extends BaseAssetTransactionBuilder {
|
|
|
69
66
|
const {
|
|
70
67
|
qualifierName,
|
|
71
68
|
addresses: targetAddresses,
|
|
72
|
-
assetData = ''
|
|
73
69
|
} = this.params;
|
|
74
70
|
|
|
75
71
|
// 2. Check if qualifier exists
|
|
@@ -85,19 +81,18 @@ class TagAddressBuilder extends BaseAssetTransactionBuilder {
|
|
|
85
81
|
const addresses = await this._getAddresses();
|
|
86
82
|
const changeAddress = await this.getChangeAddress();
|
|
87
83
|
|
|
88
|
-
// 4. Find qualifier
|
|
89
|
-
|
|
90
|
-
let
|
|
84
|
+
// 4. Find qualifier asset balance (CRITICAL: must have this)
|
|
85
|
+
let qualifierUTXOs;
|
|
86
|
+
let qualifierQuantity;
|
|
91
87
|
try {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
);
|
|
88
|
+
const selection = await this.utxoSelector.selectAssetUTXOs(addresses, qualifierName, 1);
|
|
89
|
+
qualifierUTXOs = selection.utxos;
|
|
90
|
+
qualifierQuantity = selection.totalAmount;
|
|
96
91
|
} catch (error) {
|
|
97
|
-
if (error
|
|
92
|
+
if (error.name === 'InsufficientFundsError') {
|
|
98
93
|
throw new OwnerTokenNotFoundError(
|
|
99
|
-
`You must own the qualifier
|
|
100
|
-
|
|
94
|
+
`You must own the qualifier asset (${qualifierName}) to tag/untag addresses.`,
|
|
95
|
+
qualifierName
|
|
101
96
|
);
|
|
102
97
|
}
|
|
103
98
|
throw error;
|
|
@@ -121,7 +116,7 @@ class TagAddressBuilder extends BaseAssetTransactionBuilder {
|
|
|
121
116
|
const totalXNAInput = utxoSelection.totalXNA;
|
|
122
117
|
|
|
123
118
|
// 9. Recalculate fee with actual input count
|
|
124
|
-
const actualInputCount = baseCurrencyUTXOs.length +
|
|
119
|
+
const actualInputCount = baseCurrencyUTXOs.length + qualifierUTXOs.length;
|
|
125
120
|
const actualFee = await this.estimateFee(actualInputCount, 3);
|
|
126
121
|
|
|
127
122
|
// 10. Verify we have enough XNA
|
|
@@ -139,7 +134,7 @@ class TagAddressBuilder extends BaseAssetTransactionBuilder {
|
|
|
139
134
|
);
|
|
140
135
|
const xnaChange = finalTotalInput - burnInfo.amount - actualFee;
|
|
141
136
|
|
|
142
|
-
// 12. Build inputs (XNA +
|
|
137
|
+
// 12. Build inputs (XNA + qualifier asset)
|
|
143
138
|
const inputs = [];
|
|
144
139
|
|
|
145
140
|
// Add XNA inputs
|
|
@@ -152,13 +147,14 @@ class TagAddressBuilder extends BaseAssetTransactionBuilder {
|
|
|
152
147
|
});
|
|
153
148
|
});
|
|
154
149
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
150
|
+
qualifierUTXOs.forEach(utxo => {
|
|
151
|
+
inputs.push({
|
|
152
|
+
txid: utxo.txid,
|
|
153
|
+
vout: utxo.outputIndex,
|
|
154
|
+
address: utxo.address,
|
|
155
|
+
assetName: utxo.assetName,
|
|
156
|
+
satoshis: utxo.satoshis
|
|
157
|
+
});
|
|
162
158
|
});
|
|
163
159
|
|
|
164
160
|
// 13. Build outputs (ORDER CRITICAL!)
|
|
@@ -172,39 +168,30 @@ class TagAddressBuilder extends BaseAssetTransactionBuilder {
|
|
|
172
168
|
outputs.push({ [changeAddress]: parseFloat(xnaChange.toFixed(8)) });
|
|
173
169
|
}
|
|
174
170
|
|
|
175
|
-
//
|
|
176
|
-
|
|
177
|
-
ownerTokenName,
|
|
178
|
-
changeAddress
|
|
179
|
-
);
|
|
180
|
-
outputs.push(ownerTokenReturn);
|
|
181
|
-
|
|
182
|
-
// Last: Tag/Untag operation
|
|
183
|
-
// Note: Using first address as transaction output address (protocol requirement)
|
|
171
|
+
// Last: Tag/Untag operation. The node creates the qualifier change output
|
|
172
|
+
// from the operation object itself, so this must be sent to the change address.
|
|
184
173
|
const operationOutput = isUntag
|
|
185
174
|
? OutputFormatter.formatUntagAddressesOutput({
|
|
186
|
-
|
|
187
|
-
addresses: targetAddresses
|
|
175
|
+
qualifier: qualifierName,
|
|
176
|
+
addresses: targetAddresses,
|
|
177
|
+
change_quantity: qualifierQuantity
|
|
188
178
|
})
|
|
189
179
|
: OutputFormatter.formatTagAddressesOutput({
|
|
190
|
-
|
|
180
|
+
qualifier: qualifierName,
|
|
191
181
|
addresses: targetAddresses,
|
|
192
|
-
|
|
182
|
+
change_quantity: qualifierQuantity
|
|
193
183
|
});
|
|
194
184
|
|
|
195
|
-
outputs.push({ [
|
|
185
|
+
outputs.push({ [changeAddress]: operationOutput });
|
|
196
186
|
|
|
197
187
|
// 14. Order outputs (protocol requirement)
|
|
198
188
|
const orderedOutputs = this.outputOrderer.order(outputs);
|
|
199
189
|
|
|
200
|
-
// 15.
|
|
201
|
-
this.ownerTokenManager.validateOwnerTokenReturn(inputs, orderedOutputs);
|
|
202
|
-
|
|
203
|
-
// 16. Create raw transaction
|
|
190
|
+
// 15. Create raw transaction
|
|
204
191
|
const rawTx = await this.buildRawTransaction(inputs, orderedOutputs);
|
|
205
192
|
|
|
206
|
-
//
|
|
207
|
-
const allUTXOs = [...baseCurrencyUTXOs,
|
|
193
|
+
// 16. Format and return result
|
|
194
|
+
const allUTXOs = [...baseCurrencyUTXOs, ...qualifierUTXOs];
|
|
208
195
|
|
|
209
196
|
return this.formatResult(
|
|
210
197
|
rawTx,
|
|
@@ -215,7 +202,7 @@ class TagAddressBuilder extends BaseAssetTransactionBuilder {
|
|
|
215
202
|
burnInfo.amount,
|
|
216
203
|
{
|
|
217
204
|
qualifierName,
|
|
218
|
-
|
|
205
|
+
qualifierAssetUsed: qualifierName,
|
|
219
206
|
targetAddresses,
|
|
220
207
|
addressCount,
|
|
221
208
|
operationType: isUntag ? 'UNTAG_ADDRESSES' : 'TAG_ADDRESSES'
|
package/src/constants/fees.js
CHANGED
|
@@ -13,8 +13,8 @@ const ASSET_COSTS = {
|
|
|
13
13
|
ISSUE_RESTRICTED: 3000,
|
|
14
14
|
REISSUE: 200,
|
|
15
15
|
REISSUE_RESTRICTED: 200,
|
|
16
|
-
TAG_ADDRESS: 0.
|
|
17
|
-
UNTAG_ADDRESS: 0.
|
|
16
|
+
TAG_ADDRESS: 0.2, // Per address
|
|
17
|
+
UNTAG_ADDRESS: 0.2, // Per address
|
|
18
18
|
FREEZE_ADDRESS: 0, // No cost (requires owner token)
|
|
19
19
|
UNFREEZE_ADDRESS: 0, // No cost (requires owner token)
|
|
20
20
|
FREEZE_ASSET: 0, // No cost (requires owner token)
|
|
@@ -14,7 +14,7 @@ const NETWORKS = {
|
|
|
14
14
|
TESTNET: {
|
|
15
15
|
name: 'xna-test',
|
|
16
16
|
displayName: 'Neurai Testnet',
|
|
17
|
-
addressPrefix: '
|
|
17
|
+
addressPrefix: 't',
|
|
18
18
|
assetNameMaxLength: 32, // Same as mainnet
|
|
19
19
|
defaultRPCPort: 19101,
|
|
20
20
|
coin: 'TXNA'
|
|
@@ -96,7 +96,7 @@ function getNetworkConfig(networkName) {
|
|
|
96
96
|
function detectNetworkFromAddress(address) {
|
|
97
97
|
if (address.startsWith('N')) {
|
|
98
98
|
return 'xna';
|
|
99
|
-
} else if (address.startsWith('
|
|
99
|
+
} else if (address.startsWith('t')) {
|
|
100
100
|
return 'xna-test';
|
|
101
101
|
} else {
|
|
102
102
|
throw new Error(`Cannot detect network from address: ${address}`);
|
|
@@ -52,8 +52,8 @@ class NetworkDetector {
|
|
|
52
52
|
return 'xna';
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
// Testnet addresses start with '
|
|
56
|
-
if (address.startsWith(NETWORKS.TESTNET.addressPrefix)
|
|
55
|
+
// Testnet addresses start with 't' (prefix byte 0x7f = 127)
|
|
56
|
+
if (address.startsWith(NETWORKS.TESTNET.addressPrefix)) {
|
|
57
57
|
return 'xna-test';
|
|
58
58
|
}
|
|
59
59
|
|
|
@@ -39,13 +39,18 @@ class OutputFormatter {
|
|
|
39
39
|
static formatIssueUniqueOutput(params) {
|
|
40
40
|
const { root_name, asset_tags, ipfs_hashes } = params;
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
const output = {
|
|
43
43
|
issue_unique: {
|
|
44
44
|
root_name,
|
|
45
|
-
asset_tags
|
|
46
|
-
ipfs_hashes: ipfs_hashes || []
|
|
45
|
+
asset_tags
|
|
47
46
|
}
|
|
48
47
|
};
|
|
48
|
+
|
|
49
|
+
if (ipfs_hashes && ipfs_hashes.length > 0) {
|
|
50
|
+
output.issue_unique.ipfs_hashes = ipfs_hashes;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return output;
|
|
49
54
|
}
|
|
50
55
|
|
|
51
56
|
/**
|
|
@@ -61,10 +66,11 @@ class OutputFormatter {
|
|
|
61
66
|
units,
|
|
62
67
|
reissuable,
|
|
63
68
|
has_ipfs,
|
|
64
|
-
ipfs_hash
|
|
69
|
+
ipfs_hash,
|
|
70
|
+
owner_change_address
|
|
65
71
|
} = params;
|
|
66
72
|
|
|
67
|
-
|
|
73
|
+
const output = {
|
|
68
74
|
issue_restricted: {
|
|
69
75
|
asset_name,
|
|
70
76
|
asset_quantity,
|
|
@@ -75,6 +81,12 @@ class OutputFormatter {
|
|
|
75
81
|
ipfs_hash: ipfs_hash || ''
|
|
76
82
|
}
|
|
77
83
|
};
|
|
84
|
+
|
|
85
|
+
if (owner_change_address) {
|
|
86
|
+
output.issue_restricted.owner_change_address = owner_change_address;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return output;
|
|
78
90
|
}
|
|
79
91
|
|
|
80
92
|
/**
|
|
@@ -87,10 +99,12 @@ class OutputFormatter {
|
|
|
87
99
|
asset_name,
|
|
88
100
|
asset_quantity,
|
|
89
101
|
has_ipfs,
|
|
90
|
-
ipfs_hash
|
|
102
|
+
ipfs_hash,
|
|
103
|
+
root_change_address,
|
|
104
|
+
change_quantity
|
|
91
105
|
} = params;
|
|
92
106
|
|
|
93
|
-
|
|
107
|
+
const output = {
|
|
94
108
|
issue_qualifier: {
|
|
95
109
|
asset_name,
|
|
96
110
|
asset_quantity,
|
|
@@ -98,6 +112,16 @@ class OutputFormatter {
|
|
|
98
112
|
ipfs_hash: ipfs_hash || ''
|
|
99
113
|
}
|
|
100
114
|
};
|
|
115
|
+
|
|
116
|
+
if (root_change_address) {
|
|
117
|
+
output.issue_qualifier.root_change_address = root_change_address;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (change_quantity !== undefined && change_quantity !== null) {
|
|
121
|
+
output.issue_qualifier.change_quantity = change_quantity;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return output;
|
|
101
125
|
}
|
|
102
126
|
|
|
103
127
|
/**
|
|
@@ -110,7 +134,8 @@ class OutputFormatter {
|
|
|
110
134
|
asset_name,
|
|
111
135
|
asset_quantity,
|
|
112
136
|
reissuable,
|
|
113
|
-
new_ipfs
|
|
137
|
+
new_ipfs,
|
|
138
|
+
owner_change_address
|
|
114
139
|
} = params;
|
|
115
140
|
|
|
116
141
|
const output = {
|
|
@@ -129,6 +154,10 @@ class OutputFormatter {
|
|
|
129
154
|
output.reissue.ipfs_hash = new_ipfs;
|
|
130
155
|
}
|
|
131
156
|
|
|
157
|
+
if (owner_change_address) {
|
|
158
|
+
output.reissue.owner_change_address = owner_change_address;
|
|
159
|
+
}
|
|
160
|
+
|
|
132
161
|
return output;
|
|
133
162
|
}
|
|
134
163
|
|
|
@@ -144,7 +173,8 @@ class OutputFormatter {
|
|
|
144
173
|
change_verifier,
|
|
145
174
|
new_verifier,
|
|
146
175
|
reissuable,
|
|
147
|
-
new_ipfs
|
|
176
|
+
new_ipfs,
|
|
177
|
+
owner_change_address
|
|
148
178
|
} = params;
|
|
149
179
|
|
|
150
180
|
const output = {
|
|
@@ -167,6 +197,10 @@ class OutputFormatter {
|
|
|
167
197
|
output.reissue_restricted.ipfs_hash = new_ipfs;
|
|
168
198
|
}
|
|
169
199
|
|
|
200
|
+
if (owner_change_address) {
|
|
201
|
+
output.reissue_restricted.owner_change_address = owner_change_address;
|
|
202
|
+
}
|
|
203
|
+
|
|
170
204
|
return output;
|
|
171
205
|
}
|
|
172
206
|
|
|
@@ -191,18 +225,23 @@ class OutputFormatter {
|
|
|
191
225
|
*/
|
|
192
226
|
static formatTagAddressesOutput(params) {
|
|
193
227
|
const {
|
|
194
|
-
|
|
228
|
+
qualifier,
|
|
195
229
|
addresses,
|
|
196
|
-
|
|
230
|
+
change_quantity
|
|
197
231
|
} = params;
|
|
198
232
|
|
|
199
|
-
|
|
233
|
+
const output = {
|
|
200
234
|
tag_addresses: {
|
|
201
|
-
|
|
202
|
-
addresses
|
|
203
|
-
asset_data: asset_data || ''
|
|
235
|
+
qualifier,
|
|
236
|
+
addresses
|
|
204
237
|
}
|
|
205
238
|
};
|
|
239
|
+
|
|
240
|
+
if (change_quantity !== undefined && change_quantity !== null) {
|
|
241
|
+
output.tag_addresses.change_quantity = change_quantity;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return output;
|
|
206
245
|
}
|
|
207
246
|
|
|
208
247
|
/**
|
|
@@ -212,16 +251,23 @@ class OutputFormatter {
|
|
|
212
251
|
*/
|
|
213
252
|
static formatUntagAddressesOutput(params) {
|
|
214
253
|
const {
|
|
215
|
-
|
|
216
|
-
addresses
|
|
254
|
+
qualifier,
|
|
255
|
+
addresses,
|
|
256
|
+
change_quantity
|
|
217
257
|
} = params;
|
|
218
258
|
|
|
219
|
-
|
|
259
|
+
const output = {
|
|
220
260
|
untag_addresses: {
|
|
221
|
-
|
|
261
|
+
qualifier,
|
|
222
262
|
addresses
|
|
223
263
|
}
|
|
224
264
|
};
|
|
265
|
+
|
|
266
|
+
if (change_quantity !== undefined && change_quantity !== null) {
|
|
267
|
+
output.untag_addresses.change_quantity = change_quantity;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return output;
|
|
225
271
|
}
|
|
226
272
|
|
|
227
273
|
/**
|
|
@@ -120,7 +120,7 @@ describe('AssetNameParser', () => {
|
|
|
120
120
|
describe('getOwnerTokenName', () => {
|
|
121
121
|
it('should add ! to get owner token name', () => {
|
|
122
122
|
expect(AssetNameParser.getOwnerTokenName('MYTOKEN')).to.equal('MYTOKEN!');
|
|
123
|
-
expect(AssetNameParser.getOwnerTokenName('$SECURITY')).to.equal('
|
|
123
|
+
expect(AssetNameParser.getOwnerTokenName('$SECURITY')).to.equal('SECURITY!');
|
|
124
124
|
});
|
|
125
125
|
|
|
126
126
|
it('should return as-is if already an owner token', () => {
|