Skip to content

Commit

Permalink
Add /user_deposit API endpoint for interacting with UDC
Browse files Browse the repository at this point in the history
This includes three POST requests:
- deposit to UDC
- plan withdraw from UDC
- withdraw from UDC

Resolves #5497
  • Loading branch information
manuelwedler committed Feb 25, 2021
1 parent fbbce96 commit 2862a6d
Show file tree
Hide file tree
Showing 5 changed files with 298 additions and 0 deletions.
148 changes: 148 additions & 0 deletions raiden/api/python.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
TokenNotRegistered,
UnexpectedChannelState,
UnknownTokenAddress,
UserDepositNotConfigured,
WithdrawMismatch,
)
from raiden.settings import DEFAULT_RETRY_TIMEOUT, PythonApiConfig
Expand All @@ -49,6 +50,7 @@
Address,
Any,
BlockIdentifier,
BlockNumber,
BlockTimeout,
ChannelID,
Dict,
Expand All @@ -69,6 +71,7 @@
TokenNetworkAddress,
TokenNetworkRegistryAddress,
TransactionHash,
Tuple,
WithdrawAmount,
)

Expand Down Expand Up @@ -1167,5 +1170,150 @@ def get_pending_transfers(

return transfer_tasks_view(transfer_tasks, token_address, channel_id)

def set_total_udc_deposit(self, new_total_deposit: TokenAmount) -> TransactionHash:
"""Set the `total_deposit` in the UserDeposit contract by sending an on-chain transaction.
Raises:
UserDepositNotConfigured: No UserDeposit is configured for the
Raiden node.
DepositMismatch: The new `total_deposit` is not higher than the
previous one.
DepositOverLimit: Either an overflow happened or the
`whole_balance_limit` of the UserDeposit contract is exceeded.
InsufficientFunds: Not enough tokens for the deposit.
RaidenRecoverableError: The transaction failed for any reason.
Returns: TransactionHash of the successfully mined transaction.
"""
user_deposit = self.raiden.default_user_deposit

if user_deposit is None:
raise UserDepositNotConfigured("No UserDeposit contract is configured.")

chain_state = views.state_from_raiden(self.raiden)
confirmed_block_identifier = chain_state.block_hash

current_total_deposit = user_deposit.get_total_deposit(
address=self.address, block_identifier=confirmed_block_identifier
)
addendum = new_total_deposit - current_total_deposit

whole_balance = user_deposit.whole_balance(block_identifier=confirmed_block_identifier)
whole_balance_limit = user_deposit.whole_balance_limit(
block_identifier=confirmed_block_identifier
)

token_address = user_deposit.token_address(block_identifier=confirmed_block_identifier)
token = self.raiden.proxy_manager.token(
token_address, block_identifier=confirmed_block_identifier
)
balance = token.balance_of(
address=self.address, block_identifier=confirmed_block_identifier
)

if new_total_deposit <= current_total_deposit:
raise DepositMismatch("Total deposit did not increase.")

if whole_balance + addendum > UINT256_MAX:
raise DepositOverLimit("Deposit overflow.")

if whole_balance + addendum > whole_balance_limit:
msg = f"Deposit of {addendum} would have exceeded the UDC balance limit."
raise DepositOverLimit(msg)

if balance < addendum:
msg = f"Not enough balance to deposit. Available={balance} Needed={addendum}"
raise InsufficientFunds(msg)

return user_deposit.approve_and_deposit(
beneficiary=self.address,
total_deposit=new_total_deposit,
given_block_identifier=confirmed_block_identifier,
)

def plan_udc_withdraw(self, amount: TokenAmount) -> Tuple[TransactionHash, BlockNumber]:
"""Plan a withdraw of `amount` from the UserDeposit contract by sending an on-chain
transaction.
Raises:
UserDepositNotConfigured: No UserDeposit is configured for the
Raiden node.
WithdrawMismatch: Withdrawing more than the available balance or
a zero or negative amount.
RaidenRecoverableError: The transaction failed for any reason.
Returns:
Tuple of the TransactionHash and the BlockNumber at which the
withdraw is ready.
"""
user_deposit = self.raiden.default_user_deposit

if user_deposit is None:
raise UserDepositNotConfigured("No UserDeposit contract is configured.")

chain_state = views.state_from_raiden(self.raiden)
confirmed_block_identifier = chain_state.block_hash

balance = user_deposit.get_balance(
address=self.address, block_identifier=confirmed_block_identifier
)

if amount <= 0:
raise WithdrawMismatch("Withdraw amount must be greater than zero.")

if amount > balance:
msg = f"The withdraw of {amount} is bigger than the current balance of {balance}."
raise WithdrawMismatch(msg)

return user_deposit.plan_withdraw(
amount=amount, given_block_identifier=confirmed_block_identifier
)

def withdraw_from_udc(self, amount: TokenAmount) -> TransactionHash:
"""Withdraw an `amount` from the UserDeposit contract by sending an on-chain
transaction. The withdraw has to be planned first with `plan_udc_withdraw`.
Raises:
UserDepositNotConfigured: No UserDeposit is configured for the
Raiden node.
WithdrawMismatch: Withdrawing more than the planned withdraw amount
or at an earlier block than the planned withdraw is ready.
RaidenRecoverableError: The transaction failed for any reason.
Returns: TransactionHash of the successfully mined transaction.
"""
user_deposit = self.raiden.default_user_deposit

if user_deposit is None:
raise UserDepositNotConfigured("No UserDeposit contract is configured.")

chain_state = views.state_from_raiden(self.raiden)
confirmed_block_identifier = chain_state.block_hash
block_number = chain_state.block_number

withdraw_plan = user_deposit.get_withdraw_plan(
withdrawer_address=self.address, block_identifier=confirmed_block_identifier
)
whole_balance = user_deposit.whole_balance(block_identifier=confirmed_block_identifier)

if amount <= 0:
raise WithdrawMismatch("Withdraw amount must be greater than zero.")

if amount > withdraw_plan.withdraw_amount:
raise WithdrawMismatch("Withdrawing more than planned.")

if block_number < withdraw_plan.withdraw_block:
raise WithdrawMismatch(
f"Withdrawing too early. Planned withdraw at block "
f"{withdraw_plan.withdraw_block}. Current block: {block_number}."
)

if whole_balance - amount < 0:
raise WithdrawMismatch("Whole balance underflow.")

return user_deposit.withdraw(
amount=amount, given_block_identifier=confirmed_block_identifier
)

def shutdown(self) -> None:
self.raiden.stop()
128 changes: 128 additions & 0 deletions raiden/api/rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
ShutdownResource,
StatusResource,
TokensResource,
UserDepositResource,
VersionResource,
create_blueprint,
)
Expand Down Expand Up @@ -89,6 +90,7 @@
TokenNotRegistered,
UnexpectedChannelState,
UnknownTokenAddress,
UserDepositNotConfigured,
WithdrawMismatch,
)
from raiden.network.rpc.client import JSONRPCClient
Expand Down Expand Up @@ -166,6 +168,7 @@
PendingTransfersResourceByTokenAndPartnerAddress,
"pending_transfers_resource_by_token_and_partner",
),
("/user_deposit", UserDepositResource),
("/status", StatusResource),
("/shutdown", ShutdownResource),
("/_debug/raiden_events", RaidenInternalEventsResource),
Expand Down Expand Up @@ -1326,6 +1329,131 @@ def get_pending_transfers(
except (ChannelNotFound, UnknownTokenAddress) as e:
return api_error(errors=str(e), status_code=HTTPStatus.NOT_FOUND)

def _deposit_to_udc(self, total_deposit: TokenAmount) -> Response:
log.debug(
"Depositing to UDC",
node=self.checksum_address,
total_deposit=total_deposit,
)

try:
transaction_hash = self.raiden_api.set_total_udc_deposit(total_deposit)
except (InsufficientEth, InsufficientFunds) as e:
return api_error(errors=str(e), status_code=HTTPStatus.PAYMENT_REQUIRED)
except (DepositOverLimit, DepositMismatch) as e:
return api_error(errors=str(e), status_code=HTTPStatus.CONFLICT)
except UserDepositNotConfigured as e:
return api_error(errors=str(e), status_code=HTTPStatus.NOT_FOUND)
except RaidenRecoverableError as e:
return api_error(errors=str(e), status_code=HTTPStatus.CONFLICT)

return api_response(
status_code=HTTPStatus.OK, result=dict(transaction_hash=encode_hex(transaction_hash))
)

def _plan_withdraw_from_udc(self, planned_withdraw_amount: TokenAmount) -> Response:
log.debug(
"Planning a withdraw from UDC",
node=self.checksum_address,
planned_withdraw_amount=planned_withdraw_amount,
)

try:
(transaction_hash, planned_withdraw_block_number) = self.raiden_api.plan_udc_withdraw(
planned_withdraw_amount
)
except InsufficientEth as e:
return api_error(errors=str(e), status_code=HTTPStatus.PAYMENT_REQUIRED)
except WithdrawMismatch as e:
return api_error(errors=str(e), status_code=HTTPStatus.CONFLICT)
except UserDepositNotConfigured as e:
return api_error(errors=str(e), status_code=HTTPStatus.NOT_FOUND)
except RaidenRecoverableError as e:
return api_error(errors=str(e), status_code=HTTPStatus.CONFLICT)

result = dict(
transaction_hash=encode_hex(transaction_hash),
planned_withdraw_block_number=planned_withdraw_block_number,
)
return api_response(status_code=HTTPStatus.OK, result=result)

def _withdraw_from_udc(self, amount: TokenAmount) -> Response:
log.debug(
"Withdraw from UDC",
node=self.checksum_address,
amount=amount,
)

try:
transaction_hash = self.raiden_api.withdraw_from_udc(amount)
except InsufficientEth as e:
return api_error(errors=str(e), status_code=HTTPStatus.PAYMENT_REQUIRED)
except WithdrawMismatch as e:
return api_error(errors=str(e), status_code=HTTPStatus.CONFLICT)
except UserDepositNotConfigured as e:
return api_error(errors=str(e), status_code=HTTPStatus.NOT_FOUND)
except RaidenRecoverableError as e:
return api_error(errors=str(e), status_code=HTTPStatus.CONFLICT)

return api_response(
status_code=HTTPStatus.OK, result=dict(transaction_hash=encode_hex(transaction_hash))
)

def send_udc_transaction(
self,
total_deposit: TokenAmount = None,
planned_withdraw_amount: TokenAmount = None,
withdraw_amount: TokenAmount = None,
) -> Response:
log.debug(
"Sending UDC transaction",
node=self.checksum_address,
total_deposit=total_deposit,
planned_withdraw_amount=planned_withdraw_amount,
withdraw_amount=withdraw_amount,
)

if total_deposit is not None and planned_withdraw_amount is not None:
return api_error(
errors="Cannot deposit to UDC and plan a withdraw at the same time",
status_code=HTTPStatus.BAD_REQUEST,
)

if total_deposit is not None and withdraw_amount is not None:
return api_error(
errors="Cannot deposit to UDC and withdraw at the same time",
status_code=HTTPStatus.BAD_REQUEST,
)

if withdraw_amount is not None and planned_withdraw_amount is not None:
return api_error(
errors="Cannot withdraw from UDC and plan a withdraw at the same time",
status_code=HTTPStatus.BAD_REQUEST,
)

empty_request = (
total_deposit is None and planned_withdraw_amount is None and withdraw_amount is None
)
if empty_request:
return api_error(
errors=(
"Nothing to do. Should either provide 'total_deposit', "
"'planned_withdraw_amount' or 'withdraw_amount' argument"
),
status_code=HTTPStatus.BAD_REQUEST,
)

if total_deposit is not None:
result = self._deposit_to_udc(total_deposit)

elif planned_withdraw_amount is not None:
result = self._plan_withdraw_from_udc(planned_withdraw_amount)

elif withdraw_amount is not None:
result = self._withdraw_from_udc(withdraw_amount)

return result

def get_status(self) -> Response:
if self.available:
return api_response(result=dict(status="ready"))
Expand Down
6 changes: 6 additions & 0 deletions raiden/api/v1/encoding.py
Original file line number Diff line number Diff line change
Expand Up @@ -367,3 +367,9 @@ class Meta:
"log_time",
"token_address",
)


