@xrplkit/xls26 2.4.0 → 2.5.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 +160 -101
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.5.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'
30
+ ]
31
+
32
+ const validAssetSubClasses = [
33
+ 'stablecoin',
34
+ 'commodity',
35
+ 'real_estate',
36
+ 'private_credit',
37
+ 'equity',
38
+ 'treasury',
39
+ 'other'
15
40
  ]
16
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,40 +66,40 @@ 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
  ]
@@ -84,7 +108,7 @@ const tokenFields = [
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,75 @@ 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(', ')}`
139
170
  }
140
171
  }
141
172
  ]
142
173
 
143
- const weblinkFields = [
174
+ const urlFields = [
144
175
  {
145
176
  key: 'url',
146
- essential: true,
177
+ required: true,
147
178
  validate: v => {
148
- if(!/^https?:\/\/.*$/.test(v))
149
- throw 'has to be a valid HTTP URL that starts with "http"'
179
+ if(!validUrlRegex.test(v))
180
+ throw 'must be a valid URL starting with "http" or "ipfs"'
150
181
  }
151
182
  },
152
183
  {
153
184
  key: 'type',
154
185
  validate: v => {
155
- if(!validWeblinkTypes.includes(v))
156
- throw `has to be one of (${validWeblinkTypes.join(', ')})`
186
+ if(!validUrlTypes[v])
187
+ throw `must be one of: ${Array.from(new Set(Object.values(validUrlTypes))).join(', ')}`
188
+ },
189
+ transform: v => {
190
+ return validUrlTypes[v]
157
191
  }
158
192
  },
159
193
  {
160
194
  key: 'title',
161
195
  validate: v => {
162
196
  if(typeof v !== 'string' || v.length === 0)
163
- throw 'has to be a non empty string'
197
+ throw 'must be a non empty string'
164
198
  }
165
199
  },
166
200
  ]
@@ -168,7 +202,7 @@ const weblinkFields = [
168
202
  const advisoryFields = [
169
203
  {
170
204
  key: 'address',
171
- essential: true,
205
+ required: true,
172
206
  validate: v => {
173
207
  if(!/^[rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz]{25,35}$/.test(v))
174
208
  throw 'is not a valid XRPL address'
@@ -178,7 +212,7 @@ const advisoryFields = [
178
212
  key: 'type',
179
213
  validate: v => {
180
214
  if(!validAdvisoryTypes.includes(v))
181
- throw `has to be one of (${validAdvisoryTypes.join(', ')})`
215
+ throw `must be one of: ${validAdvisoryTypes.join(', ')}`
182
216
  }
183
217
  },
184
218
  {
@@ -186,7 +220,7 @@ const advisoryFields = [
186
220
  alternativeKeys: ['desc'],
187
221
  validate: v => {
188
222
  if(typeof v !== 'string' || v.length === 0)
189
- throw 'has to be a non empty string'
223
+ throw 'must be a non empty string'
190
224
  }
191
225
  }
192
226
  ]
@@ -204,71 +238,67 @@ export function parse(str){
204
238
  let advisories = []
205
239
 
206
240
 
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)
241
+ for(let stanza of (toml.ISSUERS || toml.ACCOUNTS || [])){
242
+ let { valid, parsed: issuer, issues: issuerIssues } = parseStanza(stanza, issuerFields)
210
243
 
211
- if(valid)
212
- issuers.push(issuer)
244
+ issues.push(
245
+ ...issuerIssues.map(
246
+ issue => `[[ISSUERS]] ${issue}`
247
+ )
248
+ )
249
+
250
+ if(valid)
251
+ issuers.push(issuer)
252
+ else
253
+ continue
254
+
255
+ for(let substanza of (stanza.URLS || stanza.WEBLINKS || [])){
256
+ let { valid, parsed: url, issues: urlIssues } = parseStanza(substanza, urlFields)
257
+
258
+ if(valid){
259
+ issuer.urls = [
260
+ ...(issuer.urls || []),
261
+ url
262
+ ]
263
+ }
213
264
 
214
265
  issues.push(
215
- ...issuerIssues.map(
216
- issue => `[[ISSUERS]] ${issue}`
266
+ ...urlIssues.map(
267
+ issue => `[[ISSUERS.URLS]] ${issue}`
217
268
  )
218
269
  )
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
270
  }
239
271
  }
240
272
 
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)
273
+ for(let stanza of (toml.TOKENS || toml.CURRENCIES || [])){
274
+ let { valid, parsed: token, issues: tokenIssues } = parseStanza(stanza, tokenFields)
244
275
 
245
- if(valid)
246
- tokens.push(token)
247
-
248
- issues.push(
249
- ...tokenIssues.map(
250
- issue => `[[TOKENS]] ${issue}`
251
- )
276
+ issues.push(
277
+ ...tokenIssues.map(
278
+ issue => `[[TOKENS]] ${issue}`
252
279
  )
280
+ )
253
281
 
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
- }
282
+ if(valid)
283
+ tokens.push(token)
284
+ else
285
+ continue
286
+
287
+ for(let substanza of (stanza.URLS || stanza.WEBLINKS || [])){
288
+ let { valid, parsed: url, issues: urlIssues } = parseStanza(substanza, urlFields)
289
+
290
+ if(valid){
291
+ token.urls = [
292
+ ...(token.urls || []),
293
+ url
294
+ ]
271
295
  }
296
+
297
+ issues.push(
298
+ ...urlIssues.map(
299
+ issue => `[[TOKENS.URLS]] ${issue}`
300
+ )
301
+ )
272
302
  }
273
303
  }
274
304
 
@@ -287,6 +317,32 @@ export function parse(str){
287
317
  }
288
318
  }
289
319
 
320
+ // Issuer URLs have been dropped since Version 5
321
+ // Issuer URLs now get mapped to respective tokens
322
+
323
+ for(let issuer of issuers){
324
+ if(!issuer.urls)
325
+ continue
326
+
327
+ for(let token of tokens){
328
+ if(token.issuer !== issuer.address)
329
+ continue
330
+
331
+ token.urls = [
332
+ ...issuer.urls,
333
+ ...(token.urls || [])
334
+ ]
335
+ }
336
+
337
+ delete issuer.urls
338
+ }
339
+
340
+ for(let token of tokens){
341
+ if(!legacyAssetClasses[token.asset_class])
342
+ continue
343
+
344
+ Object.assign(token, legacyAssetClasses[token.asset_class])
345
+ }
290
346
 
291
347
  return {
292
348
  issuers,
@@ -301,7 +357,7 @@ function parseStanza(stanza, schemas){
301
357
  let issues = []
302
358
  let valid = true
303
359
 
304
- for(let { key, alternativeKeys, essential, validate } of schemas){
360
+ for(let { key, alternativeKeys, required, validate, transform } of schemas){
305
361
  let keys = [key]
306
362
 
307
363
  if(alternativeKeys)
@@ -322,11 +378,14 @@ function parseStanza(stanza, schemas){
322
378
  }
323
379
  }
324
380
 
381
+ if(transform)
382
+ value = transform(value)
383
+
325
384
  parsed[key] = value
326
385
  break
327
386
  }
328
387
 
329
- if(essential && parsed[key] === undefined){
388
+ if(required && parsed[key] === undefined){
330
389
  issues.push(`${key} field missing: skipping stanza`)
331
390
  valid = false
332
391
  }