From 7fce86b3f59f864da10cbcee8b71b8cec8c544e1 Mon Sep 17 00:00:00 2001 From: Anton Sauchyk Date: Mon, 9 Nov 2026 10:19:20 +0100 Subject: [PATCH] fix(cpmm): keep a sent but unconfirmed trade's signature under --force-amm Co-Authored-By: Claude Opus 5.5 --- src/pumpfun_cli/core/cpmm.py | 15 ++++++++++++++- src/pumpfun_cli/protocol/client.py | 6 +++++- tests/test_commands/test_trade_cmd.py | 16 ++++++++++++++++ tests/test_core/test_cpmm.py | 11 +++++++++++ tests/test_surfpool/test_trade.py | 4 +++- 5 files changed, 49 insertions(+), 3 deletions(-) diff --git a/src/pumpfun_cli/core/cpmm.py b/src/pumpfun_cli/core/cpmm.py index 8d20176..7e7d5b7 100644 --- a/src/pumpfun_cli/core/cpmm.py +++ b/src/pumpfun_cli/core/cpmm.py @@ -21,7 +21,11 @@ from pumpfun_cli.core.validate import invalid_pubkey_error, parse_pubkey from pumpfun_cli.crypto import decrypt_keypair from pumpfun_cli.protocol.address import find_ata -from pumpfun_cli.protocol.client import RpcClient, TransactionFailedError +from pumpfun_cli.protocol.client import ( + RpcClient, + TransactionFailedError, + TransactionUnconfirmedError, +) from pumpfun_cli.protocol.cpmm import ( CPMM_SLIPPAGE_ERROR_CODES, FEE_RATE_DENOMINATOR, @@ -256,6 +260,9 @@ async def buy_cpmm( return result except _NoPool as exc: return exc.result + except TransactionUnconfirmedError as exc: + # Sent, so the message's signature must reach the user. + return {"error": "unconfirmed", "message": str(exc)} except (RuntimeError, ValueError) as exc: return {"error": "cpmm_error", "message": str(exc)} finally: @@ -377,6 +384,9 @@ async def sell_cpmm( return result except _NoPool as exc: return exc.result + except TransactionUnconfirmedError as exc: + # Sent, so the message's signature must reach the user. + return {"error": "unconfirmed", "message": str(exc)} except (RuntimeError, ValueError) as exc: return {"error": "cpmm_error", "message": str(exc)} finally: @@ -400,6 +410,9 @@ async def get_cpmm_info(rpc_url: str, mint_str: str, rpc_timeout: float = 30.0) } except _NoPool as exc: return exc.result + except TransactionUnconfirmedError as exc: + # Sent, so the message's signature must reach the user. + return {"error": "unconfirmed", "message": str(exc)} except (RuntimeError, ValueError) as exc: return {"error": "cpmm_error", "message": str(exc)} finally: diff --git a/src/pumpfun_cli/protocol/client.py b/src/pumpfun_cli/protocol/client.py index f9d20db..09131e6 100644 --- a/src/pumpfun_cli/protocol/client.py +++ b/src/pumpfun_cli/protocol/client.py @@ -75,6 +75,10 @@ def build_message( ) +class TransactionUnconfirmedError(RuntimeError): + """Raised when a sent transaction can't be confirmed; it may still land.""" + + class TransactionFailedError(RuntimeError): """Raised when a confirmed transaction fails on-chain.""" @@ -182,7 +186,7 @@ async def _send_and_confirm(self, tx: VersionedTransaction, confirm: bool) -> st ) status = await self.get_transaction(signature) except Exception as exc: # noqa: BLE001 - the send already happened - raise RuntimeError( + raise TransactionUnconfirmedError( f"Sent {signature} but could not confirm it ({type(exc).__name__}). " f"Check it with: pumpfun tx-status {signature}" ) from None diff --git a/tests/test_commands/test_trade_cmd.py b/tests/test_commands/test_trade_cmd.py index ec1e64d..3ec05ee 100644 --- a/tests/test_commands/test_trade_cmd.py +++ b/tests/test_commands/test_trade_cmd.py @@ -1229,3 +1229,19 @@ def test_force_amm_on_a_pump_coin_never_tries_cpmm(tmp_path, monkeypatch, amm_re tmp_path, monkeypatch, "buy", ["--force-amm", _FAKE_MINT, "0.01"], {"amm": amm_result} ) mocks["cpmm"].assert_not_called() + + +def test_force_amm_reports_an_unconfirmed_cpmm_trade(tmp_path, monkeypatch): + """A CPMM trade that was sent but not confirmed keeps its signature.""" + out, _ = _run_routed( + tmp_path, + monkeypatch, + "buy", + ["--force-amm", _FAKE_MINT, "0.01"], + { + "amm": _NO_PUMPSWAP_POOL, + "cpmm": {"error": "unconfirmed", "message": "Sent abc123 but could not confirm it"}, + }, + ) + assert out.exit_code == 1 + assert "abc123" in out.output diff --git a/tests/test_core/test_cpmm.py b/tests/test_core/test_cpmm.py index 0a80dce..49e3d76 100644 --- a/tests/test_core/test_cpmm.py +++ b/tests/test_core/test_cpmm.py @@ -181,3 +181,14 @@ async def test_token_info_shows_the_cpmm_pool_of_a_graduated_coin(): assert info["launchlab_pool"] == str(roles["launchlab_pool"]) assert info["pool_address"] == str(roles["cpmm_pool"]) assert info["base_reserves"] == pool["base_reserve"] / 10**6 + + +async def test_a_sent_but_unconfirmed_trade_keeps_its_signature(tmp_keystore): + from pumpfun_cli.protocol.client import TransactionUnconfirmedError + + client, roles, _ = _chain(SOL_FIXTURE) + client.send_error = TransactionUnconfirmedError("Sent abc123 but could not confirm it") + mint = str(roles["base_mint"]) + result = await _call(client, buy_cpmm, "http://rpc", tmp_keystore, "testpass", mint, 0.01) + assert result["error"] == "unconfirmed" + assert "abc123" in result["message"] diff --git a/tests/test_surfpool/test_trade.py b/tests/test_surfpool/test_trade.py index 96856a5..3c3d666 100644 --- a/tests/test_surfpool/test_trade.py +++ b/tests/test_surfpool/test_trade.py @@ -184,7 +184,7 @@ async def test_sell_caps_at_the_curves_real_reserves( data = bytearray((await client.get_account_info(curve_pda)).value.data) struct.pack_into("