1 | /*
|
2 | @see https://solidity.readthedocs.io/en/v0.5.13/abi-spec.html
|
3 | */
|
4 |
|
5 | const { assert } = require('../util');
|
6 | const { sha3 } = require('../util/sign');
|
7 | const format = require('../util/format');
|
8 |
|
9 | const getCoder = require('./coder');
|
10 | const namedTuple = require('../lib/namedTuple');
|
11 | const HexStream = require('./HexStream');
|
12 |
|
13 | // ============================================================================
|
14 | function signature(type) {
|
15 | return format.hex(sha3(Buffer.from(type)));
|
16 | }
|
17 |
|
18 | function formatSignature({ name, inputs }) {
|
19 | return `${name}(${inputs.map(param => getCoder(param).type).join(',')})`;
|
20 | }
|
21 |
|
22 | function formatFullName({ name, inputs }) {
|
23 | return `${name}(${inputs.map(param => `${getCoder(param).type} ${param.indexed ? 'indexed ' : ''}${param.name}`).join(', ')})`;
|
24 | }
|
25 |
|
26 | // ----------------------------------------------------------------------------
|
27 | class 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 |
|
145 | class 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 |
|
160 | class 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 |
|
296 | module.exports = {
|
297 | formatSignature,
|
298 | formatFullName,
|
299 | FunctionCoder,
|
300 | EventCoder,
|
301 | errorCoder: new ErrorCoder(),
|
302 | };
|