Skip to content

Commit 57670ca

Browse files
feat(pyth-lazer-sdk): add ed25519 ix builder function (#2203)
* feat(pyth-lazer-sdk): add ed25519 ix builder function * fix * fix
1 parent f0659ce commit 57670ca

File tree

5 files changed

+243
-474
lines changed

5 files changed

+243
-474
lines changed

lazer/sdk/js/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@pythnetwork/pyth-lazer-sdk",
3-
"version": "0.1.1",
3+
"version": "0.1.2",
44
"description": "Pyth Lazer SDK",
55
"publishConfig": {
66
"access": "public"
@@ -60,6 +60,8 @@
6060
],
6161
"license": "Apache-2.0",
6262
"dependencies": {
63+
"@solana/buffer-layout": "^4.0.1",
64+
"@solana/web3.js": "^1.98.0",
6365
"isomorphic-ws": "^5.0.0",
6466
"ws": "^8.18.0"
6567
}

lazer/sdk/js/src/client.ts

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

lazer/sdk/js/src/ed25519.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import * as BufferLayout from "@solana/buffer-layout";
2+
import { Ed25519Program, TransactionInstruction } from "@solana/web3.js";
3+
4+
const ED25519_INSTRUCTION_LEN = 16;
5+
const SIGNATURE_LEN = 64;
6+
const PUBKEY_LEN = 32;
7+
const MAGIC_LEN = 4;
8+
const MESSAGE_SIZE_LEN = 2;
9+
10+
const ED25519_INSTRUCTION_LAYOUT = BufferLayout.struct<
11+
Readonly<{
12+
messageDataOffset: number;
13+
messageDataSize: number;
14+
messageInstructionIndex: number;
15+
numSignatures: number;
16+
padding: number;
17+
publicKeyInstructionIndex: number;
18+
publicKeyOffset: number;
19+
signatureInstructionIndex: number;
20+
signatureOffset: number;
21+
}>
22+
>([
23+
BufferLayout.u8("numSignatures"),
24+
BufferLayout.u8("padding"),
25+
BufferLayout.u16("signatureOffset"),
26+
BufferLayout.u16("signatureInstructionIndex"),
27+
BufferLayout.u16("publicKeyOffset"),
28+
BufferLayout.u16("publicKeyInstructionIndex"),
29+
BufferLayout.u16("messageDataOffset"),
30+
BufferLayout.u16("messageDataSize"),
31+
BufferLayout.u16("messageInstructionIndex"),
32+
]);
33+
34+
export const createEd25519Instruction = (
35+
message: Buffer,
36+
instructionIndex: number,
37+
startingOffset: number
38+
) => {
39+
const signatureOffset = startingOffset + MAGIC_LEN;
40+
const publicKeyOffset = signatureOffset + SIGNATURE_LEN;
41+
const messageDataSizeOffset = publicKeyOffset + PUBKEY_LEN;
42+
const messageDataOffset = messageDataSizeOffset + MESSAGE_SIZE_LEN;
43+
44+
const messageDataSize = message.readUInt16LE(
45+
messageDataSizeOffset - startingOffset
46+
);
47+
48+
const instructionData = Buffer.alloc(ED25519_INSTRUCTION_LEN);
49+
50+
ED25519_INSTRUCTION_LAYOUT.encode(
51+
{
52+
numSignatures: 1,
53+
padding: 0,
54+
signatureOffset,
55+
signatureInstructionIndex: instructionIndex,
56+
publicKeyOffset,
57+
publicKeyInstructionIndex: instructionIndex,
58+
messageDataOffset,
59+
messageDataSize: messageDataSize,
60+
messageInstructionIndex: instructionIndex,
61+
},
62+
instructionData
63+
);
64+
65+
return new TransactionInstruction({
66+
keys: [],
67+
programId: Ed25519Program.programId,
68+
data: instructionData,
69+
});
70+
};

lazer/sdk/js/src/index.ts

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

0 commit comments

Comments
 (0)