searcher-cli.py 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. import time
  2. from typing import List
  3. import click
  4. from solana.rpc.api import Client
  5. from solana.rpc.commitment import Processed
  6. from solders.keypair import Keypair
  7. from solders.pubkey import Pubkey
  8. from solders.system_program import TransferParams, transfer
  9. from solders.transaction import Transaction, VersionedTransaction
  10. from spl.memo.instructions import MemoParams, create_memo
  11. from jito_searcher_client.convert import tx_to_protobuf_packet
  12. from jito_searcher_client.generated.bundle_pb2 import Bundle
  13. from jito_searcher_client.generated.searcher_pb2 import (
  14. ConnectedLeadersRequest,
  15. NextScheduledLeaderRequest,
  16. NextScheduledLeaderResponse,
  17. PendingTxSubscriptionRequest,
  18. SendBundleRequest,
  19. )
  20. from jito_searcher_client.generated.searcher_pb2_grpc import SearcherServiceStub
  21. from jito_searcher_client.searcher import get_searcher_client
  22. @click.group("cli")
  23. @click.pass_context
  24. @click.option(
  25. "--keypair-path",
  26. help="Path to a keypair that is authenticated with the block engine.",
  27. required=True,
  28. )
  29. @click.option(
  30. "--block-engine-url",
  31. help="Block Engine URL",
  32. required=True,
  33. )
  34. def cli(
  35. ctx,
  36. keypair_path: str,
  37. block_engine_url: str,
  38. ):
  39. """
  40. This script can be used to interface with the block engine as a jito_searcher_client.
  41. """
  42. with open(keypair_path) as kp_path:
  43. kp = Keypair.from_json(kp_path.read())
  44. ctx.obj = get_searcher_client(block_engine_url, kp)
  45. @click.command("mempool-accounts")
  46. @click.pass_obj
  47. @click.argument("accounts", required=True, nargs=-1)
  48. def mempool_accounts(client: SearcherServiceStub, accounts: List[str]):
  49. """
  50. Stream pending transactions from write-locked accounts.
  51. """
  52. leader: NextScheduledLeaderResponse = client.GetNextScheduledLeader(
  53. NextScheduledLeaderRequest()
  54. )
  55. print(
  56. f"next scheduled leader is {leader.next_leader_identity} in {leader.next_leader_slot - leader.current_slot} slots"
  57. )
  58. for notification in client.SubscribePendingTransactions(
  59. PendingTxSubscriptionRequest(accounts=accounts)
  60. ):
  61. for packet in notification.transactions:
  62. print(VersionedTransaction.from_bytes(packet.data))
  63. @click.command("next-scheduled-leader")
  64. @click.pass_obj
  65. def next_scheduled_leader(client: SearcherServiceStub):
  66. """
  67. Find information on the next scheduled leader.
  68. """
  69. next_leader = client.GetNextScheduledLeader(NextScheduledLeaderRequest())
  70. print(f"{next_leader=}")
  71. @click.command("connected-leaders")
  72. @click.pass_obj
  73. def connected_leaders(client: SearcherServiceStub):
  74. """
  75. Get leaders connected to this block engine.
  76. """
  77. leaders = client.GetConnectedLeaders(ConnectedLeadersRequest())
  78. print(f"{leaders=}")
  79. @click.command("tip-accounts")
  80. @click.pass_obj
  81. def tip_accounts(client: SearcherServiceStub):
  82. """
  83. Get the tip accounts from the block engine.
  84. """
  85. accounts = client.GetNextScheduledLeader(NextScheduledLeaderRequest())
  86. print(f"{accounts=}")
  87. @click.command("send-bundle")
  88. @click.pass_obj
  89. @click.option(
  90. "--rpc-url",
  91. help="RPC URL path",
  92. type=str,
  93. required=True,
  94. )
  95. @click.option(
  96. "--payer",
  97. help="Path to payer keypair",
  98. type=str,
  99. required=True,
  100. )
  101. @click.option(
  102. "--message",
  103. help="Message in the bundle",
  104. type=str,
  105. required=True,
  106. )
  107. @click.option(
  108. "--num_txs",
  109. help="Number of transactions in the bundle (max is 5)",
  110. type=int,
  111. required=True,
  112. )
  113. @click.option(
  114. "--lamports",
  115. help="Number of lamports to tip in each transaction",
  116. type=int,
  117. required=True,
  118. )
  119. @click.option(
  120. "--tip_account",
  121. help="Tip account to tip",
  122. type=str,
  123. required=True,
  124. )
  125. def send_bundle(
  126. client: SearcherServiceStub,
  127. rpc_url: str,
  128. payer: str,
  129. message: str,
  130. num_txs: int,
  131. lamports: int,
  132. tip_account: str,
  133. ):
  134. """
  135. Send a bundle!
  136. """
  137. with open(payer) as kp_path:
  138. payer_kp = Keypair.from_json(kp_path.read())
  139. tip_account = Pubkey.from_string(tip_account)
  140. rpc_client = Client(rpc_url)
  141. balance = rpc_client.get_balance(payer_kp.pubkey()).value
  142. print(f"payer public key: {payer_kp.pubkey()} {balance=}")
  143. is_leader_slot = False
  144. print("waiting for jito leader...")
  145. while not is_leader_slot:
  146. time.sleep(0.5)
  147. next_leader: NextScheduledLeaderResponse = client.GetNextScheduledLeader(
  148. NextScheduledLeaderRequest()
  149. )
  150. num_slots_to_leader = next_leader.next_leader_slot - next_leader.current_slot
  151. print(f"waiting {num_slots_to_leader} slots to jito leader")
  152. is_leader_slot = num_slots_to_leader <= 2
  153. blockhash = rpc_client.get_latest_blockhash().value.blockhash
  154. block_height = rpc_client.get_block_height(Processed).value
  155. # Build bundle
  156. txs: List[Transaction] = []
  157. for idx in range(num_txs):
  158. ixs = [create_memo(MemoParams(program_id=Pubkey.from_string("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr"),
  159. signer=payer_kp.pubkey(),
  160. message=bytes(f"jito bundle {idx}: {message}", "utf-8")))]
  161. if idx == num_txs - 1:
  162. # Adds searcher tip on last tx
  163. ixs.append(transfer(TransferParams(
  164. from_pubkey=payer_kp.pubkey(),
  165. to_pubkey=tip_account,
  166. lamports=lamports
  167. )))
  168. tx = Transaction.new_signed_with_payer(instructions=ixs,
  169. payer=payer_kp.pubkey(),
  170. signing_keypairs=[payer_kp],
  171. recent_blockhash=blockhash
  172. )
  173. print(f"{idx=} signature={tx.signatures[0]}")
  174. txs.append(tx)
  175. # Note: setting meta.size here is important so the block engine can deserialize the packet
  176. packets = [tx_to_protobuf_packet(tx) for tx in txs]
  177. uuid_response = client.SendBundle(SendBundleRequest(bundle=Bundle(header=None, packets=packets)))
  178. print(f"bundle uuid: {uuid_response.uuid}")
  179. for tx in txs:
  180. print(rpc_client.confirm_transaction(tx.signatures[0], Processed, sleep_seconds=0.5,
  181. last_valid_block_height=block_height + 10))
  182. if __name__ == "__main__":
  183. cli.add_command(mempool_accounts)
  184. cli.add_command(next_scheduled_leader)
  185. cli.add_command(connected_leaders)
  186. cli.add_command(tip_accounts)
  187. cli.add_command(send_bundle)
  188. cli()