Skip to content

Commit

Permalink
feat(pyth-lazer-sdk): add ed25519 ix builder function (#2203)
Browse files Browse the repository at this point in the history
* feat(pyth-lazer-sdk): add ed25519 ix builder function

* fix

* fix
  • Loading branch information
keyvankhademi authored Dec 19, 2024
1 parent f0659ce commit 57670ca
Show file tree
Hide file tree
Showing 5 changed files with 243 additions and 474 deletions.
4 changes: 3 additions & 1 deletion lazer/sdk/js/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@pythnetwork/pyth-lazer-sdk",
"version": "0.1.1",
"version": "0.1.2",
"description": "Pyth Lazer SDK",
"publishConfig": {
"access": "public"
Expand Down Expand Up @@ -60,6 +60,8 @@
],
"license": "Apache-2.0",
"dependencies": {
"@solana/buffer-layout": "^4.0.1",
"@solana/web3.js": "^1.98.0",
"isomorphic-ws": "^5.0.0",
"ws": "^8.18.0"
}
Expand Down
93 changes: 93 additions & 0 deletions lazer/sdk/js/src/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import WebSocket from "isomorphic-ws";

import {
BINARY_UPDATE_FORMAT_MAGIC,
EVM_FORMAT_MAGIC,
PARSED_FORMAT_MAGIC,
type ParsedPayload,
type Request,
type Response,
SOLANA_FORMAT_MAGIC_BE,
} from "./protocol.js";

export type BinaryResponse = {
subscriptionId: number;
evm?: Buffer | undefined;
solana?: Buffer | undefined;
parsed?: ParsedPayload | undefined;
};
export type JsonOrBinaryResponse =
| {
type: "json";
value: Response;
}
| { type: "binary"; value: BinaryResponse };

const UINT16_NUM_BYTES = 2;
const UINT32_NUM_BYTES = 4;
const UINT64_NUM_BYTES = 8;

export class PythLazerClient {
ws: WebSocket;

constructor(url: string, token: string) {
const finalUrl = new URL(url);
finalUrl.searchParams.append("ACCESS_TOKEN", token);
this.ws = new WebSocket(finalUrl);
}

addMessageListener(handler: (event: JsonOrBinaryResponse) => void) {
this.ws.addEventListener("message", (event: WebSocket.MessageEvent) => {
if (typeof event.data == "string") {
handler({
type: "json",
value: JSON.parse(event.data) as Response,
});
} else if (Buffer.isBuffer(event.data)) {
let pos = 0;
const magic = event.data
.subarray(pos, pos + UINT32_NUM_BYTES)
.readUint32BE();
pos += UINT32_NUM_BYTES;
if (magic != BINARY_UPDATE_FORMAT_MAGIC) {
throw new Error("binary update format magic mismatch");
}
// TODO: some uint64 values may not be representable as Number.
const subscriptionId = Number(
event.data.subarray(pos, pos + UINT64_NUM_BYTES).readBigInt64BE()
);
pos += UINT64_NUM_BYTES;

const value: BinaryResponse = { subscriptionId };
while (pos < event.data.length) {
const len = event.data
.subarray(pos, pos + UINT16_NUM_BYTES)
.readUint16BE();
pos += UINT16_NUM_BYTES;
const magic = event.data
.subarray(pos, pos + UINT32_NUM_BYTES)
.readUint32BE();
if (magic == EVM_FORMAT_MAGIC) {
value.evm = event.data.subarray(pos, pos + len);
} else if (magic == SOLANA_FORMAT_MAGIC_BE) {
value.solana = event.data.subarray(pos, pos + len);
} else if (magic == PARSED_FORMAT_MAGIC) {
value.parsed = JSON.parse(
event.data.subarray(pos + UINT32_NUM_BYTES, pos + len).toString()
) as ParsedPayload;
} else {
throw new Error("unknown magic: " + magic.toString());
}
pos += len;
}
handler({ type: "binary", value });
} else {
throw new TypeError("unexpected event data type");
}
});
}

send(request: Request) {
this.ws.send(JSON.stringify(request));
}
}
70 changes: 70 additions & 0 deletions lazer/sdk/js/src/ed25519.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import * as BufferLayout from "@solana/buffer-layout";
import { Ed25519Program, TransactionInstruction } from "@solana/web3.js";

const ED25519_INSTRUCTION_LEN = 16;
const SIGNATURE_LEN = 64;
const PUBKEY_LEN = 32;
const MAGIC_LEN = 4;
const MESSAGE_SIZE_LEN = 2;

const ED25519_INSTRUCTION_LAYOUT = BufferLayout.struct<
Readonly<{
messageDataOffset: number;
messageDataSize: number;
messageInstructionIndex: number;
numSignatures: number;
padding: number;
publicKeyInstructionIndex: number;
publicKeyOffset: number;
signatureInstructionIndex: number;
signatureOffset: number;
}>
>([
BufferLayout.u8("numSignatures"),
BufferLayout.u8("padding"),
BufferLayout.u16("signatureOffset"),
BufferLayout.u16("signatureInstructionIndex"),
BufferLayout.u16("publicKeyOffset"),
BufferLayout.u16("publicKeyInstructionIndex"),
BufferLayout.u16("messageDataOffset"),
BufferLayout.u16("messageDataSize"),
BufferLayout.u16("messageInstructionIndex"),
]);

export const createEd25519Instruction = (
message: Buffer,
instructionIndex: number,
startingOffset: number
) => {
const signatureOffset = startingOffset + MAGIC_LEN;
const publicKeyOffset = signatureOffset + SIGNATURE_LEN;
const messageDataSizeOffset = publicKeyOffset + PUBKEY_LEN;
const messageDataOffset = messageDataSizeOffset + MESSAGE_SIZE_LEN;

const messageDataSize = message.readUInt16LE(
messageDataSizeOffset - startingOffset
);

const instructionData = Buffer.alloc(ED25519_INSTRUCTION_LEN);

ED25519_INSTRUCTION_LAYOUT.encode(
{
numSignatures: 1,
padding: 0,
signatureOffset,
signatureInstructionIndex: instructionIndex,
publicKeyOffset,
publicKeyInstructionIndex: instructionIndex,
messageDataOffset,
messageDataSize: messageDataSize,
messageInstructionIndex: instructionIndex,
},
instructionData
);

return new TransactionInstruction({
keys: [],
programId: Ed25519Program.programId,
data: instructionData,
});
};
96 changes: 3 additions & 93 deletions lazer/sdk/js/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,93 +1,3 @@
import WebSocket from "isomorphic-ws";

import {
BINARY_UPDATE_FORMAT_MAGIC,
EVM_FORMAT_MAGIC,
PARSED_FORMAT_MAGIC,
type ParsedPayload,
type Request,
type Response,
SOLANA_FORMAT_MAGIC_BE,
} from "./protocol.js";

export type BinaryResponse = {
subscriptionId: number;
evm?: Buffer | undefined;
solana?: Buffer | undefined;
parsed?: ParsedPayload | undefined;
};
export type JsonOrBinaryResponse =
| {
type: "json";
value: Response;
}
| { type: "binary"; value: BinaryResponse };

const UINT16_NUM_BYTES = 2;
const UINT32_NUM_BYTES = 4;
const UINT64_NUM_BYTES = 8;

export class PythLazerClient {
ws: WebSocket;

constructor(url: string, token: string) {
const finalUrl = new URL(url);
finalUrl.searchParams.append("ACCESS_TOKEN", token);
this.ws = new WebSocket(finalUrl);
}

addMessageListener(handler: (event: JsonOrBinaryResponse) => void) {
this.ws.addEventListener("message", (event: WebSocket.MessageEvent) => {
if (typeof event.data == "string") {
handler({
type: "json",
value: JSON.parse(event.data) as Response,
});
} else if (Buffer.isBuffer(event.data)) {
let pos = 0;
const magic = event.data
.subarray(pos, pos + UINT32_NUM_BYTES)
.readUint32BE();
pos += UINT32_NUM_BYTES;
if (magic != BINARY_UPDATE_FORMAT_MAGIC) {
throw new Error("binary update format magic mismatch");
}
// TODO: some uint64 values may not be representable as Number.
const subscriptionId = Number(
event.data.subarray(pos, pos + UINT64_NUM_BYTES).readBigInt64BE()
);
pos += UINT64_NUM_BYTES;

const value: BinaryResponse = { subscriptionId };
while (pos < event.data.length) {
const len = event.data
.subarray(pos, pos + UINT16_NUM_BYTES)
.readUint16BE();
pos += UINT16_NUM_BYTES;
const magic = event.data
.subarray(pos, pos + UINT32_NUM_BYTES)
.readUint32BE();
if (magic == EVM_FORMAT_MAGIC) {
value.evm = event.data.subarray(pos, pos + len);
} else if (magic == SOLANA_FORMAT_MAGIC_BE) {
value.solana = event.data.subarray(pos, pos + len);
} else if (magic == PARSED_FORMAT_MAGIC) {
value.parsed = JSON.parse(
event.data.subarray(pos + UINT32_NUM_BYTES, pos + len).toString()
) as ParsedPayload;
} else {
throw new Error("unknown magic: " + magic.toString());
}
pos += len;
}
handler({ type: "binary", value });
} else {
throw new TypeError("unexpected event data type");
}
});
}

send(request: Request) {
this.ws.send(JSON.stringify(request));
}
}
export * from "./client.js";
export * from "./protocol.js";
export * from "./ed25519.js";
Loading

0 comments on commit 57670ca

Please sign in to comment.