@xrplkit/xls26 1.0.0 → 2.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.
Files changed (3) hide show
  1. package/package.json +9 -2
  2. package/readme.md +67 -43
  3. package/xls26.js +190 -43
package/package.json CHANGED
@@ -1,12 +1,19 @@
1
1
  {
2
2
  "name": "@xrplkit/xls26",
3
3
  "type": "module",
4
- "version": "1.0.0",
4
+ "version": "2.0.2",
5
5
  "main": "xls26.js",
6
6
  "dependencies": {
7
7
  "@xrplkit/toml": "1.0.0"
8
8
  },
9
9
  "publishConfig": {
10
10
  "access": "public"
11
- }
11
+ },
12
+ "keywords": [
13
+ "xrpl",
14
+ "xls-26",
15
+ "parser",
16
+ "metadata",
17
+ "tokens"
18
+ ]
12
19
  }
package/readme.md CHANGED
@@ -1,55 +1,79 @@
1
1
  # XLS-26 Parsing Library
2
2
 
3
3
  This is an implementation of the [XLS-26 Standard](https://github.com/XRPLF/XRPL-Standards/discussions/71).
4
- This package exports one single function called `parse` that converts a string of a `xrp-ledger.toml` file to a JavaScript object.
4
+ This package exports one single function called `parse` that converts the string of a `xrp-ledger.toml` file to a JavaScript object.
5
+
5
6
 
6
7
  ## Example
7
- Assuming you have a file named `xrp-ledger.toml` in the current working directory. [Example File here](http://xrpl.works/.well-known/xrp-ledger.toml)
8
-
9
- import fs from 'fs'
10
- import { parse } from '@xrplkit/xls26'
11
-
12
- const tomlString = fs.readFileSync('./xrp-ledger.toml', 'utf-8')
13
- const xls26Data = parse(tomlString)
14
-
15
- console.log(xls26Data)
16
-
17
- ### Example Input
18
-
19
- [[ACCOUNTS]]
20
- address = "rxworksy7717V3w1nSQhUpaGNqydGYCaS"
21
- name = "XRPL Works"
22
- websites = ["https://xrpl.works"]
23
- description = "XRPL Works is a non-profit organization. Our goal is to simplify the ledger."
24
-
25
- [[CURRENCIES]]
26
- code = "58574F524B530000000000000000000000000000"
27
- issuer = "rxworksy7717V3w1nSQhUpaGNqydGYCaS"
28
- name = "XWORKS"
29
- icon = "https://xrpl.works/token/icon.png"
30
- description = "This token serves the purpose of demonstrating the benefits of the XLS-26 standard."
31
-
8
+ Assuming you have a file named `xrp-ledger.toml` in the current working directory. An example file can be found below.
32
9
 
33
- ### Example Output
10
+ ```javascript
11
+ import fs from 'fs'
12
+ import { parse } from '@xrplkit/xls26'
13
+
14
+ const tomlString = fs.readFileSync('./xrp-ledger.toml', 'utf-8')
15
+ const xls26Data = parse(tomlString)
16
+
17
+ console.log(xls26Data)
18
+ ```
19
+
20
+
21
+ ### Example File
34
22
 
23
+ ```toml
24
+ [[ISSUERS]]
25
+ address = "rCSCManTZ8ME9EoLrSHHYKW8PPwWMgkwr"
26
+ name = "CasinoCoin"
27
+
28
+ [[TOKENS]]
29
+ issuer = "rCSCManTZ8ME9EoLrSHHYKW8PPwWMgkwr"
30
+ currency = "CSC"
31
+ name = "CasinoCoin"
32
+ desc = "CasinoCoin (CSC) is a digital currency, developed specifically for the regulated gaming industry."
33
+ icon = "https://static.xrplmeta.org/icons/csc.png"
34
+
35
+ [[TOKENS.WEBLINKS]]
36
+ url = "https://casinocoin.im"
37
+ type = "website"
38
+ title = "Official Website"
39
+
40
+ [[TOKENS.WEBLINKS]]
41
+ url = "https://twitter.com/CasinoCoin"
42
+ type = "socialmedia"
35
43
  ```
44
+
45
+
46
+ ### Example Output
47
+
48
+ ```javascript
36
49
  {
37
- "accounts": [
38
- {
39
- "address": "rxworksy7717V3w1nSQhUpaGNqydGYCaS",
40
- "name": "XRPL Works",
41
- "description": "XRPL Works is a non-profit organization. Our goal is to simplify the ledger."
42
- }
43
- ],
44
- "currencies": [
45
- {
46
- "code": "58574F524B530000000000000000000000000000",
47
- "issuer": "rxworksy7717V3w1nSQhUpaGNqydGYCaS",
48
- "name": "XWORKS",
49
- "description": "This token serves the purpose of demonstrating the benefits of the XLS-26 standard.",
50
- "icon": "https://xrpl.works/token/icon.png"
51
- }
52
- ]
50
+ issuers: [
51
+ {
52
+ address: 'rCSCManTZ8ME9EoLrSHHYKW8PPwWMgkwr',
53
+ name: 'CasinoCoin'
54
+ }
55
+ ],
56
+ tokens: [
57
+ {
58
+ currency: 'CSC',
59
+ issuer: 'rCSCManTZ8ME9EoLrSHHYKW8PPwWMgkwr',
60
+ name: 'CasinoCoin',
61
+ desc: 'CasinoCoin (CSC) is a digital currency, developed specifically for the regulated gaming industry.',
62
+ icon: 'https://static.xrplmeta.org/icons/csc.png',
63
+ weblinks: [
64
+ {
65
+ url: 'https://casinocoin.im',
66
+ type: 'website',
67
+ title: 'Official Website'
68
+ },
69
+ {
70
+ url: 'https://twitter.com/CasinoCoin',
71
+ type: 'socialmedia'
72
+ }
73
+ ]
74
+ }
75
+ ],
76
+ issues: []
53
77
  }
54
78
 
55
79
  ```
package/xls26.js CHANGED
@@ -1,118 +1,265 @@
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
- // This package provides an implementation of a parser according to this standard.
3
+ // This package provides an implementation for a parser according to this standard.
4
4
 
5
5
 
6
6
  import { parse as parseToml } from '@xrplkit/toml'
7
7
 
8
+ const validWeblinkTypes = [
9
+ 'website',
10
+ 'socialmedia',
11
+ 'support',
12
+ 'sourcecode',
13
+ 'whitepaper',
14
+ 'audit',
15
+ 'report'
16
+ ]
8
17
 
9
- const accountFields = [
18
+ const issuerFields = [
10
19
  {
11
20
  key: 'address',
12
- validate: v => /^[rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz]{25,35}$/.test(v)
21
+ essential: true,
22
+ validate: v => {
23
+ if(!/^[rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz]{25,35}$/.test(v))
24
+ throw 'is not a valid XRPL address'
25
+ },
13
26
  },
14
27
  {
15
28
  key: 'name',
16
- validate: v => typeof v === 'string' && v.length > 0
29
+ validate: v => {
30
+ if(typeof v !== 'string' || v.length === 0)
31
+ throw 'has to be a non empty string'
32
+ }
17
33
  },
18
34
  {
19
35
  key: 'description',
20
36
  alternativeKeys: ['desc'],
21
- validate: v => typeof v === 'string' && v.length > 0
37
+ validate: v => {
38
+ if(typeof v !== 'string' || v.length === 0)
39
+ throw 'has to be a non empty string'
40
+ }
22
41
  },
23
42
  {
24
43
  key: 'icon',
25
- validate: v => /^https?:\/\/.*$/.test(v)
26
- },
27
- {
28
- key: 'links',
29
- type: 'array',
30
- validate: v => Array.isArray(v) && v.every(v => typeof v === 'string'),
44
+ alternativeKeys: ['avatar'],
45
+ validate: v => {
46
+ if(!/^https?:\/\/.*$/.test(v))
47
+ throw 'has to be a valid HTTP URL that starts with "http"'
48
+ }
31
49
  },
32
50
  {
33
- key: 'trusted',
34
- validate: v => typeof v === 'boolean'
51
+ key: 'trust_level',
52
+ validate: v => {
53
+ if(v !== parseInt(v))
54
+ throw 'has to be a integer'
55
+
56
+ if(v < 0 || v > 3)
57
+ throw 'has to be between 0 and 3'
58
+ }
35
59
  }
36
60
  ]
37
61
 
38
-
39
- const currencyFields = [
62
+ const tokenFields = [
40
63
  {
41
- key: 'code',
42
- validate: v => /^[0-9A-F]{40}$/
64
+ key: 'currency',
65
+ essential: true,
66
+ validate: v => {
67
+ if(typeof v !== 'string' && v.length < 3)
68
+ throw 'is not a valid XRPL currency code'
69
+ }
43
70
  },
44
71
  {
45
72
  key: 'issuer',
46
- validate: v => /^[rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz]{25,35}$/.test(v)
73
+ essential: true,
74
+ validate: v => {
75
+ if(!/^[rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz]{25,35}$/.test(v))
76
+ throw 'is not a valid XRPL address'
77
+ }
47
78
  },
48
79
  {
49
80
  key: 'name',
50
- validate: v => typeof v === 'string' && v.length > 0
81
+ validate: v => {
82
+ if(typeof v !== 'string' || v.length === 0)
83
+ throw 'has to be a non empty string'
84
+ }
51
85
  },
52
86
  {
53
87
  key: 'description',
54
88
  alternativeKeys: ['desc'],
55
- validate: v => typeof v === 'string' && v.length > 0
89
+ validate: v => {
90
+ if(typeof v !== 'string' || v.length === 0)
91
+ throw 'has to be a non empty string'
92
+ }
56
93
  },
57
94
  {
58
95
  key: 'icon',
59
- validate: v => /^https?:\/\/.*$/.test(v)
96
+ alternativeKeys: ['avatar'],
97
+ validate: v => {
98
+ if(!/^https?:\/\/.*$/.test(v))
99
+ throw 'has to be a valid HTTP URL that starts with "http"'
100
+ }
60
101
  },
61
102
  {
62
- key: 'links',
63
- type: 'array',
64
- validate: v => Array.isArray(v) && v.every(v => typeof v === 'string'),
103
+ key: 'trust_level',
104
+ validate: v => {
105
+ if(v !== parseInt(v))
106
+ throw 'has to be a integer'
107
+
108
+ if(v < 0 || v > 3)
109
+ throw 'has to be between 0 and 3'
110
+ }
111
+ }
112
+ ]
113
+
114
+ const weblinkFields = [
115
+ {
116
+ key: 'url',
117
+ essential: true,
118
+ validate: v => {
119
+ if(!/^https?:\/\/.*$/.test(v))
120
+ throw 'has to be a valid HTTP URL that starts with "http"'
121
+ }
65
122
  },
66
123
  {
67
- key: 'trusted',
68
- validate: v => typeof v === 'boolean'
69
- }
124
+ key: 'type',
125
+ validate: v => {
126
+ if(!validWeblinkTypes.includes(v))
127
+ throw `has to be one of (${validWeblinkTypes.join(', ')})`
128
+ }
129
+ },
130
+ {
131
+ key: 'title',
132
+ validate: v => {
133
+ if(typeof v !== 'string' || v.length === 0)
134
+ throw 'has to be a non empty string'
135
+ }
136
+ },
70
137
  ]
71
138
 
72
139
 
73
140
  export function parse(str){
74
- let toml = parseToml(str, 'camelCase')
75
- let accounts = []
76
- let currencies = []
141
+ try{
142
+ var toml = parseToml(str)
143
+ }catch(error){
144
+ throw new Error(`Failed to parse .toml: Syntax error at line ${error.line}:${error.column}`)
145
+ }
146
+
147
+ let issuers = []
148
+ let tokens = []
149
+ let issues = []
150
+
151
+
152
+ if(toml.ISSUERS){
153
+ for(let stanza of toml.ISSUERS){
154
+ let { valid, parsed: issuer, issues: issuerIssues } = parseStanza(stanza, issuerFields)
155
+
156
+ if(valid)
157
+ issuers.push(issuer)
158
+
159
+ issues.push(
160
+ ...issuerIssues.map(
161
+ issue => `[[ISSUERS]] ${issue}`
162
+ )
163
+ )
164
+
165
+ if(valid && stanza.WEBLINKS){
166
+ for(let substanza of stanza.WEBLINKS){
167
+ let { valid, parsed: weblink, issues: weblinkIssues } = parseStanza(substanza, weblinkFields)
77
168
 
78
- if(toml.accounts){
79
- accounts = toml.accounts
80
- .map(account => parseStanza(account, accountFields))
169
+ if(valid){
170
+ issuer.weblinks = [
171
+ ...(issuer.weblinks || []),
172
+ weblink
173
+ ]
174
+ }
175
+
176
+ issues.push(
177
+ ...weblinkIssues.map(
178
+ issue => `[[WEBLINK]] ${issue}`
179
+ )
180
+ )
181
+ }
182
+ }
183
+ }
81
184
  }
82
185
 
83
- if(toml.currencies){
84
- currencies = toml.currencies
85
- .map(currency => parseStanza(currency, currencyFields))
186
+ if(toml.TOKENS){
187
+ for(let stanza of toml.TOKENS){
188
+ let { valid, parsed: token, issues: tokenIssues } = parseStanza(stanza, tokenFields)
189
+
190
+ if(valid)
191
+ tokens.push(token)
192
+
193
+ issues.push(
194
+ ...tokenIssues.map(
195
+ issue => `[[TOKENS]] ${issue}`
196
+ )
197
+ )
198
+
199
+ if(valid && stanza.WEBLINKS){
200
+ for(let substanza of stanza.WEBLINKS){
201
+ let { valid, parsed: weblink, issues: weblinkIssues } = parseStanza(substanza, weblinkFields)
202
+
203
+ if(valid){
204
+ token.weblinks = [
205
+ ...(token.weblinks || []),
206
+ weblink
207
+ ]
208
+ }
209
+
210
+ issues.push(
211
+ ...weblinkIssues.map(
212
+ issue => `[[WEBLINK]] ${issue}`
213
+ )
214
+ )
215
+ }
216
+ }
217
+ }
86
218
  }
87
219
 
220
+
88
221
  return {
89
- accounts,
90
- currencies
222
+ issuers,
223
+ tokens,
224
+ issues
91
225
  }
92
226
  }
93
227
 
94
228
  function parseStanza(stanza, schemas){
95
229
  let parsed = {}
230
+ let issues = []
231
+ let valid = true
96
232
 
97
- for(let { key, alternativeKeys, validate } of schemas){
233
+ for(let { key, alternativeKeys, essential, validate } of schemas){
98
234
  let keys = [key]
99
235
 
100
236
  if(alternativeKeys)
101
237
  keys.push(...alternativeKeys)
102
238
 
103
239
  for(let k of keys){
104
- if(!stanza.hasOwnProperty(k))
240
+ if(stanza[k] === undefined)
105
241
  continue
106
242
 
107
243
  let value = stanza[k]
108
244
 
109
- if(validate && !validate(value))
110
- continue
245
+ if(validate){
246
+ try{
247
+ validate(value)
248
+ }catch(issue){
249
+ issues.push(`${key} field: ${issue}`)
250
+ break
251
+ }
252
+ }
111
253
 
112
254
  parsed[k] = value
113
255
  break
114
256
  }
257
+
258
+ if(essential && parsed[key] === undefined){
259
+ issues.push(`${key} field missing: skipping stanza`)
260
+ valid = false
261
+ }
115
262
  }
116
263
 
117
- return parsed
264
+ return { valid, parsed, issues }
118
265
  }