@aeternity/aepp-sdk
Version:
SDK for the æternity blockchain
665 lines (633 loc) • 24.6 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.
*/
import { generateKeyPair, encodeContractAddress, encodeBase64Check } from '../utils/crypto'
import {
options,
changeStatus,
changeState,
call,
send,
emit,
channelId,
disconnect,
fsmId
} from './internal'
import { unpackTx, buildTx } from '../tx/builder'
function encodeRlpTx (rlpBinary) {
return `tx_${encodeBase64Check(rlpBinary)}`
}
async function appendSignature (tx, signFn) {
const { signatures, encodedTx } = unpackTx(tx).tx
const result = await signFn(encodeRlpTx(encodedTx.rlpEncoded))
if (typeof result === 'string') {
const { tx: signedTx, txType } = unpackTx(result)
return encodeRlpTx(buildTx({
signatures: signatures.concat(signedTx.signatures),
encodedTx: signedTx.encodedTx.rlpEncoded
}, txType).rlpEncoded)
}
return result
}
function handleUnexpectedMessage (channel, message, state) {
if (state && state.reject) {
state.reject(Object.assign(
new Error(`Unexpected message received:\n\n${JSON.stringify(message)}`),
{ wsMessage: message }
))
}
return { handler: channelOpen }
}
export function awaitingConnection (channel, message, state) {
if (message.method === 'channels.info') {
if (['channel_accept', 'funding_created'].includes(message.params.data.event)) {
changeStatus(channel, {
channel_accept: 'accepted',
funding_created: 'halfSigned'
}[message.params.data.event])
return { handler: awaitingChannelCreateTx }
}
if (message.params.data.event === 'channel_reestablished') {
return { handler: awaitingOpenConfirmation }
}
if (message.params.data.event === 'fsm_up') {
fsmId.set(channel, message.params.data.fsm_id)
return { handler: awaitingConnection }
}
return { handler: awaitingConnection }
}
if (message.method === 'channels.error') {
emit(channel, 'error', new Error(message.payload.message))
return { handler: channelClosed }
}
}
export async function awaitingReconnection (channel, message, state) {
if (message.method === 'channels.info') {
if (message.params.data.event === 'fsm_up') {
fsmId.set(channel, message.params.data.fsm_id)
changeState(channel, (await call(channel, 'channels.get.offchain_state', {})).signed_tx)
return { handler: channelOpen }
}
}
return handleUnexpectedMessage(channel, message, state)
}
export async function awaitingChannelCreateTx (channel, message, state) {
const tag = {
initiator: 'initiator_sign',
responder: 'responder_sign'
}[options.get(channel).role]
if (message.method === `channels.sign.${tag}`) {
if (message.params.data.tx) {
const signedTx = await options.get(channel).sign(tag, message.params.data.tx)
send(channel, { jsonrpc: '2.0', method: `channels.${tag}`, params: { tx: signedTx } })
return { handler: awaitingOnChainTx }
}
const signedTx = await appendSignature(message.params.data.signed_tx, tx => options.get(channel).sign(tag, tx))
send(channel, { jsonrpc: '2.0', method: `channels.${tag}`, params: { signed_tx: signedTx } })
return { handler: awaitingOnChainTx }
}
}
export function awaitingOnChainTx (channel, message, state) {
if (message.method === 'channels.on_chain_tx') {
if (
message.params.data.info === 'funding_signed' &&
options.get(channel).role === 'initiator'
) {
return { handler: awaitingBlockInclusion }
}
if (
message.params.data.info === 'funding_created' &&
options.get(channel).role === 'responder'
) {
return { handler: awaitingBlockInclusion }
}
}
if (
message.method === 'channels.info' &&
message.params.data.event === 'funding_signed' &&
options.get(channel).role === 'initiator'
) {
channelId.set(channel, message.params.channel_id)
changeStatus(channel, 'signed')
return { handler: awaitingOnChainTx }
}
}
export function awaitingBlockInclusion (channel, message, state) {
if (message.method === 'channels.info') {
const handler = {
funding_created: awaitingBlockInclusion,
own_funding_locked: awaitingBlockInclusion,
funding_locked: awaitingOpenConfirmation
}[message.params.data.event]
if (handler) {
return { handler }
}
}
if (message.method === 'channels.on_chain_tx') {
emit(channel, 'onChainTx', message.params.data.tx, {
info: message.params.data.info,
type: message.params.data.type
})
return { handler: awaitingBlockInclusion }
}
}
export function awaitingOpenConfirmation (channel, message, state) {
if (message.method === 'channels.info' && message.params.data.event === 'open') {
channelId.set(channel, message.params.channel_id)
return { handler: awaitingInitialState }
}
}
export function awaitingInitialState (channel, message, state) {
if (message.method === 'channels.update') {
changeState(channel, message.params.data.state)
return { handler: channelOpen }
}
}
export async function channelOpen (channel, message, state) {
switch (message.method) {
case 'channels.info':
switch (message.params.data.event) {
case 'update':
case 'withdraw_created':
case 'deposit_created':
return { handler: awaitingTxSignRequest }
case 'own_withdraw_locked':
case 'withdraw_locked':
case 'own_deposit_locked':
case 'deposit_locked':
case 'peer_disconnected':
case 'channel_reestablished':
case 'open':
// TODO: Better handling of peer_disconnected event.
//
// We should enter intermediate state where offchain transactions
// are blocked until channel is reestablished.
emit(channel, message.params.data.event)
return { handler: channelOpen }
case 'fsm_up':
fsmId.set(channel, message.params.data.fsm_id)
return { handler: channelOpen }
case 'timeout':
case 'close_mutual':
return { handler: channelOpen }
case 'closing':
changeStatus(channel, 'closing')
return { handler: channelOpen }
case 'closed_confirmed':
changeStatus(channel, 'closed')
return { handler: channelClosed }
case 'died':
changeStatus(channel, 'died')
return { handler: channelClosed }
case 'shutdown':
return { handler: channelOpen }
}
break
case 'channels.on_chain_tx':
emit(channel, 'onChainTx', message.params.data.tx, {
info: message.params.data.info,
type: message.params.data.type
})
return { handler: channelOpen }
case 'channels.leave':
// TODO: emit event
return { handler: channelOpen }
case 'channels.update':
changeState(channel, message.params.data.state)
return { handler: channelOpen }
case 'channels.sign.shutdown_sign_ack':
return awaitingTxSignRequest(channel, message, state)
}
}
channelOpen.enter = (channel) => {
changeStatus(channel, 'open')
}
export async function awaitingOffChainTx (channel, message, state) {
if (message.method === 'channels.sign.update') {
const { sign } = state
if (message.params.data.tx) {
const signedTx = await sign(message.params.data.tx, { updates: message.params.data.updates })
send(channel, { jsonrpc: '2.0', method: 'channels.update', params: { tx: signedTx } })
return { handler: awaitingOffChainUpdate, state }
}
const signedTx = await appendSignature(message.params.data.signed_tx, tx =>
sign(tx, { updates: message.params.data.updates })
)
if (typeof signedTx === 'string') {
send(channel, { jsonrpc: '2.0', method: 'channels.update', params: { signed_tx: signedTx } })
return { handler: awaitingOffChainUpdate, state }
}
if (typeof signedTx === 'number') {
send(channel, { jsonrpc: '2.0', method: 'channels.update', params: { error: signedTx } })
return { handler: awaitingOffChainTx, state }
}
}
if (message.method === 'channels.error') {
state.reject(new Error(message.data.message))
return { handler: channelOpen }
}
if (message.error) {
const { data = [] } = message.error
if (data.find(i => i.code === 1001)) {
state.reject(new Error('Insufficient balance'))
} else if (data.find(i => i.code === 1002)) {
state.reject(new Error('Amount cannot be negative'))
} else {
state.reject(new Error(message.error.message))
}
return { handler: channelOpen }
}
if (message.method === 'channels.conflict') {
state.resolve({
accepted: false,
errorCode: message.params.data.error_code,
errorMessage: message.params.data.error_msg
})
return { handler: channelOpen }
}
if (message.method === 'channels.info') {
if (message.params.data.event === 'aborted_update') {
state.resolve({ accepted: false })
return { handler: channelOpen }
}
}
return handleUnexpectedMessage(channel, message, state)
}
export function awaitingOffChainUpdate (channel, message, state) {
if (message.method === 'channels.update') {
changeState(channel, message.params.data.state)
state.resolve({ accepted: true, signedTx: message.params.data.state })
return { handler: channelOpen }
}
if (message.method === 'channels.conflict') {
state.resolve({
accepted: false,
errorCode: message.params.data.error_code,
errorMessage: message.params.data.error_msg
})
return { handler: channelOpen }
}
if (message.method === 'channels.info') {
if (message.params.data.event === 'aborted_update') {
state.resolve({ accepted: false })
return { handler: channelOpen }
}
}
if (message.error) {
state.reject(new Error(message.error.message))
return { handler: channelOpen }
}
return handleUnexpectedMessage(channel, message, state)
}
export async function awaitingTxSignRequest (channel, message, state) {
const [, tag] = message.method.match(/^channels\.sign\.([^.]+)$/) || []
if (tag) {
if (message.params.data.tx) {
const signedTx = await options.get(channel).sign(tag, message.params.data.tx, {
updates: message.params.data.updates
})
if (signedTx) {
send(channel, { jsonrpc: '2.0', method: `channels.${tag}`, params: { tx: signedTx } })
return { handler: channelOpen }
}
} else {
const signedTx = await appendSignature(message.params.data.signed_tx, tx =>
options.get(channel).sign(tag, tx, { updates: message.params.data.updates })
)
if (typeof signedTx === 'string') {
send(channel, { jsonrpc: '2.0', method: `channels.${tag}`, params: { signed_tx: signedTx } })
return { handler: channelOpen }
}
if (typeof signedTx === 'number') {
send(channel, { jsonrpc: '2.0', method: `channels.${tag}`, params: { error: signedTx } })
return { handler: awaitingUpdateConflict, state }
}
}
// soft-reject via competing update
send(channel, {
jsonrpc: '2.0',
method: 'channels.update.new',
params: {
from: generateKeyPair().publicKey,
to: generateKeyPair().publicKey,
amount: 1
}
})
return { handler: awaitingUpdateConflict, state }
}
return handleUnexpectedMessage(channel, message, state)
}
export function awaitingUpdateConflict (channel, message, state) {
if (message.error) {
return { handler: awaitingUpdateConflict, state }
}
if (message.method === 'channels.conflict') {
return { handler: channelOpen }
}
return handleUnexpectedMessage(channel, message, state)
}
export async function awaitingShutdownTx (channel, message, state) {
if (message.method === 'channels.sign.shutdown_sign') {
if (message.params.data.tx) {
const signedTx = await state.sign(message.params.data.tx)
send(channel, { jsonrpc: '2.0', method: 'channels.shutdown_sign', params: { tx: signedTx } })
return { handler: awaitingShutdownOnChainTx, state }
}
const signedTx = await appendSignature(message.params.data.signed_tx, tx => state.sign(tx))
send(channel, { jsonrpc: '2.0', method: 'channels.shutdown_sign', params: { signed_tx: signedTx } })
return { handler: awaitingShutdownOnChainTx, state }
}
return handleUnexpectedMessage(channel, message, state)
}
export function awaitingShutdownOnChainTx (channel, message, state) {
if (message.method === 'channels.on_chain_tx') {
// state.resolve(message.params.data.tx)
return { handler: channelClosed, state }
}
return handleUnexpectedMessage(channel, message, state)
}
export function awaitingLeave (channel, message, state) {
if (message.method === 'channels.leave') {
state.resolve({ channelId: message.params.channel_id, signedTx: message.params.data.state })
disconnect(channel)
return { handler: channelClosed }
}
if (message.method === 'channels.error') {
state.reject(new Error(message.data.message))
return { handler: channelOpen }
}
return handleUnexpectedMessage(channel, message, state)
}
export async function awaitingWithdrawTx (channel, message, state) {
if (message.method === 'channels.sign.withdraw_tx') {
const { sign } = state
if (message.params.data.tx) {
const signedTx = await sign(message.params.data.tx, { updates: message.params.data.updates })
send(channel, { jsonrpc: '2.0', method: 'channels.withdraw_tx', params: { tx: signedTx } })
return { handler: awaitingWithdrawCompletion, state }
}
const signedTx = await appendSignature(message.params.data.signed_tx, tx =>
sign(tx, { updates: message.params.data.updates })
)
if (typeof signedTx === 'string') {
send(channel, { jsonrpc: '2.0', method: 'channels.withdraw_tx', params: { signed_tx: signedTx } })
return { handler: awaitingWithdrawCompletion, state }
}
if (typeof signedTx === 'number') {
send(channel, { jsonrpc: '2.0', method: 'channels.withdraw_tx', params: { error: signedTx } })
return { handler: awaitingWithdrawCompletion, state }
}
}
return handleUnexpectedMessage(channel, message, state)
}
export function awaitingWithdrawCompletion (channel, message, state) {
if (message.method === 'channels.on_chain_tx') {
if (state.onOnChainTx) {
state.onOnChainTx(message.params.data.tx)
}
return { handler: awaitingWithdrawCompletion, state }
}
if (message.method === 'channels.info') {
if (['own_withdraw_locked', 'withdraw_locked'].includes(message.params.data.event)) {
const callback = {
own_withdraw_locked: state.onOwnWithdrawLocked,
withdraw_locked: state.onWithdrawLocked
}[message.params.data.event]
if (callback) {
callback()
}
return { handler: awaitingWithdrawCompletion, state }
}
}
if (message.method === 'channels.update') {
changeState(channel, message.params.data.state)
state.resolve({ accepted: true, signedTx: message.params.data.state })
return { handler: channelOpen }
}
if (message.method === 'channels.conflict') {
state.resolve({
accepted: false,
errorCode: message.params.data.error_code,
errorMessage: message.params.data.error_msg
})
return { handler: channelOpen }
}
if (message.method === 'channels.info') {
if (message.params.data.event === 'aborted_update') {
state.resolve({ accepted: false })
return { handler: channelOpen }
}
}
return handleUnexpectedMessage(channel, message, state)
}
export async function awaitingDepositTx (channel, message, state) {
if (message.method === 'channels.sign.deposit_tx') {
const { sign } = state
if (message.params.data.tx) {
const signedTx = await sign(message.params.data.tx, { updates: message.params.data.updates })
send(channel, { jsonrpc: '2.0', method: 'channels.deposit_tx', params: { tx: signedTx } })
return { handler: awaitingDepositCompletion, state }
}
const signedTx = await appendSignature(message.params.data.signed_tx, tx =>
sign(tx, { updates: message.params.data.updates })
)
if (typeof signedTx === 'string') {
send(channel, { jsonrpc: '2.0', method: 'channels.deposit_tx', params: { signed_tx: signedTx } })
return { handler: awaitingDepositCompletion, state }
}
if (typeof signedTx === 'number') {
send(channel, { jsonrpc: '2.0', method: 'channels.deposit_tx', params: { error: signedTx } })
return { handler: awaitingDepositCompletion, state }
}
}
return handleUnexpectedMessage(channel, message, state)
}
export function awaitingDepositCompletion (channel, message, state) {
if (message.method === 'channels.on_chain_tx') {
if (state.onOnChainTx) {
state.onOnChainTx(message.params.data.tx)
}
return { handler: awaitingDepositCompletion, state }
}
if (message.method === 'channels.info') {
if (['own_deposit_locked', 'deposit_locked'].includes(message.params.data.event)) {
const callback = {
own_deposit_locked: state.onOwnDepositLocked,
deposit_locked: state.onDepositLocked
}[message.params.data.event]
if (callback) {
callback()
}
return { handler: awaitingDepositCompletion, state }
}
}
if (message.method === 'channels.update') {
changeState(channel, message.params.data.state)
state.resolve({ accepted: true, signedTx: message.params.data.state })
return { handler: channelOpen }
}
if (message.method === 'channels.conflict') {
state.resolve({
accepted: false,
errorCode: message.params.data.error_code,
errorMessage: message.params.data.error_msg
})
return { handler: channelOpen }
}
if (message.method === 'channels.info') {
if (message.params.data.event === 'aborted_update') {
state.resolve({ accepted: false })
return { handler: channelOpen }
}
}
return handleUnexpectedMessage(channel, message, state)
}
export async function awaitingNewContractTx (channel, message, state) {
if (message.method === 'channels.sign.update') {
if (message.params.data.tx) {
const signedTx = await state.sign(message.params.data.tx)
send(channel, { jsonrpc: '2.0', method: 'channels.update', params: { tx: signedTx } })
return { handler: awaitingNewContractCompletion, state }
}
const signedTx = await appendSignature(message.params.data.signed_tx, tx => state.sign(tx))
if (typeof signedTx === 'string') {
send(channel, { jsonrpc: '2.0', method: 'channels.update', params: { signed_tx: signedTx } })
return { handler: awaitingNewContractCompletion, state }
}
if (typeof signedTx === 'number') {
send(channel, { jsonrpc: '2.0', method: 'channels.update', params: { error: signedTx } })
return { handler: awaitingNewContractCompletion, state }
}
}
return handleUnexpectedMessage(channel, message, state)
}
export function awaitingNewContractCompletion (channel, message, state) {
if (message.method === 'channels.update') {
const { round } = unpackTx(message.params.data.state).tx.encodedTx.tx
const owner = options.get(channel)[{
initiator: 'initiatorId',
responder: 'responderId'
}[options.get(channel).role]]
changeState(channel, message.params.data.state)
state.resolve({
accepted: true,
address: encodeContractAddress(owner, round),
signedTx: message.params.data.state
})
return { handler: channelOpen }
}
if (message.method === 'channels.conflict') {
state.resolve({
accepted: false,
errorCode: message.params.data.error_code,
errorMessage: message.params.data.error_msg
})
return { handler: channelOpen }
}
if (message.method === 'channels.info') {
if (message.params.data.event === 'aborted_update') {
state.resolve({ accepted: false })
return { handler: channelOpen }
}
}
return handleUnexpectedMessage(channel, message, state)
}
export async function awaitingCallContractUpdateTx (channel, message, state) {
if (message.method === 'channels.sign.update') {
if (message.params.data.tx) {
const signedTx = await state.sign(message.params.data.tx, { updates: message.params.data.updates })
send(channel, { jsonrpc: '2.0', method: 'channels.update', params: { tx: signedTx } })
return { handler: awaitingCallContractCompletion, state }
}
const signedTx = await appendSignature(message.params.data.signed_tx, tx => state.sign(tx, { updates: message.params.data.updates }))
if (typeof signedTx === 'string') {
send(channel, { jsonrpc: '2.0', method: 'channels.update', params: { signed_tx: signedTx } })
return { handler: awaitingCallContractCompletion, state }
}
if (typeof signedTx === 'number') {
send(channel, { jsonrpc: '2.0', method: 'channels.update', params: { error: signedTx } })
return { handler: awaitingCallContractCompletion, state }
}
}
return handleUnexpectedMessage(channel, message, state)
}
export async function awaitingCallContractForceProgressUpdate (channel, message, state) {
if (message.method === 'channels.sign.force_progress_tx') {
const signedTx = await appendSignature(message.params.data.signed_tx, tx =>
state.sign(tx, { updates: message.params.data.updates })
)
send(channel, { jsonrpc: '2.0', method: 'channels.force_progress_sign', params: { signed_tx: signedTx } })
return { handler: awaitingForceProgressCompletion, state }
}
return handleUnexpectedMessage(channel, message, state)
}
export function awaitingForceProgressCompletion (channel, message, state) {
console.log('From force progress await completion ->', message)
if (message.method === 'channels.on_chain_tx') {
if (state.onOnChainTx) {
state.onOnChainTx(message.params.data)
}
emit(channel, 'onChainTx', message.params.data.tx, {
info: message.params.data.info,
type: message.params.data.type
})
state.resolve({ accepted: true, tx: message.params.data.tx })
}
return handleUnexpectedMessage(channel, message, state)
}
export function awaitingCallContractCompletion (channel, message, state) {
if (message.method === 'channels.update') {
changeState(channel, message.params.data.state)
state.resolve({ accepted: true, signedTx: message.params.data.state })
return { handler: channelOpen }
}
if (message.method === 'channels.conflict') {
state.resolve({
accepted: false,
errorCode: message.params.data.error_code,
errorMessage: message.params.data.error_msg
})
return { handler: channelOpen }
}
if (message.method === 'channels.info') {
if (message.params.data.event === 'aborted_update') {
state.resolve({ accepted: false })
return { handler: channelOpen }
}
}
return handleUnexpectedMessage(channel, message, state)
}
export function awaitingCallsPruned (channels, message, state) {
if (message.method === 'channels.calls_pruned.reply') {
state.resolve()
return { handler: channelOpen }
}
state.reject(new Error('Unexpected message received'))
return { handler: channelClosed }
}
export function channelClosed (channel, message, state) {
if (!state) return { handler: channelClosed }
if (message.params.data.event === 'closing') return { handler: channelClosed, state }
if (message.params.data.info === 'channel_closed') {
state.closeTx = message.params.data.tx
return { handler: channelClosed, state }
}
if (message.params.data.event === 'closed_confirmed') {
state.resolve(state.closeTx)
return { handler: channelClosed }
}
return { handler: channelClosed, state }
}