@stellar-expert/contract-wasm-interface-parser 1.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/LICENSE +21 -0
- package/index.js +38 -0
- package/package.json +16 -0
- package/src/wasm-section-reader.js +79 -0
- package/src/xdr-reader.js +148 -0
- package/src/xdr-spec-parser.js +121 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 StellarExpert Team
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/index.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import {xdr} from '@stellar/stellar-base'
|
|
2
|
+
import {XdrReader} from './src/xdr-reader'
|
|
3
|
+
import {WasmSectionReader} from './src/wasm-section-reader'
|
|
4
|
+
import {parseContractMeta, parseSpec} from './src/xdr-spec-parser'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Parse contract metadata from WASM sections
|
|
8
|
+
* @param {Buffer} rawWasm
|
|
9
|
+
* @return {{}}
|
|
10
|
+
*/
|
|
11
|
+
export function parseContractMetadata(rawWasm) {
|
|
12
|
+
const wasmSectionReader = new WasmSectionReader(rawWasm)
|
|
13
|
+
const sections = wasmSectionReader.readCustomSections()
|
|
14
|
+
const res = {}
|
|
15
|
+
for (const section of sections) {
|
|
16
|
+
switch (section.name) {
|
|
17
|
+
case 'contractenvmetav0':
|
|
18
|
+
res.interfaceVersion = xdr.ScEnvMetaEntry.fromXDR(section.contents).value().toString()
|
|
19
|
+
break
|
|
20
|
+
case 'contractmetav0':
|
|
21
|
+
Object.assign(res, parseContractMeta(parseEntries(section.contents, xdr.ScMetaEntry)))
|
|
22
|
+
break
|
|
23
|
+
case 'contractspecv0':
|
|
24
|
+
Object.assign(res, parseSpec(parseEntries(section.contents, xdr.ScSpecEntry)))
|
|
25
|
+
break
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return res
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function parseEntries(buffer, xdrContract) {
|
|
32
|
+
const reader = new XdrReader(buffer)
|
|
33
|
+
const entries = []
|
|
34
|
+
while (!reader.eof) {
|
|
35
|
+
entries.push(xdrContract.read(reader))
|
|
36
|
+
}
|
|
37
|
+
return entries
|
|
38
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@stellar-expert/contract-wasm-interface-parser",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Contract interface metadata parser for binary WASM",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"module": "./index.js",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/stellar-expert/contract-wasm-interface-parser.git"
|
|
10
|
+
},
|
|
11
|
+
"author": "orbitlens <orbit@stellar.expert>",
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"peerDependencies": {
|
|
14
|
+
"@stellar/stellar-base": "^10.0.1"
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimalistic WASM metadata reader
|
|
3
|
+
*/
|
|
4
|
+
export class WasmSectionReader {
|
|
5
|
+
constructor(data) {
|
|
6
|
+
this.data = data
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @type {Buffer}
|
|
11
|
+
* @private
|
|
12
|
+
*/
|
|
13
|
+
data
|
|
14
|
+
/**
|
|
15
|
+
* Current reader position
|
|
16
|
+
* @type {number}
|
|
17
|
+
* @private
|
|
18
|
+
*/
|
|
19
|
+
pointer = 8
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Read custom metadata sections from raw WASM
|
|
23
|
+
* @return {{name: string, contents: Buffer}[]}
|
|
24
|
+
*/
|
|
25
|
+
readCustomSections() {
|
|
26
|
+
const sections = []
|
|
27
|
+
while (this.pointer < this.data.length) {
|
|
28
|
+
const sectionType = this.readUint8()
|
|
29
|
+
let length = this.readVarUint32()
|
|
30
|
+
if (sectionType === 0) { //accumulate only custom sections
|
|
31
|
+
const sectionStart = this.pointer
|
|
32
|
+
const name = this.readString()
|
|
33
|
+
length -= this.pointer - sectionStart
|
|
34
|
+
sections.push({
|
|
35
|
+
name,
|
|
36
|
+
contents: this.data.subarray(this.pointer, this.pointer + length)
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
this.pointer += length
|
|
40
|
+
}
|
|
41
|
+
return sections
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @return {number}
|
|
46
|
+
* @private
|
|
47
|
+
*/
|
|
48
|
+
readUint8() {
|
|
49
|
+
return this.data.readUint8(this.pointer++)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @return {number}
|
|
54
|
+
* @private
|
|
55
|
+
*/
|
|
56
|
+
readVarUint32() {
|
|
57
|
+
let result = 0
|
|
58
|
+
let shift = 0
|
|
59
|
+
while (true) {
|
|
60
|
+
let byte = this.readUint8()
|
|
61
|
+
result |= (byte & 0x7f) << shift
|
|
62
|
+
shift += 7
|
|
63
|
+
if ((byte & 0x80) === 0)
|
|
64
|
+
break
|
|
65
|
+
}
|
|
66
|
+
return result >>> 0
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* @return {string}
|
|
71
|
+
* @private
|
|
72
|
+
*/
|
|
73
|
+
readString() {
|
|
74
|
+
const length = this.readVarUint32()
|
|
75
|
+
const contents = this.data.subarray(this.pointer, this.pointer + length)
|
|
76
|
+
this.pointer += length
|
|
77
|
+
return contents.toString('utf-8')
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
export class XdrReader {
|
|
2
|
+
/**
|
|
3
|
+
* @constructor
|
|
4
|
+
* @param {Buffer} source - Buffer containing serialized data
|
|
5
|
+
*/
|
|
6
|
+
constructor(source) {
|
|
7
|
+
if (!Buffer.isBuffer(source)) {
|
|
8
|
+
if (source instanceof Array) {
|
|
9
|
+
source = Buffer.from(source)
|
|
10
|
+
} else
|
|
11
|
+
throw new Error('Source not specified')
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
this._buffer = source
|
|
15
|
+
this._length = source.length
|
|
16
|
+
this._index = 0
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @type {Buffer}
|
|
21
|
+
* @private
|
|
22
|
+
* @readonly
|
|
23
|
+
*/
|
|
24
|
+
_buffer
|
|
25
|
+
/**
|
|
26
|
+
* @type {Number}
|
|
27
|
+
* @private
|
|
28
|
+
* @readonly
|
|
29
|
+
*/
|
|
30
|
+
_length
|
|
31
|
+
/**
|
|
32
|
+
* @type {Number}
|
|
33
|
+
* @private
|
|
34
|
+
* @readonly
|
|
35
|
+
*/
|
|
36
|
+
_index
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Check if the reader reached the end of the input buffer
|
|
40
|
+
* @return {Boolean}
|
|
41
|
+
*/
|
|
42
|
+
get eof() {
|
|
43
|
+
return this._index === this._length
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Advance reader position, check padding and overflow
|
|
48
|
+
* @param {Number} size - Bytes to read
|
|
49
|
+
* @return {Number} Position to read from
|
|
50
|
+
* @private
|
|
51
|
+
*/
|
|
52
|
+
advance(size) {
|
|
53
|
+
const from = this._index
|
|
54
|
+
// advance cursor position
|
|
55
|
+
this._index += size
|
|
56
|
+
// check buffer boundaries
|
|
57
|
+
if (this._length < this._index)
|
|
58
|
+
throw new Error(
|
|
59
|
+
'attempt to read outside the boundary of the buffer'
|
|
60
|
+
)
|
|
61
|
+
// check that padding is correct for Opaque and String
|
|
62
|
+
const padding = 4 - (size % 4 || 4)
|
|
63
|
+
if (padding > 0) {
|
|
64
|
+
for (let i = 0; i < padding; i++)
|
|
65
|
+
if (this._buffer[this._index + i] !== 0)
|
|
66
|
+
// all bytes in the padding should be zeros
|
|
67
|
+
throw new Error('Invalid padding')
|
|
68
|
+
this._index += padding
|
|
69
|
+
}
|
|
70
|
+
return from
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Reset reader position
|
|
75
|
+
* @return {void}
|
|
76
|
+
*/
|
|
77
|
+
rewind() {
|
|
78
|
+
this._index = 0
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Read byte array from the buffer
|
|
83
|
+
* @param {Number} size - Bytes to read
|
|
84
|
+
* @return {Buffer} - Sliced portion of the underlying buffer
|
|
85
|
+
*/
|
|
86
|
+
read(size) {
|
|
87
|
+
const from = this.advance(size)
|
|
88
|
+
return this._buffer.subarray(from, from + size)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Read i32 from buffer
|
|
93
|
+
* @return {Number}
|
|
94
|
+
*/
|
|
95
|
+
readInt32BE() {
|
|
96
|
+
return this._buffer.readInt32BE(this.advance(4))
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Read u32 from buffer
|
|
101
|
+
* @return {Number}
|
|
102
|
+
*/
|
|
103
|
+
readUInt32BE() {
|
|
104
|
+
return this._buffer.readUInt32BE(this.advance(4))
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Read i64 from buffer
|
|
109
|
+
* @return {BigInt}
|
|
110
|
+
*/
|
|
111
|
+
readBigInt64BE() {
|
|
112
|
+
return this._buffer.readBigInt64BE(this.advance(8))
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Read u64 from buffer
|
|
117
|
+
* @return {BigInt}
|
|
118
|
+
*/
|
|
119
|
+
readBigUInt64BE() {
|
|
120
|
+
return this._buffer.readBigUInt64BE(this.advance(8))
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Read float from buffer
|
|
125
|
+
* @return {Number}
|
|
126
|
+
*/
|
|
127
|
+
readFloatBE() {
|
|
128
|
+
return this._buffer.readFloatBE(this.advance(4))
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Read double from buffer
|
|
133
|
+
* @return {Number}
|
|
134
|
+
*/
|
|
135
|
+
readDoubleBE() {
|
|
136
|
+
return this._buffer.readDoubleBE(this.advance(8))
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Ensure that input buffer has been consumed in full, otherwise it's a type mismatch
|
|
141
|
+
* @return {void}
|
|
142
|
+
* @throws {Error}
|
|
143
|
+
*/
|
|
144
|
+
ensureInputConsumed() {
|
|
145
|
+
if (this._index !== this._length)
|
|
146
|
+
throw new Error(`Invalid XDR contract typecast - source buffer not entirely consumed`)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
export function parseContractMeta(meta) {
|
|
2
|
+
const res = {}
|
|
3
|
+
for (const {_value} of meta) {
|
|
4
|
+
const key = _value.key().toString()
|
|
5
|
+
const val = _value.val().toString()
|
|
6
|
+
switch (key) {
|
|
7
|
+
case 'rsver':
|
|
8
|
+
res.rustVersion = val
|
|
9
|
+
break
|
|
10
|
+
case 'rssdkver':
|
|
11
|
+
res.sdkVersion = val
|
|
12
|
+
break
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return res
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function parseSpec(entries) {
|
|
19
|
+
const res = {}
|
|
20
|
+
|
|
21
|
+
function addSpec(key, descriptor) {
|
|
22
|
+
let spec = res[key]
|
|
23
|
+
if (spec === undefined) {
|
|
24
|
+
spec = res[key] = []
|
|
25
|
+
}
|
|
26
|
+
spec.push(descriptor)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
for (const spec of entries) {
|
|
30
|
+
const value = spec.value()
|
|
31
|
+
switch (spec._arm) {
|
|
32
|
+
case 'functionV0':
|
|
33
|
+
addSpec('functions', {
|
|
34
|
+
name: value.name().toString(),
|
|
35
|
+
inputs: value.inputs().map(parseParameter),
|
|
36
|
+
outputs: value.outputs().map(parseParameterType)
|
|
37
|
+
})
|
|
38
|
+
break
|
|
39
|
+
case 'udtStructV0':
|
|
40
|
+
addSpec('structs', {
|
|
41
|
+
name: parseStructName(value),
|
|
42
|
+
fields: value.fields().map(parseParameter)
|
|
43
|
+
})
|
|
44
|
+
break
|
|
45
|
+
case 'udtUnionV0':
|
|
46
|
+
addSpec('unions', {
|
|
47
|
+
name: parseStructName(value),
|
|
48
|
+
cases: value.cases().reduce((agg, c) => {
|
|
49
|
+
const value = c.value()
|
|
50
|
+
const caseType = {
|
|
51
|
+
name: value.name().toString()
|
|
52
|
+
}
|
|
53
|
+
if (value.type) {
|
|
54
|
+
caseType.type = value.type().map(parseParameterType)
|
|
55
|
+
}
|
|
56
|
+
agg[c.switch().name.replace('scSpecUdtUnionCase', '')] = caseType
|
|
57
|
+
return agg
|
|
58
|
+
}, {})
|
|
59
|
+
})
|
|
60
|
+
break
|
|
61
|
+
case 'udtEnumV0':
|
|
62
|
+
addSpec('enums', {
|
|
63
|
+
name: parseStructName(value),
|
|
64
|
+
cases: value.cases().reduce((agg, c) => {
|
|
65
|
+
const value = c.value()
|
|
66
|
+
agg[value.name().toString()] = value.value() //TODO: check
|
|
67
|
+
return agg
|
|
68
|
+
}, {})
|
|
69
|
+
})
|
|
70
|
+
break
|
|
71
|
+
case 'udtErrorEnumV0':
|
|
72
|
+
addSpec('errors', {
|
|
73
|
+
name: parseStructName(value),
|
|
74
|
+
cases: value.cases().map(c => ({
|
|
75
|
+
name: c.name().toString(),
|
|
76
|
+
value: c.value()
|
|
77
|
+
}))
|
|
78
|
+
})
|
|
79
|
+
break
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return res
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function parseStructName(value) {
|
|
86
|
+
let structName = value.name().toString()
|
|
87
|
+
const lib = value.lib()
|
|
88
|
+
if (lib.length) {
|
|
89
|
+
structName += ':' + lib.toString()
|
|
90
|
+
}
|
|
91
|
+
return structName
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function parseParameterType(type) {
|
|
95
|
+
const typeName = type.switch().name
|
|
96
|
+
switch (typeName) {
|
|
97
|
+
case 'scSpecTypeOption':
|
|
98
|
+
return `option<${parseParameterType(type.valueType())}>`
|
|
99
|
+
case 'scSpecTypeBytesN':
|
|
100
|
+
return `bytesn<${type.value().n()}>`
|
|
101
|
+
case 'scSpecTypeVec':
|
|
102
|
+
return `vec<${parseParameterType(type.value().elementType())}>`
|
|
103
|
+
case 'scSpecTypeMap':
|
|
104
|
+
return `map<${parseParameterType(type.value().keyType())},${parseParameter(type.value().valueType())}>`
|
|
105
|
+
case 'scSpecTypeResult':
|
|
106
|
+
return `result<${parseParameterType(type.value().okType())},${parseParameter(type.value().errorType())}>`
|
|
107
|
+
case 'scSpecTypeTuple':
|
|
108
|
+
return `tuple<${type.value().valueTypes().map(parseParameterType).join()}>`
|
|
109
|
+
case 'scSpecTypeUdt':
|
|
110
|
+
return `udt<${type.value().name()}>`
|
|
111
|
+
default:
|
|
112
|
+
return typeName.replace('scSpecType', '').toLowerCase()
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function parseParameter(param) {
|
|
117
|
+
return {
|
|
118
|
+
name: param.name().toString(),
|
|
119
|
+
type: parseParameterType(param.type())
|
|
120
|
+
}
|
|
121
|
+
}
|