From 8505dcfb82d9e00eb0b93bcff301c00161f0746b Mon Sep 17 00:00:00 2001 From: naz_dou <41945483+nduchak@users.noreply.github.com> Date: Mon, 29 Jul 2019 15:33:01 +0300 Subject: [PATCH] feat(ACI): Implement sophia `variant` type (#567) * feat(ACI): Implement sophia `variant` type. Improve arguments validation * feat(ACI): Implement `vars` injection to `typeDef` ``` datatype myOption('a) = Node | Some('a) entrypoint optionFn(v: myOption(string)): myOption(string = v) ``` * fix(lint): Fix linter error * feat(ACI): Add validation for sophia `datatype` simple and generic variant * feat(ACI): Allow pass datatype as object(for generic variants) or string. Add tests --- es/contract/aci/index.js | 4 ++ es/contract/aci/transformation.js | 70 +++++++++++++++++++++++++++++-- test/integration/contract.js | 24 +++++++++++ 3 files changed, 94 insertions(+), 4 deletions(-) diff --git a/es/contract/aci/index.js b/es/contract/aci/index.js index 6204f60901..59b52b9940 100644 --- a/es/contract/aci/index.js +++ b/es/contract/aci/index.js @@ -40,6 +40,10 @@ import AsyncInit from '../../utils/async-init' async function prepareArgsForEncode (aci, params) { if (!aci || !aci.arguments) return params // Validation + if (aci.arguments.length > params.length) { + throw new Error(`Function "${aci.name}" require ${aci.arguments.length} arguments of types [${aci.arguments.map(a => JSON.stringify(a.type))}] but get [${params.map(JSON.stringify)}]`) + } + validateArguments(aci, params) const bindings = aci.bindings // Cast argument from JS to Sophia type diff --git a/es/contract/aci/transformation.js b/es/contract/aci/transformation.js index f0280209c3..55790f5365 100644 --- a/es/contract/aci/transformation.js +++ b/es/contract/aci/transformation.js @@ -15,9 +15,31 @@ export const SOPHIA_TYPES = [ 'oracleQuery', 'hash', 'signature', - 'bytes' + 'bytes', + 'variant' ].reduce((acc, type) => ({ ...acc, [type]: type }), {}) +export function injectVars (t, aciType) { + const [[baseType, generic]] = Object.entries(aciType.typedef) + const [[_, varianValue]] = Object.entries(t) + switch (baseType) { + case SOPHIA_TYPES.variant: + return { + [baseType]: generic.map(el => { + const [tag, gen] = Object.entries(el)[0] + return { + [tag]: gen.map(type => { + const index = aciType.vars.map(e => e.name).indexOf(type) + return index === -1 + ? type + : varianValue[index] + }) + } + }) + } + } +} + /** * Ling Type Defs * @param t @@ -25,11 +47,15 @@ export const SOPHIA_TYPES = [ * @return {Object} */ export function linkTypeDefs (t, bindings) { - const [_, typeDef] = t.split('.') + const [_, typeDef] = typeof t === 'object' ? Object.keys(t)[0].split('.') : t.split('.') const aciType = [ ...bindings.typedef, - { name: 'state', typedef: bindings.state } + { name: 'state', typedef: bindings.state, vars: [] } ].find(({ name }) => name === typeDef) + if (aciType.vars.length) { + aciType.typedef = injectVars(t, aciType) + } + return aciType.typedef } @@ -43,9 +69,13 @@ 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) { + if ( + (typeof t === 'string' && t.indexOf(bindings.contractName) !== -1) || + (typeof t === 'object' && Object.keys(t)[0] && Object.keys(t)[0].indexOf(bindings.contractName) !== -1) + ) { t = linkTypeDefs(t, bindings) } + // Map, Tuple, List, Record, Bytes if (typeof t === 'object') { const [[baseType, generic]] = Object.entries(t) @@ -98,11 +128,24 @@ export async function transform (type, value, { bindings } = {}) { )}}` case SOPHIA_TYPES.map: return transformMap(value, generic, { bindings }) + case SOPHIA_TYPES.variant: + return transformVariant(value, generic, { bindings }) } return `${value}` } +async function transformVariant (value, generic, { bindings }) { + const [[variant, variantArgs]] = typeof value === 'string' ? [[value, []]] : Object.entries(value) + const [[v, type]] = Object.entries(generic.find(o => Object.keys(o)[0].toLowerCase() === variant.toLowerCase())) + return `${v}${!type.length + ? '' + : `(${await Promise.all(variantArgs.slice(0, type.length).map(async (el, i) => transform(type[i], el, { + bindings + })))})` + }` +} + export async function transformMap (value, generic, { bindings }) { if (value instanceof Map) { value = Array.from(value.entries()) @@ -194,10 +237,29 @@ export function transformDecodedData (aci, result, { skipTransformDecoded = fals */ 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.variant: + return Joi.alternatives().try([ + Joi.string().valid( + ...generic.reduce((acc, el) => { + const [[t, g]] = Object.entries(el) + if (!g || !g.length) acc.push(t) + return acc + }, []) + ), + Joi.object(generic + .reduce( + (acc, el) => { + const variant = Object.keys(el)[0] + return { ...acc, [variant]: Joi.array() } + }, + {}) + ).or(...generic.map(e => Object.keys(e)[0])) + ]) case SOPHIA_TYPES.string: return Joi.string().error(getJoiErrorMsg) case SOPHIA_TYPES.address: diff --git a/test/integration/contract.js b/test/integration/contract.js index b8b9f7bf43..b50b82758d 100644 --- a/test/integration/contract.js +++ b/test/integration/contract.js @@ -45,6 +45,8 @@ contract StateContract = record state = { value: string, key: number, testOption: option(string) } record yesEr = { t: number} + datatype dateUnit = Year | Month | Day + entrypoint init(value: string, key: int, testOption: option(string)) : state = { value = value, key = key, testOption = testOption } entrypoint retrieve() : (string, int) = (state.value, state.key) @@ -79,6 +81,8 @@ contract StateContract = entrypoint bytesFn(s: bytes(32)): bytes(32) = s entrypoint usingExternalLib(s: int): int = Test.double(s) + + entrypoint datTypeFn(s: dateUnit): dateUnit = s ` const encodedNumberSix = 'cb_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAaKNdnK' @@ -435,6 +439,26 @@ describe('Contract', function () { res.decodedResult.should.be.equal(4) }) }) + describe('DATATYPE', function () { + it('Invalid type', async () => { + try { + await contractObject.methods.datTypeFn({}) + } catch (e) { + e.message.should.be.equal('"Argument" at position 0 fails because ["0" must be a string, "value" must contain at least one of [Year, Month, Day]]') + } + }) + it('Invalid variant', async () => { + try { + await contractObject.methods.datTypeFn("asdcxz") + } catch (e) { + e.message.should.be.equal('"Argument" at position 0 fails because ["0" must be one of [Year, Month, Day], "0" must be an object]') + } + }) + it('Valid', async () => { + const res = await contractObject.methods.datTypeFn("Year" || { Year: []}) + JSON.stringify(res.decodedResult).should.be.equal(JSON.stringify({ Year: [] })) + }) + }) describe('Hash', function () { it('Invalid type', async () => { try {