@aeternity/aepp-sdk
Version:
SDK for the æternity blockchain
385 lines (378 loc) • 12.5 kB
JavaScript
import { snakeToPascal } from '../utils/string.js';
import { MIN_GAS_PRICE, Tag } from '../tx/builder/constants.js';
import { signAndNotify, awaitingCompletion, channelClosed, channelOpen, handleUnexpectedMessage } from './handlers.js';
import { notify, call, changeState, emit } from './internal.js';
import Channel from './Base.js';
import ChannelSpend from './Spend.js';
import { ChannelError, UnexpectedChannelMessageError } from '../utils/errors.js';
import { unpackTx } from '../tx/builder/index.js';
import { buildContractId } from '../tx/builder/helpers.js';
function snakeToPascalObjKeys(obj) {
return Object.entries(obj).reduce((result, [key, val]) => ({
...result,
[snakeToPascal(key)]: val
}), {});
}
/**
* @category state channel
*/
export default class ChannelContract extends ChannelSpend {
static async initialize(options) {
return Channel._initialize(new ChannelContract(), options);
}
/**
* Trigger create contract update
*
* The create contract update is creating a contract inside the channel's internal state tree.
* The update is a change to be applied on top of the latest state.
*
* That would create a contract with the poster being the owner of it. Poster commits initially
* a deposit amount of coins to the new contract.
*
* @param options - Options
* @param options.code - Api encoded compiled AEVM byte code
* @param options.callData - Api encoded compiled AEVM call data for the code
* @param options.deposit - Initial amount the owner of the contract commits to it
* @param options.vmVersion - Version of the Virtual Machine
* @param options.abiVersion - Version of the Application Binary Interface
* @param sign - Function which verifies and signs create contract transaction
* @example
* ```js
* channel.createContract({
* code: 'cb_HKtpipK4aCgYb17wZ...',
* callData: 'cb_1111111111111111...',
* deposit: 10,
* vmVersion: 3,
* abiVersion: 1
* }).then(({ accepted, signedTx, address }) => {
* if (accepted) {
* console.log('New contract has been created')
* console.log('Contract address:', address)
* } else {
* console.log('New contract has been rejected')
* }
* })
* ```
*/
async createContract({
code,
callData,
deposit,
vmVersion,
abiVersion
}, sign) {
return this.enqueueAction(() => {
notify(this, 'channels.update.new_contract', {
code,
call_data: callData,
deposit,
vm_version: vmVersion,
abi_version: abiVersion
});
return {
handler: async (_, message, state) => {
if (message.method !== 'channels.sign.update') {
return handleUnexpectedMessage(this, message, state);
}
await signAndNotify(this, 'channels.update', message.params.data, async tx => state.sign(tx));
return {
handler: (_2, message2, state2) => awaitingCompletion(this, message2, state2, () => {
const params = unpackTx(message2.params.data.state, Tag.SignedTx).encodedTx;
if (params.tag !== Tag.ChannelOffChainTx) {
throw new ChannelError(`Tag should be ${Tag[Tag.ChannelOffChainTx]}, got ${Tag[params.tag]} instead`);
}
const addressKey = this._options.role === 'initiator' ? 'initiatorId' : 'responderId';
const owner = this._options[addressKey];
changeState(this, message2.params.data.state);
const address = buildContractId(owner, params.round);
emit(this, 'newContract', address);
state2.resolve({
accepted: true,
address,
signedTx: message2.params.data.state
});
return {
handler: channelOpen
};
}),
state
};
},
state: {
sign
}
};
});
}
/**
* Trigger call a contract update
*
* The call contract update is calling a preexisting contract inside the channel's
* internal state tree. The update is a change to be applied on top of the latest state.
*
* That would call a contract with the poster being the caller of it. Poster commits
* an amount of coins to the contract.
*
* The call would also create a call object inside the channel state tree. It contains
* the result of the contract call.
*
* It is worth mentioning that the gas is not consumed, because this is an off-chain
* contract call. It would be consumed if it were an on-chain one. This could happen
* if a call with a similar computation amount is to be forced on-chain.
*
* @param options - Options
* @param sign - Function which verifies and signs contract call transaction
* @example
* ```js
* channel.callContract({
* contract: 'ct_9sRA9AVE4BYTAkh5RNfJYmwQe1NZ4MErasQLXZkFWG43TPBqa',
* callData: 'cb_1111111111111111...',
* amount: 0,
* abiVersion: 1
* }).then(({ accepted, signedTx }) => {
* if (accepted) {
* console.log('Contract called succesfully')
* } else {
* console.log('Contract call has been rejected')
* }
* })
* ```
*/
async callContract({
amount,
callData,
contract,
abiVersion
}, sign) {
return this.enqueueAction(() => {
notify(this, 'channels.update.call_contract', {
amount,
call_data: callData,
contract_id: contract,
abi_version: abiVersion
});
return {
handler: async (_, message, state) => {
if (message.method !== 'channels.sign.update') {
return handleUnexpectedMessage(this, message, state);
}
await signAndNotify(this, 'channels.update', message.params.data, async tx => state.sign(tx, {
updates: message.params.data.updates
}));
return {
handler: (_2, message2, state2) => awaitingCompletion(this, message2, state2, () => {
changeState(this, message2.params.data.state);
state2.resolve({
accepted: true,
signedTx: message2.params.data.state
});
return {
handler: channelOpen
};
}),
state
};
},
state: {
sign
}
};
});
}
/**
* Trigger a force progress contract call
* This call is going on-chain
* @param options - Options
* @param sign - Function which verifies and signs contract force progress transaction
* @param callbacks - Callbacks
* @example
* ```js
* channel.forceProgress({
* contract: 'ct_9sRA9AVE4BYTAkh5RNfJYmwQe1NZ4MErasQLXZkFWG43TPBqa',
* callData: 'cb_1111111111111111...',
* amount: 0,
* abiVersion: 1,
* gasPrice: 1000005554
* }).then(({ accepted, signedTx }) => {
* if (accepted) {
* console.log('Contract force progress call successful')
* } else {
* console.log('Contract force progress call has been rejected')
* }
* })
* ```
*/
async forceProgress({
amount,
callData,
contract,
abiVersion,
gasLimit = 1000000,
gasPrice = MIN_GAS_PRICE
}, sign, {
onOnChainTx
} = {}) {
return this.enqueueAction(() => {
notify(this, 'channels.force_progress', {
amount,
call_data: callData,
contract_id: contract,
abi_version: abiVersion,
gas_price: gasPrice,
gas: gasLimit
});
return {
handler: async (_, message, state) => {
if (message.method !== 'channels.sign.force_progress_tx') {
return handleUnexpectedMessage(this, message, state);
}
await signAndNotify(this, 'channels.force_progress_sign', message.params.data, async tx => state.sign(tx, {
updates: message.params.data.updates
}));
return {
handler: (_2, message2, state2) => {
if (message2.method === 'channels.on_chain_tx') {
state2.onOnChainTx?.(message2.params.data.tx);
emit(this, 'onChainTx', message2.params.data.tx, {
info: message2.params.data.info,
type: message2.params.data.type
});
state2.resolve({
accepted: true,
tx: message2.params.data.tx
});
// TODO: shouldn't be unexpected message in this case
}
return handleUnexpectedMessage(this, message2, state2);
},
state
};
},
state: {
sign,
onOnChainTx
}
};
});
}
/**
* Call contract using dry-run
*
* In order to get the result of a potential contract call, one might need to
* dry-run a contract call. It takes the exact same arguments as a call would
* and returns the call object.
*
* The call is executed in the channel's state, but it does not impact the state
* whatsoever. It uses as an environment the latest channel's state and the current
* top of the blockchain as seen by the node.
*
* @param options - Options
* @example
* ```js
* channel.callContractStatic({
* contract: 'ct_9sRA9AVE4BYTAkh5RNfJYmwQe1NZ4MErasQLXZkFWG43TPBqa',
* callData: 'cb_1111111111111111...',
* amount: 0,
* abiVersion: 1
* }).then(({ returnValue, gasUsed }) => {
* console.log('Returned value:', returnValue)
* console.log('Gas used:', gasUsed)
* })
* ```
*/
async callContractStatic({
amount,
callData,
contract,
abiVersion
}) {
return snakeToPascalObjKeys(await call(this, 'channels.dry_run.call_contract', {
amount,
call_data: callData,
contract_id: contract,
abi_version: abiVersion
}));
}
/**
* Get contract call result
*
* The combination of a caller, contract and a round of execution determines the
* contract call. Providing an incorrect set of those results in an error response.
*
* @param options - Options
* @param options.caller - Address of contract caller
* @param options.contract - Address of the contract
* @param options.round - Round when contract was called
* @example
* ```js
* channel.getContractCall({
* caller: 'ak_Y1NRjHuoc3CGMYMvCmdHSBpJsMDR6Ra2t5zjhRcbtMeXXLpLH',
* contract: 'ct_9sRA9AVE4BYTAkh5RNfJYmwQe1NZ4MErasQLXZkFWG43TPBqa',
* round: 3
* }).then(({ returnType, returnValue }) => {
* if (returnType === 'ok') console.log(returnValue)
* })
* ```
*/
async getContractCall({
caller,
contract,
round
}) {
return snakeToPascalObjKeys(await call(this, 'channels.get.contract_call', {
caller_id: caller,
contract_id: contract,
round
}));
}
/**
* Get the latest contract state
*
* @param contract - Address of the contract
* @example
* ```js
* channel.getContractState(
* 'ct_9sRA9AVE4BYTAkh5RNfJYmwQe1NZ4MErasQLXZkFWG43TPBqa'
* ).then(({ contract }) => {
* console.log('deposit:', contract.deposit)
* })
* ```
*/
async getContractState(contract) {
const result = await call(this, 'channels.get.contract', {
pubkey: contract
});
return snakeToPascalObjKeys({
...result,
contract: snakeToPascalObjKeys(result.contract)
});
}
/**
* Clean up all locally stored contract calls
*
* Contract calls are kept locally in order for the participant to be able to look them up.
* They consume memory and in order for the participant to free it - one can prune all messages.
* This cleans up all locally stored contract calls and those will no longer be available for
* fetching and inspection.
*/
async cleanContractCalls() {
return this.enqueueAction(() => {
notify(this, 'channels.clean_contract_calls');
return {
handler(_, message, state) {
if (message.method === 'channels.calls_pruned.reply') {
state.resolve();
return {
handler: channelOpen
};
}
state.reject(new UnexpectedChannelMessageError('Unexpected message received'));
return {
handler: channelClosed
};
}
};
});
}
}
//# sourceMappingURL=Contract.js.map