# Copyright (C) 2019 alfred richardsn
#
# This file is part of TellerBot.
#
# TellerBot is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with TellerBot. If not, see <https://www.gnu.org/licenses/>.
"""Handlers for escrow exchange."""
import asyncio
import typing
from dataclasses import replace
from decimal import Decimal
from functools import wraps
from time import time
from aiogram import types
from aiogram.dispatcher import FSMContext
from aiogram.dispatcher.filters.state import any_state
from aiogram.types import InlineKeyboardButton
from aiogram.types import InlineKeyboardMarkup
from aiogram.types import ParseMode
from aiogram.types import User
from aiogram.utils import markdown
from bson.decimal128 import Decimal128
from bson.objectid import ObjectId
from src import referral_system as rs
from src import states
from src.bot import dp
from src.bot import tg
from src.config import config
from src.database import database
from src.escrow import get_escrow_instance
from src.escrow import SUPPORTED_BANKS
from src.escrow.blockchain import StreamBlockchain
from src.escrow.escrow_offer import EscrowOffer
from src.handlers.base import private_handler
from src.handlers.base import start_keyboard
from src.i18n import i18n
from src.money import money
from src.money import MoneyValueError
from src.money import normalize
[docs]async def get_card_number(
text: str, chat_id: int
) -> typing.Optional[typing.Tuple[str, str]]:
"""Parse first and last 4 digits from card number in ``text``.
If parsing is unsuccessful, send warning to ``chat_id`` and return
None. Otherwise return tuple of first and last 4 digits of card number.
"""
if len(text) < 8:
await tg.send_message(chat_id, i18n("send_at_least_8_digits"))
return None
first = text[:4]
last = text[-4:]
if not first.isdigit() or not last.isdigit():
await tg.send_message(chat_id, i18n("digits_parsing_error"))
return None
return (first, last)
[docs]@dp.async_task
async def call_later(delay: float, callback: typing.Callable, *args, **kwargs):
"""Call ``callback(*args, **kwargs)`` asynchronously after ``delay`` seconds."""
await asyncio.sleep(delay)
return await callback(*args, **kwargs)
[docs]def escrow_callback_handler(*args, state=any_state, **kwargs):
"""Simplify handling callback queries during escrow exchange.
Add offer of ``EscrowOffer`` to arguments of decorated callback query handler.
"""
def decorator(
handler: typing.Callable[[types.CallbackQuery, EscrowOffer], typing.Any]
):
@wraps(handler)
@dp.callback_query_handler(*args, state=state, **kwargs)
async def wrapper(call: types.CallbackQuery):
offer_id = call.data.split()[1]
offer = await database.escrow.find_one({"_id": ObjectId(offer_id)})
if not offer:
await call.answer(i18n("offer_not_active"))
return
return await handler(call, EscrowOffer(**offer))
return wrapper
return decorator
[docs]def escrow_message_handler(*args, **kwargs):
"""Simplify handling messages during escrow exchange.
Add offer of ``EscrowOffer`` to arguments of decorated private message handler.
"""
def decorator(handler: typing.Callable[[types.Message, EscrowOffer], typing.Any]):
@wraps(handler)
@private_handler(*args, **kwargs)
async def wrapper(message: types.Message, state: FSMContext):
offer = await database.escrow.find_one(
{"pending_input_from": message.from_user.id}
)
if not offer:
await tg.send_message(message.chat.id, i18n("offer_not_active"))
return
return await handler(message, EscrowOffer(**offer))
return wrapper
return decorator
[docs]async def get_insurance(offer: EscrowOffer) -> Decimal:
"""Get insurance of escrow asset in ``offer`` taking limits into account."""
offer_sum = offer[f"sum_{offer.type}"].to_decimal()
asset = offer.escrow
limits = await get_escrow_instance(asset).get_limits(asset)
if not limits:
return offer_sum
insured = min(offer_sum, limits.single)
cursor = database.escrow.aggregate(
[{"$group": {"_id": 0, "insured_total": {"$sum": "$insured"}}}]
)
if await cursor.fetch_next:
insured_total = cursor.next_object()["insured_total"]
if insured_total != 0:
insured_total = insured_total.to_decimal()
total_difference = limits.total - insured_total - insured
if total_difference < 0:
insured += total_difference
return normalize(insured)
[docs]@escrow_message_handler(state=states.Escrow.amount)
async def set_escrow_sum(message: types.Message, offer: EscrowOffer):
"""Set sum and ask for fee payment agreement."""
try:
offer_sum = money(message.text)
except MoneyValueError as exception:
await tg.send_message(message.chat.id, str(exception))
return
order = await database.orders.find_one({"_id": offer.order})
order_sum = order.get(offer.sum_currency)
if order_sum and offer_sum > order_sum.to_decimal():
await tg.send_message(message.chat.id, i18n("exceeded_order_sum"))
return
update_dict = {offer.sum_currency: Decimal128(offer_sum)}
new_currency = "sell" if offer.sum_currency == "sum_buy" else "buy"
update_dict[f"sum_{new_currency}"] = Decimal128(
normalize(offer_sum * order[f"price_{new_currency}"].to_decimal())
)
escrow_sum = update_dict[f"sum_{offer.type}"]
escrow_fee = Decimal(config.ESCROW_FEE_PERCENTS) / Decimal("100")
update_dict["sum_fee_up"] = Decimal128(
normalize(escrow_sum.to_decimal() * (Decimal("1") + escrow_fee))
)
update_dict["sum_fee_down"] = Decimal128(
normalize(escrow_sum.to_decimal() * (Decimal("1") - escrow_fee))
)
offer = replace(offer, **update_dict) # type: ignore
if offer.sum_currency == offer.type:
insured = await get_insurance(offer)
update_dict["insured"] = Decimal128(insured)
if offer_sum > insured:
keyboard = InlineKeyboardMarkup()
keyboard.add(
InlineKeyboardButton(
i18n("continue"), callback_data=f"accept_insurance {offer._id}"
),
InlineKeyboardButton(
i18n("cancel"), callback_data=f"init_cancel {offer._id}"
),
)
answer = i18n("exceeded_insurance {amount} {currency}").format(
amount=insured, currency=offer.escrow
)
answer += "\n" + i18n("exceeded_insurance_options")
await tg.send_message(message.chat.id, answer, reply_markup=keyboard)
else:
await ask_fee(message.from_user.id, message.chat.id, offer)
await offer.update_document({"$set": update_dict, "$unset": {"sum_currency": True}})
[docs]async def ask_fee(user_id: int, chat_id: int, offer: EscrowOffer):
"""Ask fee of any party."""
answer = (
i18n("ask_fee {fee_percents}").format(fee_percents=config.ESCROW_FEE_PERCENTS)
+ " "
)
if (user_id == offer.init["id"]) == (offer.type == "buy"):
answer += i18n("will_pay {amount} {currency}")
sum_fee_field = "sum_fee_up"
else:
answer += i18n("will_get {amount} {currency}")
sum_fee_field = "sum_fee_down"
keyboard = InlineKeyboardMarkup()
keyboard.add(
InlineKeyboardButton(i18n("yes"), callback_data=f"accept_fee {offer._id}"),
InlineKeyboardButton(i18n("no"), callback_data=f"decline_fee {offer._id}"),
)
answer = answer.format(amount=offer[sum_fee_field], currency=offer.escrow)
await tg.send_message(chat_id, answer, reply_markup=keyboard)
await states.Escrow.fee.set()
[docs]@escrow_callback_handler(
lambda call: call.data.startswith("accept_insurance "), state=states.Escrow.amount
)
async def accept_insurance(call: types.CallbackQuery, offer: EscrowOffer):
"""Ask for fee payment agreement after accepting partial insurance."""
await ask_fee(call.from_user.id, call.message.chat.id, offer)
[docs]@escrow_callback_handler(
lambda call: call.data.startswith("init_cancel "), state=states.Escrow.amount
)
async def init_cancel(call: types.CallbackQuery, offer: EscrowOffer):
"""Cancel offer on initiator's request."""
await offer.delete_document()
await call.answer()
await tg.send_message(
call.message.chat.id, i18n("escrow_cancelled"), reply_markup=start_keyboard()
)
await dp.current_state().finish()
[docs]async def full_card_number_request(chat_id: int, offer: EscrowOffer):
"""Ask to send full card number."""
[docs]async def ask_credentials(
call: types.CallbackQuery,
offer: EscrowOffer,
):
"""Update offer with ``update_dict`` and start asking transfer information.
Ask to choose bank if user is initiator and there is a fiat
currency. Otherwise ask receive address.
"""
await call.answer()
is_user_init = call.from_user.id == offer.init["id"]
has_fiat_currency = "RUB" in {offer.buy, offer.sell}
if has_fiat_currency:
if is_user_init:
keyboard = InlineKeyboardMarkup()
for bank in SUPPORTED_BANKS:
keyboard.row(
InlineKeyboardButton(bank, callback_data=f"bank {offer._id} {bank}")
)
await tg.send_message(
call.message.chat.id, i18n("choose_bank"), reply_markup=keyboard
)
await states.Escrow.bank.set()
else:
if offer.type == "buy":
request_user = offer.init
answer_user = offer.counter
currency = offer.sell
else:
request_user = offer.counter
answer_user = offer.init
currency = offer.buy
keyboard = InlineKeyboardMarkup()
keyboard.add(
InlineKeyboardButton(
i18n("sent"), callback_data=f"card_sent {offer._id}"
)
)
mention = markdown.link(
answer_user["mention"], User(id=answer_user["id"]).url
)
await tg.send_message(
request_user["id"],
i18n(
"request_full_card_number {currency} {user}",
locale=request_user["locale"],
).format(currency=currency, user=mention),
reply_markup=keyboard,
parse_mode=ParseMode.MARKDOWN,
)
state = FSMContext(dp.storage, request_user["id"], request_user["id"])
await state.set_state(states.Escrow.full_card.state)
answer = i18n(
"asked_full_card_number {user}", locale=answer_user["locale"]
).format(
user=markdown.link(
request_user["mention"], User(id=request_user["id"]).url
)
)
await tg.send_message(
answer_user["id"],
answer,
parse_mode=ParseMode.MARKDOWN,
)
return
await tg.send_message(
call.message.chat.id,
i18n("ask_address {currency}").format(
currency=offer.sell if is_user_init else offer.buy
),
)
await offer.update_document({"$set": {"pending_input_from": call.from_user.id}})
await states.Escrow.receive_address.set()
[docs]@escrow_callback_handler(
lambda call: call.data.startswith("accept_fee "), state=states.Escrow.fee
)
async def pay_fee(call: types.CallbackQuery, offer: EscrowOffer):
"""Accept fee and start asking transfer information."""
await ask_credentials(call, offer)
[docs]@escrow_callback_handler(
lambda call: call.data.startswith("decline_fee "), state=states.Escrow.fee
)
async def decline_fee(call: types.CallbackQuery, offer: EscrowOffer):
"""Decline fee and start asking transfer information."""
if (call.from_user.id == offer.init["id"]) == (offer.type == "buy"):
sum_fee_field = "sum_fee_up"
else:
sum_fee_field = "sum_fee_down"
await offer.update_document({"$set": {sum_fee_field: offer[f"sum_{offer.type}"]}})
await ask_credentials(call, offer)
[docs]@escrow_callback_handler(
lambda call: call.data.startswith("bank "), state=states.Escrow.bank
)
async def choose_bank(call: types.CallbackQuery, offer: EscrowOffer):
"""Set chosen bank and continue.
Because bank is chosen by initiator, ask for receive address if
they receive escrow asset.
"""
bank = call.data.split()[2]
if bank not in SUPPORTED_BANKS:
await call.answer(i18n("bank_not_supported"))
return
update_dict = {"bank": bank}
await call.answer()
update_dict["pending_input_from"] = call.from_user.id
await offer.update_document({"$set": update_dict})
if offer.sell == "RUB":
await tg.send_message(
call.message.chat.id,
i18n("send_first_and_last_4_digits_of_card_number {currency}").format(
currency=offer.sell
),
)
await states.Escrow.receive_card_number.set()
else:
await tg.send_message(
call.message.chat.id,
i18n("ask_address {currency}").format(currency=offer.sell),
)
await states.Escrow.receive_address.set()
[docs]@escrow_message_handler(state=states.Escrow.full_card)
async def full_card_number_message(message: types.Message, offer: EscrowOffer):
"""React to sent message while sending full card number to fiat sender."""
if message.from_user.id == offer.init["id"]:
user = offer.counter
else:
user = offer.init
mention = markdown.link(user["mention"], User(id=user["id"]).url)
await tg.send_message(
message.chat.id,
i18n("wrong_full_card_number_receiver {user}").format(user=mention),
parse_mode=ParseMode.MARKDOWN,
)
[docs]@escrow_callback_handler(
lambda call: call.data.startswith("card_sent "), state=states.Escrow.full_card
)
async def full_card_number_sent(call: types.CallbackQuery, offer: EscrowOffer):
"""Confirm that full card number is sent and ask for first and last 4 digits."""
await offer.update_document({"$set": {"pending_input_from": call.from_user.id}})
await call.answer()
if call.from_user.id == offer.init["id"]:
counter = offer.counter
await tg.send_message(
counter["id"], i18n("ask_address {currency}").format(currency=offer.buy)
)
await tg.send_message(
call.message.chat.id,
i18n("exchange_continued {user}").format(
user=markdown.link(counter["mention"], User(id=counter["id"]).url)
),
parse_mode=ParseMode.MARKDOWN,
)
await offer.update_document({"$set": {"pending_input_from": counter["id"]}})
counter_state = FSMContext(dp.storage, counter["id"], counter["id"])
await counter_state.set_state(states.Escrow.receive_address.state)
await states.Escrow.receive_card_number.set()
else:
await tg.send_message(
call.message.chat.id,
i18n("send_first_and_last_4_digits_of_card_number {currency}").format(
currency=offer.sell if offer.type == "buy" else offer.buy
),
)
await states.Escrow.receive_card_number.set()
[docs]@escrow_message_handler(state=states.Escrow.receive_card_number)
async def set_receive_card_number(message: types.Message, offer: EscrowOffer):
"""Create address from first and last 4 digits of card number and ask send address.
First and last 4 digits of card number are sent by fiat receiver,
so their send address is escrow asset address.
"""
card_number = await get_card_number(message.text, message.chat.id)
if not card_number:
return
if message.from_user.id == offer.init["id"]:
user_field = "init"
else:
user_field = "counter"
await offer.update_document(
{"$set": {f"{user_field}.receive_address": ("*" * 8).join(card_number)}}
)
await tg.send_message(
message.chat.id,
i18n("ask_address {currency}").format(currency=offer.escrow),
)
await states.Escrow.send_address.set()
[docs]@escrow_message_handler(state=states.Escrow.receive_address)
async def set_receive_address(message: types.Message, offer: EscrowOffer):
"""Set escrow asset receiver's address and ask for sender's information.
If there is a fiat currency, which is indicated by existing
``bank`` field, and user is a fiat sender, ask their name on card.
Otherwise ask escrow asset sender's address.
"""
if len(message.text) >= 150:
await tg.send_message(
message.chat.id,
i18n("exceeded_character_limit {limit} {sent}").format(
limit=150, sent=len(message.text)
),
)
return
if message.from_user.id == offer.init["id"]:
user_field = "init"
send_currency = offer.buy
ask_name = offer.bank and offer.type == "sell"
else:
user_field = "counter"
send_currency = offer.sell
ask_name = offer.bank and offer.type == "buy"
await offer.update_document(
{"$set": {f"{user_field}.receive_address": message.text}}
)
if ask_name:
await tg.send_message(
message.chat.id,
i18n("send_name_patronymic_surname"),
)
await states.Escrow.name.set()
else:
await tg.send_message(
message.chat.id,
i18n("ask_address {currency}").format(currency=send_currency),
)
await states.Escrow.send_address.set()
[docs]@escrow_message_handler(state=states.Escrow.send_address)
async def set_send_address(message: types.Message, offer: EscrowOffer):
"""Set send address of any party."""
if len(message.text) >= 150:
await tg.send_message(
message.chat.id,
i18n("exceeded_character_limit {limit} {sent}").format(
limit=150, sent=len(message.text)
),
)
return
if message.from_user.id == offer.init["id"]:
await set_init_send_address(message.text, message, offer)
else:
await set_counter_send_address(message.text, message, offer)
[docs]@escrow_message_handler(state=states.Escrow.name)
async def set_name(message: types.Message, offer: EscrowOffer):
"""Set fiat sender's name on card and ask for first and last 4 digits."""
name = message.text.split()
if len(name) != 3:
await tg.send_message(
message.chat.id,
i18n("wrong_word_count {word_count}").format(word_count=3),
)
return
name[2] = name[2][0] + "." # Leaving the first letter of surname with dot
if offer.type == "buy":
user_field = "counter"
currency = offer.sell
else:
user_field = "init"
currency = offer.buy
await offer.update_document(
{"$set": {f"{user_field}.name": " ".join(name).upper()}}
)
await tg.send_message(
message.chat.id,
i18n("send_first_and_last_4_digits_of_card_number {currency}").format(
currency=currency
),
)
await states.Escrow.send_card_number.set()
[docs]@escrow_message_handler(state=states.Escrow.send_card_number)
async def set_send_card_number(message: types.Message, offer: EscrowOffer):
"""Set first and last 4 digits of any party."""
card_number = await get_card_number(message.text, message.chat.id)
if not card_number:
return
address = ("*" * 8).join(card_number)
if message.from_user.id == offer.init["id"]:
await set_init_send_address(address, message, offer)
else:
await set_counter_send_address(address, message, offer)
[docs]async def set_init_send_address(
address: str, message: types.Message, offer: EscrowOffer
):
"""Set ``address`` as sender's address of initiator.
Send offer to counteragent.
"""
locale = offer.counter["locale"]
buy_keyboard = InlineKeyboardMarkup()
buy_keyboard.row(
InlineKeyboardButton(
i18n("show_order", locale=locale), callback_data=f"get_order {offer.order}"
)
)
buy_keyboard.add(
InlineKeyboardButton(
i18n("accept", locale=locale), callback_data=f"accept {offer._id}"
),
InlineKeyboardButton(
i18n("decline", locale=locale), callback_data=f"decline {offer._id}"
),
)
mention = markdown.link(offer.init["mention"], User(id=offer.init["id"]).url)
answer = i18n(
"escrow_offer_notification {user} {sell_amount} {sell_currency} "
"for {buy_amount} {buy_currency}",
locale=locale,
).format(
user=mention,
sell_amount=offer.sum_sell,
sell_currency=offer.sell,
buy_amount=offer.sum_buy,
buy_currency=offer.buy,
)
if offer.bank:
answer += " " + i18n("using {bank}", locale=locale).format(bank=offer.bank)
answer += "."
update_dict = {"init.send_address": address}
if offer.type == "sell":
insured = await get_insurance(offer)
update_dict["insured"] = Decimal128(insured)
if offer[f"sum_{offer.type}"].to_decimal() > insured:
answer += "\n" + i18n("exceeded_insurance {amount} {currency}").format(
amount=insured, currency=offer.escrow
)
await offer.update_document(
{"$set": update_dict, "$unset": {"pending_input_from": True}}
)
await tg.send_message(
offer.counter["id"],
answer,
reply_markup=buy_keyboard,
parse_mode=ParseMode.MARKDOWN,
)
sell_keyboard = InlineKeyboardMarkup()
sell_keyboard.add(
InlineKeyboardButton(i18n("cancel"), callback_data=f"escrow_cancel {offer._id}")
)
await tg.send_message(
message.from_user.id, i18n("offer_sent"), reply_markup=sell_keyboard
)
await dp.current_state().finish()
[docs]@escrow_callback_handler(lambda call: call.data.startswith("accept "))
async def accept_offer(call: types.CallbackQuery, offer: EscrowOffer):
"""React to counteragent accepting offer by asking for fee payment agreement."""
await offer.update_document(
{"$set": {"pending_input_from": call.message.chat.id, "react_time": time()}}
)
await call.answer()
await ask_fee(call.from_user.id, call.message.chat.id, offer)
[docs]@escrow_callback_handler(lambda call: call.data.startswith("decline "))
async def decline_offer(call: types.CallbackQuery, offer: EscrowOffer):
"""React to counteragent declining offer."""
offer.react_time = time()
await offer.delete_document()
await tg.send_message(
offer.init["id"],
i18n("escrow_offer_declined", locale=offer.init["locale"]),
)
await call.answer()
await tg.send_message(call.message.chat.id, i18n("offer_declined"))
[docs]def create_memo(
offer: EscrowOffer,
*,
transfer: bool,
counter_send_address: typing.Optional[str] = None,
):
"""Create memo for transfer."""
if transfer:
template = "from {escrow_send_address} "
else:
template = "to {escrow_receive_address} "
template += (
"for {not_escrow_amount} {not_escrow_currency} "
"from {not_escrow_send_address} to {not_escrow_receive_address} "
"via escrow service on https://t.me/TellerBot"
)
if counter_send_address is None:
counter_send_address = offer.counter["send_address"]
if offer.type == "buy":
return template.format(
**{
"escrow_send_address": offer.init["send_address"],
"escrow_receive_address": offer.counter["receive_address"],
"not_escrow_amount": offer.sum_sell,
"not_escrow_currency": offer.sell,
"not_escrow_send_address": counter_send_address,
"not_escrow_receive_address": offer.init["receive_address"],
}
)
elif offer.type == "sell":
return template.format(
**{
"escrow_send_address": counter_send_address,
"escrow_receive_address": offer.init["receive_address"],
"not_escrow_amount": offer.sum_buy,
"not_escrow_currency": offer.buy,
"not_escrow_send_address": offer.init["send_address"],
"not_escrow_receive_address": offer.counter["receive_address"],
}
)
[docs]@escrow_callback_handler(lambda call: call.data.startswith("check_transaction "))
async def check_transaction(call: types.CallbackQuery, offer: EscrowOffer):
"""Start transaction check."""
if offer.type == "buy":
from_address = offer.init["send_address"]
elif offer.type == "sell":
from_address = offer.counter["send_address"]
escrow_instance = get_escrow_instance(offer.escrow)
await call.answer(i18n("transaction_check_starting"))
success = await escrow_instance.check_transaction(
offer_id=offer._id,
from_address=from_address,
amount_with_fee=offer["sum_fee_up"].to_decimal(),
amount_without_fee=offer[f"sum_{offer.type}"].to_decimal(),
asset=offer.escrow,
memo=offer.memo,
transaction_time=offer.transaction_time,
)
if not success:
await tg.send_message(call.message.chat.id, i18n("transaction_not_found"))
[docs]async def set_counter_send_address(
address: str, message: types.Message, offer: EscrowOffer
):
"""Set ``address`` as sender's address of counteragent.
Ask for escrow asset transfer.
"""
memo = create_memo(offer, transfer=False, counter_send_address=address)
if offer.type == "buy":
escrow_user = offer.init
from_address = offer.init["send_address"]
send_reply = True
elif offer.type == "sell":
escrow_user = offer.counter
from_address = address
send_reply = False
keyboard = InlineKeyboardMarkup()
escrow_instance = get_escrow_instance(offer.escrow)
transaction_time = time()
if isinstance(escrow_instance, StreamBlockchain):
await escrow_instance.add_to_queue(
offer_id=offer._id,
from_address=from_address,
amount_with_fee=offer["sum_fee_up"].to_decimal(),
amount_without_fee=offer[f"sum_{offer.type}"].to_decimal(),
asset=offer.escrow,
memo=memo,
transaction_time=transaction_time,
)
else:
keyboard.add(
InlineKeyboardButton(
i18n("check", locale=escrow_user["locale"]),
callback_data=f"check_transaction {offer._id}",
)
)
keyboard.add(
InlineKeyboardButton(
i18n("cancel", locale=escrow_user["locale"]),
callback_data=f"escrow_cancel {offer._id}",
)
)
escrow_address = markdown.bold(escrow_instance.address)
answer = i18n(
"send {amount} {currency} {address}", locale=escrow_user["locale"]
).format(amount=offer.sum_fee_up, currency=offer.escrow, address=escrow_address)
answer += " " + i18n("with_memo", locale=escrow_user["locale"])
answer += ":\n" + markdown.code(memo)
await tg.send_message(
escrow_user["id"], answer, reply_markup=keyboard, parse_mode=ParseMode.MARKDOWN
)
if send_reply:
keyboard = InlineKeyboardMarkup()
keyboard.add(
InlineKeyboardButton(
i18n("cancel"), callback_data=f"escrow_cancel {offer._id}"
)
)
await tg.send_message(
message.chat.id,
i18n("transfer_information_sent")
+ " "
+ i18n("transaction_completion_notification_promise"),
reply_markup=keyboard,
parse_mode=ParseMode.MARKDOWN,
)
update = {
"counter.send_address": address,
"transaction_time": transaction_time,
"memo": memo,
}
await offer.update_document(
{"$set": update, "$unset": {"pending_input_from": True}}
)
await dp.current_state().finish()
[docs]@escrow_callback_handler(lambda call: call.data.startswith("escrow_cancel "))
async def cancel_offer(call: types.CallbackQuery, offer: EscrowOffer):
"""React to offer cancellation.
While first party is transferring, second party can't cancel offer,
because we can't be sure that first party hasn't already completed
transfer before confirming.
"""
if offer.trx_id:
return await call.answer(i18n("cancel_after_transfer"))
if offer.memo:
if offer.type == "buy":
escrow_user = offer.init
elif offer.type == "sell":
escrow_user = offer.counter
if call.from_user.id != escrow_user["id"]:
return await call.answer(i18n("cancel_before_verification"))
escrow_instance = get_escrow_instance(offer.escrow)
if isinstance(escrow_instance, StreamBlockchain):
escrow_instance.remove_from_queue(offer._id)
sell_answer = i18n("escrow_cancelled", locale=offer.init["locale"])
buy_answer = i18n("escrow_cancelled", locale=offer.counter["locale"])
offer.cancel_time = time()
await offer.delete_document()
await call.answer()
await tg.send_message(offer.init["id"], sell_answer, reply_markup=start_keyboard())
await tg.send_message(
offer.counter["id"], buy_answer, reply_markup=start_keyboard()
)
sell_state = FSMContext(dp.storage, offer.init["id"], offer.init["id"])
buy_state = FSMContext(dp.storage, offer.counter["id"], offer.counter["id"])
await sell_state.finish()
await buy_state.finish()
[docs]async def edit_keyboard(
offer_id: ObjectId, chat_id: int, message_id: int, keyboard: InlineKeyboardMarkup
):
"""Edit inline keyboard markup of message.
:param offer_id: Primary key value of offer document connected with message.
:param chat_id: Telegram chat ID of message.
:param message_id: Telegram ID of message.
:param keyboard: New inline keyboard markup.
"""
offer_document = await database.escrow.find_one({"_id": offer_id})
if offer_document:
await tg.edit_message_reply_markup(chat_id, message_id, reply_markup=keyboard)
[docs]@escrow_callback_handler(lambda call: call.data.startswith("tokens_sent "))
async def final_offer_confirmation(call: types.CallbackQuery, offer: EscrowOffer):
"""Ask not escrow asset receiver to confirm transfer."""
if not offer.unsent:
await call.answer(i18n("transfer_already_confirmed"))
return
await offer.update_document({"$unset": {"unsent": True}})
if offer.type == "buy":
confirm_user = offer.init
other_user = offer.counter
currency = offer.sell
elif offer.type == "sell":
confirm_user = offer.counter
other_user = offer.init
currency = offer.buy
keyboard = InlineKeyboardMarkup()
keyboard.add(
InlineKeyboardButton(
i18n("yes", locale=confirm_user["locale"]),
callback_data=f"escrow_complete {offer._id}",
)
)
reply = await tg.send_message(
confirm_user["id"],
i18n(
"receiving_confirmation {currency} {user}", locale=confirm_user["locale"]
).format(currency=currency, user=other_user["send_address"]),
reply_markup=keyboard,
)
keyboard.add(
InlineKeyboardButton(
i18n("no", locale=confirm_user["locale"]),
callback_data=f"escrow_validate {offer._id}",
)
)
await call_later(
60 * 10,
edit_keyboard,
offer._id,
confirm_user["id"],
reply.message_id,
keyboard,
)
await call.answer()
await tg.send_message(
other_user["id"],
i18n(
"complete_escrow_promise",
locale=other_user["locale"],
),
reply_markup=start_keyboard(),
)
[docs]async def add_cashback(
currency, amount, sum_fee_up, sum_fee_down, sender_user, recipient_user
):
"""Create cashback documents from escrow exchange."""
fees = (
(sum_fee_up - amount, sender_user, sender_user["send_address"]),
(amount - sum_fee_down, recipient_user, recipient_user["receive_address"]),
)
cashback = []
current_time = time()
for fee, user, user_address in fees:
if not fee:
continue
categories = (
(rs.PERSONAL_CATEGORY, user["id"], user_address),
(rs.REFERRED_CATEGORY, user.get("referrer"), None),
(rs.REFERRED_BY_REFERALS_CATEGORY, user.get("referrer_of_referrer"), None),
)
for category, user_id, address in categories:
if not user_id:
break
count = await database.users.count_documents({"referrer": user_id})
if not count:
continue
document = {
"id": user_id,
"currency": currency,
"amount": rs.bonus_coefficient(category, count) * fee,
"time": current_time,
}
if address:
document["address"] = address
cashback.append(document)
if cashback:
await database.cashback.insert_many(cashback)
[docs]@escrow_callback_handler(lambda call: call.data.startswith("escrow_complete "))
@dp.async_task
async def complete_offer(call: types.CallbackQuery, offer: EscrowOffer):
"""Release escrow asset and finish exchange."""
if offer.type == "buy":
recipient_user = offer.counter
sender_user = offer.init
amount = offer.sum_buy.to_decimal() # type: ignore
elif offer.type == "sell":
recipient_user = offer.init
sender_user = offer.counter
amount = offer.sum_sell.to_decimal() # type: ignore
sum_fee_up = offer.sum_fee_up.to_decimal() # type: ignore
sum_fee_down = offer.sum_fee_down.to_decimal() # type: ignore
await call.answer(i18n("escrow_completing"))
escrow_instance = get_escrow_instance(offer.escrow)
trx_url = await escrow_instance.transfer(
recipient_user["receive_address"],
sum_fee_down,
offer.escrow,
memo=create_memo(offer, transfer=True),
)
if sender_user["send_address"] != recipient_user["receive_address"]:
add_cashback(
offer.escrow, amount, sum_fee_up, sum_fee_down, sender_user, recipient_user
)
answer = i18n("escrow_completed", locale=sender_user["locale"])
recipient_answer = i18n("escrow_completed", locale=recipient_user["locale"])
recipient_answer += " " + markdown.link(
i18n("escrow_sent {amount} {currency}", locale=recipient_user["locale"]).format(
amount=amount, currency=offer.escrow
),
trx_url,
)
await offer.delete_document()
await tg.send_message(
recipient_user["id"],
recipient_answer,
reply_markup=start_keyboard(),
parse_mode=ParseMode.MARKDOWN,
)
await tg.send_message(sender_user["id"], answer, reply_markup=start_keyboard())
[docs]@escrow_callback_handler(lambda call: call.data.startswith("escrow_validate "))
async def validate_offer(call: types.CallbackQuery, offer: EscrowOffer):
"""Ask support for manual verification of exchange."""
if offer.type == "buy":
sender = offer.counter
receiver = offer.init
currency = offer.sell
elif offer.type == "sell":
sender = offer.init
receiver = offer.counter
currency = offer.buy
escrow_instance = get_escrow_instance(offer.escrow)
answer = "{0}\n{1} sender: {2}{3}\n{1} receiver: {4}{5}\nBank: {6}\nMemo: {7}"
answer = answer.format(
markdown.link("Unconfirmed escrow.", escrow_instance.trx_url(offer.trx_id)),
currency,
markdown.link(sender["mention"], User(id=sender["id"]).url),
" ({})".format(sender["name"]) if "name" in sender else "",
markdown.link(receiver["mention"], User(id=receiver["id"]).url),
" ({})".format(receiver["name"]) if "name" in receiver else "",
offer.bank,
markdown.code(offer.memo),
)
await tg.send_message(config.SUPPORT_CHAT_ID, answer, parse_mode=ParseMode.MARKDOWN)
await offer.delete_document()
await call.answer()
await tg.send_message(
call.message.chat.id,
i18n("request_validation_promise"),
reply_markup=start_keyboard(),
)