diff --git a/.github/pyproject.toml b/.github/pyproject.toml index a86d6072..ff8c5dad 100644 --- a/.github/pyproject.toml +++ b/.github/pyproject.toml @@ -19,11 +19,11 @@ asyncpg = "*" base58 = "*" black = "*" mypy = "*" -openai = "*" +openai = "^0.20.0" phonenumbers = "*" prometheus_async = "*" prometheus_client = "*" -protobuf = "*" +protobuf = "^3" pycryptodome = "*" pylint = "*" PyQRCode = "^1.2.1" @@ -67,7 +67,7 @@ disable= [ "consider-using-with", "consider-using-from-import", "fixme", - "no-self-use", + # "no-self-use", "unspecified-encoding", # handled by black "format", diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index a92ed091..345277b0 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -21,7 +21,7 @@ jobs: key: '${{ runner.os }}-poetry-v2-${{ hashFiles(''**/poetry.lock'') }}' - name: Install dependencies if: steps.cache-poetry.outputs.cache-hit != 'true' - run: cp .github/pyproject.toml .; rm poetry.lock; poetry install + run: cp .github/pyproject.toml .; rm poetry.lock; poetry run pip install --upgrade pip; poetry install - name: List dependency versions (check this if you get confusing results) run: poetry show --tree # - name: Setup tmate session # this lets you SSH into the container diff --git a/forest/core.py b/forest/core.py index 9d8aa948..4d138991 100755 --- a/forest/core.py +++ b/forest/core.py @@ -351,6 +351,10 @@ async def enqueue_blob_messages(self, blob: JSON) -> None: if "result" in blob: if isinstance(blob["result"], dict): message_blob = blob + elif isinstance(blob["result"], list): + # Message expects a dict with an id, but signal-cli listContacts returns a list + # we're usually only get a single contact, so we massage this + message_blob = {"id": blob["id"], "result": blob["result"][0]} else: logging.warning(blob["result"]) if "error" in blob: @@ -1049,6 +1053,17 @@ async def do_uptime(self, _: Message) -> str: return t +def compose_payment_content(receipt: str, note: str) -> dict: + # serde expects bytes to be u8[], not b64 + tx = {"mobileCoin": {"receipt": u8(receipt)}} + note = note or "check out this java-free payment notification" + payment = {"Item": {"notification": {"note": note, "Transaction": tx}}} + # SignalServiceMessageContent protobuf represented as JSON (spicy) + # destination is outside the content so it doesn't matter, + # but it does contain the bot's profileKey + return {"dataMessage": {"body": None, "payment": payment}} + + class PayBot(ExtrasBot): @requires_admin async def do_fsr(self, msg: Message) -> Response: @@ -1129,9 +1144,19 @@ async def payment_response(self, msg: Message, amount_pmob: int) -> Response: async def get_signalpay_address(self, recipient: str) -> Optional[str]: "get a receipient's mobilecoin address" - result = await self.signal_rpc_request("getPayAddress", peer_name=recipient) + if utils.AUXIN: + result = await self.signal_rpc_request("getPayAddress", peer_name=recipient) + b64_address = ( + result.blob.get("Address", {}) + .get("mobileCoinAddress", {}) + .get("address") + ) + if result.error or not b64_address: + logging.info("bad address: %s", result.blob) + return None + result = await self.signal_rpc_request("listContacts", recipient=recipient) b64_address = ( - result.blob.get("Address", {}).get("mobileCoinAddress", {}).get("address") + result.blob.get("result", {}).get("profile", {}).get("mobileCoinAddress") ) if result.error or not b64_address: logging.info("bad address: %s", result.blob) @@ -1207,6 +1232,39 @@ async def build_gift_code(self, amount_pmob: int) -> list[str]: f"redeemable for {str(mc_util.pmob2mob(amount_pmob - FEE_PMOB)).rstrip('0')} MOB", ] + async def confirm_tx_timeout(self, tx_id: str, recipient: str, timeout: int) -> str: + logging.debug("Attempting to confirm tx status for %s", recipient) + status = "tx_status_pending" + for i in range(timeout): + tx_status = await self.mob_request( + "get_transaction_log", transaction_log_id=tx_id + ) + status = ( + tx_status.get("result", {}).get("transaction_log", {}).get("status") + ) + if status == "tx_status_succeeded": + logging.info( + "Tx to %s suceeded - tx data: %s", + recipient, + tx_status.get("result"), + ) + break + if status == "tx_status_failed": + logging.warning( + "Tx to %s failed - tx data: %s", + recipient, + tx_status.get("result"), + ) + break + await asyncio.sleep(1) + if status == "tx_status_pending": + logging.warning( + "Tx to %s timed out - tx data: %s", + recipient, + tx_status.get("result"), + ) + return status + # FIXME: clarify signature and return details/docs async def send_payment( # pylint: disable=too-many-locals self, @@ -1272,51 +1330,34 @@ async def send_payment( # pylint: disable=too-many-locals msg.status, msg.transaction_log_id = "tx_status_failed", tx_id return msg - receipt_resp = await self.mob_request( - "create_receiver_receipts", - tx_proposal=prop, - account_id=await self.mobster.get_account(), - ) - content = await self.fs_receipt_to_payment_message_content( - receipt_resp, receipt_message - ) - # pass our beautifully composed JSON content to auxin. - # message body is ignored in this case. - payment_notif = await self.send_message(recipient, "", content=content) - resp_future = asyncio.create_task(self.wait_for_response(rpc_id=payment_notif)) - - if confirm_tx_timeout: - logging.debug("Attempting to confirm tx status for %s", recipient) - status = "tx_status_pending" - for i in range(confirm_tx_timeout): - tx_status = await self.mob_request( - "get_transaction_log", transaction_log_id=tx_id - ) - status = ( - tx_status.get("result", {}).get("transaction_log", {}).get("status") - ) - if status == "tx_status_succeeded": - logging.info( - "Tx to %s suceeded - tx data: %s", - recipient, - tx_status.get("result"), - ) - break - if status == "tx_status_failed": - logging.warning( - "Tx to %s failed - tx data: %s", - recipient, - tx_status.get("result"), - ) - break - await asyncio.sleep(1) - - if status == "tx_status_pending": - logging.warning( - "Tx to %s timed out - tx data: %s", - recipient, - tx_status.get("result"), + full_service_receipt = ( + await self.mob_request( + "create_receiver_receipts", + tx_proposal=prop, + account_id=account_id, + ) + )["result"]["receiver_receipts"][0] + # this gets us a Receipt protobuf + b64_receipt = mc_util.full_service_receipt_to_b64_receipt(full_service_receipt) + if utils.AUXIN: + content = compose_payment_content(b64_receipt, receipt_message) + # pass our beautifully composed JSON content to auxin. + # message body is ignored in this case. + payment_notif = await self.send_message(recipient, "", content=content) + resp_future = asyncio.create_task( + self.wait_for_response(rpc_id=payment_notif) + ) + else: + resp_future = asyncio.create_task( + self.signal_rpc_request( + "sendPaymentNotification", + receipt=b64_receipt, + note=receipt_message, + recipient=recipient, ) + ) + if confirm_tx_timeout: + status = await self.confirm_tx_timeout(tx_id, recipient, confirm_tx_timeout) resp = await resp_future # the calling function can use these to check the payment status resp.status, resp.transaction_log_id = status, tx_id # type: ignore diff --git a/forest/payments_monitor.py b/forest/payments_monitor.py index f66ca749..98eca91b 100644 --- a/forest/payments_monitor.py +++ b/forest/payments_monitor.py @@ -156,7 +156,7 @@ async def split_txos_slow( built = 0 i = 0 utxos: list[tuple[str, int]] = list(reversed((await self.get_utxos()).items())) - if sum([value for _, value in utxos]) < output_millimob * 1e9 * target_quantity: + if sum(value for _, value in utxos) < output_millimob * 1e9 * target_quantity: return "insufficient MOB" while built < (target_quantity + 3): if len(utxos) < 1: diff --git a/forest/pghelp.py b/forest/pghelp.py index 806250e0..e806b815 100644 --- a/forest/pghelp.py +++ b/forest/pghelp.py @@ -150,7 +150,7 @@ def finish_init(self) -> None: for k in self.queries: if AUTOCREATE and "create" in k and "index" in k: self.logger.info(f"creating index via {k}") - self.__getattribute__(f"sync_{k}")() + getattr(self, f"sync_{k}")() _autocreating_table = False diff --git a/hotline/hotline.py b/hotline/hotline.py index 725d1aef..46d7ca98 100644 --- a/hotline/hotline.py +++ b/hotline/hotline.py @@ -549,6 +549,7 @@ async def do_help(self, msg: Message) -> Response: @hide async def do_remove(self, msg: Message) -> Response: """Removes a given event by code, if the msg.uuid is the owner.""" + # pylint: disable=unnecessary-dunder-call if not await self.check_user_owns(msg.uuid, msg.arg1 or ""): return f"Sorry, it doesn't look like you own {msg.arg1}." parameters = [] diff --git a/pyproject.toml b/pyproject.toml index 4fc1351e..fd501e87 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,7 +63,7 @@ disable= [ "consider-using-with", "consider-using-from-import", "fixme", - "no-self-use", + # "no-self-use", "unspecified-encoding", # handled by black "format",