UNPKG

7.92 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
43var properties = {
44 contractName: {
45 sources: ["contractName", "contract_name"]
46 },
47 abi: {
48 sources: ["abi", "interface"],
49 transform: function(value) {
50 if (typeof value === "string") {
51 try {
52 value = JSON.parse(value);
53 } catch (e) {
54 value = undefined;
55 }
56 }
57 if (Array.isArray(value)) {
58 return sanitizedValue(value);
59 }
60 return value;
61 }
62 },
63 metadata: {
64 sources: ["metadata"]
65 },
66 bytecode: {
67 sources: ["bytecode", "binary", "unlinked_binary", "evm.bytecode.object"],
68 transform: function(value) {
69 if (value && value.indexOf("0x") !== 0) {
70 value = "0x" + value;
71 }
72 return value;
73 }
74 },
75 deployedBytecode: {
76 sources: [
77 "deployedBytecode",
78 "runtimeBytecode",
79 "evm.deployedBytecode.object"
80 ],
81 transform: function(value) {
82 if (value && value.indexOf("0x") !== 0) {
83 value = "0x" + value;
84 }
85 return value;
86 }
87 },
88 sourceMap: {
89 sources: ["sourceMap", "srcmap", "evm.bytecode.sourceMap"]
90 },
91 deployedSourceMap: {
92 sources: [
93 "deployedSourceMap",
94 "srcmapRuntime",
95 "evm.deployedBytecode.sourceMap"
96 ]
97 },
98 source: {},
99 sourcePath: {},
100 ast: {},
101 legacyAST: {
102 transform: function(value, obj) {
103 var schemaVersion = obj.schemaVersion || "0.0.0";
104
105 // legacyAST introduced in v2.0.0
106 if (schemaVersion[0] < 2) {
107 return obj.ast;
108 } else {
109 return value;
110 }
111 }
112 },
113 compiler: {},
114 networks: {
115 transform: function(value, obj) {
116 if (value === undefined) {
117 value = {};
118 }
119 if (obj.network_id && value[obj.network_id]) {
120 value[obj.network_id].events = obj.events;
121 value[obj.network_id].links = obj.links;
122 }
123 return value;
124 }
125 },
126 schemaVersion: {
127 sources: ["schemaVersion", "schema_version"]
128 },
129 updatedAt: {
130 sources: ["updatedAt", "updated_at"],
131 transform: function(value) {
132 if (typeof value === "number") {
133 value = new Date(value).toISOString();
134 }
135 return value;
136 }
137 },
138 devdoc: {},
139 userdoc: {}
140};
141
142/**
143 * Construct a getter for a given key, possibly applying some post-retrieve
144 * transformation on the resulting value.
145 *
146 * @return {Function} Accepting dirty object and returning value || undefined
147 */
148function getter(key, transform) {
149 if (transform === undefined) {
150 transform = function(x) {
151 return x;
152 };
153 }
154
155 return function(obj) {
156 try {
157 return transform(obj[key]);
158 } catch (e) {
159 return undefined;
160 }
161 };
162}
163
164/**
165 * Chains together a series of function(obj) -> value, passing resulting
166 * returned value to next function in chain.
167 *
168 * Accepts any number of functions passed as arguments
169 * @return {Function} Accepting initial object, returning end-of-chain value
170 *
171 * Assumes all intermediary values to be objects, with well-formed sequence
172 * of operations.
173 */
174function chain() {
175 var getters = Array.prototype.slice.call(arguments);
176 return function(obj) {
177 return getters.reduce(function(cur, get) {
178 return get(cur);
179 }, obj);
180 };
181}
182
183// Schema module
184//
185
186var TruffleContractSchema = {
187 // Return a promise to validate a contract object
188 // - Resolves as validated `contractObj`
189 // - Rejects with list of errors from schema validator
190 validate: function(contractObj) {
191 var ajv = new Ajv({ verbose: true });
192 ajv.addSchema(abiSchema);
193 ajv.addSchema(networkObjectSchema);
194 ajv.addSchema(contractObjectSchema);
195 if (ajv.validate("contract-object.spec.json", contractObj)) {
196 return contractObj;
197 } else {
198 const message = `Schema validation failed. Errors:\n\n${ajv.errors
199 .map(
200 ({
201 keyword,
202 dataPath,
203 schemaPath,
204 params,
205 message,
206 data,
207 schema, // eslint-disable-line no-unused-vars
208 parentSchema
209 }) =>
210 util.format(
211 "%s (%s):\n%s\n",
212 message,
213 keyword,
214 util.inspect(
215 {
216 dataPath,
217 schemaPath,
218 params,
219 data,
220 parentSchema
221 },
222 { depth: 5 }
223 )
224 )
225 )
226 .join("\n")}`;
227 const error = new Error(message);
228 error.errors = ajv.errors;
229 throw error;
230 }
231 },
232
233 // accepts as argument anything that can be turned into a contract object
234 // returns a contract object
235 normalize: function(objDirty, options) {
236 options = options || {};
237 var normalized = {};
238
239 // iterate over each property
240 Object.keys(properties).forEach(function(key) {
241 var property = properties[key];
242 var value; // normalized value || undefined
243
244 // either used the defined sources or assume the key will only ever be
245 // listed as its canonical name (itself)
246 var sources = property.sources || [key];
247
248 // iterate over sources until value is defined or end of list met
249 for (var i = 0; value === undefined && i < sources.length; i++) {
250 var source = sources[i];
251 // string refers to path to value in objDirty, split and chain
252 // getters
253 if (typeof source === "string") {
254 var traversals = source.split(".").map(function(k) {
255 return getter(k);
256 });
257 source = chain.apply(null, traversals);
258 }
259
260 // source should be a function that takes the objDirty and returns
261 // value or undefined
262 value = source(objDirty);
263 }
264
265 // run source-agnostic transform on value
266 // (e.g. make sure bytecode begins 0x)
267 if (property.transform) {
268 value = property.transform(value, objDirty);
269 }
270
271 // add resulting (possibly undefined) to normalized obj
272 normalized[key] = value;
273 });
274
275 // Copy x- options
276 Object.keys(objDirty).forEach(function(key) {
277 if (key.indexOf("x-") === 0) {
278 normalized[key] = getter(key)(objDirty);
279 }
280 });
281
282 // update schema version
283 normalized.schemaVersion = pkgVersion;
284
285 if (options.validate) {
286 this.validate(normalized);
287 }
288
289 return normalized;
290 }
291};
292
293module.exports = TruffleContractSchema;