From 48d36f9be30805a476590282bffae9944134eb41 Mon Sep 17 00:00:00 2001 From: Denis Davidyuk Date: Fri, 22 Oct 2021 20:29:31 +0300 Subject: [PATCH] refactor!: make contractDeploy a wrapper, remove unused code --- src/ae/contract.js | 35 +--- src/contract/aci/helpers.js | 49 ----- src/contract/aci/index.js | 63 ++++-- src/contract/aci/transformation.js | 306 ----------------------------- src/index.js | 2 - test/integration/contract.js | 2 +- 6 files changed, 60 insertions(+), 397 deletions(-) delete mode 100644 src/contract/aci/helpers.js diff --git a/src/ae/contract.js b/src/ae/contract.js index ab70729eee..93b1b76542 100644 --- a/src/ae/contract.js +++ b/src/ae/contract.js @@ -27,7 +27,6 @@ */ import Ae from './' -import * as R from 'ramda' import ContractCompilerAPI from '../contract/compiler' import ContractBase from '../contract' import getContractInstance from '../contract/aci' @@ -122,10 +121,11 @@ async function contractCall (source, contractAddress, name, args, options) { * @function * @alias module:@aeternity/aepp-sdk/es/ae/contract * @category async + * @deprecated * @param {String} code Compiled contract * @param {String} source Contract source code - * @param {Array|String} initState Arguments of contract constructor(init) function. Can be array of arguments or callData string - * @param {Object} [options={}] Transaction options (fee, ttl, gas, amount, deposit) + * @param {Array} params Arguments of contract constructor(init) function. Can be array of arguments or callData string + * @param {Object} [options] Transaction options (fee, ttl, gas, amount, deposit) * @param {Object} [options.filesystem={}] Contract external namespaces map* @return {Promise} Result object * @return {Promise} Result object * @example @@ -140,31 +140,10 @@ async function contractCall (source, contractAddress, name, args, options) { * callStatic: (fnName, args = [], options) => Static all contract function * } */ -async function contractDeploy (code, source, initState = [], options = {}) { - const opt = { ...this.Ae.defaults, ...options, deposit: DEPOSIT } - const callData = Array.isArray(initState) ? await this.contractEncodeCallDataAPI(source, 'init', initState, opt) : initState - const ownerId = await this.address(opt) - - const { tx, contractId } = await this.contractCreateTx(R.merge(opt, { - callData, - code, - ownerId - })) - - const { hash, rawTx, result, txData } = await this._sendAndProcess(tx, source, 'init', opt) - return Object.freeze({ - result, - owner: ownerId, - transaction: hash, - rawTx, - txData, - address: contractId, - call: (name, args, options) => - this.contractCall(source, contractId, name, args, { ...opt, ...options }), - callStatic: (name, args, options) => - this.contractCallStatic(source, contractId, name, args, { ...opt, ...options }), - createdAt: new Date() - }) +async function contractDeploy (code, source, params, options) { + const contract = await this.getContractInstance(source, options) + contract.compiled = code + return contract.deploy(params, options) } /** diff --git a/src/contract/aci/helpers.js b/src/contract/aci/helpers.js deleted file mode 100644 index c1dd0f6c98..0000000000 --- a/src/contract/aci/helpers.js +++ /dev/null @@ -1,49 +0,0 @@ -import * as R from 'ramda' -import { transform, validateArguments } from './transformation' - -/** - * Get function schema from contract ACI object - * @param {Object} aci Contract ACI object - * @param {String} name Function name - * @param external - * @return {Object} function ACI - */ -export function getFunctionACI (aci, name, external) { - if (!aci) throw new Error('ACI required') - const fn = aci.functions.find(f => f.name === name) - if (!fn && name !== 'init') throw new Error(`Function ${name} doesn't exist in contract`) - - return { - ...fn, - bindings: [ - { - state: aci.state, - type_defs: aci.type_defs, - name: aci.name - }, - ...external.map(R.pick(['state', 'type_defs', 'name'])) - ], - event: aci.event ? aci.event.variant : [] - } -} - -/** - * Validated contract call arguments using contract ACI - * @function validateCallParams - * @rtype (aci: Object, params: Array) => Object - * @param {Object} aci Contract ACI - * @param {Array} params Contract call arguments - * @return Promise{Array} Object with validation errors - */ -export const prepareArgsForEncode = async (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 - return Promise.all(aci.arguments.map(async ({ type }, i) => transform(type, params[i], bindings))) -} diff --git a/src/contract/aci/index.js b/src/contract/aci/index.js index 7cfb532f26..364ace60af 100644 --- a/src/contract/aci/index.js +++ b/src/contract/aci/index.js @@ -25,11 +25,35 @@ import * as R from 'ramda' import BigNumber from 'bignumber.js' import { Encoder as Calldata } from '@aeternity/aepp-calldata' -import { getFunctionACI, prepareArgsForEncode } from './helpers' import { decodeEvents } from './transformation' -import { DRY_RUN_ACCOUNT } from '../../tx/builder/schema' +import { DRY_RUN_ACCOUNT, DEPOSIT } from '../../tx/builder/schema' import TxObject from '../../tx/tx-object' +/** + * Get function schema from contract ACI object + * @param {Object} aci Contract ACI object + * @param {String} name Function name + * @param external + * @return {Object} function ACI + */ +function getFunctionACI (aci, name, external) { + const fn = aci.functions.find(f => f.name === name) + if (!fn && name !== 'init') throw new Error(`Function ${name} doesn't exist in contract`) + + return { + ...fn, + bindings: [ + { + state: aci.state, + type_defs: aci.type_defs, + name: aci.name + }, + ...external.map(R.pick(['state', 'type_defs', 'name'])) + ], + event: aci.event ? aci.event.variant : [] + } +} + /** * Generate contract ACI object with predefined js methods for contract usage - can be used for creating a reference to already deployed contracts * @alias module:@aeternity/aepp-sdk/es/contract/aci @@ -95,20 +119,37 @@ export default async function getContractInstance (source, { aci, contractAddres * Deploy contract * @alias module:@aeternity/aepp-sdk/es/contract/aci * @rtype (init: Array, options: Object) => ContractInstance: Object - * @param {Array} init Contract init function arguments array - * @param {Object} [options={}] options + * @param {Array} params Contract init function arguments array + * @param {Object} [options] options * @return {Object} deploy info */ - instance.deploy = async (init = [], options = {}) => { - const opt = { ...instance.options, ...options } - + instance.deploy = async (params = [], options) => { + const opt = { ...instance.options, ...options, deposit: DEPOSIT } if (!instance.compiled) await instance.compile(opt) - if (opt.callStatic) return instance.call('init', init, opt) + if (opt.callStatic) return instance.call('init', params, opt) - const fnACI = getFunctionACI(instance.aci, 'init', instance.externalAci) - init = await prepareArgsForEncode(fnACI, init) - instance.deployInfo = await this.contractDeploy(instance.compiled, opt.source || instance.source, init, opt) + const source = opt.source || instance.source + const ownerId = await this.address(opt) + const { tx, contractId } = await this.contractCreateTx(R.merge(opt, { + callData: instance.calldata.encode(instance.aci.name, 'init', params), + code: instance.compiled, + ownerId + })) + const { hash, rawTx, result, txData } = await this._sendAndProcess(tx, source, 'init', opt) + instance.deployInfo = Object.freeze({ + result, + owner: ownerId, + transaction: hash, + rawTx, + txData, + address: contractId, + call: (name, args, options) => + this.contractCall(source, contractId, name, args, { ...opt, ...options }), + callStatic: (name, args, options) => + this.contractCallStatic(source, contractId, name, args, { ...opt, ...options }), + createdAt: new Date() + }) return instance.deployInfo } diff --git a/src/contract/aci/transformation.js b/src/contract/aci/transformation.js index 56d0dcfd9b..2b7041d706 100644 --- a/src/contract/aci/transformation.js +++ b/src/contract/aci/transformation.js @@ -1,5 +1,3 @@ -import Joi from 'joi' -import { isHex } from '../../utils/string' import { toBytes } from '../../utils/bytes' import { decode } from '../../tx/builder/helpers' import { parseBigNumber } from '../../utils/bignumber' @@ -69,307 +67,3 @@ function decodeEventField (field, type) { return toBytes(field, true) } } - -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} - */ -function linkTypeDefs (t, bindings) { - const [root, typeDef] = typeof t === 'object' ? Object.keys(t)[0].split('.') : t.split('.') - const contractTypeDefs = bindings.find(c => c.name === root) - const aciType = [ - ...contractTypeDefs.type_defs, - { name: 'state', typedef: contractTypeDefs.state, vars: [] } - ].find(({ name }) => name === typeDef) - if (aciType.vars.length) { - aciType.typedef = injectVars(t, aciType) - } - return isTypedDefOrState(aciType.typedef, bindings) ? linkTypeDefs(aciType.typedef, bindings) : aciType.typedef -} - -const isTypedDefOrState = (t, bindings) => { - if (!['string', 'object'].includes(typeof t)) return false - - t = typeof t === 'object' ? Object.keys(t)[0] : t - const [root, ...path] = t.split('.') - // Remote Contract Address - if (!path.length) return false - return bindings.map(c => c.name).includes(root) -} - -const isRemoteAddress = (t) => { - if (typeof t !== 'string') return false - const [root, ...path] = t.split('.') - return !path.length && !Object.values(SOPHIA_TYPES).includes(root) -} - -/** - * Parse sophia type - * @param type - * @param bindings - * @return {Object} - */ -export function readType (type, bindings) { - let [t] = Array.isArray(type) ? type : [type] - - // If remote address - if (isRemoteAddress(t)) return { t: SOPHIA_TYPES.address } - // Link State and typeDef - if (isTypedDefOrState(t, bindings)) 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 function transform (type, value, bindings) { - const { t, generic } = readType(type, bindings) - - switch (t) { - case SOPHIA_TYPES.ChainTtl: - return `${value}` - case SOPHIA_TYPES.string: - return `"${value}"` - case SOPHIA_TYPES.list: - return `[${value.map(el => transform(generic, el, bindings))}]` - case SOPHIA_TYPES.tuple: - return `(${value.map((el, i) => transform(generic[i], el, bindings))})` - case SOPHIA_TYPES.option: { - return value === undefined ? 'None' : `Some(${transform(generic, value, bindings)})` - } - case SOPHIA_TYPES.hash: - case SOPHIA_TYPES.bytes: - case SOPHIA_TYPES.signature: - if (typeof value === 'string') { - if (isHex(value)) return `#${value}` - } - return `#${Buffer.from(value).toString('hex')}` - case SOPHIA_TYPES.record: - return `{${generic.reduce( - (acc, { name, type }, i) => { - acc += `${i !== 0 ? ',' : ''}${name} = ${transform(type, value[name], bindings)}` - return acc - }, - '' - )}}` - case SOPHIA_TYPES.map: - return transformMap(value, generic, bindings) - case SOPHIA_TYPES.variant: - return transformVariant(value, generic, bindings) - } - - return `${value}` -} - -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 - ? '' - : `(${variantArgs.slice(0, type.length).map((el, i) => transform(type[i], el, bindings))})` - }` -} - -function transformMap (value, generic, bindings) { - if (!Array.isArray(value)) { - if (value.entries) value = Array.from(value.entries()) - else if (value instanceof Object) value = Object.entries(value) - } - - return [ - '{', - value - .map(([key, value]) => [ - `[${transform(generic[0], key, bindings)}]`, - transform(generic[1], value, bindings) - ].join(' = ')) - .join(), - '}' - ].join('') -} - -// FUNCTION RETURN VALUE TRANSFORMATION ↓↓↓ - -/** - * Transform decoded data to JS type - * @param aci - * @param result - * @param bindings - * @return {*} - */ -export function transformDecodedData (aci, result, bindings) { - 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 - */ -function prepareSchema (type, bindings) { - const { t, generic } = readType(type, bindings) - - switch (t) { - case SOPHIA_TYPES.int: - return Joi.number().unsafe() - 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.ChainTtl: - return Joi.string() - case SOPHIA_TYPES.string: - return Joi.string() - case SOPHIA_TYPES.address: - return Joi.string().regex(/^(ak_|ct_|ok_|oq_)/) - case SOPHIA_TYPES.bool: - return Joi.boolean() - case SOPHIA_TYPES.list: - return Joi.array().items(prepareSchema(generic, bindings)) - case SOPHIA_TYPES.tuple: - return Joi.array().ordered(...generic.map(type => prepareSchema(type, bindings).required())).label('Tuple argument') - case SOPHIA_TYPES.record: - return Joi.object( - generic.reduce((acc, { name, type }) => ({ ...acc, [name]: prepareSchema(type, bindings) }), {}) - ) - case SOPHIA_TYPES.hash: - return JoiBinary.binary().encoding('hex').length(32) - case SOPHIA_TYPES.bytes: - return JoiBinary.binary().encoding('hex').length(generic) - case SOPHIA_TYPES.signature: - return JoiBinary.binary().encoding('hex').length(64) - case SOPHIA_TYPES.option: - return prepareSchema(generic, bindings).optional() - // @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() - } -} - -/** - * Custom Joi Validator for binary type - */ -const JoiBinary = Joi.extend((joi) => ({ - type: 'binary', - base: joi.binary(), - coerce (value) { - if (typeof value !== 'string') return { value: Buffer.from(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, aci.bindings).label(`[${params[i]}]`)) - ).sparse(true).label('Argument') - const { error } = validationSchema.validate(params, { abortEarly: false }) - if (error) { - throw error - } -} diff --git a/src/index.js b/src/index.js index ed0305e8a4..772208234b 100644 --- a/src/index.js +++ b/src/index.js @@ -21,7 +21,6 @@ import * as Bytes from './utils/bytes' import * as TxBuilder from './tx/builder' import * as TxBuilderHelper from './tx/builder/helpers' import * as SCHEMA from './tx/builder/schema' -import * as ACIHelpers from './contract/aci/helpers' import * as ACITransformation from './contract/aci/transformation' import * as AmountFormatter from './utils/amount-formatter' import HdWallet from './utils/hd-wallet' @@ -64,7 +63,6 @@ export { Bytes, Contract, ContractCompilerAPI, - ACIHelpers, ACITransformation, ChainNode, RpcAepp, diff --git a/test/integration/contract.js b/test/integration/contract.js index e7d3f57f46..0cd79d7e98 100644 --- a/test/integration/contract.js +++ b/test/integration/contract.js @@ -333,7 +333,7 @@ describe('Contract', function () { }) it('initializes contract state', async () => { - const data = '"Hello World!"' + const data = 'Hello World!' return sdk.contractCompile(stateContract) .then(bytecode => bytecode.deploy([data])) .then(deployed => deployed.call('retrieve'))