import logging
from enum import Enum
from eth_utils import decode_hex, is_same_address
from typing import Callable
from microraiden.client.context import Context
from microraiden.utils import (
get_event_blocking,
create_signed_contract_transaction,
sign_balance_proof,
verify_closing_sig,
keccak256
)
log = logging.getLogger(__name__)
[docs]class Channel:
[docs] class State(Enum):
open = 1
settling = 2
closed = 3
def __init__(
self,
core: Context,
sender: str,
receiver: str,
block: int,
deposit: int = 0,
balance: int = 0,
state: State = State.open,
on_settle: Callable[['Channel'], None] = lambda channel: None
):
self._balance = 0
self._balance_sig = None
self.core = core
self.sender = sender
self.receiver = receiver
self.deposit = deposit
self.block = block
self.update_balance(balance)
self.state = state
self.on_settle = on_settle
assert self.block is not None
assert self._balance_sig
@property
def balance(self):
return self._balance
@property
def key(self) -> bytes:
return keccak256(self.sender, self.receiver, self.block)
[docs] def update_balance(self, value):
self._balance = value
self._balance_sig = self.sign()
@property
def balance_sig(self):
return self._balance_sig
[docs] def sign(self):
return sign_balance_proof(
self.core.private_key,
self.receiver,
self.block,
self.balance,
self.core.channel_manager.address
)
[docs] def topup(self, deposit):
"""
Attempts to increase the deposit in an existing channel. Block until confirmation.
"""
if self.state != Channel.State.open:
log.error('Channel must be open to be topped up.')
return
token_balance = self.core.token.call().balanceOf(self.core.address)
if token_balance < deposit:
log.error(
'Insufficient tokens available for the specified topup ({}/{})'
.format(token_balance, deposit)
)
return None
log.info('Topping up channel to {} created at block #{} by {} tokens.'.format(
self.receiver, self.block, deposit
))
current_block = self.core.web3.eth.blockNumber
data = (decode_hex(self.sender) +
decode_hex(self.receiver) +
self.block.to_bytes(4, byteorder='big'))
tx = create_signed_contract_transaction(
self.core.private_key,
self.core.token,
'transfer',
[
self.core.channel_manager.address,
deposit,
data
]
)
self.core.web3.eth.sendRawTransaction(tx)
log.debug('Waiting for topup confirmation event...')
event = get_event_blocking(
self.core.channel_manager,
'ChannelToppedUp',
from_block=current_block + 1,
argument_filters={
'_sender_address': self.sender,
'_receiver_address': self.receiver,
'_open_block_number': self.block
}
)
if event:
log.debug('Successfully topped up channel in block {}.'.format(event['blockNumber']))
self.deposit += deposit
return event
else:
log.error('No event received.')
return None
[docs] def close(self, balance=None):
"""
Attempts to request close on a channel. An explicit balance can be given to override the
locally stored balance signature. Blocks until a confirmation event is received or timeout.
"""
if self.state != Channel.State.open:
log.error('Channel must be open to request a close.')
return
log.info('Requesting close of channel to {} created at block #{}.'.format(
self.receiver, self.block
))
current_block = self.core.web3.eth.blockNumber
if balance is not None:
self.update_balance(balance)
tx = create_signed_contract_transaction(
self.core.private_key,
self.core.channel_manager,
'uncooperativeClose',
[
self.receiver,
self.block,
self.balance
]
)
self.core.web3.eth.sendRawTransaction(tx)
log.debug('Waiting for close confirmation event...')
event = get_event_blocking(
self.core.channel_manager,
'ChannelCloseRequested',
from_block=current_block + 1,
argument_filters={
'_sender_address': self.sender,
'_receiver_address': self.receiver,
'_open_block_number': self.block
}
)
if event:
log.debug('Successfully sent channel close request in block {}.'.format(
event['blockNumber']
))
self.state = Channel.State.settling
return event
else:
log.error('No event received.')
return None
[docs] def close_cooperatively(self, closing_sig: bytes):
"""
Attempts to close the channel immediately by providing a hash of the channel's balance
proof signed by the receiver. This signature must correspond to the balance proof stored in
the passed channel state.
"""
if self.state == Channel.State.closed:
log.error('Channel must not be closed already to be closed cooperatively.')
return None
log.info('Attempting to cooperatively close channel to {} created at block #{}.'.format(
self.receiver, self.block
))
current_block = self.core.web3.eth.blockNumber
receiver_recovered = verify_closing_sig(
self.sender,
self.block,
self.balance,
closing_sig,
self.core.channel_manager.address
)
if not is_same_address(receiver_recovered, self.receiver):
log.error('Invalid closing signature.')
return None
tx = create_signed_contract_transaction(
self.core.private_key,
self.core.channel_manager,
'cooperativeClose',
[
self.receiver,
self.block,
self.balance,
self.balance_sig,
closing_sig
]
)
self.core.web3.eth.sendRawTransaction(tx)
log.debug('Waiting for settle confirmation event...')
event = get_event_blocking(
self.core.channel_manager,
'ChannelSettled',
from_block=current_block + 1,
argument_filters={
'_sender_address': self.sender,
'_receiver_address': self.receiver,
'_open_block_number': self.block
}
)
if event:
log.debug('Successfully closed channel in block {}.'.format(event['blockNumber']))
self.state = Channel.State.closed
return event
else:
log.error('No event received.')
return None
[docs] def settle(self):
"""
Attempts to settle a channel that has passed its settlement period. If a channel cannot be
settled yet, the call is ignored with a warning. Blocks until a confirmation event is
received or timeout.
"""
if self.state != Channel.State.settling:
log.error('Channel must be in the settlement period to settle.')
return None
log.info('Attempting to settle channel to {} created at block #{}.'.format(
self.receiver, self.block
))
_, _, settle_block, _, _ = self.core.channel_manager.call().getChannelInfo(
self.sender, self.receiver, self.block
)
current_block = self.core.web3.eth.blockNumber
wait_remaining = settle_block - current_block
if wait_remaining > 0:
log.warning('{} more blocks until this channel can be settled. Aborting.'.format(
wait_remaining
))
return None
tx = create_signed_contract_transaction(
self.core.private_key,
self.core.channel_manager,
'settle',
[
self.receiver,
self.block
]
)
self.core.web3.eth.sendRawTransaction(tx)
log.debug('Waiting for settle confirmation event...')
event = get_event_blocking(
self.core.channel_manager,
'ChannelSettled',
from_block=current_block + 1,
argument_filters={
'_sender_address': self.sender,
'_receiver_address': self.receiver,
'_open_block_number': self.block
}
)
if event:
log.debug('Successfully settled channel in block {}.'.format(event['blockNumber']))
self.state = Channel.State.closed
self.on_settle(self)
return event
else:
log.error('No event received.')
return None
[docs] def create_transfer(self, value):
"""
Updates the given channel's balance and balance signature with the new value. The signature
is returned and stored in the channel state.
"""
assert value >= 0
if value > self.deposit - self.balance:
log.error(
'Insufficient funds on channel. Needed: {}. Available: {}/{}.'
.format(value, self.deposit - self.balance, self.deposit)
)
return None
log.debug('Signing new transfer of value {} on channel to {} created at block #{}.'.format(
value, self.receiver, self.block
))
if self.state == Channel.State.closed:
log.error('Channel must be open to create a transfer.')
return None
self.update_balance(self.balance + value)
return self.balance_sig
[docs] def is_valid(self) -> bool:
return self.sign() == self.balance_sig and self.balance <= self.deposit
[docs] def is_suitable(self, value: int):
return self.deposit - self.balance >= value