UNPKG

@aeternity/aepp-sdk

Version:

SDK for the æternity blockchain

535 lines (525 loc) 19.6 kB
var _Contract; function _classPrivateFieldInitSpec(e, t, a) { _checkPrivateRedeclaration(e, t), t.set(e, a); } function _classPrivateMethodInitSpec(e, a) { _checkPrivateRedeclaration(e, a), a.add(e); } function _checkPrivateRedeclaration(e, t) { if (t.has(e)) throw new TypeError("Cannot initialize the same private elements twice on an object"); } function _classPrivateFieldSet(s, a, r) { return s.set(_assertClassBrand(s, a), r), r; } function _classPrivateFieldGet(s, a) { return s.get(_assertClassBrand(s, a)); } function _assertClassBrand(e, t, n) { if ("function" == typeof e ? e === t : e.has(t)) return arguments.length < 3 ? t : n; throw new TypeError("Private element is not present on this object"); } /** * 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 */ import { Encoder as Calldata } from '@aeternity/aepp-calldata'; import { Tag, DRY_RUN_ACCOUNT } from '../tx/builder/constants.js'; import { buildContractIdByContractTx, unpackTx, buildTxAsync, buildTxHash } from '../tx/builder/index.js'; import { decode } from '../utils/encoder.js'; import { MissingContractDefError, MissingContractAddressError, InactiveContractError, BytecodeMismatchError, DuplicateContractError, MissingFunctionNameError, InvalidMethodInvocationError, NotPayableFunctionError, TypeError as _TypeError, NodeInvocationError, IllegalArgumentError, NoSuchContractFunctionError, MissingEventDefinitionError, AmbiguousEventDefinitionError, UnexpectedTsError, InternalError, NoWalletConnectedError, ContractError } from '../utils/errors.js'; import { hash as calcHash } from '../utils/crypto.js'; import { getAccount, resolveName, txDryRun } from '../chain.js'; import { sendTransaction } from '../send-transaction.js'; import { isAccountNotFoundError } from '../utils/other.js'; import { isName, produceNameId } from '../tx/builder/helpers.js'; /** * @category contract */ /** * @category contract */ var _Contract_brand = /*#__PURE__*/new WeakSet(); var _aciContract = /*#__PURE__*/new WeakMap(); /** * Generate contract ACI object with predefined js methods for contract usage - can be used for * creating a reference to already deployed contracts * @category contract * @param options - Options object * @returns JS Contract API * @example * ```js * const contractIns = await Contract.initialize({ ...aeSdk.getContext(), sourceCode }) * await contractIns.$deploy([321]) or await contractIns.init(321) * const callResult = await contractIns.$call('setState', [123]) * const staticCallResult = await contractIns.$call('setState', [123], { callStatic: true }) * ``` * Also you can call contract like: `await contractIns.setState(123, options)` * Then sdk decide to make on-chain or static call (dry-run API) transaction based on function is * stateful or not */ class Contract { /** * Compile contract * @returns bytecode */ async $compile() { if (this.$options.bytecode != null) return this.$options.bytecode; if (this.$options.onCompiler == null) throw new IllegalArgumentError("Can't compile without compiler"); if (this.$options.sourceCode != null) { const { bytecode } = await this.$options.onCompiler.compileBySourceCode(this.$options.sourceCode, this.$options.fileSystem); this.$options.bytecode = bytecode; } if (this.$options.sourceCodePath != null) { const { bytecode } = await this.$options.onCompiler.compile(this.$options.sourceCodePath); this.$options.bytecode = bytecode; } if (this.$options.bytecode == null) { throw new IllegalArgumentError("Can't compile without sourceCode and sourceCodePath"); } return this.$options.bytecode; } async $getCallResultByTxHash(hash, fnName, options) { const { callInfo } = await this.$options.onNode.getTransactionInfoByHash(hash); if (callInfo == null) { throw new ContractError(`callInfo is not available for transaction ${hash}`); } const callInfoTyped = callInfo; return { ..._assertClassBrand(_Contract_brand, this, _getCallResult).call(this, callInfoTyped, fnName, undefined, options), result: callInfoTyped }; } async _estimateGas(name, params, options = {}) { const { result } = await this.$call(name, params, { ...options, callStatic: true }); if (result == null) throw new UnexpectedTsError(); const { gasUsed } = result; // taken from https://github.com/aeternity/aepp-sdk-js/issues/1286#issuecomment-977814771 return Math.floor(gasUsed * 1.25); } /** * Deploy contract * @param params - Contract init function arguments array * @param options - Options * @returns deploy info */ async $deploy(params, options) { var _opt$gasLimit; const { callStatic, ...opt } = { ...this.$options, ...options }; if (this.$options.bytecode == null) await this.$compile(); if (callStatic === true) return this.$call('init', params, { ...opt, callStatic }); if (this.$options.address != null) throw new DuplicateContractError(); if (opt.onAccount == null) throw new IllegalArgumentError("Can't deploy without account"); const ownerId = opt.onAccount.address; if (this.$options.bytecode == null) throw new IllegalArgumentError("Can't deploy without bytecode"); const tx = await buildTxAsync({ _isInternalBuild: true, ...opt, tag: Tag.ContractCreateTx, gasLimit: (_opt$gasLimit = opt.gasLimit) !== null && _opt$gasLimit !== void 0 ? _opt$gasLimit : await this._estimateGas('init', params, opt), callData: this._calldata.encode(this._name, 'init', params), code: this.$options.bytecode, ownerId }); const { hash, ...other } = await _assertClassBrand(_Contract_brand, this, _sendAndProcess).call(this, tx, 'init', { ...opt, onAccount: opt.onAccount }); this.$options.address = buildContractIdByContractTx(other.rawTx); return { ...other, ...(other.result?.log != null && { decodedEvents: this.$decodeEvents(other.result.log, opt) }), owner: ownerId, transaction: hash, address: this.$options.address }; } /** * Get function schema from contract ACI object * @param name - Function name * @returns function ACI */ /** * Call contract function * @param fn - Function name * @param params - Array of function arguments * @param options - Array of function arguments * @returns CallResult */ async $call(fn, params, options = {}) { var _opt$gasLimit2; const { callStatic, top, ...opt } = { ...this.$options, ...options }; const fnAci = _assertClassBrand(_Contract_brand, this, _getFunctionAci).call(this, fn); const { address, name } = this.$options; // TODO: call `produceNameId` on buildTx side const contractId = name != null ? produceNameId(name) : address; const { onNode } = opt; if (fn == null) throw new MissingFunctionNameError(); if (fn === 'init' && callStatic !== true) throw new InvalidMethodInvocationError('"init" can be called only via dryRun'); if (fn !== 'init' && opt.amount != null && Number(opt.amount) > 0 && !fnAci.payable) { throw new NotPayableFunctionError(opt.amount, fn); } let callerId; try { if (opt.onAccount == null) throw new InternalError('Use fallback account'); callerId = opt.onAccount.address; } catch (error) { const useFallbackAccount = callStatic === true && (error instanceof _TypeError && error.message === 'Account should be an address (ak-prefixed string), or instance of AccountBase, got undefined instead' || error instanceof NoWalletConnectedError || error instanceof InternalError && error.message === 'Use fallback account'); if (!useFallbackAccount) throw error; callerId = DRY_RUN_ACCOUNT.pub; } const callData = this._calldata.encode(this._name, fn, params); if (callStatic === true) { if (opt.nonce == null) { const topOption = top != null && { [typeof top === 'number' ? 'height' : 'hash']: top }; const account = await getAccount(callerId, { ...topOption, onNode }).catch(error => { if (!isAccountNotFoundError(error)) throw error; return { kind: 'basic', nonce: 0 }; }); opt.nonce = account.kind === 'generalized' ? 0 : account.nonce + 1; } const txOpt = { ...opt, onNode, callData }; let tx; if (fn === 'init') { if (this.$options.bytecode == null) throw new IllegalArgumentError('Can\'t dry-run "init" without bytecode'); tx = await buildTxAsync({ ...txOpt, tag: Tag.ContractCreateTx, code: this.$options.bytecode, ownerId: callerId }); } else { if (contractId == null) throw new MissingContractAddressError("Can't dry-run contract without address"); tx = await buildTxAsync({ ...txOpt, tag: Tag.ContractCallTx, callerId, contractId }); } const { callObj, ...dryRunOther } = await txDryRun(tx, callerId, { ...opt, top }); if (callObj == null) { throw new InternalError(`callObj is not available for transaction ${tx}`); } const callInfoTyped = callObj; return { ...dryRunOther, ..._assertClassBrand(_Contract_brand, this, _getCallResult).call(this, callInfoTyped, fn, tx, opt), tx: unpackTx(tx), result: callInfoTyped, rawTx: tx, hash: buildTxHash(tx), txData: undefined }; } if (top != null) throw new IllegalArgumentError("Can't handle `top` option in on-chain contract call"); if (contractId == null) throw new MissingContractAddressError("Can't call contract without address"); const tx = await buildTxAsync({ _isInternalBuild: true, ...opt, tag: Tag.ContractCallTx, gasLimit: (_opt$gasLimit2 = opt.gasLimit) !== null && _opt$gasLimit2 !== void 0 ? _opt$gasLimit2 : await this._estimateGas(fn, params, opt), callerId, contractId, callData }); if (opt.onAccount == null) throw new IllegalArgumentError("Can't call contract on chain without account"); return _assertClassBrand(_Contract_brand, this, _sendAndProcess).call(this, tx, fn, { ...opt, onAccount: opt.onAccount }); } /** * @param ctAddress - Contract address that emitted event * @param nameHash - Hash of emitted event name * @param options - Options * @returns Contract name * @throws {@link MissingEventDefinitionError} * @throws {@link AmbiguousEventDefinitionError} */ /** * Decode Events * @param events - Array of encoded events (callRes.result.log) * @param options - Options * @returns DecodedEvents */ $decodeEvents(events, { omitUnknown, ...opt } = {}) { return events.map(event => { let contractName; try { contractName = _assertClassBrand(_Contract_brand, this, _getContractNameByEvent).call(this, event.address, event.topics[0], opt); } catch (error) { if ((omitUnknown !== null && omitUnknown !== void 0 ? omitUnknown : false) && error instanceof MissingEventDefinitionError) return null; throw error; } const decoded = this._calldata.decodeEvent(contractName, event.data, event.topics); const [name, args] = Object.entries(decoded)[0]; return { name, args, contract: { name: contractName, address: event.address } }; }).filter(e => e != null); } static async initialize({ onCompiler, onNode, bytecode, aci, address, sourceCodePath, sourceCode, fileSystem, validateBytecode, ...otherOptions }) { if (aci == null && onCompiler != null) { let res; if (sourceCodePath != null) res = await onCompiler.compile(sourceCodePath); if (sourceCode != null) res = await onCompiler.compileBySourceCode(sourceCode, fileSystem); if (res != null) { aci = res.aci; bytecode !== null && bytecode !== void 0 ? bytecode : bytecode = res.bytecode; } } if (aci == null) throw new MissingContractDefError(); let name; if (address != null) { address = await resolveName(address, 'contract_pubkey', { resolveByNode: true, onNode }); if (isName(address)) name = address; } if (address == null && sourceCode == null && sourceCodePath == null && bytecode == null) { throw new MissingContractAddressError("Can't create instance by ACI without address"); } if (address != null) { const contract = await onNode.getContract(address); if (contract.active == null) throw new InactiveContractError(address); } if (validateBytecode === true) { if (address == null) throw new MissingContractAddressError("Can't validate bytecode without contract address"); const onChanBytecode = (await onNode.getContractCode(address)).bytecode; let isValid = false; if (bytecode != null) isValid = bytecode === onChanBytecode;else if (sourceCode != null) { if (onCompiler == null) throw new IllegalArgumentError("Can't validate bytecode without compiler"); isValid = await onCompiler.validateBySourceCode(onChanBytecode, sourceCode, fileSystem); } else if (sourceCodePath != null) { if (onCompiler == null) throw new IllegalArgumentError("Can't validate bytecode without compiler"); isValid = await onCompiler.validate(onChanBytecode, sourceCodePath); } if (!isValid) { throw new BytecodeMismatchError((sourceCode !== null && sourceCode !== void 0 ? sourceCode : sourceCodePath) != null ? 'source code' : 'bytecode'); } } return new ContractWithMethods({ onCompiler, onNode, sourceCode, sourceCodePath, bytecode, aci, address, name, fileSystem, ...otherOptions }); } /** * @param options - Options */ constructor({ aci, ...otherOptions }) { _classPrivateMethodInitSpec(this, _Contract_brand); _classPrivateFieldInitSpec(this, _aciContract, void 0); this._aci = aci; const aciLast = aci[aci.length - 1]; if (aciLast.contract == null) { throw new IllegalArgumentError(`The last 'aci' item should have 'contract' key, got ${Object.keys(aciLast)} keys instead`); } _classPrivateFieldSet(_aciContract, this, aciLast.contract); this._name = _classPrivateFieldGet(_aciContract, this).name; this._calldata = new Calldata(aci); this.$options = otherOptions; /** * Generate proto function based on contract function using Contract ACI schema * All function can be called like: * ```js * await contract.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: * ```js * await contract.testFunction({ callStatic: true }) // use call-static (dry-run) * await contract.testFunction({ callStatic: false }) // send tx on-chain * ``` */ Object.assign(this, Object.fromEntries(_classPrivateFieldGet(_aciContract, this).functions.map(({ name, arguments: aciArgs, stateful }) => { const callStatic = name !== 'init' && !stateful; return [name, async (...args) => { const options = args.length === aciArgs.length + 1 ? args.pop() : {}; if (typeof options !== 'object') throw new _TypeError(`Options should be an object: ${options}`); if (name === 'init') return this.$deploy(args, { callStatic, ...options }); return this.$call(name, args, { callStatic, ...options }); }]; }))); } } _Contract = Contract; function _getCallResult({ returnType, returnValue, log }, fnName, transaction, options) { let message; switch (returnType) { case 'ok': { const fnAci = _assertClassBrand(_Contract_brand, this, _getFunctionAci).call(this, fnName); return { decodedResult: this._calldata.decode(this._name, fnAci.name, returnValue), decodedEvents: this.$decodeEvents(log, options) }; } case 'revert': message = this._calldata.decodeFateString(returnValue); break; case 'error': message = decode(returnValue).toString(); if (/Expected \d+ arguments, got \d+/.test(message)) { throw new BytecodeMismatchError('ACI', `. Error provided by node: "${message}".`); } if (/Trying to call undefined function: <<\d+,\d+,\d+,\d+>>/.test(message)) { throw new BytecodeMismatchError('ACI', `. Error provided by node: "${message}", function name: ${fnName}.`); } break; default: throw new InternalError(`Unknown return type: ${returnType}`); } throw new NodeInvocationError(message, transaction); } async function _sendAndProcess(tx, fnName, options) { const txData = await sendTransaction(tx, { ...this.$options, ...options }); return { hash: txData.hash, tx: unpackTx(txData.rawTx), txData, rawTx: txData.rawTx, // TODO: disallow `waitMined: false` to make `decodedResult` required ...(txData.blockHeight != null && (await this.$getCallResultByTxHash(txData.hash, fnName, options))) }; } function _getFunctionAci(name) { const fn = _classPrivateFieldGet(_aciContract, this).functions.find(f => f.name === name); if (fn != null) { return fn; } if (name === 'init') { return { arguments: [], name: 'init', payable: false, returns: 'unit', stateful: true }; } throw new NoSuchContractFunctionError(name); } function _getContractNameByEvent(ctAddress, nameHash, { contractAddressToName }) { const addressToName = { ...this.$options.contractAddressToName, ...contractAddressToName }; if (addressToName[ctAddress] != null) return addressToName[ctAddress]; // TODO: consider using a third-party library const isEqual = (a, b) => JSON.stringify(a) === JSON.stringify(b); const contracts = this._aci.map(({ contract }) => contract).filter(contract => contract?.event); const matchedEvents = contracts.map(contract => [contract.name, contract.event.variant]).map(([name, events]) => events.map(event => [name, Object.keys(event)[0], Object.values(event)[0]])).flat().filter(([, eventName]) => BigInt(`0x${calcHash(eventName).toString('hex')}`) === nameHash).filter(([,, type], idx, arr) => !arr.slice(0, idx).some(el => isEqual(el[2], type))); switch (matchedEvents.length) { case 0: throw new MissingEventDefinitionError(nameHash.toString(), ctAddress); case 1: return matchedEvents[0][0]; default: throw new AmbiguousEventDefinitionError(ctAddress, matchedEvents); } } /** * @category contract */ // eslint-disable-next-line @typescript-eslint/no-redeclare const ContractWithMethods = Contract; export default ContractWithMethods; //# sourceMappingURL=Contract.js.map