UNPKG

10.9 kBJavaScriptView Raw
1/**
2 * ABI encoding and decoding
3 * @module abi
4 */
5
6let padStart = require('lodash.padstart')
7let padEnd = require('lodash.padend')
8let {isString, isObject, isArray, isNumber} = require('underscore')
9let BN = require('bn.js')
10
11let {
12 copyString,
13 prependZeroX,
14 toBuffer,
15 removeLeadingZeroX,
16 bufferToHex
17} = require('./formats')
18
19let {keccak256} = require('./crypto')
20let solidity = require('./solidity')
21let values = require('./values')
22let {createChecksumAddress} = require('./accounts')
23
24/**
25 * Shared between function and event signatures. Creates a name/type combination
26 * and hashes it to get the hex signature.
27 * @param {string} val
28 * @return {string} hash
29 */
30function fnHashBuffer(val) {
31 let op
32
33 if (isString(val) === true) {
34 op = copyString(val)
35 }
36
37 if (isObject(val) === true) {
38 op = copyString(val.name)
39
40 if (isArray(val.inputs) === true) {
41 op += '(' + val.inputs.map(item => item.type).join(',') + ')'
42 }
43 }
44
45 return keccak256(op).slice(0, values.solidity.types.function.byteLength)
46}
47
48/**
49 * Pad left or right depending on direction
50 * @param {string} direction left or right
51 * @param {number} length
52 * @param {string} val
53 * @return {string}
54 */
55let abiPad = (direction, length, val) => {
56 return (direction === 'left' ? padStart : padEnd)(val, length, '0')
57}
58
59/**
60 * Encode padded ABI string value
61 * @param {string} val
62 * @return {string} hex
63 */
64function encodeAbiString(val) {
65 let buf = toBuffer(val)
66 let bufHex = buf.toString('hex')
67 let valOp = abiPad(
68 values.solidity.types.string.pad,
69 values.solidity.types.string.stringLength,
70 bufHex
71 )
72 return valOp
73}
74
75/**
76 * Encode padded boolean to ABI format
77 * @param {boolean} val
78 * @return {string}
79 */
80function encodeAbiBoolean(val) {
81 return copyString(
82 val === true
83 ? values.solidity.types.bool.one
84 : values.solidity.types.bool.zero
85 )
86}
87
88/**
89 * A padded ABI formatted number
90 * @param {number} val
91 * @return {string}
92 */
93function encodeAbiNumber(val) {
94 return abiPad(
95 values.solidity.types.uint.pad,
96 values.solidity.types.uint.stringLength,
97 toBuffer(val).toString('hex')
98 )
99}
100
101/**
102 * ABI encoded Aion address
103 * @param {string} val
104 * @return {string}
105 */
106function encodeAbiAddress(val) {
107 return removeLeadingZeroX(val).toLowerCase()
108}
109
110// replaces the need for switch case
111let abiTypeEncoders = {
112 string: encodeAbiString,
113 bytes: encodeAbiString,
114 bool: encodeAbiBoolean,
115 uint: encodeAbiNumber,
116 int: encodeAbiNumber,
117 fixed: encodeAbiNumber,
118 ufixed: encodeAbiNumber,
119 address: encodeAbiAddress
120}
121
122/**
123 * Encode event to its ABI signature
124 * @method encodeEventSignature
125 * @param {string|object} val
126 * @return {string}
127 */
128function encodeEventSignature(val) {
129 return prependZeroX(fnHashBuffer(val).toString('hex'))
130}
131
132/**
133 * Encode function to its ABI signature
134 * @method encodeFunctionSignature
135 * @param {string|object} val
136 * @return {string}
137 */
138function encodeFunctionSignature(val) {
139 return prependZeroX(
140 fnHashBuffer(val)
141 .slice(0, values.solidity.types.function.byteLengthEncoded)
142 .toString('hex')
143 )
144}
145
146/**
147 * Array reducer summing up all the items
148 * @param {number} op accumulator
149 * @param {number} item
150 * @return {number}
151 */
152let sumLengthReduction = (op, item) => (op = op + item.length)
153
154/**
155 * Input an array of strings and calculate the length in bytes
156 * @param {array} val
157 * @return {number}
158 */
159let stringArrayByteLength = val => val.reduce(sumLengthReduction, 0) / 2
160
161/**
162 * Converts from arrays of types and params into a data structure
163 *
164 * It's used by other functions in this module to build ABI encoding and
165 * to give better information if the developer is curious to know
166 * each line of bytes.
167 *
168 * @param {array} options.types
169 * @param {array} options.params
170 * @return {object}
171 */
172function encodeParametersIntermediate({types, params}) {
173 let parsedTypes = types.map(solidity.parseType)
174
175 let useTopLevelOffsets = parsedTypes.some(
176 item => item.hasDynamicDimensions === true
177 )
178
179 let op = []
180 let rows = []
181
182 parsedTypes.forEach((parsedType, paramIndex) => {
183 let param = params[paramIndex]
184 let {baseType, dimensions, hasDimensions, hasDynamicDimensions} = parsedType
185 let valueEncoder = abiTypeEncoders[baseType]
186 let paramOp = []
187 let paramLen = 0
188
189 function addParamItem(item) {
190 paramOp.push(valueEncoder(item))
191 }
192
193 if (isArray(param) === false) {
194 paramLen = 1
195 addParamItem(param)
196 }
197
198 if (isArray(param) === true) {
199 paramLen = param.length
200 if (hasDynamicDimensions === true) {
201 paramOp.push(encodeAbiNumber(paramLen))
202 }
203 param.forEach(addParamItem)
204 }
205
206 let rowByteLen = stringArrayByteLength(paramOp)
207
208 rows.push({
209 hasDimensions,
210 dimensions,
211 rowByteLen,
212 paramLen,
213 paramOp
214 })
215 })
216
217 let offset = 0
218
219 /*if (useTopLevelOffsets === true) {
220 // first item is this many bytes down
221 offset += rows.length * 16
222 op.push(encodeAbiNumber(offset))
223
224 rows.forEach((item, index) => {
225 if (index === rows.length - 1) {
226 return
227 }
228 offset += item.rowByteLen
229 op.push(encodeAbiNumber(offset))
230 })
231 }*/
232
233 rows.forEach(item => {
234 op = op.concat(item.paramOp)
235 })
236
237 return {
238 parsedTypes,
239 rows,
240 lines: op
241 }
242}
243
244/**
245 * Encode a list of parameters to ABI signature
246 * @method encodeParameters
247 * @param {array} types
248 * @param {array} params
249 * @return {string}
250 */
251function encodeParameters(types, params) {
252 let op = encodeParametersIntermediate({types, params})
253 op = op.lines
254 op = op.join('')
255 op = prependZeroX(op)
256 return op
257}
258
259/**
260 * Encode parameter to ABI signature
261 * @method encodeParameter
262 * @param {string} type
263 * @param {string|array|object} param
264 * @return {string}
265 */
266function encodeParameter(type, param) {
267 return encodeParameters([type], [param])
268}
269
270/**
271 * Encode function call to ABI signature
272 * @method encodeFunctionCall
273 * @param {object} jsonInterface
274 * @param {array} params
275 * @return {string}
276 */
277function encodeFunctionCall(jsonInterface, params) {
278 let functionName = jsonInterface.name
279 let functionHash = encodeFunctionSignature(functionName)
280 let types = jsonInterface.inputs.map(item => item.type)
281 let typesParams = removeLeadingZeroX(encodeParameters(types, params))
282 return functionHash + typesParams
283}
284
285function decodeAbiString(val) {
286 return toBuffer(val).toString('utf8')
287}
288
289function decodeAbiBytes(val) {
290 return toBuffer(val)
291}
292
293function decodeAbiBoolean(val) {
294 return val.pop() === 1 ? true : false
295}
296
297function decodeAbiNumber(val) {
298 return new BN(bufferToHex(val), 'hex').toNumber();
299}
300
301function decodeAbiAddress(val) {
302 return createChecksumAddress(toBuffer(val).toString('hex'))
303}
304
305let abiTypeDecodes = {
306 string: decodeAbiString,
307 bytes: decodeAbiBytes,
308 bool: decodeAbiBoolean,
309 uint: decodeAbiNumber,
310 int: decodeAbiNumber,
311 fixed: decodeAbiNumber,
312 ufixed: decodeAbiNumber,
313 address: decodeAbiAddress
314}
315
316/**
317 * Decode the parameters hex into an array of decoded values
318 * @method decodeParameters
319 * @param {array} types
320 * @param {string} val
321 * @return {array}
322 */
323function decodeParameters(types, val) {
324 let typeList = []
325
326 if (isArray(types) === true && isString(types[0]) === true) {
327 // array of string types
328 typeList = types
329 }
330
331 if (isArray(types) === true && isString(types[0].type) === true) {
332 // json interface
333 typeList = types.map(item => item.type)
334 }
335
336 if (isObject(types) === true && types.type !== undefined) {
337 // one from decode paramter
338 typeList = [types.type]
339 }
340
341 let parsedTypes = typeList.map(solidity.parseType)
342
343 let useTopLevelOffsets = parsedTypes.length > 1 && parsedTypes.some(
344 item => item.hasDynamicDimensions === true
345 )
346
347 let bytes = toBuffer(val)
348 let op = []
349 let cursor = 0
350 let previousByteLength = 16
351 let outerOffsets = []
352
353 function readBytes(length) {
354 let op = bytes.slice(cursor, cursor + length)
355 cursor += length
356 return op
357 }
358
359 if (useTopLevelOffsets === true) {
360 parsedTypes.forEach(() => {
361 outerOffsets.push(decodeAbiNumber(readBytes(16)))
362 })
363 console.log('outerOffsets', outerOffsets)
364 }
365
366 parsedTypes.forEach((parsedType, paramIndex) => {
367 let {baseType, dimensions, hasDimensions, hasDynamicDimensions} = parsedType
368 let {byteLength} = values.solidity.types[baseType]
369 let dynamicType = values.solidity.types[baseType].dynamic
370 let valueDecoder = abiTypeDecodes[baseType]
371 let offset = 0
372 let innerOffsets = []
373 let paramOp = []
374
375 if (hasDynamicDimensions === true) {
376 dimensions.forEach(() => {
377 innerOffsets.push(decodeAbiNumber(readBytes(16)))
378 })
379 console.log('inner offsets', innerOffsets)
380 }
381
382 //
383 // simple single type param
384 //
385 if (hasDimensions === false) {
386 console.log('simple single type param')
387 op.push(valueDecoder(readBytes(byteLength)))
388 return
389 }
390
391 //
392 // simple fixed-length array
393 //
394 if (hasDimensions === true && hasDynamicDimensions === false) {
395 console.log('simple fixed-length array')
396 let {length} = dimensions[0]
397 for (let i = 0; i < length; i += 1) {
398 paramOp.push(valueDecoder(readBytes(byteLength)))
399 }
400 return op.push(paramOp)
401 }
402
403 //
404 // shifting to complex mode with offsets
405 //
406
407 //
408 // we know it's an array now
409 //
410 let {length} = dimensions[0]
411
412 if (isNumber(length) === true) {
413 console.log('array with length')
414 for (let i = 0; i < length; i += 1) {
415 paramOp.push(valueDecoder(readBytes(byteLength)))
416 }
417 return op.push(paramOp)
418 }
419
420 console.log('dynamic array')
421
422 dimensions.forEach((dimension, dimensionIndex) => {
423 let {dynamic, length} = dimension
424 let dimensionOp = []
425
426 if (dynamic === true) {
427 length = decodeAbiNumber(readBytes(16))
428 }
429
430 for (let i = 0; i < length; i += 1) {
431 paramOp.push(valueDecoder(readBytes(byteLength)))
432 }
433
434 // paramOp.push(dimensionOp)
435 })
436
437 op.push(paramOp)
438 })
439
440 return op
441}
442
443/**
444 * Decode a parameter value from it's ABI encoding
445 * @method decodeParameter
446 * @param {string} type
447 * @param {string} val
448 * @return {string}
449 */
450function decodeParameter(type, val) {
451 return decodeParameters([type], val)[0]
452}
453
454/**
455 * ABI decoded log data
456 * @method decodeLog
457 * @param {array} inputs
458 * @param {string} val
459 * @param {array} topics
460 * @return {array}
461 */
462function decodeLog(/*inputs, val, topics*/) {
463 throw new Error(`decodeLog not yet implemented`)
464}
465
466module.exports = {
467 // for testing and analyzing
468 encodeParametersIntermediate,
469
470 // web3 implementations
471 encodeFunctionSignature,
472 encodeEventSignature,
473 encodeParameter,
474 encodeParameters,
475 encodeFunctionCall,
476 decodeParameter,
477 decodeParameters,
478 decodeLog
479}