@xrplkit/xls26 1.0.0 → 2.0.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.
- package/package.json +1 -1
- package/readme.md +61 -37
- package/xls26.js +188 -42
package/package.json
CHANGED
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
|
|
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.
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
8
|
+
Assuming you have a file named `xrp-ledger.toml` in the current working directory. An example file can be found below.
|
|
9
|
+
|
|
10
|
+
```
|
|
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
|
|
22
|
+
|
|
23
|
+
```
|
|
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"
|
|
43
|
+
```
|
|
44
|
+
|
|
32
45
|
|
|
33
46
|
### Example Output
|
|
34
47
|
|
|
35
48
|
```
|
|
36
49
|
{
|
|
37
|
-
|
|
50
|
+
issuers: [
|
|
38
51
|
{
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
"description": "XRPL Works is a non-profit organization. Our goal is to simplify the ledger."
|
|
52
|
+
address: 'rCSCManTZ8ME9EoLrSHHYKW8PPwWMgkwr',
|
|
53
|
+
name: 'CasinoCoin'
|
|
42
54
|
}
|
|
43
55
|
],
|
|
44
|
-
|
|
56
|
+
tokens: [
|
|
45
57
|
{
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
+
]
|
|
51
74
|
}
|
|
52
|
-
]
|
|
75
|
+
],
|
|
76
|
+
issues: []
|
|
53
77
|
}
|
|
54
78
|
|
|
55
79
|
```
|
package/xls26.js
CHANGED
|
@@ -1,100 +1,235 @@
|
|
|
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
|
|
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
|
|
18
|
+
const issuerFields = [
|
|
10
19
|
{
|
|
11
20
|
key: 'address',
|
|
12
|
-
|
|
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 =>
|
|
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 =>
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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: '
|
|
34
|
-
validate: v =>
|
|
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: '
|
|
42
|
-
|
|
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
|
-
|
|
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 =>
|
|
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 =>
|
|
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
|
-
|
|
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: '
|
|
63
|
-
|
|
64
|
-
|
|
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: '
|
|
68
|
-
validate: v =>
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
141
|
+
try{
|
|
142
|
+
var toml = parseToml(str, 'camelCase')
|
|
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
|
+
if(toml.issuers){
|
|
152
|
+
for(let stanza of toml.issuers){
|
|
153
|
+
let { valid, parsed: issuer, issues: issuerIssues } = parseStanza(stanza, issuerFields)
|
|
154
|
+
|
|
155
|
+
if(valid)
|
|
156
|
+
issuers.push(issuer)
|
|
157
|
+
|
|
158
|
+
issues.push(
|
|
159
|
+
...issuerIssues.map(
|
|
160
|
+
issue => `[[ISSUERS]] ${issue}`
|
|
161
|
+
)
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
if(valid && stanza.weblinks){
|
|
165
|
+
for(let substanza of stanza.weblinks){
|
|
166
|
+
let { valid, parsed: weblink, issues: weblinkIssues } = parseStanza(substanza, weblinkFields)
|
|
77
167
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
168
|
+
if(valid){
|
|
169
|
+
issuer.weblinks = [
|
|
170
|
+
...(token.weblinks || []),
|
|
171
|
+
weblink
|
|
172
|
+
]
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
issues.push(
|
|
176
|
+
...weblinkIssues.map(
|
|
177
|
+
issue => `[[WEBLINK]] ${issue}`
|
|
178
|
+
)
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
81
183
|
}
|
|
82
184
|
|
|
83
|
-
if(toml.
|
|
84
|
-
|
|
85
|
-
|
|
185
|
+
if(toml.tokens){
|
|
186
|
+
for(let stanza of toml.tokens){
|
|
187
|
+
let { valid, parsed: token, issues: tokenIssues } = parseStanza(stanza, tokenFields)
|
|
188
|
+
|
|
189
|
+
if(valid)
|
|
190
|
+
tokens.push(token)
|
|
191
|
+
|
|
192
|
+
issues.push(
|
|
193
|
+
...tokenIssues.map(
|
|
194
|
+
issue => `[[TOKENS]] ${issue}`
|
|
195
|
+
)
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
if(valid && stanza.weblinks){
|
|
199
|
+
for(let substanza of stanza.weblinks){
|
|
200
|
+
let { valid, parsed: weblink, issues: weblinkIssues } = parseStanza(substanza, weblinkFields)
|
|
201
|
+
|
|
202
|
+
if(valid){
|
|
203
|
+
token.weblinks = [
|
|
204
|
+
...(token.weblinks || []),
|
|
205
|
+
weblink
|
|
206
|
+
]
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
issues.push(
|
|
210
|
+
...weblinkIssues.map(
|
|
211
|
+
issue => `[[WEBLINK]] ${issue}`
|
|
212
|
+
)
|
|
213
|
+
)
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
86
217
|
}
|
|
87
218
|
|
|
219
|
+
|
|
88
220
|
return {
|
|
89
|
-
|
|
90
|
-
|
|
221
|
+
issuers,
|
|
222
|
+
tokens,
|
|
223
|
+
issues
|
|
91
224
|
}
|
|
92
225
|
}
|
|
93
226
|
|
|
94
227
|
function parseStanza(stanza, schemas){
|
|
95
228
|
let parsed = {}
|
|
229
|
+
let issues = []
|
|
230
|
+
let valid = true
|
|
96
231
|
|
|
97
|
-
for(let { key, alternativeKeys, validate } of schemas){
|
|
232
|
+
for(let { key, alternativeKeys, essential, validate } of schemas){
|
|
98
233
|
let keys = [key]
|
|
99
234
|
|
|
100
235
|
if(alternativeKeys)
|
|
@@ -106,13 +241,24 @@ function parseStanza(stanza, schemas){
|
|
|
106
241
|
|
|
107
242
|
let value = stanza[k]
|
|
108
243
|
|
|
109
|
-
if(validate
|
|
110
|
-
|
|
244
|
+
if(validate){
|
|
245
|
+
try{
|
|
246
|
+
validate(value)
|
|
247
|
+
}catch(issue){
|
|
248
|
+
issues.push(`${key} field: ${issue}`)
|
|
249
|
+
break
|
|
250
|
+
}
|
|
251
|
+
}
|
|
111
252
|
|
|
112
253
|
parsed[k] = value
|
|
113
254
|
break
|
|
114
255
|
}
|
|
256
|
+
|
|
257
|
+
if(essential && !parsed.hasOwnProperty(key)){
|
|
258
|
+
issues.push(`${key} field missing: skipping stanza`)
|
|
259
|
+
valid = false
|
|
260
|
+
}
|
|
115
261
|
}
|
|
116
262
|
|
|
117
|
-
return parsed
|
|
263
|
+
return { valid, parsed, issues }
|
|
118
264
|
}
|