@aeternity/aepp-sdk
Version:
SDK for the æternity blockchain
535 lines (525 loc) • 19.6 kB
JavaScript
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