@xrplkit/xls26 2.4.0 → 2.6.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.
Files changed (3) hide show
  1. package/package.json +1 -1
  2. package/readme.md +44 -54
  3. package/xls26.js +183 -102
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@xrplkit/xls26",
3
3
  "type": "module",
4
- "version": "2.4.0",
4
+ "version": "2.6.0",
5
5
  "main": "xls26.js",
6
6
  "dependencies": {
7
7
  "@xrplkit/toml": "1.0.0"
package/readme.md CHANGED
@@ -22,29 +22,27 @@ console.log(xls26Data)
22
22
 
23
23
  ```toml
24
24
  [[ISSUERS]]
25
- address = "rHXuEaRYnnJHbDeuBH5w8yPh5uwNVh5zAg"
26
- name = "Aesthetes"
27
-
28
- [[ISSUERS.WEBLINKS]]
29
- url = "https://aesthetes.art"
30
- type = "info"
31
- title = "Official Website"
32
-
33
- [[ISSUERS.WEBLINKS]]
34
- url = "https://twitter.com/aesthetes_art"
35
- type = "socialmedia"
25
+ address = "rMxCKbEDwqr76QuheSUMdEGf4B9xJ8m5De"
26
+ name = "Ripple"
27
+ desc = "We're building the Internet of Value."
36
28
 
37
29
  [[TOKENS]]
38
- issuer = "rHXuEaRYnnJHbDeuBH5w8yPh5uwNVh5zAg"
39
- currency = "ELS"
40
- name = "Elysian"
41
- desc = "The first Token for the Art and NFT Industry running on the XRPL."
42
- icon = "https://static.xrplmeta.org/icons/els.png"
43
- asset_class = "cryptocurrency"
30
+ issuer = "rMxCKbEDwqr76QuheSUMdEGf4B9xJ8m5De"
31
+ currency = "RLUSD"
32
+ name = "Ripple USD"
33
+ desc = "Ripple USD (RLUSD) is natively issued on the XRP Ledger and Ethereum blockchains and is enabled with a number of features to ensure strict adherence to compliance standards, flexibility for developers, and security for holders."
34
+ icon = "https://ripple.com/assets/rlusd-logo.png"
35
+ asset_class = "rwa"
36
+ asset_subclass = "stablecoin"
37
+
38
+ [[TOKENS.URLS]]
39
+ url = "https://ripple.com"
40
+ type = "website"
41
+ title = "Official Website"
44
42
 
45
- [[TOKENS.WEBLINKS]]
46
- url = "https://twitter.com/Elysianers"
47
- type = "community"
43
+ [[TOKENS.URLS]]
44
+ url = "https://x.com/ripple"
45
+ type = "social"
48
46
  ```
49
47
 
50
48
 
@@ -52,40 +50,32 @@ type = "community"
52
50
 
53
51
  ```javascript
54
52
  {
55
- issuers: [
56
- {
57
- address: 'rHXuEaRYnnJHbDeuBH5w8yPh5uwNVh5zAg',
58
- name: 'Aesthetes',
59
- weblinks: [
60
- {
61
- url: 'https://aesthetes.art',
62
- type: 'info',
63
- title: 'Official Website'
64
- },
65
- {
66
- url: 'https://twitter.com/aesthetes_art',
67
- type: 'socialmedia'
68
- }
69
- ]
70
- }
71
- ],
72
- tokens: [
53
+ issuers: [
54
+ {
55
+ address: 'rMxCKbEDwqr76QuheSUMdEGf4B9xJ8m5De',
56
+ name: 'Ripple',
57
+ desc: "We're building the Internet of Value."
58
+ }
59
+ ],
60
+ tokens: [
61
+ {
62
+ currency: 'RLUSD',
63
+ issuer: 'rMxCKbEDwqr76QuheSUMdEGf4B9xJ8m5De',
64
+ name: 'Ripple USD',
65
+ desc: 'Ripple USD (RLUSD) is natively issued on the XRP Ledger and Ethereum blockchains and is enabled with a number of features to ensure strict adherence to compliance standards, flexibility for developers, and security for holders.',
66
+ icon: 'https://ripple.com/assets/rlusd-logo.png',
67
+ asset_class: 'rwa',
68
+ asset_subclass: 'stablecoin',
69
+ urls: [
73
70
  {
74
- currency: 'ELS',
75
- issuer: 'rHXuEaRYnnJHbDeuBH5w8yPh5uwNVh5zAg',
76
- name: 'Elysian',
77
- desc: 'The first Token for the Art and NFT Industry running on the XRPL.',
78
- icon: 'https://static.xrplmeta.org/icons/els.png',
79
- asset_class: 'cryptocurrency',
80
- weblinks: [
81
- {
82
- url: 'https://twitter.com/Elysianers',
83
- type: 'community'
84
- }
85
- ]
86
- }
87
- ],
88
- issues: []
71
+ url: 'https://ripple.com',
72
+ type: 'website',
73
+ title: 'Official Website'
74
+ },
75
+ { url: 'https://x.com/ripple', type: 'social' }
76
+ ]
77
+ }
78
+ ],
79
+ issues: []
89
80
  }
90
-
91
81
  ```
package/xls26.js CHANGED
@@ -1,19 +1,50 @@
1
1
  // The XLS-26 standard adds additional asset metadata fields to the existing xrp-ledger.toml standard,
2
2
  // https://github.com/XRPLF/XRPL-Standards/discussions/71
3
3
  // This package provides an implementation for a parser according to this standard.
4
+ // Version 5 from 2025-06-06.
4
5
 
5
6
 
6
7
  import { parse as parseToml } from '@xrplkit/toml'
7
8
 
8
- const validWeblinkTypes = [
9
- 'info',
10
- 'socialmedia',
11
- 'community',
12
- 'support',
13
- 'whitepaper',
14
- 'certificate'
9
+ const validUrlRegex = /^(https?)|(ipfs):\/\/.*$/
10
+ const validUrlTypes = {
11
+ website: 'website',
12
+ social: 'social',
13
+ docs: 'docs',
14
+ other: 'other',
15
+ info: 'website',
16
+ socialmedia: 'social',
17
+ community: 'social',
18
+ support: 'website',
19
+ whitepaper: 'docs',
20
+ certificate: 'docs',
21
+ }
22
+
23
+ const validAssetClasses = [
24
+ 'rwa',
25
+ 'memes',
26
+ 'wrapped',
27
+ 'gaming',
28
+ 'defi',
29
+ 'other'
15
30
  ]
16
31
 
32
+ const validAssetSubClasses = [
33
+ 'stablecoin',
34
+ 'commodity',
35
+ 'real_estate',
36
+ 'private_credit',
37
+ 'equity',
38
+ 'treasury',
39
+ 'other'
40
+ ]
41
+
42
+ const legacyAssetClasses = {
43
+ fiat: { asset_class: 'rwa', asset_subclass: 'stablecoin' },
44
+ commodity: { asset_class: 'rwa', asset_subclass: 'commodity' },
45
+ equity: { asset_class: 'rwa', asset_subclass: 'equity' }
46
+ }
47
+
17
48
  const validAdvisoryTypes = [
18
49
  'scam',
19
50
  'spam',
@@ -22,17 +53,10 @@ const validAdvisoryTypes = [
22
53
  'hijacked'
23
54
  ]
24
55
 
25
- const validAssetClasses = [
26
- 'fiat',
27
- 'commodity',
28
- 'equity',
29
- 'cryptocurrency'
30
- ]
31
-
32
56
  const issuerFields = [
33
57
  {
34
58
  key: 'address',
35
- essential: true,
59
+ required: true,
36
60
  validate: v => {
37
61
  if(!/^[rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz]{25,35}$/.test(v))
38
62
  throw 'is not a valid XRPL address'
@@ -42,49 +66,49 @@ const issuerFields = [
42
66
  key: 'name',
43
67
  validate: v => {
44
68
  if(typeof v !== 'string' || v.length === 0)
45
- throw 'has to be a non empty string'
69
+ throw 'must be a non empty string'
46
70
  }
47
71
  },
48
72
  {
49
- key: 'description',
50
- alternativeKeys: ['desc'],
73
+ key: 'desc',
74
+ alternativeKeys: ['description'],
51
75
  validate: v => {
52
76
  if(typeof v !== 'string' || v.length === 0)
53
- throw 'has to be a non empty string'
77
+ throw 'must be a non empty string'
54
78
  }
55
79
  },
56
80
  {
57
81
  key: 'domain',
58
82
  validate: v => {
59
83
  if(typeof v !== 'string' || v.length === 0)
60
- throw 'has to be a non empty string'
84
+ throw 'must be a non empty string'
61
85
  }
62
86
  },
63
87
  {
64
88
  key: 'icon',
65
89
  alternativeKeys: ['avatar'],
66
90
  validate: v => {
67
- if(!/^https?:\/\/.*$/.test(v))
68
- throw 'has to be a valid HTTP URL that starts with "http"'
91
+ if(!validUrlRegex.test(v))
92
+ throw 'must be a valid URL that starts with "http" or "ipfs"'
69
93
  }
70
94
  },
71
95
  {
72
96
  key: 'trust_level',
73
97
  validate: v => {
74
98
  if(v !== parseInt(v))
75
- throw 'has to be a integer'
99
+ throw 'must be a integer'
76
100
 
77
101
  if(v < 0 || v > 3)
78
- throw 'has to be between 0 and 3'
102
+ throw 'must be between 0 and 3'
79
103
  }
80
104
  }
81
105
  ]
82
106
 
83
- const tokenFields = [
107
+ const iouTokenFields = [
84
108
  {
85
109
  key: 'currency',
86
110
  alternativeKeys: ['code'],
87
- essential: true,
111
+ required: true,
88
112
  validate: v => {
89
113
  if(typeof v !== 'string' && v.length < 3)
90
114
  throw 'is not a valid XRPL currency code'
@@ -92,7 +116,7 @@ const tokenFields = [
92
116
  },
93
117
  {
94
118
  key: 'issuer',
95
- essential: true,
119
+ required: true,
96
120
  validate: v => {
97
121
  if(!/^[rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz]{25,35}$/.test(v))
98
122
  throw 'is not a valid XRPL address'
@@ -102,65 +126,97 @@ const tokenFields = [
102
126
  key: 'name',
103
127
  validate: v => {
104
128
  if(typeof v !== 'string' || v.length === 0)
105
- throw 'has to be a non empty string'
129
+ throw 'must be a non empty string'
106
130
  }
107
131
  },
108
132
  {
109
- key: 'description',
110
- alternativeKeys: ['desc'],
133
+ key: 'desc',
134
+ alternativeKeys: ['description'],
111
135
  validate: v => {
112
136
  if(typeof v !== 'string' || v.length === 0)
113
- throw 'has to be a non empty string'
137
+ throw 'must be a non empty string'
114
138
  }
115
139
  },
116
140
  {
117
141
  key: 'icon',
118
142
  alternativeKeys: ['avatar'],
119
143
  validate: v => {
120
- if(!/^https?:\/\/.*$/.test(v))
121
- throw 'has to be a valid HTTP URL that starts with "http"'
144
+ if(!validUrlRegex.test(v))
145
+ throw 'must be a valid URL starting with "http" or "ipfs"'
122
146
  }
123
147
  },
124
148
  {
125
149
  key: 'trust_level',
126
150
  validate: v => {
127
151
  if(v !== parseInt(v))
128
- throw 'has to be a integer'
152
+ throw 'must be a integer'
129
153
 
130
154
  if(v < 0 || v > 3)
131
- throw 'has to be between 0 and 3'
155
+ throw 'must be between 0 and 3'
132
156
  }
133
157
  },
134
158
  {
135
159
  key: 'asset_class',
136
160
  validate: v => {
137
- if(!validAssetClasses.includes(v))
138
- throw `needs to be one of the following: ${validAssetClasses.join(', ')}`
161
+ if(!legacyAssetClasses[v] && !validAssetClasses.includes(v))
162
+ throw `must be one of: ${validAssetClasses.join(', ')}`
163
+ }
164
+ },
165
+ {
166
+ key: 'asset_subclass',
167
+ validate: v => {
168
+ if(!validAssetSubClasses.includes(v))
169
+ throw `must be one of: ${validAssetSubClasses.join(', ')}`
170
+ }
171
+ }
172
+ ]
173
+
174
+ const mpTokenFields = [
175
+ {
176
+ key: 'mpt_issuance_id',
177
+ required: true,
178
+ validate: v => {
179
+ if(!/^[0-9a-fA-F]{48}$/.test(v))
180
+ throw 'is not a valid mpt_issuance_id'
181
+ }
182
+ },
183
+ {
184
+ key: 'trust_level',
185
+ required: true,
186
+ validate: v => {
187
+ if(v !== parseInt(v))
188
+ throw 'must be a integer'
189
+
190
+ if(v < 0 || v > 3)
191
+ throw 'must be between 0 and 3'
139
192
  }
140
193
  }
141
194
  ]
142
195
 
143
- const weblinkFields = [
196
+ const urlFields = [
144
197
  {
145
198
  key: 'url',
146
- essential: true,
199
+ required: true,
147
200
  validate: v => {
148
- if(!/^https?:\/\/.*$/.test(v))
149
- throw 'has to be a valid HTTP URL that starts with "http"'
201
+ if(!validUrlRegex.test(v))
202
+ throw 'must be a valid URL starting with "http" or "ipfs"'
150
203
  }
151
204
  },
152
205
  {
153
206
  key: 'type',
154
207
  validate: v => {
155
- if(!validWeblinkTypes.includes(v))
156
- throw `has to be one of (${validWeblinkTypes.join(', ')})`
208
+ if(!validUrlTypes[v])
209
+ throw `must be one of: ${Array.from(new Set(Object.values(validUrlTypes))).join(', ')}`
210
+ },
211
+ transform: v => {
212
+ return validUrlTypes[v]
157
213
  }
158
214
  },
159
215
  {
160
216
  key: 'title',
161
217
  validate: v => {
162
218
  if(typeof v !== 'string' || v.length === 0)
163
- throw 'has to be a non empty string'
219
+ throw 'must be a non empty string'
164
220
  }
165
221
  },
166
222
  ]
@@ -168,7 +224,7 @@ const weblinkFields = [
168
224
  const advisoryFields = [
169
225
  {
170
226
  key: 'address',
171
- essential: true,
227
+ required: true,
172
228
  validate: v => {
173
229
  if(!/^[rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz]{25,35}$/.test(v))
174
230
  throw 'is not a valid XRPL address'
@@ -178,7 +234,7 @@ const advisoryFields = [
178
234
  key: 'type',
179
235
  validate: v => {
180
236
  if(!validAdvisoryTypes.includes(v))
181
- throw `has to be one of (${validAdvisoryTypes.join(', ')})`
237
+ throw `must be one of: ${validAdvisoryTypes.join(', ')}`
182
238
  }
183
239
  },
184
240
  {
@@ -186,7 +242,7 @@ const advisoryFields = [
186
242
  alternativeKeys: ['desc'],
187
243
  validate: v => {
188
244
  if(typeof v !== 'string' || v.length === 0)
189
- throw 'has to be a non empty string'
245
+ throw 'must be a non empty string'
190
246
  }
191
247
  }
192
248
  ]
@@ -204,71 +260,67 @@ export function parse(str){
204
260
  let advisories = []
205
261
 
206
262
 
207
- if((toml.ISSUERS || toml.ACCOUNTS)){
208
- for(let stanza of (toml.ISSUERS || toml.ACCOUNTS)){
209
- let { valid, parsed: issuer, issues: issuerIssues } = parseStanza(stanza, issuerFields)
263
+ for(let stanza of (toml.ISSUERS || toml.ACCOUNTS || [])){
264
+ let { valid, parsed: issuer, issues: issuerIssues } = parseStanza(stanza, issuerFields)
210
265
 
211
- if(valid)
212
- issuers.push(issuer)
266
+ issues.push(
267
+ ...issuerIssues.map(
268
+ issue => `[[ISSUERS]] ${issue}`
269
+ )
270
+ )
271
+
272
+ if(valid)
273
+ issuers.push(issuer)
274
+ else
275
+ continue
276
+
277
+ for(let substanza of (stanza.URLS || stanza.WEBLINKS || [])){
278
+ let { valid, parsed: url, issues: urlIssues } = parseStanza(substanza, urlFields)
279
+
280
+ if(valid){
281
+ issuer.urls = [
282
+ ...(issuer.urls || []),
283
+ url
284
+ ]
285
+ }
213
286
 
214
287
  issues.push(
215
- ...issuerIssues.map(
216
- issue => `[[ISSUERS]] ${issue}`
288
+ ...urlIssues.map(
289
+ issue => `[[ISSUERS.URLS]] ${issue}`
217
290
  )
218
291
  )
219
-
220
- if(valid && stanza.WEBLINKS){
221
- for(let substanza of stanza.WEBLINKS){
222
- let { valid, parsed: weblink, issues: weblinkIssues } = parseStanza(substanza, weblinkFields)
223
-
224
- if(valid){
225
- issuer.weblinks = [
226
- ...(issuer.weblinks || []),
227
- weblink
228
- ]
229
- }
230
-
231
- issues.push(
232
- ...weblinkIssues.map(
233
- issue => `[[WEBLINK]] ${issue}`
234
- )
235
- )
236
- }
237
- }
238
292
  }
239
293
  }
240
294
 
241
- if((toml.TOKENS || toml.CURRENCIES)){
242
- for(let stanza of (toml.TOKENS || toml.CURRENCIES)){
243
- let { valid, parsed: token, issues: tokenIssues } = parseStanza(stanza, tokenFields)
295
+ for(let stanza of (toml.TOKENS || toml.CURRENCIES || [])){
296
+ let { valid, parsed: token, issues: tokenIssues } = parseStanza(stanza, stanza['mpt_issuance_id'] != null ? mpTokenFields : iouTokenFields)
244
297
 
245
- if(valid)
246
- tokens.push(token)
247
-
248
- issues.push(
249
- ...tokenIssues.map(
250
- issue => `[[TOKENS]] ${issue}`
251
- )
298
+ issues.push(
299
+ ...tokenIssues.map(
300
+ issue => `[[TOKENS]] ${issue}`
252
301
  )
302
+ )
253
303
 
254
- if(valid && stanza.WEBLINKS){
255
- for(let substanza of stanza.WEBLINKS){
256
- let { valid, parsed: weblink, issues: weblinkIssues } = parseStanza(substanza, weblinkFields)
257
-
258
- if(valid){
259
- token.weblinks = [
260
- ...(token.weblinks || []),
261
- weblink
262
- ]
263
- }
264
-
265
- issues.push(
266
- ...weblinkIssues.map(
267
- issue => `[[WEBLINK]] ${issue}`
268
- )
269
- )
270
- }
304
+ if(valid)
305
+ tokens.push(token)
306
+ else
307
+ continue
308
+
309
+ for(let substanza of (stanza.URLS || stanza.WEBLINKS || [])){
310
+ let { valid, parsed: url, issues: urlIssues } = parseStanza(substanza, urlFields)
311
+
312
+ if(valid){
313
+ token.urls = [
314
+ ...(token.urls || []),
315
+ url
316
+ ]
271
317
  }
318
+
319
+ issues.push(
320
+ ...urlIssues.map(
321
+ issue => `[[TOKENS.URLS]] ${issue}`
322
+ )
323
+ )
272
324
  }
273
325
  }
274
326
 
@@ -287,6 +339,32 @@ export function parse(str){
287
339
  }
288
340
  }
289
341
 
342
+ // Issuer URLs have been dropped since Version 5
343
+ // Issuer URLs now get mapped to respective tokens
344
+
345
+ for(let issuer of issuers){
346
+ if(!issuer.urls)
347
+ continue
348
+
349
+ for(let token of tokens){
350
+ if(token.issuer !== issuer.address)
351
+ continue
352
+
353
+ token.urls = [
354
+ ...issuer.urls,
355
+ ...(token.urls || [])
356
+ ]
357
+ }
358
+
359
+ delete issuer.urls
360
+ }
361
+
362
+ for(let token of tokens){
363
+ if(!legacyAssetClasses[token.asset_class])
364
+ continue
365
+
366
+ Object.assign(token, legacyAssetClasses[token.asset_class])
367
+ }
290
368
 
291
369
  return {
292
370
  issuers,
@@ -301,7 +379,7 @@ function parseStanza(stanza, schemas){
301
379
  let issues = []
302
380
  let valid = true
303
381
 
304
- for(let { key, alternativeKeys, essential, validate } of schemas){
382
+ for(let { key, alternativeKeys, required, validate, transform } of schemas){
305
383
  let keys = [key]
306
384
 
307
385
  if(alternativeKeys)
@@ -322,11 +400,14 @@ function parseStanza(stanza, schemas){
322
400
  }
323
401
  }
324
402
 
403
+ if(transform)
404
+ value = transform(value)
405
+
325
406
  parsed[key] = value
326
407
  break
327
408
  }
328
409
 
329
- if(essential && parsed[key] === undefined){
410
+ if(required && parsed[key] === undefined){
330
411
  issues.push(`${key} field missing: skipping stanza`)
331
412
  valid = false
332
413
  }