@aeternity/aepp-sdk
Version:
SDK for the æternity blockchain
463 lines (436 loc) • 16.7 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.
*/
/**
* 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