UNPKG

7.94 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 networkType: {},
139 devdoc: {},
140 userdoc: {}
141};
142
143/**
144 * Construct a getter for a given key, possibly applying some post-retrieve
145 * transformation on the resulting value.
146 *
147 * @return {Function} Accepting dirty object and returning value || undefined
148 */
149function getter(key, transform) {
150 if (transform === undefined) {
151 transform = function(x) {
152 return x;
153 };
154 }
155
156 return function(obj) {
157 try {
158 return transform(obj[key]);
159 } catch (e) {
160 return undefined;
161 }
162 };
163}
164
165/**
166 * Chains together a series of function(obj) -> value, passing resulting
167 * returned value to next function in chain.
168 *
169 * Accepts any number of functions passed as arguments
170 * @return {Function} Accepting initial object, returning end-of-chain value
171 *
172 * Assumes all intermediary values to be objects, with well-formed sequence
173 * of operations.
174 */
175function chain() {
176 var getters = Array.prototype.slice.call(arguments);
177 return function(obj) {
178 return getters.reduce(function(cur, get) {
179 return get(cur);
180 }, obj);
181 };
182}
183
184// Schema module
185//
186
187var TruffleContractSchema = {
188 // Return a promise to validate a contract object
189 // - Resolves as validated `contractObj`
190 // - Rejects with list of errors from schema validator
191 validate: function(contractObj) {
192 var ajv = new Ajv({ verbose: true });
193 ajv.addSchema(abiSchema);
194 ajv.addSchema(networkObjectSchema);
195 ajv.addSchema(contractObjectSchema);
196 if (ajv.validate("contract-object.spec.json", contractObj)) {
197 return contractObj;
198 } else {
199 const message = `Schema validation failed. Errors:\n\n${ajv.errors
200 .map(
201 ({
202 keyword,
203 dataPath,
204 schemaPath,
205 params,
206 message,
207 data,
208 schema, // eslint-disable-line no-unused-vars
209 parentSchema
210 }) =>
211 util.format(
212 "%s (%s):\n%s\n",
213 message,
214 keyword,
215 util.inspect(
216 {
217 dataPath,
218 schemaPath,
219 params,
220 data,
221 parentSchema
222 },
223 { depth: 5 }
224 )
225 )
226 )
227 .join("\n")}`;
228 const error = new Error(message);
229 error.errors = ajv.errors;
230 throw error;
231 }
232 },
233
234 // accepts as argument anything that can be turned into a contract object
235 // returns a contract object
236 normalize: function(objDirty, options) {
237 options = options || {};
238 var normalized = {};
239
240 // iterate over each property
241 Object.keys(properties).forEach(function(key) {
242 var property = properties[key];
243 var value; // normalized value || undefined
244
245 // either used the defined sources or assume the key will only ever be
246 // listed as its canonical name (itself)
247 var sources = property.sources || [key];
248
249 // iterate over sources until value is defined or end of list met
250 for (var i = 0; value === undefined && i < sources.length; i++) {
251 var source = sources[i];
252 // string refers to path to value in objDirty, split and chain
253 // getters
254 if (typeof source === "string") {
255 var traversals = source.split(".").map(function(k) {
256 return getter(k);
257 });
258 source = chain.apply(null, traversals);
259 }
260
261 // source should be a function that takes the objDirty and returns
262 // value or undefined
263 value = source(objDirty);
264 }
265
266 // run source-agnostic transform on value
267 // (e.g. make sure bytecode begins 0x)
268 if (property.transform) {
269 value = property.transform(value, objDirty);
270 }
271
272 // add resulting (possibly undefined) to normalized obj
273 normalized[key] = value;
274 });
275
276 // Copy x- options
277 Object.keys(objDirty).forEach(function(key) {
278 if (key.indexOf("x-") === 0) {
279 normalized[key] = getter(key)(objDirty);
280 }
281 });
282
283 // update schema version
284 normalized.schemaVersion = pkgVersion;
285
286 if (options.validate) {
287 this.validate(normalized);
288 }
289
290 return normalized;
291 }
292};
293
294module.exports = TruffleContractSchema;