class UserDepositPostSchema(BaseSchema):
total_deposit = IntegerToStringField(default=None, missing=None)
planned_withdraw_amount = IntegerToStringField(default=None, missing=None)
withdraw_amount = IntegerToStringField(default=None, missing=None)
10 changes: 10 additions & 0 deletions raiden/api/v1/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
MintTokenSchema,
PaymentSchema,
RaidenEventsRequestSchema,
UserDepositPostSchema,
)
from raiden.utils.typing import TYPE_CHECKING, Address, Any, TargetAddress, TokenAddress

Expand Down Expand Up @@ -255,6 +256,15 @@ def get(self, token_address: TokenAddress, partner_address: Address) -> Response
return self.rest_api.get_pending_transfers(token_address, partner_address)


class UserDepositResource(BaseResource):
post_schema = UserDepositPostSchema()

@if_api_available
def post(self) -> Response:
kwargs = validate_json(self.post_schema)
return self.rest_api.send_udc_transaction(**kwargs)


class StatusResource(BaseResource):
def get(self) -> Response:
return self.rest_api.get_status()
Expand Down
6 changes: 6 additions & 0 deletions raiden/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -390,3 +390,9 @@ class MatrixSyncMaxTimeoutReached(RaidenRecoverableError):

class ConfigurationError(RaidenError):
""" Raised when there is something wrong with the provided Raiden Configuration/arguments """


class UserDepositNotConfigured(RaidenRecoverableError):
"""Raised when trying to perform operations on a user deposit contract but none has been
configured for the Raiden node.
"""

0 comments on commit 2862a6d

Please sign in to comment.