UNPKG

9.22 kBJavaScriptView Raw
1/*
2 @see https://solidity.readthedocs.io/en/v0.5.13/abi-spec.html
3 */
4
5const { assert } = require('../util');
6const { sha3 } = require('../util/sign');
7const format = require('../util/format');
8
9const getCoder = require('./coder');
10const namedTuple = require('../lib/namedTuple');
11const HexStream = require('./HexStream');
12
13// ============================================================================
14function signature(type) {
15 return format.hex(sha3(Buffer.from(type)));
16}
17
18function formatSignature({ name, inputs }) {
19 return `${name}(${inputs.map(param => getCoder(param).type).join(',')})`;
20}
21
22function formatFullName({ name, inputs }) {
23 return `${name}(${inputs.map(param => `${getCoder(param).type} ${param.indexed ? 'indexed ' : ''}${param.name}`).join(', ')})`;
24}
25
26// ----------------------------------------------------------------------------
27class FunctionCoder {
28 /**
29 * Function coder
30 *
31 * @param name {string}
32 * @param [inputs] {array}
33 * @param [outputs] {array}
34 *
35 * @example
36 * > abi = { name: 'func', inputs: [{ type: 'int' }, { type: 'bool' }], outputs: [{ type: 'int' }] }
37 * > coder = new FunctionCoder(abi)
38 FunctionCoder {
39 name: 'func',
40 fullName: 'func(int256 , bool )',
41 inputs: [ { type: 'int' }, { type: 'bool' } ],
42 outputs: [ { type: 'int' } ],
43 type: 'func(int256,bool)'
44 }
45 */
46 constructor({ name, inputs = [], outputs = [] }) {
47 this.name = name; // example: "add"
48 this.fullName = formatFullName({ name, inputs }); // example: "add(uint,uint)"
49 this.type = formatSignature({ name, inputs }); // example: "add(uint number, uint count)"
50 this.signature = signature(this.type).slice(0, 10); // example: "0xb8966352"
51
52 this.inputCoder = getCoder({ type: 'tuple', components: inputs });
53 this.outputCoder = getCoder({ type: 'tuple', components: outputs });
54 }
55
56 /**
57 * Get function signature by abi (json interface)
58 *
59 * @param array {array}
60 * @return {string}
61 *
62 * @example
63 * > abi = { name: 'func', inputs: [{ type: 'int' }, { type: 'bool' }], outputs: [{ type: 'int' }] }
64 * > coder = new FunctionCoder(abi)
65 * > coder.encodeData([100, true])
66 "0x1eee72c100000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000001"
67 */
68 encodeData(array) {
69 const hex = format.hex(this.inputCoder.encode(array));
70 return `${this.signature}${hex.substring(2)}`;
71 }
72
73 /**
74 * Decode data hex with inputs by abi (json interface)
75 *
76 * @param hex {string} - Hex string
77 * @return {array} NamedTuple
78 *
79 * @example
80 * > abi = { name: 'func', inputs: [{ type: 'int' }, { type: 'bool' }], outputs: [{ type: 'int' }] }
81 * > coder = new FunctionCoder(abi)
82 * > result = coder.decodeData('0x15fb272000000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000001')
83 NamedTuple(0,1) [ 100n, true ]
84 * > console.log([...result])
85 [ 100n, true ]
86 * > console.log(result[0])
87 100
88 * > console.log(result[1])
89 true
90 */
91 decodeData(hex) {
92 const prefix = hex.slice(0, this.signature.length);
93 const data = hex.slice(this.signature.length);
94 const stream = new HexStream(data);
95
96 assert(prefix === this.signature, {
97 message: 'decodeData unexpected signature',
98 expect: this.signature,
99 got: prefix,
100 coder: this,
101 });
102
103 const result = this.inputCoder.decode(stream);
104 assert(stream.eof(), {
105 message: 'hex length to large',
106 expect: `${stream.string.length}`,
107 got: stream.index,
108 coder: this,
109 });
110
111 return result;
112 }
113
114 /**
115 * Decode hex with outputs by abi (json interface)
116 *
117 * @param hex {string} - Hex string
118 * @return {array} NamedTuple
119 *
120 * @example
121 * > abi = { name: 'func', inputs: [{ type: 'int' }, { type: 'bool' }], outputs: [{ type: 'int' }] }
122 * > coder = new FunctionCoder(abi)
123 * > result = coder.decodeOutputs('0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff')
124 NamedTuple(0) [ -1n ]
125 * > console.log([...result])
126 [-1n]
127 * > console.log(result[0])
128 -1n
129 */
130 decodeOutputs(hex) {
131 const stream = new HexStream(hex);
132 const result = this.outputCoder.decode(stream);
133
134 assert(stream.eof(), {
135 message: 'hex length to large',
136 expect: `${stream.string.length}`,
137 got: stream.index,
138 coder: this,
139 });
140
141 return result.length <= 1 ? result[0] : result;
142 }
143}
144
145class ErrorCoder extends FunctionCoder {
146 constructor() {
147 super({ name: 'Error', inputs: [{ type: 'string', name: 'message' }] });
148 }
149
150 decodeError(error) {
151 try {
152 const { message } = this.decodeData(error.data);
153 return new Error(message);
154 } catch (e) {
155 return error;
156 }
157 }
158}
159
160class EventCoder {
161 /**
162 * Event coder
163 *
164 * @param options {object}
165 * @param options.anonymous {boolean}
166 * @param options.name {string}
167 * @param options.inputs {array}
168 *
169 * @example
170 * > abi = {
171 name: 'EventName',
172 anonymous: false,
173 inputs: [
174 {
175 indexed: true,
176 name: 'account',
177 type: 'address',
178 },
179 {
180 indexed: false,
181 name: 'number',
182 type: 'uint',
183 },
184 ],
185 }
186 * > coder = new EventCoder(abi)
187 EventCoder {
188 anonymous: false,
189 name: 'EventName',
190 inputs: [
191 { indexed: true, name: 'account', type: 'address' },
192 { indexed: false, name: 'number', type: 'uint' }
193 ],
194 type: 'EventName(address,uint256)',
195 NamedTuple: [Function: NamedTuple(account,number)]
196 }
197 */
198 constructor({ anonymous, name, inputs = [] } = {}) {
199 this.anonymous = anonymous;
200 this.name = name; // example: "Event"
201 this.fullName = formatFullName({ name, inputs }); // example: "Event(address)"
202 this.type = formatSignature({ name, inputs }); // example: "Event(address indexed account)"
203 this.signature = signature(this.type); // example: "0x50d7c806d0f7913f321946784dee176a42aa55b5dd83371fc57dcedf659085e0"
204
205 this.inputs = inputs;
206 this.dataCoder = getCoder({ type: 'tuple', components: inputs.filter(component => !component.indexed) });
207
208 this.NamedTuple = namedTuple(...inputs.map((input, index) => input.name || `${index}`));
209 }
210
211 /**
212 * Encode topics by params
213 *
214 * @param array {*[]}
215 * @return {string[]}
216 * @example
217 * > coder = new EventCoder(abi)
218 * > coder.encodeTopics(['0x0123456789012345678901234567890123456789', null])
219 ['0x0000000000000000000000000123456789012345678901234567890123456789']
220 */
221 encodeTopics(array) {
222 assert(array.length === this.inputs.length, {
223 message: 'length not match',
224 expect: this.inputs.length,
225 got: array.length,
226 coder: this,
227 });
228
229 const topics = [];
230 this.inputs.forEach((component, index) => {
231 if (component.indexed) {
232 const value = array[index];
233
234 topics.push(value === null ? null : format.hex(getCoder(component).encodeIndex(value)));
235 }
236 });
237
238 return topics;
239 }
240
241 /**
242 * Decode log
243 *
244 * @param topics {array} - Array of hex sting
245 * @param data {string} - Hex string
246 * @return {array} NamedTuple
247 *
248 * @example
249 * > coder = new EventCoder(abi)
250 * > result = coder.decodeLog({
251 data: '0x000000000000000000000000000000000000000000000000000000000000000a',
252 topics: [
253 '0xb0333e0e3a6b99318e4e2e0d7e5e5f93646f9cbf62da1587955a4092bf7df6e7',
254 '0x0000000000000000000000000123456789012345678901234567890123456789',
255 ],
256 })
257 NamedTuple(account,number) [ '0x0123456789012345678901234567890123456789', 10n ]
258 * > console.log([...result])
259 [ 0x0123456789012345678901234567890123456789, 10n ]
260 * > console.log(result.account) // `account` a field name in abi
261 "0x0123456789012345678901234567890123456789"
262 * > console.log(result.number) // `number` a field name in abi
263 10n
264 */
265 decodeLog({ topics, data }) {
266 assert(this.anonymous || topics[0] === this.signature, {
267 message: 'decodeLog unexpected topic',
268 expect: this.signature,
269 got: topics[0],
270 coder: this,
271 });
272
273 const stream = new HexStream(data);
274 const notIndexedNamedTuple = this.dataCoder.decode(stream);
275
276 assert(stream.eof(), {
277 message: 'hex length to large',
278 expect: `${stream.string.length}`,
279 got: stream.index,
280 coder: this,
281 });
282
283 let offset = this.anonymous ? 0 : 1;
284 const array = this.inputs.map(component => {
285 if (component.indexed) {
286 return getCoder(component).decodeIndex(topics[offset++]); // eslint-disable-line no-plusplus
287 } else {
288 return notIndexedNamedTuple[component.name];
289 }
290 });
291
292 return new this.NamedTuple(...array);
293 }
294}
295
296module.exports = {
297 formatSignature,
298 formatFullName,
299 FunctionCoder,
300 EventCoder,
301 errorCoder: new ErrorCoder(),
302};