UNPKG

@aeternity/aepp-sdk

Version:
463 lines (436 loc) 16.7 kB
/* * 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. */ /** * Contract module - routines to interact with the æternity contract * * High level documentation of the contracts are available at * https://github.com/aeternity/protocol/tree/master/contracts and * * @module @aeternity/aepp-sdk/es/ae/contract * @export Contract * @example import { Contract } from '@aeternity/aepp-sdk' */ import Ae from './' import * as R from 'ramda' import ContractCompilerAPI from '../contract/compiler' import ContractBase from '../contract' import getContractInstance from '../contract/aci' import NodePool from '../node-pool' import { AMOUNT, DEPOSIT, DRY_RUN_ACCOUNT, GAS, MIN_GAS_PRICE } from '../tx/builder/schema' import { decode, produceNameId } from '../tx/builder/helpers' import TxObject from '../tx/tx-object' async function _sendAndProcess (tx, source, name, options) { const txData = await this.send(tx, options) const result = { hash: txData.hash, tx: TxObject({ tx: txData.rawTx }), txData, rawTx: txData.rawTx } if (options.waitMined === false) return result const txInfo = await this.getTxInfo(txData.hash) await this._handleCallError(source, name, txInfo) return { ...result, result: txInfo } } /** * Handle contract call error * @function * @private * @alias module:@aeternity/aepp-sdk/es/ae/contract * @category async * @param {String} source contract source code * @param {String} name name of called method * @param {Object} result call result object * @throws Error Decoded error * @return {Promise<void>} */ async function _handleCallError (source, name, result) { if (result.returnType === 'ok') return const error = await this.contractDecodeCallResultAPI(source, name, result.returnValue, result.returnType) const message = error[{ revert: 'abort' }[result.returnType] || result.returnType][0] throw new Error(`Invocation failed${message ? `: "${message}"` : ''}`) } /** * Encode call data for contract call * @function * @alias module:@aeternity/aepp-sdk/es/ae/contract * @category async * @param {String} source Contract source code * @param {String} name Name of function to call * @param {Array} args Argument's for call * @param {Object} [options={}] Options * @param {Object} [options.filesystem={}] Contract external namespaces map * @return {Promise<String>} */ async function contractEncodeCall (source, name, args, options) { return this.contractEncodeCallDataAPI(source, name, args, options) } /** * Decode contract call result data * @function * @alias module:@aeternity/aepp-sdk/es/ae/contract * @category async * @param {String} source - source code * @param {String } fn - function name * @param {String} callValue - result call data * @param {String} callResult - result status * @param {Object} [options={}] Options * @param {Object} [options.filesystem={}] Contract external namespaces map * @return {Promise<String>} Result object * @example * const decodedData = await client.contractDecodeData(SourceCode ,'functionName', 'cb_asdasdasd...', 'ok|revert')lt */ async function contractDecodeData (source, fn, callValue, callResult, options) { return this.contractDecodeCallResultAPI(source, fn, callValue, callResult, options) } /** * Static contract call(using dry-run) * @function * @alias module:@aeternity/aepp-sdk/es/ae/contract * @category async * @param {String} source Contract source code * @param {String} address Contract address * @param {String} name Name of function to call * @param {Array|String} args Argument's or callData for call/deploy transaction * @param {Object} [options] * @param {Number|String} [options.top] Block height or hash on which you want to call contract * @param {String} [options.bytecode] Block hash on which you want to call contract * @param {Object} [options.filesystem] Contract external namespaces map * @return {Promise<Object>} Result object * @example * const callResult = await client.contractCallStatic(source, address, fnName, args) * { * result: TX_DATA, * decode: (type) => Decode call result * } */ async function contractCallStatic (source, address, name, args = [], options = {}) { const callerId = await this.address(options).catch(() => DRY_RUN_ACCOUNT.pub) if (typeof options.top === 'number') { options.top = (await this.getKeyBlock(options.top)).hash } const txOpt = { ...this.Ae.defaults, ...options, callData: Array.isArray(args) ? await this.contractEncodeCall(source, name, args, options) : args, nonce: options.top && (await this.getAccount(callerId, { hash: options.top })).nonce + 1 } const tx = name === 'init' ? (await this.contractCreateTx({ ...txOpt, code: options.bytecode, ownerId: callerId })).tx : await this.contractCallTx({ ...txOpt, callerId, contractId: await this.resolveName(address, 'ct', { resolveByNode: true }) }) const { callObj, ...dryRunOther } = await this.txDryRun(tx, callerId, options) await this._handleCallError(source, name, callObj) const { returnType, returnValue } = callObj return { ...dryRunOther, tx: TxObject({ tx }), result: callObj, decode: () => this.contractDecodeData(source, name, returnValue, returnType, options) } } /** * Call contract function * @function * @alias module:@aeternity/aepp-sdk/es/ae/contract * @category async * @param {String} source Contract source code * @param {String} address Contract address or AENS name * @param {String} name Name of function to call * @param {Array|String} argsOrCallData Argument's array or callData for call function * @param {Object} [options={}] Transaction options (fee, ttl, gas, amount, deposit) * @param {Object} [options.filesystem={}] Contract external namespaces map* @return {Promise<Object>} Result object * @example * const callResult = await client.contractCall(source, address, fnName, args = [], options) * { * hash: TX_HASH, * result: TX_DATA, * decode: (type) => Decode call result * } */ async function contractCall (source, address, name, argsOrCallData = [], options = {}) { const opt = R.merge(this.Ae.defaults, options) const tx = await this.contractCallTx(R.merge(opt, { callerId: await this.address(opt), contractId: await this.resolveName(address, 'ct', { resolveByNode: true }), callData: Array.isArray(argsOrCallData) ? await this.contractEncodeCall(source, name, argsOrCallData, opt) : argsOrCallData })) const { hash, rawTx, result, txData } = await this._sendAndProcess(tx, source, name, opt) return { hash, rawTx, result, txData, decode: () => result ? this.contractDecodeData(source, name, result.returnValue, result.returnType, opt) : {} } } /** * Deploy contract to the node * @function * @alias module:@aeternity/aepp-sdk/es/ae/contract * @category async * @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 {Object} [options.filesystem={}] Contract external namespaces map* @return {Promise<Object>} Result object * @return {Promise<Object>} Result object * @example * const deployed = await client.contractDeploy(bytecode, source, init = [], options) * { * owner: OWNER_PUB_KEY, * transaction: TX_HASH, * address: CONTRACT_ADDRESS, * createdAt: Date, * result: DEPLOY_TX_DATA, * call: (fnName, args = [], options) => Call contract function, * callStatic: (fnName, args = [], options) => Static all contract function * } */ async function contractDeploy (code, source, initState = [], options = {}) { const opt = { ...this.Ae.defaults, ...options } const callData = Array.isArray(initState) ? await this.contractEncodeCall(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() }) } /** * Compile contract source code * @function * @alias module:@aeternity/aepp-sdk/es/ae/contract * @category async * @param {String} source Contract sourece code * @param {Object} [options={}] Transaction options (fee, ttl, gas, amount, deposit) * @param {Object} [options.filesystem={}] Contract external namespaces map* @return {Promise<Object>} Result object * @return {Promise<Object>} Result object * @example * const compiled = await client.contractCompile(SOURCE_CODE) * { * bytecode: CONTRACT_BYTE_CODE, * deploy: (init = [], options = {}) => Deploy Contract, * encodeCall: (fnName, args = []) => Prepare callData * } */ async function contractCompile (source, options = {}) { const opt = { ...this.Ae.defaults, ...options } const bytecode = await this.compileContractAPI(source, options) return Object.freeze({ encodeCall: (name, args) => this.contractEncodeCall(source, name, args, opt), deploy: (init, options) => this.contractDeploy(bytecode, source, init, { ...opt, ...options }), deployStatic: (init, options) => this.contractCallStatic(source, null, 'init', init, { ...opt, ...options, bytecode }), bytecode }) } /** * Utility method to create a delegate signature for a contract * @function * @alias module:@aeternity/aepp-sdk/es/ae/contract * @category async * @param {String[]} ids The list of id's to prepend * @param {Object} [opt={}] options * @param {{ onAccount: String | Object }} [opt={}] opt Options * @return {Promise<String>} Signature in hex representation */ async function delegateSignatureCommon (ids = [], opt = {}) { return this.sign( Buffer.concat( [ Buffer.from(this.getNetworkId(opt)), decode(await this.address(opt)), ...ids.map(e => decode(e)) ] ), opt ).then(s => Buffer.from(s).toString('hex')) } /** * Helper to generate a signature to delegate a name pre-claim to a contract. * @function * @alias module:@aeternity/aepp-sdk/es/ae/contract * @category async * @param {String} contractId Contract Id * @param {{ onAccount: String | Object }} [opt={}] opt Options * @return {Promise<String>} Signature for delegation */ async function delegateNamePreclaimSignature (contractId, opt = {}) { return this.delegateSignatureCommon([contractId], opt) } /** * Helper to generate a signature to delegate a name claim to a contract. * @function * @alias module:@aeternity/aepp-sdk/es/ae/contract * @category async * @param {String} name The name being claimed * @param {String} contractId Contract Id * @param {{ onAccount: String | Object }} [opt={}] opt Options * @return {Promise<String>} Signature for delegation */ async function delegateNameClaimSignature (contractId, name, opt = {}) { return this.delegateSignatureCommon([produceNameId(name), contractId], opt) } /** * Helper to generate a signature to delegate a name transfer to a contract. * @function * @alias module:@aeternity/aepp-sdk/es/ae/contract * @category async * @param {String} contractId Contract Id * @param {String} name The name being transferred * @param {{ onAccount: String | Object }} [opt={}] opt Options * @return {Promise<String>} Signature for delegation */ async function delegateNameTransferSignature (contractId, name, opt = {}) { return this.delegateSignatureCommon([produceNameId(name), contractId], opt) } /** * Helper to generate a signature to delegate a name revoke to a contract. * @function * @alias module:@aeternity/aepp-sdk/es/ae/contract * @category async * @param {String} contractId Contract Id * @param {String} name The name being revoked * @param {{ onAccount: String | Object }} [opt={}] opt Options * @return {Promise<String>} Signature for delegation */ async function delegateNameRevokeSignature (contractId, name, opt = {}) { return this.delegateSignatureCommon([produceNameId(name), contractId], opt) } /** * Helper to generate a signature to delegate a Oracle register to a contract. * @function * @alias module:@aeternity/aepp-sdk/es/ae/contract * @category async * @param {String} contractId Contract Id * @param {{ onAccount: String | Object }} [opt={}] opt Options * @return {Promise<String>} Signature for delegation */ async function delegateOracleRegisterSignature (contractId, opt = {}) { return this.delegateSignatureCommon([contractId], opt) } /** * Helper to generate a signature to delegate a Oracle extend to a contract. * @function * @alias module:@aeternity/aepp-sdk/es/ae/contract * @category async * @param {String} contractId Contract Id * @param {{ onAccount: String | Object }} [opt={}] opt Options * @return {Promise<String>} Signature for delegation */ async function delegateOracleExtendSignature (contractId, opt = {}) { return this.delegateSignatureCommon([contractId], opt) } /** * Helper to generate a signature to delegate a Oracle respond to a contract. * @function * @alias module:@aeternity/aepp-sdk/es/ae/contract * @category async * @param {String} queryId Oracle Query Id * @param {String} contractId Contract Id * @param {{ onAccount: String | Object }} [opt={}] opt Options * @return {Promise<String>} Signature for delegation */ async function delegateOracleRespondSignature (queryId, contractId, opt = {}) { return this.delegateSignatureCommon([queryId, contractId], opt) } /** * Contract Stamp * * Provide contract implementation * {@link module:@aeternity/aepp-sdk/es/ae--Ae} clients. * @function * @alias module:@aeternity/aepp-sdk/es/ae/contract * @rtype Stamp * @param {Object} [options={}] - Initializer object * @return {Object} Contract instance * @example * import Transaction from '@aeternity/aepp-sdk/es/tx/tx * import MemoryAccount from '@aeternity/aepp-sdk/es/account/memory * import ChainNode from '@aeternity/aepp-sdk/es/chain/node * import ContractCompilerAPI from '@aeternity/aepp-sdk/es/contract/compiler * // or using bundle * import { * Transaction, * MemoryAccount, * ChainNode, * ContractCompilerAPI * } from '@aeternity/aepp-sdk * * const ContractWithAE = await Contract * .compose(Transaction, MemoryAccount, ChainNode) // AE implementation * .compose(ContractCompilerAPI) // ContractBase implementation * const client = await ContractWithAe({ url, internalUrl, compilerUrl, keypair, ... }) * */ export const ContractAPI = Ae.compose(ContractBase, { methods: { getContractInstance, contractCompile, contractCallStatic, contractDeploy, contractCall, contractEncodeCall, contractDecodeData, _handleCallError, _sendAndProcess, // Delegation for contract // AENS delegateSignatureCommon, delegateNamePreclaimSignature, delegateNameClaimSignature, delegateNameTransferSignature, delegateNameRevokeSignature, // Oracle delegateOracleRegisterSignature, delegateOracleExtendSignature, delegateOracleRespondSignature }, deepProps: { Ae: { defaults: { deposit: DEPOSIT, gasPrice: MIN_GAS_PRICE, amount: AMOUNT, gas: GAS } } } }) export const Contract = ContractAPI.compose(NodePool) export const ContractWithCompiler = Contract.compose(ContractCompilerAPI) export default ContractWithCompiler