UNPKG

@aeternity/aepp-sdk

Version:
316 lines (295 loc) 9.65 kB
/* eslint-disable no-unused-vars */ import Joi from 'joi-browser' export const SOPHIA_TYPES = [ 'int', 'string', 'tuple', 'address', 'bool', 'list', 'map', 'record', 'option', 'oracle', 'oracleQuery', 'hash', 'signature', 'bytes' ].reduce((acc, type) => ({ ...acc, [type]: type }), {}) /** * Ling Type Defs * @param t * @param bindings * @return {Object} */ export function linkTypeDefs (t, bindings) { const [_, typeDef] = t.split('.') const aciType = [ ...bindings.typedef, { name: 'state', typedef: bindings.state } ].find(({ name }) => name === typeDef) return aciType.typedef } /** * Parse sophia type * @param type * @param returnType * @return {Object} */ export function readType (type, { bindings } = {}) { let [t] = Array.isArray(type) ? type : [type] // Link State and typeDef if (typeof t === 'string' && t.indexOf(bindings.contractName) !== -1) { t = linkTypeDefs(t, bindings) } // Map, Tuple, List, Record, Bytes if (typeof t === 'object') { const [[baseType, generic]] = Object.entries(t) return { t: baseType, generic } } // Base types if (typeof t === 'string') return { t } } // FUNCTION ARGUMENTS TRANSFORMATION ↓↓↓ /** * Transform JS type to Sophia-type * @param type * @param value * @param bindings * @return {string} */ export async function transform (type, value, { bindings } = {}) { let { t, generic } = readType(type, { bindings }) switch (t) { case SOPHIA_TYPES.string: return `"${value}"` case SOPHIA_TYPES.list: return `[${await Promise.all(value.map(async el => transform(generic, el, { bindings })))}]` case SOPHIA_TYPES.tuple: return `(${await Promise.all(value.map(async (el, i) => transform(generic[i], el, { bindings })))})` case SOPHIA_TYPES.option: const optionV = await value.catch(e => undefined) return optionV === undefined ? 'None' : `Some(${await transform(generic, optionV, { bindings })})` case SOPHIA_TYPES.hash: case SOPHIA_TYPES.bytes: case SOPHIA_TYPES.signature: return `#${typeof value === 'string' ? value : Buffer.from(value).toString('hex')}` case SOPHIA_TYPES.record: return `{${await generic.reduce( async (acc, { name, type }, i) => { acc = await acc acc += `${i !== 0 ? ',' : ''}${name} = ${await transform(type, value[name], { bindings })}` return acc }, '' )}}` case SOPHIA_TYPES.map: return transformMap(value, generic, { bindings }) } return `${value}` } export async function transformMap (value, generic, { bindings }) { if (value instanceof Map) { value = Array.from(value.entries()) } if (!Array.isArray(value) && value instanceof Object) { value = Object.entries(value) } return `{${await value .reduce( async (acc, [key, value], i) => { acc = await acc if (i !== 0) acc += ',' acc += `[${await transform(generic[0], key, { bindings })}] = ${await transform(generic[1], value, { bindings })}` return acc }, `` ) }}` } // FUNCTION RETURN VALUE TRANSFORMATION ↓↓↓ /** * Transform decoded data to JS type * @param aci * @param result * @param transformDecodedData * @return {*} */ export function transformDecodedData (aci, result, { skipTransformDecoded = false, addressPrefix = 'ak', bindings } = {}) { if (skipTransformDecoded) return result const { t, generic } = readType(aci, { bindings }) switch (t) { case SOPHIA_TYPES.bool: return !!result case SOPHIA_TYPES.address: return result === 0 ? 0 : result case SOPHIA_TYPES.hash: case SOPHIA_TYPES.bytes: case SOPHIA_TYPES.signature: return result.split('#')[1] case SOPHIA_TYPES.map: const [keyT, valueT] = generic return result .reduce( (acc, [key, val]) => { key = transformDecodedData(keyT, key, { bindings }) val = transformDecodedData(valueT, val, { bindings }) acc.push([key, val]) return acc }, [] ) case SOPHIA_TYPES.option: if (result === 'None') return undefined const [[variantType, [value]]] = Object.entries(result) return variantType === 'Some' ? transformDecodedData(generic, value, { bindings }) : undefined case SOPHIA_TYPES.list: return result.map((value) => transformDecodedData(generic, value, { bindings })) case SOPHIA_TYPES.tuple: return result.map((value, i) => { return transformDecodedData(generic[i], value, { bindings }) }) case SOPHIA_TYPES.record: const genericMap = generic.reduce((acc, val) => ({ ...acc, [val.name]: { type: val.type } }), {}) return Object.entries(result).reduce( (acc, [name, value]) => ({ ...acc, [name]: transformDecodedData(genericMap[name].type, value, { bindings }) }), {} ) } return result } // FUNCTION ARGUMENTS VALIDATION ↓↓↓ /** * Prepare Joi validation schema for sophia types * @param type * @param bindings * @return {Object} JoiSchema */ export function prepareSchema (type, { bindings } = {}) { let { t, generic } = readType(type, { bindings }) if (!Object.keys(SOPHIA_TYPES).includes(t)) t = SOPHIA_TYPES.address // Handle Contract address transformation switch (t) { case SOPHIA_TYPES.int: return Joi.number().error(getJoiErrorMsg) case SOPHIA_TYPES.string: return Joi.string().error(getJoiErrorMsg) case SOPHIA_TYPES.address: return Joi.string().regex(/^(ak_|ct_|ok_|oq_)/).error(getJoiErrorMsg) case SOPHIA_TYPES.bool: return Joi.boolean().error(getJoiErrorMsg) case SOPHIA_TYPES.list: return Joi.array().items(prepareSchema(generic, { bindings })).error(getJoiErrorMsg) case SOPHIA_TYPES.tuple: return Joi.array().ordered(generic.map(type => prepareSchema(type, { bindings }).required())).label('Tuple argument').error(getJoiErrorMsg) case SOPHIA_TYPES.record: return Joi.object( generic.reduce((acc, { name, type }) => ({ ...acc, [name]: prepareSchema(type, { bindings }) }), {}) ).error(getJoiErrorMsg) case SOPHIA_TYPES.hash: return JoiBinary.binary().bufferCheck(32).error(getJoiErrorMsg) case SOPHIA_TYPES.bytes: return JoiBinary.binary().bufferCheck(generic).error(getJoiErrorMsg) case SOPHIA_TYPES.signature: return JoiBinary.binary().bufferCheck(64).error(getJoiErrorMsg) case SOPHIA_TYPES.option: return Joi.object().type(Promise).error(getJoiErrorMsg) // @Todo Need to transform Map to Array of arrays before validating it // case SOPHIA_TYPES.map: // return Joi.array().items(Joi.array().ordered(generic.map(type => prepareSchema(type)))) default: return Joi.any() } } /** * Parse Joi validation error message * @param errors * @return {Object} JoiError */ export function getJoiErrorMsg (errors) { return errors.map(err => { const { path, type, context } = err let value = context.hasOwnProperty('value') ? context.value : context.label value = typeof value === 'object' ? JSON.stringify(value).slice(1).slice(0, -1) : value switch (type) { case 'string.base': return ({ ...err, message: `Value "${value}" at path: [${path}] not a string` }) case 'number.base': return ({ ...err, message: `Value "${value}" at path: [${path}] not a number` }) case 'boolean.base': return ({ ...err, message: `Value "${value}" at path: [${path}] not a boolean` }) case 'array.base': return ({ ...err, message: `Value "${value}" at path: [${path}] not a array` }) case 'object.base': return ({ ...err, message: `Value '${value}' at path: [${path}] not a object` }) case 'object.type': return ({ ...err, message: `Value '${value}' at path: [${path}] not a ${context.type}` }) case 'binary.bufferCheck': return ({ ...err, message: `Value '${Buffer.from(value).toString('hex')}' at path: [${path}] not a ${context.size} bytes` }) default: return err } }) } /** * Custom Joi Validator for binary type */ const JoiBinary = Joi.extend((joi) => ({ name: 'binary', base: joi.any(), pre (value, state, options) { if (options.convert && typeof value === 'string') { try { return Buffer.from(value, 'hex') } catch (e) { return undefined } } return Buffer.from(value) }, rules: [ { name: 'bufferCheck', params: { size: joi.number().required() }, validate (params, value, state, options) { value = value === 'string' ? Buffer.from(value, 'hex') : Buffer.from(value) if (!Buffer.isBuffer(value)) { return this.createError('binary.base', { value }, state, options) } if (value.length !== params.size) { return this.createError('binary.bufferCheck', { value, size: params.size }, state, options) } return value } } ] })) /** * Validation contract function arguments * @param aci * @param params */ export function validateArguments (aci, params) { const validationSchema = Joi.array().ordered( aci.arguments .map(({ type }, i) => prepareSchema(type, { bindings: aci.bindings }).label(`[${params[i]}]`)) ).label('Argument') const { error } = Joi.validate(params, validationSchema, { abortEarly: false }) if (error) { throw error } }