@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 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
+ }