Skip to content

Commit

Permalink
feat(ACI): Implement sophia variant type (#567)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
nduchak authored Jul 29, 2019
1 parent 11c85eb commit 8505dcf
Show file tree
Hide file tree
Showing 3 changed files with 94 additions and 4 deletions.
4 changes: 4 additions & 0 deletions es/contract/aci/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
70 changes: 66 additions & 4 deletions es/contract/aci/transformation.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,47 @@ 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
* @param bindings
* @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
}

Expand All @@ -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)
Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -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:
Expand Down
24 changes: 24 additions & 0 deletions test/integration/contract.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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 {
Expand Down

0 comments on commit 8505dcf

Please sign in to comment.