@@ -756,3 +756,313 @@ def test_buy_json_output_has_expected_keys(tmp_path, monkeypatch):
756756 "explorer" ,
757757 }
758758 assert expected_keys .issubset (data .keys ())
759+
760+
761+ # --- auto-routing command-layer tests ---
762+
763+
764+ def _setup_wallet (tmp_path , monkeypatch ):
765+ """Create a wallet and set env vars for command-layer tests."""
766+ monkeypatch .setenv ("XDG_CONFIG_HOME" , str (tmp_path ))
767+ monkeypatch .setenv ("PUMPFUN_PASSWORD" , "testpass" )
768+
769+ from solders .keypair import Keypair
770+
771+ from pumpfun_cli .crypto import encrypt_keypair
772+
773+ config_dir = tmp_path / "pumpfun-cli"
774+ config_dir .mkdir ()
775+ encrypt_keypair (Keypair (), "testpass" , config_dir / "wallet.enc" )
776+
777+
778+ def test_buy_graduated_fallback_forwards_slippage (tmp_path , monkeypatch ):
779+ """--slippage 5 forwarded to buy_pumpswap on graduated fallback."""
780+ _setup_wallet (tmp_path , monkeypatch )
781+
782+ with (
783+ patch ("pumpfun_cli.commands.trade.buy_token" , new_callable = AsyncMock ) as mock_buy ,
784+ patch ("pumpfun_cli.commands.trade.buy_pumpswap" , new_callable = AsyncMock ) as mock_pumpswap ,
785+ ):
786+ mock_buy .return_value = {"error" : "graduated" , "message" : "Token has graduated." }
787+ mock_pumpswap .return_value = {
788+ "action" : "buy" ,
789+ "venue" : "pumpswap" ,
790+ "mint" : _FAKE_MINT ,
791+ "sol_spent" : 0.01 ,
792+ "tokens_received" : 100.0 ,
793+ "signature" : "sig" ,
794+ "explorer" : "https://solscan.io/tx/sig" ,
795+ }
796+
797+ result = runner .invoke (
798+ app ,
799+ ["--json" , "--rpc" , "http://rpc" , "buy" , "--slippage" , "5" , _FAKE_MINT , "0.01" ],
800+ )
801+
802+ assert result .exit_code == 0
803+ call_args = mock_pumpswap .call_args
804+ assert call_args [0 ][5 ] == 5 or call_args .kwargs .get ("slippage" ) == 5
805+
806+
807+ def test_sell_graduated_fallback_forwards_slippage (tmp_path , monkeypatch ):
808+ """--slippage 5 forwarded to sell_pumpswap on graduated fallback."""
809+ _setup_wallet (tmp_path , monkeypatch )
810+
811+ with (
812+ patch ("pumpfun_cli.commands.trade.sell_token" , new_callable = AsyncMock ) as mock_sell ,
813+ patch ("pumpfun_cli.commands.trade.sell_pumpswap" , new_callable = AsyncMock ) as mock_pumpswap ,
814+ ):
815+ mock_sell .return_value = {"error" : "graduated" , "message" : "Token has graduated." }
816+ mock_pumpswap .return_value = {
817+ "action" : "sell" ,
818+ "venue" : "pumpswap" ,
819+ "mint" : _FAKE_MINT ,
820+ "sol_received" : 0.01 ,
821+ "tokens_sold" : 100.0 ,
822+ "signature" : "sig" ,
823+ "explorer" : "https://solscan.io/tx/sig" ,
824+ }
825+
826+ result = runner .invoke (
827+ app ,
828+ ["--json" , "--rpc" , "http://rpc" , "sell" , "--slippage" , "5" , _FAKE_MINT , "all" ],
829+ )
830+
831+ assert result .exit_code == 0
832+ call_args = mock_pumpswap .call_args
833+ assert call_args [0 ][5 ] == 5 or call_args .kwargs .get ("slippage" ) == 5
834+
835+
836+ def test_buy_graduated_fallback_forwards_priority_fee (tmp_path , monkeypatch ):
837+ """--priority-fee + --compute-units forwarded to buy_pumpswap on graduated fallback."""
838+ _setup_wallet (tmp_path , monkeypatch )
839+
840+ with (
841+ patch ("pumpfun_cli.commands.trade.buy_token" , new_callable = AsyncMock ) as mock_buy ,
842+ patch ("pumpfun_cli.commands.trade.buy_pumpswap" , new_callable = AsyncMock ) as mock_pumpswap ,
843+ ):
844+ mock_buy .return_value = {"error" : "graduated" , "message" : "Token has graduated." }
845+ mock_pumpswap .return_value = {
846+ "action" : "buy" ,
847+ "venue" : "pumpswap" ,
848+ "mint" : _FAKE_MINT ,
849+ "sol_spent" : 0.01 ,
850+ "tokens_received" : 100.0 ,
851+ "signature" : "sig" ,
852+ "explorer" : "https://solscan.io/tx/sig" ,
853+ }
854+
855+ result = runner .invoke (
856+ app ,
857+ [
858+ "--json" ,
859+ "--rpc" ,
860+ "http://rpc" ,
861+ "--priority-fee" ,
862+ "55000" ,
863+ "--compute-units" ,
864+ "350000" ,
865+ "buy" ,
866+ _FAKE_MINT ,
867+ "0.01" ,
868+ ],
869+ )
870+
871+ assert result .exit_code == 0
872+ call_kwargs = mock_pumpswap .call_args .kwargs
873+ assert call_kwargs .get ("priority_fee" ) == 55000
874+ assert call_kwargs .get ("compute_units" ) == 350000
875+
876+
877+ def test_buy_graduated_fallback_forwards_dry_run (tmp_path , monkeypatch ):
878+ """--dry-run forwarded to buy_pumpswap on graduated fallback."""
879+ _setup_wallet (tmp_path , monkeypatch )
880+
881+ with (
882+ patch ("pumpfun_cli.commands.trade.buy_token" , new_callable = AsyncMock ) as mock_buy ,
883+ patch ("pumpfun_cli.commands.trade.buy_pumpswap" , new_callable = AsyncMock ) as mock_pumpswap ,
884+ ):
885+ mock_buy .return_value = {"error" : "graduated" , "message" : "Token has graduated." }
886+ mock_pumpswap .return_value = {
887+ "dry_run" : True ,
888+ "action" : "buy" ,
889+ "venue" : "pumpswap" ,
890+ "mint" : _FAKE_MINT ,
891+ "sol_in" : 0.01 ,
892+ "expected_tokens" : 100.0 ,
893+ "effective_price_sol" : 0.0001 ,
894+ "spot_price_sol" : 0.00009 ,
895+ "price_impact_pct" : 1.0 ,
896+ "min_tokens_out" : 95.0 ,
897+ "slippage_pct" : 15 ,
898+ }
899+
900+ result = runner .invoke (
901+ app ,
902+ ["--json" , "--rpc" , "http://rpc" , "buy" , "--dry-run" , _FAKE_MINT , "0.01" ],
903+ )
904+
905+ assert result .exit_code == 0
906+ data = json .loads (result .output )
907+ assert data ["dry_run" ] is True
908+ assert data ["venue" ] == "pumpswap"
909+ call_kwargs = mock_pumpswap .call_args
910+ assert call_kwargs .kwargs .get ("dry_run" ) is True or call_kwargs [1 ].get ("dry_run" ) is True
911+
912+
913+ def test_sell_graduated_fallback_forwards_dry_run (tmp_path , monkeypatch ):
914+ """--dry-run forwarded to sell_pumpswap on graduated fallback."""
915+ _setup_wallet (tmp_path , monkeypatch )
916+
917+ with (
918+ patch ("pumpfun_cli.commands.trade.sell_token" , new_callable = AsyncMock ) as mock_sell ,
919+ patch ("pumpfun_cli.commands.trade.sell_pumpswap" , new_callable = AsyncMock ) as mock_pumpswap ,
920+ ):
921+ mock_sell .return_value = {"error" : "graduated" , "message" : "Token has graduated." }
922+ mock_pumpswap .return_value = {
923+ "dry_run" : True ,
924+ "action" : "sell" ,
925+ "venue" : "pumpswap" ,
926+ "mint" : _FAKE_MINT ,
927+ "tokens_in" : 100.0 ,
928+ "expected_sol" : 0.01 ,
929+ "effective_price_sol" : 0.0001 ,
930+ "spot_price_sol" : 0.00009 ,
931+ "price_impact_pct" : - 1.0 ,
932+ "min_sol_out" : 0.0085 ,
933+ "slippage_pct" : 15 ,
934+ }
935+
936+ result = runner .invoke (
937+ app ,
938+ ["--json" , "--rpc" , "http://rpc" , "sell" , "--dry-run" , _FAKE_MINT , "100" ],
939+ )
940+
941+ assert result .exit_code == 0
942+ data = json .loads (result .output )
943+ assert data ["dry_run" ] is True
944+ assert data ["venue" ] == "pumpswap"
945+
946+
947+ def test_buy_graduated_fallback_forwards_confirm (tmp_path , monkeypatch ):
948+ """--confirm forwarded to buy_pumpswap on graduated fallback."""
949+ _setup_wallet (tmp_path , monkeypatch )
950+
951+ with (
952+ patch ("pumpfun_cli.commands.trade.buy_token" , new_callable = AsyncMock ) as mock_buy ,
953+ patch ("pumpfun_cli.commands.trade.buy_pumpswap" , new_callable = AsyncMock ) as mock_pumpswap ,
954+ ):
955+ mock_buy .return_value = {"error" : "graduated" , "message" : "Token has graduated." }
956+ mock_pumpswap .return_value = {
957+ "action" : "buy" ,
958+ "venue" : "pumpswap" ,
959+ "mint" : _FAKE_MINT ,
960+ "sol_spent" : 0.01 ,
961+ "tokens_received" : 100.0 ,
962+ "signature" : "sig" ,
963+ "explorer" : "https://solscan.io/tx/sig" ,
964+ "confirmed" : True ,
965+ }
966+
967+ result = runner .invoke (
968+ app ,
969+ ["--json" , "--rpc" , "http://rpc" , "buy" , "--confirm" , _FAKE_MINT , "0.01" ],
970+ )
971+
972+ assert result .exit_code == 0
973+ call_kwargs = mock_pumpswap .call_args
974+ assert call_kwargs .kwargs .get ("confirm" ) is True or call_kwargs [1 ].get ("confirm" ) is True
975+
976+
977+ def test_buy_not_found_no_pumpswap_fallback (tmp_path , monkeypatch ):
978+ """not_found does NOT trigger pumpswap fallback."""
979+ _setup_wallet (tmp_path , monkeypatch )
980+
981+ with (
982+ patch ("pumpfun_cli.commands.trade.buy_token" , new_callable = AsyncMock ) as mock_buy ,
983+ patch ("pumpfun_cli.commands.trade.buy_pumpswap" , new_callable = AsyncMock ) as mock_pumpswap ,
984+ ):
985+ mock_buy .return_value = {"error" : "not_found" , "message" : "No bonding curve found." }
986+
987+ result = runner .invoke (
988+ app ,
989+ ["--rpc" , "http://rpc" , "buy" , _FAKE_MINT , "0.01" ],
990+ )
991+
992+ assert result .exit_code != 0
993+ mock_pumpswap .assert_not_called ()
994+
995+
996+ def test_sell_not_found_no_pumpswap_fallback (tmp_path , monkeypatch ):
997+ """not_found does NOT trigger pumpswap fallback for sell."""
998+ _setup_wallet (tmp_path , monkeypatch )
999+
1000+ with (
1001+ patch ("pumpfun_cli.commands.trade.sell_token" , new_callable = AsyncMock ) as mock_sell ,
1002+ patch ("pumpfun_cli.commands.trade.sell_pumpswap" , new_callable = AsyncMock ) as mock_pumpswap ,
1003+ ):
1004+ mock_sell .return_value = {"error" : "not_found" , "message" : "No bonding curve found." }
1005+
1006+ result = runner .invoke (
1007+ app ,
1008+ ["--rpc" , "http://rpc" , "sell" , _FAKE_MINT , "all" ],
1009+ )
1010+
1011+ assert result .exit_code != 0
1012+ mock_pumpswap .assert_not_called ()
1013+
1014+
1015+ def test_buy_graduated_fallback_pumpswap_error (tmp_path , monkeypatch ):
1016+ """pumpswap error surfaces correctly after graduated fallback."""
1017+ _setup_wallet (tmp_path , monkeypatch )
1018+
1019+ with (
1020+ patch ("pumpfun_cli.commands.trade.buy_token" , new_callable = AsyncMock ) as mock_buy ,
1021+ patch ("pumpfun_cli.commands.trade.buy_pumpswap" , new_callable = AsyncMock ) as mock_pumpswap ,
1022+ ):
1023+ mock_buy .return_value = {"error" : "graduated" , "message" : "Token has graduated." }
1024+ mock_pumpswap .return_value = {
1025+ "error" : "pumpswap_error" ,
1026+ "message" : "No PumpSwap pool found for this token." ,
1027+ }
1028+
1029+ result = runner .invoke (
1030+ app ,
1031+ ["--rpc" , "http://rpc" , "buy" , _FAKE_MINT , "0.01" ],
1032+ )
1033+
1034+ assert result .exit_code != 0
1035+ assert "PumpSwap" in result .output or "pumpswap" in result .output .lower ()
1036+
1037+
1038+ def test_sell_graduated_fallback_sell_all (tmp_path , monkeypatch ):
1039+ """sell 'all' through graduated fallback works."""
1040+ _setup_wallet (tmp_path , monkeypatch )
1041+
1042+ with (
1043+ patch ("pumpfun_cli.commands.trade.sell_token" , new_callable = AsyncMock ) as mock_sell ,
1044+ patch ("pumpfun_cli.commands.trade.sell_pumpswap" , new_callable = AsyncMock ) as mock_pumpswap ,
1045+ ):
1046+ mock_sell .return_value = {"error" : "graduated" , "message" : "Token has graduated." }
1047+ mock_pumpswap .return_value = {
1048+ "action" : "sell" ,
1049+ "venue" : "pumpswap" ,
1050+ "mint" : _FAKE_MINT ,
1051+ "sol_received" : 0.05 ,
1052+ "tokens_sold" : 500.0 ,
1053+ "signature" : "sellall_sig" ,
1054+ "explorer" : "https://solscan.io/tx/sellall_sig" ,
1055+ }
1056+
1057+ result = runner .invoke (
1058+ app ,
1059+ ["--json" , "--rpc" , "http://rpc" , "sell" , _FAKE_MINT , "all" ],
1060+ )
1061+
1062+ assert result .exit_code == 0
1063+ data = json .loads (result .output )
1064+ assert data ["venue" ] == "pumpswap"
1065+ assert data ["tokens_sold" ] == 500.0
1066+ # Verify "all" was passed through
1067+ call_args = mock_pumpswap .call_args
1068+ assert call_args [0 ][4 ] == "all"
0 commit comments