UNPKG

9.65 kBJavaScriptView Raw
1var pkgVersion = require("./package.json").version;
2var Ajv = require("ajv");
3var util = require("util");
4
5var contractObjectSchema = require("./spec/contract-object.spec.json");
6var networkObjectSchema = require("./spec/network-object.spec.json");
7var abiSchema = require("./spec/abi.spec.json");
8
9/**
10 * Property definitions for Contract Objects
11 *
12 * Describes canonical output properties as sourced from some "dirty" input
13 * object. Describes normalization process to account for deprecated and/or
14 * nonstandard keys and values.
15 *
16 * Maps (key -> property) where:
17 * - `key` is the top-level output key matching up with those in the schema
18 * - `property` is an object with optional values:
19 * - `sources`: list of sources (see below); default `key`
20 * - `transform`: function(value) -> transformed value; default x -> x
21 *
22 * Each source represents a means to select a value from dirty object.
23 * Allows:
24 * - dot-separated (`.`) string, corresponding to path to value in dirty
25 * object
26 * - function(dirtyObj) -> (cleanValue | undefined)
27 *
28 * The optional `transform` parameter standardizes value regardless of source,
29 * for purposes of ensuring data type and/or string schemas.
30 */
31
32// helper that ensures abi's do not contain function signatures
33const sanitizedValue = dirtyValueArray => {
34 let sanitizedValueArray = [];
35 dirtyValueArray.forEach(item => {
36 let sanitizedItem = Object.assign({}, item);
37 delete sanitizedItem.signature;
38 sanitizedValueArray.push(sanitizedItem);
39 });
40 return sanitizedValueArray;
41};
42
43// filter `signature` property from an event
44const sanitizeEvent = dirtyEvent =>
45 Object.entries(dirtyEvent).reduce(
46 (acc, [property, value]) =>
47 property === "signature"
48 ? acc
49 : Object.assign(acc, { [property]: value }),
50 {}
51 );
52
53// sanitize aggregrate events given a `network-object.spec.json#events` object
54const sanitizeAllEvents = dirtyEvents =>
55 Object.entries(dirtyEvents).reduce(
56 (acc, [property, event]) =>
57 Object.assign(acc, { [property]: sanitizeEvent(event) }),
58 {}
59 );
60
61var properties = {
62 contractName: {
63 sources: ["contractName", "contract_name"]
64 },
65 abi: {
66 sources: ["abi", "interface"],
67 transform: function (value) {
68 if (typeof value === "string") {
69 try {
70 value = JSON.parse(value);
71 } catch (_) {
72 value = undefined;
73 }
74 }
75 if (Array.isArray(value)) {
76 return sanitizedValue(value);
77 }
78 return value;
79 }
80 },
81 metadata: {
82 sources: ["metadata"]
83 },
84 bytecode: {
85 sources: ["bytecode", "binary", "unlinked_binary", "evm.bytecode.object"],
86 transform: function (value) {
87 if (value && value.indexOf("0x") !== 0) {
88 value = "0x" + value;
89 }
90 return value;
91 }
92 },
93 deployedBytecode: {
94 sources: [
95 "deployedBytecode",
96 "runtimeBytecode",
97 "evm.deployedBytecode.object"
98 ],
99 transform: function (value) {
100 if (value && value.indexOf("0x") !== 0) {
101 value = "0x" + value;
102 }
103 return value;
104 }
105 },
106 immutableReferences: {},
107 generatedSources: {},
108 deployedGeneratedSources: {},
109 sourceMap: {
110 transform: function (value) {
111 if (typeof value === "string") {
112 try {
113 return JSON.parse(value);
114 } catch (_) {
115 return value;
116 }
117 } else {
118 return value;
119 }
120 },
121 sources: ["sourceMap", "srcmap", "evm.bytecode.sourceMap"]
122 },
123 deployedSourceMap: {
124 transform: function (value) {
125 if (typeof value === "string") {
126 try {
127 return JSON.parse(value);
128 } catch (_) {
129 return value;
130 }
131 } else {
132 return value;
133 }
134 },
135 sources: [
136 "deployedSourceMap",
137 "srcmapRuntime",
138 "evm.deployedBytecode.sourceMap"
139 ]
140 },
141 source: {},
142 sourcePath: {},
143 ast: {},
144 legacyAST: {
145 transform: function (value, obj) {
146 if (value) {
147 return value;
148 } else {
149 return obj.ast;
150 }
151 }
152 },
153 compiler: {},
154 networks: {
155 /**
156 * Normalize a networks object. Currently this makes sure `events` are
157 * always sanitized and `links` is extracted when copying from
158 * a TruffleContract context object.
159 *
160 * @param {object} value - the target object
161 * @param {object | TruffleContract} obj - the context, or source object.
162 * @return {object} The normalized Network object
163 */
164 transform: function (value = {}, obj) {
165 // Sanitize value's events for known networks
166 Object.keys(value).forEach(networkId => {
167 if (value[networkId].events) {
168 value[networkId].events = sanitizeAllEvents(value[networkId].events);
169 }
170 });
171
172 // Set and sanitize the current networks property from the
173 // TruffleContract. Note: obj is a TruffleContract if it has
174 // `network_id` attribute
175 const networkId = obj.network_id;
176 if (networkId && value.hasOwnProperty(networkId)) {
177 value[networkId].links = obj.links;
178 value[networkId].events = sanitizeAllEvents(obj.events);
179 }
180
181 return value;
182 }
183 },
184 schemaVersion: {
185 sources: ["schemaVersion", "schema_version"]
186 },
187 updatedAt: {
188 sources: ["updatedAt", "updated_at"],
189 transform: function (value) {
190 if (typeof value === "number") {
191 value = new Date(value).toISOString();
192 }
193 return value;
194 }
195 },
196 networkType: {},
197 devdoc: {},
198 userdoc: {},
199 db: {}
200};
201
202/**
203 * Construct a getter for a given key, possibly applying some post-retrieve
204 * transformation on the resulting value.
205 *
206 * @return {Function} Accepting dirty object and returning value || undefined
207 */
208function getter(key, transform) {
209 if (transform === undefined) {
210 transform = function (x) {
211 return x;
212 };
213 }
214
215 return function (obj) {
216 try {
217 return transform(obj[key]);
218 } catch (_) {
219 return undefined;
220 }
221 };
222}
223
224/**
225 * Chains together a series of function(obj) -> value, passing resulting
226 * returned value to next function in chain.
227 *
228 * Accepts any number of functions passed as arguments
229 * @return {Function} Accepting initial object, returning end-of-chain value
230 *
231 * Assumes all intermediary values to be objects, with well-formed sequence
232 * of operations.
233 */
234function chain() {
235 var getters = Array.prototype.slice.call(arguments);
236 return function (obj) {
237 return getters.reduce(function (cur, get) {
238 return get(cur);
239 }, obj);
240 };
241}
242
243// Schema module
244//
245
246var TruffleContractSchema = {
247 // Return a promise to validate a contract object
248 // - Resolves as validated `contractObj`
249 // - Rejects with list of errors from schema validator
250 validate: function (contractObj) {
251 var ajv = new Ajv({ verbose: true });
252 ajv.addSchema(abiSchema);
253 ajv.addSchema(networkObjectSchema);
254 ajv.addSchema(contractObjectSchema);
255 if (ajv.validate("contract-object.spec.json", contractObj)) {
256 return contractObj;
257 } else {
258 const message = `Schema validation failed. Errors:\n\n${ajv.errors
259 .map(
260 ({
261 keyword,
262 dataPath,
263 schemaPath,
264 params,
265 message,
266 data,
267 parentSchema
268 }) =>
269 util.format(
270 "%s (%s):\n%s\n",
271 message,
272 keyword,
273 util.inspect(
274 {
275 dataPath,
276 schemaPath,
277 params,
278 data,
279 parentSchema
280 },
281 { depth: 5 }
282 )
283 )
284 )
285 .join("\n")}`;
286 const error = new Error(message);
287 error.errors = ajv.errors;
288 throw error;
289 }
290 },
291
292 // accepts as argument anything that can be turned into a contract object
293 // returns a contract object
294 normalize: function (objDirty, options) {
295 options = options || {};
296 var normalized = {};
297
298 // iterate over each property
299 Object.keys(properties).forEach(function (key) {
300 var property = properties[key];
301 var value; // normalized value || undefined
302
303 // either used the defined sources or assume the key will only ever be
304 // listed as its canonical name (itself)
305 var sources = property.sources || [key];
306
307 // iterate over sources until value is defined or end of list met
308 for (var i = 0; value === undefined && i < sources.length; i++) {
309 var source = sources[i];
310 // string refers to path to value in objDirty, split and chain
311 // getters
312 if (typeof source === "string") {
313 var traversals = source.split(".").map(function (k) {
314 return getter(k);
315 });
316 source = chain.apply(null, traversals);
317 }
318
319 // source should be a function that takes the objDirty and returns
320 // value or undefined
321 value = source(objDirty);
322 }
323
324 // run source-agnostic transform on value
325 // (e.g. make sure bytecode begins 0x)
326 if (property.transform) {
327 value = property.transform(value, objDirty);
328 }
329
330 // add resulting (possibly undefined) to normalized obj
331 normalized[key] = value;
332 });
333
334 // Copy x- options
335 Object.keys(objDirty).forEach(function (key) {
336 if (key.indexOf("x-") === 0) {
337 normalized[key] = getter(key)(objDirty);
338 }
339 });
340
341 // update schema version
342 normalized.schemaVersion = pkgVersion;
343
344 if (options.validate) {
345 this.validate(normalized);
346 }
347
348 return normalized;
349 }
350};
351
352module.exports = TruffleContractSchema;