Skip to content

Commit

Permalink
signal-cli payments!!! (#217)
Browse files Browse the repository at this point in the history
* add sendPaymentNotification

* listContacts is a dict?

* added signal-cli version of get_signalpay_address

* typo in sendPaymentNotification, sends mob

* ignore/fix some errors

* refactor confirm_tx_timeout

* fix more checks

* upgrade pip in ci?

* improve comments and debug ci pip version

* upgrade pip inside poetry?

* specify openai version

* fix

Co-authored-by: Olivia Schaefer <taygetea@gmail.com>
  • Loading branch information
technillogue and taygetea authored Jun 29, 2022
1 parent 5cc197c commit 8730825
Show file tree
Hide file tree
Showing 7 changed files with 95 additions and 53 deletions.
6 changes: 3 additions & 3 deletions .github/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
133 changes: 87 additions & 46 deletions forest/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion forest/payments_monitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion forest/pghelp.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions hotline/hotline.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = []
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down

0 comments on commit 8730825

Please sign in to comment.