diff --git a/protocols/morpho/governance_v2.py b/protocols/morpho/governance_v2.py index c53804b..16934bb 100644 --- a/protocols/morpho/governance_v2.py +++ b/protocols/morpho/governance_v2.py @@ -244,11 +244,28 @@ def _format_ts(ts: int) -> str: return datetime.fromtimestamp(ts).strftime("%Y-%m-%d %H:%M:%S") +def _format_countdown(ts: int) -> str: + """Human-friendly time remaining until ``ts``, e.g. ``(3 days)``.""" + seconds = ts - int(datetime.now().timestamp()) + if seconds <= 0: + return "(now)" + days = seconds / 86400 + if days >= 1: + n = round(days) + return f"({n} day{'s' if n != 1 else ''})" + hours = seconds / 3600 + if hours >= 1: + n = round(hours) + return f"({n} hour{'s' if n != 1 else ''})" + n = max(round(seconds / 60), 1) + return f"({n} minute{'s' if n != 1 else ''})" + + def _explorer_link(chain: Chain, tx_hash: str) -> str: base = chain.explorer_url if not base or not tx_hash: return tx_hash - return f"[{tx_hash[:10]}…]({base}/tx/{tx_hash})" + return f"[{tx_hash}…]({base}/tx/{tx_hash})" def _operation_label(snapshot: V2GovernanceSnapshot, pc: PendingConfig) -> str: @@ -277,7 +294,7 @@ def _alert_pending_new(snapshot: V2GovernanceSnapshot, pc: PendingConfig, operat f"⏳ V2 [{snapshot.name}]({get_vault_url(snapshot.address, snapshot.chain)}) " f"on {snapshot.chain.name}\n" f"📥 Submitted: {operation_label}\n" - f"⏰ Executable at: {_format_ts(pc.valid_at)}\n" + f"⏰ Executable at: {_format_ts(pc.valid_at)} {_format_countdown(pc.valid_at)}\n" f"🔗 Tx: {_explorer_link(snapshot.chain, pc.tx_hash)}", PROTOCOL, ) diff --git a/protocols/morpho/v2_decoders.py b/protocols/morpho/v2_decoders.py index 3399d54..fa1ffa1 100644 --- a/protocols/morpho/v2_decoders.py +++ b/protocols/morpho/v2_decoders.py @@ -186,11 +186,13 @@ def _format_args(sig: str, args: tuple[Any, ...], chain: Chain | None = None) -> """Render a decoded argument tuple per signature.""" name = _function_name(sig) - if name in ("addAdapter", "removeAdapter"): - return f"adapter {_format_address(args[0])}" + if name == "removeAdapter": + return "" + if name == "addAdapter": + return f"{_format_address(args[0])}" if name == "setIsAllocator": addr, flag = args - return f"allocator {_format_address(addr)} = {bool(flag)}" + return f"{_format_address(addr)} = {bool(flag)}" if name in ( "setReceiveSharesGate", "setSendSharesGate", diff --git a/tests/test_morpho_v2_decoders.py b/tests/test_morpho_v2_decoders.py index 5dc8f82..88c03be 100644 --- a/tests/test_morpho_v2_decoders.py +++ b/tests/test_morpho_v2_decoders.py @@ -61,21 +61,21 @@ def test_add_adapter(self): data = _build("addAdapter(address)", ["address"], [A1]) self.assertEqual( decode_submit(data), - f"addAdapter(adapter {Web3.to_checksum_address(A1)})", + f"addAdapter({Web3.to_checksum_address(A1)})", ) def test_remove_adapter(self): data = _build("removeAdapter(address)", ["address"], [A2]) self.assertEqual( decode_submit(data), - f"removeAdapter(adapter {Web3.to_checksum_address(A2)})", + "removeAdapter()", ) def test_set_is_allocator_grant(self): data = _build("setIsAllocator(address,bool)", ["address", "bool"], [A1, True]) self.assertEqual( decode_submit(data), - f"setIsAllocator(allocator {Web3.to_checksum_address(A1)} = True)", + f"setIsAllocator({Web3.to_checksum_address(A1)} = True)", ) def test_set_is_allocator_revoke(self):