Skip to content

Commit cf11bfc

Browse files
committed
feat: 支持 jito #4
1 parent e726814 commit cf11bfc

File tree

19 files changed

+564
-345
lines changed

19 files changed

+564
-345
lines changed

example.config.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ unit_limit = 81000
2222
unit_price = 3000000
2323
preflight_check = false
2424
tx_simulate = false
25+
use_jito = true
26+
# jito_api 可根据服务器地址选择,就近原则 https://docs.jito.wtf/lowlatencytxnsend/#api
27+
jito_api = "https://mainnet.block-engine.jito.wtf"
2528

2629
[api]
2730
helius_api_base_url = "https://api.helius.xyz/v0"

src/common/config.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
BaseModel,
1212
ConfigDict,
1313
Field,
14+
HttpUrl,
1415
MySQLDsn,
1516
RedisDsn,
1617
field_validator,
@@ -152,6 +153,16 @@ class TradingConfig(BaseModel):
152153
unit_limit: int
153154
tx_simulate: bool = False
154155
preflight_check: bool = False
156+
use_jito: bool = True
157+
jito_api: str = "https://mainnet.block-engine.jito.wtf"
158+
159+
@field_validator("jito_api", mode="before")
160+
def validate_jito_api(cls, value: str) -> str:
161+
try:
162+
HttpUrl(value)
163+
except ValueError:
164+
raise ValueError(f"Invalid Jito API URL: {value}")
165+
return value
155166

156167

157168
class APIConfig(BaseModel):

src/common/utils/jito.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import base64
2+
from typing import Literal
3+
4+
import base58
5+
import httpx
6+
from loguru import logger
7+
from solana.rpc.types import TxOpts
8+
from solders.signature import Signature # type: ignore
9+
from solders.transaction import VersionedTransaction # type: ignore
10+
11+
from common.config import settings
12+
13+
14+
class JitoClient:
15+
def __init__(self) -> None:
16+
self.client = httpx.AsyncClient(
17+
base_url=settings.trading.jito_api,
18+
)
19+
20+
async def send_transaction(
21+
self,
22+
transaction: VersionedTransaction,
23+
opts: TxOpts | None = None,
24+
encoding: Literal["base64", "base58"] = "base64",
25+
) -> Signature:
26+
"""Send a signed transaction.
27+
28+
Args:
29+
transaction (VersionedTransaction): The signed transaction to send
30+
opts (TxOpts | None, optional): The transaction options. Defaults to None.
31+
32+
Returns:
33+
Signature: The transaction signature
34+
"""
35+
path = "/api/v1/transactions"
36+
if opts is None:
37+
opts = TxOpts(skip_preflight=not settings.trading.preflight_check)
38+
39+
txn = bytes(transaction)
40+
if encoding == "base64":
41+
txn_encoded = base64.b64encode(txn).decode("utf-8")
42+
elif encoding == "base58":
43+
txn_encoded = base58.b58encode(txn).decode("utf-8")
44+
else:
45+
raise ValueError("encoding must be either 'base64' or 'base58'")
46+
47+
resp = await self.client.post(
48+
path,
49+
json={
50+
"id": 1,
51+
"jsonrpc": "2.0",
52+
"method": "sendTransaction",
53+
"params": [
54+
txn_encoded,
55+
{"encoding": encoding},
56+
],
57+
},
58+
)
59+
# {
60+
# "jsonrpc": "2.0",
61+
# "result": "2id3YC2jK9G5Wo2phDx4gJVAew8DcY5NAojnVuao8rkxwPYPe8cSwE5GzhEgJA2y8fVjDEo6iR6ykBvDxrTQrtpb",
62+
# "id": 1,
63+
# }
64+
js = resp.json()
65+
bundle_id = resp.headers.get("x-bundle-id")
66+
sig = Signature.from_string(js["result"])
67+
logger.info(f"Bundle ID: {bundle_id}, Signature: {sig}")
68+
return sig

src/trading/executor.py

Lines changed: 24 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,22 @@
66
from common.log import logger
77
from common.models.tg_bot.user import User
88
from common.types.swap import SwapEvent
9+
from common.utils.jito import JitoClient
910
from db.session import NEW_ASYNC_SESSION, provide_session
1011
from trading.swap import SwapDirection, SwapInType
1112
from common.utils.raydium import RaydiumAPI
1213
from cache.launch import LaunchCache
13-
from .swap_protocols import Gmgn, Pump
14+
from trading.transaction import TradingRoute, TradingService
15+
from common.config import settings
1416

1517
PUMP_FUN_PROGRAM_ID = str(PUMP_FUN_PROGRAM)
1618
RAY_V4_PROGRAM_ID = str(RAY_V4)
1719

1820

1921
class TradingExecutor:
2022
def __init__(self, client: AsyncClient):
21-
self._client = client
23+
self._rpc_client = client
24+
self._jito_client = JitoClient()
2225
self._raydium_api = RaydiumAPI()
2326
self._launch_cache = LaunchCache()
2427

@@ -84,39 +87,31 @@ async def exec(self, swap_event: SwapEvent):
8487

