UNPKG

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