pyth_utils.py 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. import logging
  2. import os
  3. import json
  4. import socketserver
  5. import subprocess
  6. import sys
  7. from http.client import HTTPConnection
  8. # A generic unprivileged payer account with funds
  9. SOL_PAYER_KEYPAIR = os.environ.get(
  10. "SOL_PAYER_KEYPAIR", "/solana-secrets/solana-devnet.json"
  11. )
  12. # Settings specific to local devnet Pyth instance
  13. PYTH = os.environ.get("PYTH", "./pyth")
  14. PYTH_ADMIN = os.environ.get("PYTH_ADMIN", "./pyth_admin")
  15. PYTH_KEY_STORE = os.environ.get("PYTH_KEY_STORE", "/home/pyth/.pythd")
  16. PYTH_PROGRAM_KEYPAIR = os.environ.get(
  17. "PYTH_PROGRAM_KEYPAIR", f"{PYTH_KEY_STORE}/publish_key_pair.json"
  18. )
  19. PYTH_PUBLISHER_KEYPAIR = os.environ.get(
  20. "PYTH_PUBLISHER_KEYPAIR", f"{PYTH_KEY_STORE}/publish_key_pair.json"
  21. )
  22. # How long to sleep between mock Pyth price updates
  23. PYTH_PUBLISHER_INTERVAL_SECS = float(os.environ.get("PYTH_PUBLISHER_INTERVAL_SECS", "5"))
  24. PYTH_TEST_SYMBOL_COUNT = int(os.environ.get("PYTH_TEST_SYMBOL_COUNT", "11"))
  25. PYTH_DYNAMIC_SYMBOL_COUNT = int(os.environ.get("PYTH_DYNAMIC_SYMBOL_COUNT", "3"))
  26. # If above 0, adds a new test symbol periodically, waiting at least
  27. # the given number of seconds in between
  28. #
  29. # NOTE: the new symbols are added in the HTTP endpoint used by the
  30. # p2w-attest service in Tilt. You may need to wait to see p2w-attest
  31. # pick up brand new symbols
  32. PYTH_NEW_SYMBOL_INTERVAL_SECS = int(os.environ.get("PYTH_NEW_SYMBOL_INTERVAL_SECS", "30"))
  33. PYTH_MAPPING_KEYPAIR = os.environ.get(
  34. "PYTH_MAPPING_KEYPAIR", f"{PYTH_KEY_STORE}/mapping_key_pair.json"
  35. )
  36. # SOL RPC settings
  37. SOL_RPC_HOST = os.environ.get("SOL_RPC_HOST", "solana-devnet")
  38. SOL_RPC_PORT = int(os.environ.get("SOL_RPC_PORT", 8899))
  39. SOL_RPC_URL = os.environ.get(
  40. "SOL_RPC_URL", "http://{0}:{1}".format(SOL_RPC_HOST, SOL_RPC_PORT)
  41. )
  42. # A TCP port we open when a service is ready
  43. READINESS_PORT = int(os.environ.get("READINESS_PORT", "2000"))
  44. def run_or_die(args, die=True, debug=False, **kwargs):
  45. """
  46. Opinionated subprocess.run() call with fancy logging
  47. """
  48. args_readable = " ".join(args)
  49. print(f"CMD RUN\t{args_readable}", file=sys.stderr)
  50. sys.stderr.flush()
  51. ret = subprocess.run(args, text=True, **kwargs)
  52. if ret.returncode == 0:
  53. print(f"CMD OK\t{args_readable}", file=sys.stderr)
  54. else:
  55. print(f"CMD FAIL {ret.returncode}\t{args_readable}", file=sys.stderr)
  56. if debug:
  57. out = ret.stdout if ret.stdout is not None else "<not captured>"
  58. err = ret.stderr if ret.stderr is not None else "<not captured>"
  59. print(f"CMD STDOUT\n{out}", file=sys.stderr)
  60. print(f"CMD STDERR\n{err}", file=sys.stderr)
  61. sys.stderr.flush()
  62. if ret.returncode != 0:
  63. if die:
  64. sys.exit(ret.returncode)
  65. else:
  66. print(f'{"CMD DIE FALSE"}', file=sys.stderr)
  67. sys.stderr.flush()
  68. return ret
  69. def pyth_run_or_die(subcommand, args=[], debug=False, **kwargs):
  70. """
  71. Pyth boilerplate in front of run_or_die.
  72. """
  73. return run_or_die(
  74. [PYTH, subcommand] + args + (["-d"] if debug else [])
  75. + ["-k", PYTH_KEY_STORE]
  76. + ["-r", SOL_RPC_HOST]
  77. + ["-c", "finalized"]
  78. + ["-x"], # These means to bypass transaction proxy server. In this setup it's not running and it's required to bypass
  79. **kwargs,
  80. )
  81. def pyth_admin_run_or_die(subcommand, args=[], debug=False, **kwargs):
  82. """
  83. Pyth_admin boilerplate in front of run_or_die.
  84. """
  85. return run_or_die(
  86. [PYTH_ADMIN, subcommand] + args + (["-d"] if debug else [])
  87. + ["-n"] # These commands require y/n confirmation. This bypasses that
  88. + ["-k", PYTH_KEY_STORE]
  89. + ["-r", SOL_RPC_HOST]
  90. + ["-c", "finalized"],
  91. **kwargs,
  92. )
  93. def sol_run_or_die(subcommand, args=[], **kwargs):
  94. """
  95. Solana boilerplate in front of run_or_die
  96. """
  97. return run_or_die(["solana", subcommand] + args + ["--url", SOL_RPC_URL], **kwargs)
  98. def get_json(host, port, path):
  99. conn = HTTPConnection(host, port)
  100. conn.request("GET", path)
  101. res = conn.getresponse()
  102. # starstwith because the header value may include optional fields after (like charset)
  103. if res.getheader("Content-Type").startswith("application/json"):
  104. return json.load(res)
  105. else:
  106. logging.error(f"Error getting {host}:{port}{path} : Content-Type was not application/json")
  107. logging.error(f"HTTP response code: {res.getcode()}")
  108. logging.error(f"HTTP headers: {res.getheaders()}")
  109. logging.error(f"Message: {res.msg}")
  110. sys.exit(1)
  111. def get_pyth_accounts(host, port):
  112. return get_json(host, port, "/")
  113. class ReadinessTCPHandler(socketserver.StreamRequestHandler):
  114. def handle(self):
  115. """TCP black hole"""
  116. self.rfile.read(64)
  117. def readiness():
  118. """
  119. Accept connections from readiness probe
  120. """
  121. with socketserver.TCPServer(
  122. ("0.0.0.0", READINESS_PORT), ReadinessTCPHandler
  123. ) as srv:
  124. print(f"Opening port {READINESS_PORT} for readiness TCP probe")
  125. srv.serve_forever()