Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion src/pumpfun_cli/core/cpmm.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand Down
6 changes: 5 additions & 1 deletion src/pumpfun_cli/protocol/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""

Expand Down Expand Up @@ -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
Expand Down
16 changes: 16 additions & 0 deletions tests/test_commands/test_trade_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
11 changes: 11 additions & 0 deletions tests/test_core/test_cpmm.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
4 changes: 3 additions & 1 deletion tests/test_surfpool/test_trade.py
Original file line number Diff line number Diff line change
Expand Up @@ -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("<Q", data, 32, 10_000_000) # real_quote_reserves: 0.01 SOL
async with httpx.AsyncClient() as http:
await http.post(
resp = await http.post(
surfpool_rpc,
json={
"jsonrpc": "2.0",
Expand All @@ -193,6 +193,8 @@ async def test_sell_caps_at_the_curves_real_reserves(
"params": [str(curve_pda), {"data": bytes(data).hex()}],
},
)
resp.raise_for_status()
assert "error" not in resp.json(), resp.json()
curve = parse_bonding_curve(bytes((await client.get_account_info(curve_pda)).value.data))
assert curve["real_quote_reserves"] == 10_000_000

Expand Down
Loading