@aeternity/aepp-sdk
Version:
SDK for the æternity blockchain
195 lines (179 loc) • 7.88 kB
JavaScript
/*
* ISC License (ISC)
* Copyright (c) 2018 aeternity developers
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
* REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
* LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
* OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
* PERFORMANCE OF THIS SOFTWARE.
*/
/**
* ContractACI module
*
* @module @aeternity/aepp-sdk/es/contract/aci
* @export ContractACI
* @example import ContractACI from '@aeternity/aepp-sdk/es/contract/aci'
*/
import * as R from 'ramda'
import { validateArguments, transform, transformDecodedData } from './transformation'
import { buildContractMethods, getFunctionACI } from './helpers'
import AsyncInit from '../../utils/async-init'
/**
* 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
*/
async function prepareArgsForEncode (aci, params) {
if (!aci || !aci.arguments) return params
// Validation
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
})))
}
/**
* Generate contract ACI object with predefined js methods for contract usage
* @alias module:@aeternity/aepp-sdk/es/contract/aci
* @param {String} source Contract source code
* @param {Object} [options] Options object
* @param {Object} [options.aci] Contract ACI
* @param {Object} [options.contractAddress] Contract address
* @param {Object} [options.opt] Contract options
* @return {ContractInstance} JS Contract API
* @example
* const contractIns = await client.getContractInstance(sourceCode)
* await contractIns.deploy([321]) or await contractIns.methods.init(321)
* const callResult = await contractIns.call('setState', [123]) or await contractIns.methods.setState.send(123, options)
* const staticCallResult = await contractIns.call('setState', [123], { callStatic: true }) or await contractIns.methods.setState.get(123, options)
* Also you can call contract like: await contractIns.methods.setState(123, options)
* Then sdk decide to make on-chain or static call(dry-run API) transaction based on function is stateful or not
*/
async function getContractInstance (source, { aci, contractAddress, opt } = {}) {
aci = aci || await this.contractGetACI(source)
const defaultOptions = {
skipArgsConvert: false,
skipTransformDecoded: false,
callStatic: false,
deposit: 0,
gasPrice: 1000000000, // min gasPrice 1e9
amount: 0,
gas: 1600000 - 21000,
top: null, // using for contract call static
waitMined: true,
verify: false
}
const instance = {
interface: R.defaultTo(null, R.prop('interface', aci)),
aci: R.defaultTo(null, R.path(['encoded_aci', 'contract'], aci)),
source,
compiled: null,
deployInfo: { address: contractAddress },
options: R.merge(defaultOptions, opt),
compilerVersion: this.compilerVersion,
setOptions (opt) {
this.options = R.merge(this.options, opt)
}
}
/**
* Compile contract
* @alias module:@aeternity/aepp-sdk/es/contract/aci
* @rtype () => ContractInstance: Object
* @return {ContractInstance} Contract ACI object with predefined js methods for contract usage
*/
instance.compile = compile({ client: this, instance })
/**
* Deploy contract
* @alias module:@aeternity/aepp-sdk/es/contract/aci
* @rtype (init: Array, options: Object = { skipArgsConvert: false }) => ContractInstance: Object
* @param {Array} init Contract init function arguments array
* @param {Object} [options={}] options Options object
* @param {Boolean} [options.skipArgsConvert=false] Skip Validation and Transforming arguments before prepare call-data
* @return {ContractInstance} Contract ACI object with predefined js methods for contract usage
*/
instance.deploy = deploy({ client: this, instance })
/**
* Call contract function
* @alias module:@aeternity/aepp-sdk/es/contract/aci
* @rtype (init: Array, options: Object = { skipArgsConvert: false, skipTransformDecoded: false, callStatic: false }) => CallResult: Object
* @param {String} fn Function name
* @param {Array} params Array of function arguments
* @param {Object} [options={}] Array of function arguments
* @param {Boolean} [options.skipArgsConvert=false] Skip Validation and Transforming arguments before prepare call-data
* @param {Boolean} [options.skipTransformDecoded=false] Skip Transform decoded data to JS type
* @param {Boolean} [options.callStatic=false] Static function call
* @return {Object} CallResult
*/
instance.call = call({ client: this, instance })
/**
* Generate proto function based on contract function using Contract ACI schema
* All function can be called like:
* 'await contract.methods.testFunction()' -> then sdk will decide to use dry-run or send tx on-chain base on if function stateful or not.
* Also you can manually do that:
* `await contract.methods.testFunction.get()` -> use call-static(dry-run)
* `await contract.methods.testFunction.send()` -> send tx on-chain
*/
instance.methods = buildContractMethods(instance)()
return instance
}
const call = ({ client, instance }) => async (fn, params = [], options = {}) => {
const opt = R.merge(instance.options, options)
const fnACI = getFunctionACI(instance.aci, fn)
const source = opt.source || instance.source
if (!fn) throw new Error('Function name is required')
if (!instance.deployInfo.address) throw new Error('You need to deploy contract before calling!')
params = !opt.skipArgsConvert ? await prepareArgsForEncode(fnACI, params) : params
const result = opt.callStatic
? await client.contractCallStatic(source, instance.deployInfo.address, fn, params, {
top: opt.top,
options: opt
})
: await client.contractCall(source, instance.deployInfo.address, fn, params, opt)
return {
...result,
decodedResult: await transformDecodedData(
fnACI.returns,
await result.decode(),
{ ...opt, compilerVersion: instance.compilerVersion, bindings: fnACI.bindings }
)
}
}
const deploy = ({ client, instance }) => async (init = [], options = {}) => {
const opt = R.merge(instance.options, options)
const fnACI = getFunctionACI(instance.aci, 'init')
if (!instance.compiled) await instance.compile()
init = !opt.skipArgsConvert ? await prepareArgsForEncode(fnACI, init) : init
const { owner, transaction, address, createdAt, result, rawTx } = await client.contractDeploy(instance.compiled, opt.source || instance.source, init, opt)
instance.deployInfo = { owner, transaction, address, createdAt, result, rawTx }
return instance.deployInfo
}
const compile = ({ client, instance }) => async () => {
const { bytecode } = await client.contractCompile(instance.source)
instance.compiled = bytecode
return instance.compiled
}
/**
* Contract ACI Stamp
*
* @function
* @alias module:@aeternity/aepp-sdk/es/contract/aci
* @rtype Stamp
* @return {Object} Contract compiler instance
* @example ContractACI()
*/
export default AsyncInit.compose({
methods: {
getContractInstance
}
})