UNPKG

@aeternity/aepp-sdk

Version:

SDK for the æternity blockchain

357 lines (352 loc) 12.1 kB
import { notify, call, changeState } from './internal.js'; import { pause } from '../utils/other.js'; import Channel from './Base.js'; import { ChannelConnectionError } from '../utils/errors.js'; import { awaitingCompletion, channelOpen, handleUnexpectedMessage, signAndNotify } from './handlers.js'; import { unpackEntry } from '../tx/builder/entry/index.js'; export default class ChannelSpend extends Channel { /** * Trigger a transfer update * * The transfer update is moving coins from one channel account to another. * The update is a change to be applied on top of the latest state. * * Sender and receiver are the channel parties. Both the initiator and responder * can take those roles. Any public key outside the channel is considered invalid. * * @param from - Sender's public address * @param to - Receiver's public address * @param amount - Transaction amount * @param sign - Function which verifies and signs offchain transaction * @param metadata - Metadata * @example * ```js * channel.update( * 'ak_Y1NRjHuoc3CGMYMvCmdHSBpJsMDR6Ra2t5zjhRcbtMeXXLpLH', * 'ak_V6an1xhec1xVaAhLuak7QoEbi6t7w5hEtYWp9bMKaJ19i6A9E', * 10, * async (tx) => await account.signTransaction(tx) * ).then(({ accepted, signedTx }) => * if (accepted) { * console.log('Update has been accepted') * } * ) * ``` */ async update(from, to, amount, sign, metadata = []) { return this.enqueueAction(() => { notify(this, 'channels.update.new', { from, to, amount, meta: metadata }); const awaitingOffChainTx = async (_, message, state) => { if (message.method === 'channels.sign.update') { const isError = await signAndNotify(this, 'channels.update', message.params.data, async tx => sign(tx, { updates: message.params.data.updates })); if (isError) return { handler: awaitingOffChainTx, state }; return { handler: (_2, message2) => awaitingCompletion(this, message2, state, () => { changeState(this, message2.params.data.state); state.resolve({ accepted: true, signedTx: message2.params.data.state }); return { handler: channelOpen }; }), state }; } if (message.method === 'channels.error') { state.reject(new ChannelConnectionError(message.data.message)); return { handler: channelOpen }; } return awaitingCompletion(this, message, state); }; return { handler: awaitingOffChainTx }; }); } /** * Get proof of inclusion * * If a certain address of an account or a contract is not found * in the state tree - the response is an error. * * @param addresses - Addresses * @param addresses.accounts - List of account addresses to include in poi * @param addresses.contracts - List of contract addresses to include in poi * @example * ```js * channel.poi({ * accounts: [ * 'ak_Y1NRjHuoc3CGMYMvCmdHSBpJsMDR6Ra2t5zjhRcbtMeXXLpLH', * 'ak_V6an1xhec1xVaAhLuak7QoEbi6t7w5hEtYWp9bMKaJ19i6A9E' * ], * contracts: ['ct_2dCUAWYZdrWfACz3a2faJeKVTVrfDYxCQHCqAt5zM15f3u2UfA'] * }).then(poi => console.log(poi)) * ``` */ async poi({ accounts, contracts }) { const { poi } = await call(this, 'channels.get.poi', { accounts, contracts }); return unpackEntry(poi); } /** * Get balances * * The accounts param contains a list of addresses to fetch balances of. * Those can be either account balances or a contract ones, encoded as an account addresses. * * If a certain account address had not being found in the state tree - it is simply * skipped in the response. * * @param accounts - List of addresses to fetch balances from * @example * ```js * channel.balances([ * 'ak_Y1NRjHuoc3CGMYMvCmdHSBpJsMDR6Ra2t5zjhRcbtMeXXLpLH', * 'ak_V6an1xhec1xVaAhLuak7QoEbi6t7w5hEtYWp9bMKaJ19i6A9E' * 'ct_2dCUAWYZdrWfACz3a2faJeKVTVrfDYxCQHCqAt5zM15f3u2UfA' * ]).then(balances => * console.log(balances['ak_Y1NRjHuoc3CGMYMvCmdHSBpJsMDR6Ra2t5zjhRcbtMeXXLpLH']) * ) * ``` */ async balances(accounts) { return Object.fromEntries((await call(this, 'channels.get.balances', { accounts })).map(item => [item.account, item.balance])); } async awaitingActionTx(action, message, state) { if (message.method !== `channels.sign.${action}_tx`) { return handleUnexpectedMessage(this, message, state); } const awaitingActionCompletion = (_, message2) => { if (message2.method === 'channels.on_chain_tx') { state.onOnChainTx?.(message2.params.data.tx); return { handler: awaitingActionCompletion, state }; } if (message2.method === 'channels.info' && [`own_${action}_locked`, `${action}_locked`].includes(message2.params.data.event)) { const Action = action === 'deposit' ? 'Deposit' : 'Withdraw'; const isOwn = message2.params.data.event.startsWith('own_'); state[`on${isOwn ? 'Own' : ''}${Action}Locked`]?.(); return { handler: awaitingActionCompletion, state }; } return awaitingCompletion(this, message2, state, () => { changeState(this, message2.params.data.state); state.resolve({ accepted: true, signedTx: message2.params.data.state }); return { handler: channelOpen }; }); }; const { sign } = state; await signAndNotify(this, `channels.${action}_tx`, message.params.data, async tx => sign(tx, { updates: message.params.data.updates })); return { handler: awaitingActionCompletion, state }; } /** * Withdraw coins from the channel * * After the channel had been opened any of the participants can initiate a withdrawal. * The process closely resembles the update. The most notable difference is that the * transaction has been co-signed: it is channel_withdraw_tx and after the procedure * is finished - it is being posted on-chain. * * Any of the participants can initiate a withdrawal. The only requirements are: * * - Channel is already opened * - No off-chain update/deposit/withdrawal is currently being performed * - Channel is not being closed or in a solo closing state * - The withdrawal amount must be equal to or greater than zero, and cannot exceed * the available balance on the channel (minus the channel_reserve) * * After the other party had signed the withdraw transaction, the transaction is posted * on-chain and onOnChainTx callback is called with on-chain transaction as first argument. * After computing transaction hash it can be tracked on the chain: entering the mempool, * block inclusion and a number of confirmations. * * After the minimum_depth block confirmations onOwnWithdrawLocked callback is called * (without any arguments). * * When the other party had confirmed that the block height needed is reached * onWithdrawLocked callback is called (without any arguments). * * @param amount - Amount of coins to withdraw * @param sign - Function which verifies and signs withdraw transaction * @param callbacks - Callbacks * @example * ```js * channel.withdraw( * 100, * async (tx) => await account.signTransaction(tx), * { onOnChainTx: (tx) => console.log('on_chain_tx', tx) } * ).then(({ accepted, signedTx }) => { * if (accepted) { * console.log('Withdrawal has been accepted') * } else { * console.log('Withdrawal has been rejected') * } * }) * ``` */ async withdraw(amount, sign, { onOnChainTx, onOwnWithdrawLocked, onWithdrawLocked } = {}) { return this.enqueueAction(() => { notify(this, 'channels.withdraw', { amount }); return { handler: async (_, message, state) => this.awaitingActionTx('withdraw', message, state), state: { sign, onOnChainTx, onOwnWithdrawLocked, onWithdrawLocked } }; }); } /** * Deposit coins into the channel * * After the channel had been opened any of the participants can initiate a deposit. * The process closely resembles the update. The most notable difference is that the * transaction has been co-signed: it is channel_deposit_tx and after the procedure * is finished - it is being posted on-chain. * * Any of the participants can initiate a deposit. The only requirements are: * * - Channel is already opened * - No off-chain update/deposit/withdrawal is currently being performed * - Channel is not being closed or in a solo closing state * - The deposit amount must be equal to or greater than zero, and cannot exceed * the available balance on the channel (minus the channel_reserve) * * After the other party had signed the deposit transaction, the transaction is posted * on-chain and onOnChainTx callback is called with on-chain transaction as first argument. * After computing transaction hash it can be tracked on the chain: entering the mempool, * block inclusion and a number of confirmations. * * After the minimum_depth block confirmations onOwnDepositLocked callback is called * (without any arguments). * * When the other party had confirmed that the block height needed is reached * onDepositLocked callback is called (without any arguments). * * @param amount - Amount of coins to deposit * @param sign - Function which verifies and signs deposit transaction * @param callbacks - Callbacks * @example * ```js * channel.deposit( * 100, * async (tx) => await account.signTransaction(tx), * { onOnChainTx: (tx) => console.log('on_chain_tx', tx) } * ).then(({ accepted, state }) => { * if (accepted) { * console.log('Deposit has been accepted') * console.log('The new state is:', state) * } else { * console.log('Deposit has been rejected') * } * }) * ``` */ async deposit(amount, sign, { onOnChainTx, onOwnDepositLocked, onDepositLocked } = {}) { return this.enqueueAction(() => { notify(this, 'channels.deposit', { amount }); return { handler: async (_, message, state) => this.awaitingActionTx('deposit', message, state), state: { sign, onOnChainTx, onOwnDepositLocked, onDepositLocked } }; }); } /** * Send generic message * * If message is an object it will be serialized into JSON string * before sending. * * If there is ongoing update that has not yet been finished the message * will be sent after that update is finalized. * * @param message - Message * @param recipient - Address of the recipient * @example * ```js * channel.sendMessage( * 'hello world', * 'ak_Y1NRjHuoc3CGMYMvCmdHSBpJsMDR6Ra2t5zjhRcbtMeXXLpLH' * ) * ``` */ async sendMessage(message, recipient) { const info = typeof message === 'object' ? JSON.stringify(message) : message; if (this.status() === 'connecting') { await new Promise(resolve => { const onStatusChanged = status => { if (status === 'connecting') return; resolve(); this.off('statusChanged', onStatusChanged); }; this.on('statusChanged', onStatusChanged); }); // For some reason we can't immediately send a message when connection is // established. Thus we wait 500ms which seems to work. await pause(500); } notify(this, 'channels.message', { info, to: recipient }); } } //# sourceMappingURL=Spend.js.map