client.ts 2.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293
  1. import WebSocket from "isomorphic-ws";
  2. import {
  3. BINARY_UPDATE_FORMAT_MAGIC,
  4. EVM_FORMAT_MAGIC,
  5. PARSED_FORMAT_MAGIC,
  6. type ParsedPayload,
  7. type Request,
  8. type Response,
  9. SOLANA_FORMAT_MAGIC_BE,
  10. } from "./protocol.js";
  11. export type BinaryResponse = {
  12. subscriptionId: number;
  13. evm?: Buffer | undefined;
  14. solana?: Buffer | undefined;
  15. parsed?: ParsedPayload | undefined;
  16. };
  17. export type JsonOrBinaryResponse =
  18. | {
  19. type: "json";
  20. value: Response;
  21. }
  22. | { type: "binary"; value: BinaryResponse };
  23. const UINT16_NUM_BYTES = 2;
  24. const UINT32_NUM_BYTES = 4;
  25. const UINT64_NUM_BYTES = 8;
  26. export class PythLazerClient {
  27. ws: WebSocket;
  28. constructor(url: string, token: string) {
  29. const finalUrl = new URL(url);
  30. finalUrl.searchParams.append("ACCESS_TOKEN", token);
  31. this.ws = new WebSocket(finalUrl);
  32. }
  33. addMessageListener(handler: (event: JsonOrBinaryResponse) => void) {
  34. this.ws.addEventListener("message", (event: WebSocket.MessageEvent) => {
  35. if (typeof event.data == "string") {
  36. handler({
  37. type: "json",
  38. value: JSON.parse(event.data) as Response,
  39. });
  40. } else if (Buffer.isBuffer(event.data)) {
  41. let pos = 0;
  42. const magic = event.data
  43. .subarray(pos, pos + UINT32_NUM_BYTES)
  44. .readUint32BE();
  45. pos += UINT32_NUM_BYTES;
  46. if (magic != BINARY_UPDATE_FORMAT_MAGIC) {
  47. throw new Error("binary update format magic mismatch");
  48. }
  49. // TODO: some uint64 values may not be representable as Number.
  50. const subscriptionId = Number(
  51. event.data.subarray(pos, pos + UINT64_NUM_BYTES).readBigInt64BE()
  52. );
  53. pos += UINT64_NUM_BYTES;
  54. const value: BinaryResponse = { subscriptionId };
  55. while (pos < event.data.length) {
  56. const len = event.data
  57. .subarray(pos, pos + UINT16_NUM_BYTES)
  58. .readUint16BE();
  59. pos += UINT16_NUM_BYTES;
  60. const magic = event.data
  61. .subarray(pos, pos + UINT32_NUM_BYTES)
  62. .readUint32BE();
  63. if (magic == EVM_FORMAT_MAGIC) {
  64. value.evm = event.data.subarray(pos, pos + len);
  65. } else if (magic == SOLANA_FORMAT_MAGIC_BE) {
  66. value.solana = event.data.subarray(pos, pos + len);
  67. } else if (magic == PARSED_FORMAT_MAGIC) {
  68. value.parsed = JSON.parse(
  69. event.data.subarray(pos + UINT32_NUM_BYTES, pos + len).toString()
  70. ) as ParsedPayload;
  71. } else {
  72. throw new Error("unknown magic: " + magic.toString());
  73. }
  74. pos += len;
  75. }
  76. handler({ type: "binary", value });
  77. } else {
  78. throw new TypeError("unexpected event data type");
  79. }
  80. });
  81. }
  82. send(request: Request) {
  83. this.ws.send(JSON.stringify(request));
  84. }
  85. }