8588
if should_use_pump:
8689
logger.info("Program ID is PUMP")
87-
sig = await Pump(self._client).swap(
88-
keypair=keypair,
89-
token_address=token_address,
90-
ui_amount=swap_event.ui_amount,
91-
swap_direction=swap_direction,
92-
slippage_bps=slippage_bps,
93-
in_type=swap_in_type,
94-
priority_fee=swap_event.priority_fee,
95-
)
90+
trade_route = TradingRoute.PUMP
9691
# NOTE: 测试下来不是很理想,暂时使用备选方案
9792
elif swap_event.program_id == RAY_V4_PROGRAM_ID:
9893
logger.info("Program ID is RayV4")
99-
sig = await Gmgn(self._client).swap(
100-
keypair=keypair,
101-
token_address=token_address,
102-
ui_amount=swap_event.ui_amount,
103-
swap_direction=swap_direction,
104-
slippage_bps=slippage_bps,
105-
in_type=swap_in_type,
106-
priority_fee=swap_event.priority_fee,
107-
)
94+
trade_route = TradingRoute.GMGN
10895
elif program_id is None or program_id == RAY_V4_PROGRAM_ID:
10996
logger.warning("Program ID is Unknown, So We use thrid party to trade")
110-
sig = await Gmgn(self._client).swap(
111-
keypair=keypair,
112-
token_address=token_address,
113-
ui_amount=swap_event.ui_amount,
114-
swap_direction=swap_direction,
115-
slippage_bps=slippage_bps,
116-
in_type=swap_in_type,
117-
priority_fee=swap_event.priority_fee,
118-
)
97+
trade_route = TradingRoute.GMGN
11998
else:
12099
raise ValueError(f"Program ID is not supported, {swap_event.program_id}")
121100

101+
sig = await TradingService.create(
102+
trade_route,
103+
self._rpc_client,
104+
use_jito=settings.trading.use_jito,
105+
jito_client=self._jito_client,
106+
).swap(
107+
keypair,
108+
token_address,
109+
swap_event.ui_amount,
110+
swap_direction,
111+
slippage_bps,
112+
swap_in_type,
113+
use_jito=settings.trading.use_jito,
114+
priority_fee=swap_event.priority_fee,
115+
)
116+
122117
return sig

src/trading/swap_protocols/__init__.py

Lines changed: 0 additions & 5 deletions
This file was deleted.

src/trading/swap_protocols/proto.py

Lines changed: 0 additions & 71 deletions
This file was deleted.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from trading.transaction.base import TransactionSender
2+
from trading.transaction.builders.base import TransactionBuilder
3+
from trading.transaction.factory import TransactionFactory, TradingService
4+
from trading.transaction.protocol import TradingRoute
5+
from trading.transaction.sender import DefaultTransactionSender, JitoTransactionSender
6+
7+
__all__ = [
8+
"TransactionBuilder",
9+
"TransactionSender",
10+
"TransactionFactory",
11+
"TradingService",
12+
"TradingRoute",
13+
"DefaultTransactionSender",
14+
"JitoTransactionSender",
15+
]

src/trading/transaction/base.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
from abc import ABC, abstractmethod
2+
from typing import Optional
3+
4+
from solana.rpc.async_api import AsyncClient
5+
from solana.rpc.types import TxOpts
6+
from solders.signature import Signature
7+
from solders.transaction import VersionedTransaction
8+
9+
from common.utils.jito import JitoClient
10+
11+
12+
class TransactionSender(ABC):
13+
"""交易发送器的抽象基类"""
14+
15+
def __init__(self, client: AsyncClient | JitoClient):
16+
self.client = client
17+
18+
@abstractmethod
19+
async def send_transaction(
20+
self,
21+
transaction: VersionedTransaction,
22+
opts: Optional[TxOpts] = None,
23+
) -> Signature:
24+
"""发送交易
25+
26+
Args:
27+
transaction (VersionedTransaction): 要发送的交易
28+
opts (Optional[TxOpts], optional): 交易选项. Defaults to None.
29+
30+
Returns:
31+
Signature: 交易签名
32+
"""
33+
pass
34+
35+
@abstractmethod
36+
async def simulate_transaction(
37+
self,
38+
transaction: VersionedTransaction,
39+
) -> bool:
40+
"""模拟交易
41+
42+
Args:
43+
transaction (VersionedTransaction): 要模拟的交易
44+
45+
Returns:
46+
bool: 模拟是否成功
47+
"""
48+
pass
File renamed without changes.
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
from abc import ABC, abstractmethod
2+
from typing import Optional
3+
4+
from solana.rpc.async_api import AsyncClient
5+
from solders.keypair import Keypair
6+
from solders.transaction import VersionedTransaction
7+
8+
from trading.swap import SwapDirection, SwapInType
9+
10+
11+
class TransactionBuilder(ABC):
12+
"""交易构建器的抽象基类"""
13+
14+
def __init__(self, rpc_client: AsyncClient):
15+
self.rpc_client = rpc_client
16+
17+
@abstractmethod
18+
async def build_swap_transaction(
19+
self,
20+
keypair: Keypair,
21+
token_address: str,
22+
ui_amount: float,
23+
swap_direction: SwapDirection,
24+
slippage_bps: int,
25+
in_type: SwapInType | None = None,
26+
use_jito: bool = False,
27+
priority_fee: Optional[float] = None,
28+
) -> VersionedTransaction:
29+
"""构建交易
30+
31+
Args:
32+
keypair (Keypair): 钱包密钥对
33+
token_address (str): 代币地址
34+
ui_amount (float): 交易数量
35+
swap_direction (SwapDirection): 交易方向
36+
slippage_bps (int): 滑点,以 bps 为单位
37+
in_type (SwapInType | None, optional): 输入类型. Defaults to None.
38+
use_jito (bool, optional): 是否使用 Jito. Defaults to False.
39+
priority_fee (Optional[float], optional): 优先费用. Defaults to None.
40+
41+
Returns:
42+
VersionedTransaction: 构建好的交易
43+
"""
44+
pass

0 commit comments

Comments
 (0)