@xrplkit/xls26 2.3.1 → 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 -4
  2. package/readme.md +44 -54
  3. package/xls26.js +161 -101
package/package.json CHANGED
@@ -1,14 +1,11 @@
1
1
  {
2
2
  "name": "@xrplkit/xls26",
3
3
  "type": "module",
4
- "version": "2.3.1",
4
+ "version": "2.5.0",
5
5
  "main": "xls26.js",
6
6
  "dependencies": {
7
7
  "@xrplkit/toml": "1.0.0"
8
8
  },
9
- "publishConfig": {
10
- "access": "public"
11
- },
12
9
  "keywords": [
13
10
  "xrpl",
14
11
  "xls-26",
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
  ]
@@ -83,7 +107,8 @@ const issuerFields = [
83
107
  const tokenFields = [
84
108
  {
85
109
  key: 'currency',
86
- essential: true,
110
+ alternativeKeys: ['code'],
111
+ required: true,
87
112
  validate: v => {
88
113
  if(typeof v !== 'string' && v.length < 3)
89
114
  throw 'is not a valid XRPL currency code'
@@ -91,7 +116,7 @@ const tokenFields = [
91
116
  },
92
117
  {
93
118
  key: 'issuer',
94
- essential: true,
119
+ required: true,
95
120
  validate: v => {
96
121
  if(!/^[rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz]{25,35}$/.test(v))
97
122
  throw 'is not a valid XRPL address'
@@ -101,65 +126,75 @@ const tokenFields = [
101
126
  key: 'name',
102
127
  validate: v => {
103
128
  if(typeof v !== 'string' || v.length === 0)
104
- throw 'has to be a non empty string'
129
+ throw 'must be a non empty string'
105
130
  }
106
131
  },
107
132
  {
108
- key: 'description',
109
- alternativeKeys: ['desc'],
133
+ key: 'desc',
134
+ alternativeKeys: ['description'],
110
135
  validate: v => {
111
136
  if(typeof v !== 'string' || v.length === 0)
112
- throw 'has to be a non empty string'
137
+ throw 'must be a non empty string'
113
138
  }
114
139
  },
115
140
  {
116
141
  key: 'icon',
117
142
  alternativeKeys: ['avatar'],
118
143
  validate: v => {
119
- if(!/^https?:\/\/.*$/.test(v))
120
- 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"'
121
146
  }
122
147
  },
123
148
  {
124
149
  key: 'trust_level',
125
150
  validate: v => {
126
151
  if(v !== parseInt(v))
127
- throw 'has to be a integer'
152
+ throw 'must be a integer'
128
153
 
129
154
  if(v < 0 || v > 3)
130
- throw 'has to be between 0 and 3'
155
+ throw 'must be between 0 and 3'
131
156
  }
132
157
  },
133
158
  {
134
159
  key: 'asset_class',
135
160
  validate: v => {
136
- if(!validAssetClasses.includes(v))
137
- 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(', ')}`
138
170
  }
139
171
  }
140
172
  ]
141
173
 
142
- const weblinkFields = [
174
+ const urlFields = [
143
175
  {
144
176
  key: 'url',
145
- essential: true,
177
+ required: true,
146
178
  validate: v => {
147
- if(!/^https?:\/\/.*$/.test(v))
148
- 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"'
149
181
  }
150
182
  },
151
183
  {
152
184
  key: 'type',
153
185
  validate: v => {
154
- if(!validWeblinkTypes.includes(v))
155
- 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]
156
191
  }
157
192
  },
158
193
  {
159
194
  key: 'title',
160
195
  validate: v => {
161
196
  if(typeof v !== 'string' || v.length === 0)
162
- throw 'has to be a non empty string'
197
+ throw 'must be a non empty string'
163
198
  }
164
199
  },
165
200
  ]
@@ -167,7 +202,7 @@ const weblinkFields = [
167
202
  const advisoryFields = [
168
203
  {
169
204
  key: 'address',
170
- essential: true,
205
+ required: true,
171
206
  validate: v => {
172
207
  if(!/^[rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz]{25,35}$/.test(v))
173
208
  throw 'is not a valid XRPL address'
@@ -177,7 +212,7 @@ const advisoryFields = [
177
212
  key: 'type',
178
213
  validate: v => {
179
214
  if(!validAdvisoryTypes.includes(v))
180
- throw `has to be one of (${validAdvisoryTypes.join(', ')})`
215
+ throw `must be one of: ${validAdvisoryTypes.join(', ')}`
181
216
  }
182
217
  },
183
218
  {
@@ -185,7 +220,7 @@ const advisoryFields = [
185
220
  alternativeKeys: ['desc'],
186
221
  validate: v => {
187
222
  if(typeof v !== 'string' || v.length === 0)
188
- throw 'has to be a non empty string'
223
+ throw 'must be a non empty string'
189
224
  }
190
225
  }
191
226
  ]
@@ -203,71 +238,67 @@ export function parse(str){
203
238
  let advisories = []
204
239
 
205
240
 
206
- if(toml.ISSUERS){
207
- for(let stanza of toml.ISSUERS){
208
- 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)
209
243
 
210
- if(valid)
211
- 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
+ }
212
264
 
213
265
  issues.push(
214
- ...issuerIssues.map(
215
- issue => `[[ISSUERS]] ${issue}`
266
+ ...urlIssues.map(
267
+ issue => `[[ISSUERS.URLS]] ${issue}`
216
268
  )
217
269
  )
218
-
219
- if(valid && stanza.WEBLINKS){
220
- for(let substanza of stanza.WEBLINKS){
221
- let { valid, parsed: weblink, issues: weblinkIssues } = parseStanza(substanza, weblinkFields)
222
-
223
- if(valid){
224
- issuer.weblinks = [
225
- ...(issuer.weblinks || []),
226
- weblink
227
- ]
228
- }
229
-
230
- issues.push(
231
- ...weblinkIssues.map(
232
- issue => `[[WEBLINK]] ${issue}`
233
- )
234
- )
235
- }
236
- }
237
270
  }
238
271
  }
239
272
 
240
- if(toml.TOKENS){
241
- for(let stanza of toml.TOKENS){
242
- 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)
243
275
 
244
- if(valid)
245
- tokens.push(token)
246
-
247
- issues.push(
248
- ...tokenIssues.map(
249
- issue => `[[TOKENS]] ${issue}`
250
- )
276
+ issues.push(
277
+ ...tokenIssues.map(
278
+ issue => `[[TOKENS]] ${issue}`
251
279
  )
280
+ )
252
281
 
253
- if(valid && stanza.WEBLINKS){
254
- for(let substanza of stanza.WEBLINKS){
255
- let { valid, parsed: weblink, issues: weblinkIssues } = parseStanza(substanza, weblinkFields)
256
-
257
- if(valid){
258
- token.weblinks = [
259
- ...(token.weblinks || []),
260
- weblink
261
- ]
262
- }
263
-
264
- issues.push(
265
- ...weblinkIssues.map(
266
- issue => `[[WEBLINK]] ${issue}`
267
- )
268
- )
269
- }
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
+ ]
270
295
  }
296
+
297
+ issues.push(
298
+ ...urlIssues.map(
299
+ issue => `[[TOKENS.URLS]] ${issue}`
300
+ )
301
+ )
271
302
  }
272
303
  }
273
304
 
@@ -286,6 +317,32 @@ export function parse(str){
286
317
  }
287
318
  }
288
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
+ }
289
346
 
290
347
  return {
291
348
  issuers,
@@ -300,7 +357,7 @@ function parseStanza(stanza, schemas){
300
357
  let issues = []
301
358
  let valid = true
302
359
 
303
- for(let { key, alternativeKeys, essential, validate } of schemas){
360
+ for(let { key, alternativeKeys, required, validate, transform } of schemas){
304
361
  let keys = [key]
305
362
 
306
363
  if(alternativeKeys)
@@ -321,11 +378,14 @@ function parseStanza(stanza, schemas){
321
378
  }
322
379
  }
323
380
 
381
+ if(transform)
382
+ value = transform(value)
383
+
324
384
  parsed[key] = value
325
385
  break
326
386
  }
327
387
 
328
- if(essential && parsed[key] === undefined){
388
+ if(required && parsed[key] === undefined){
329
389
  issues.push(`${key} field missing: skipping stanza`)
330
390
  valid = false
331
391
  }