Skip to content

Commit 2bc4785

Browse files
feat(pyth-lazer-sui-js): Init Lazer Sui SDK (#3017)
* feat(lazer/sui-js-sdk): initial Sui TS SDK for parse_and_verify_le_ecdsa_update; add workspace entry and docs Co-Authored-By: Tejas Badadare <[email protected]> * chore(lazer/sui-js-sdk): fix tsconfig extends to repo root; enable esModuleInterop/skipLibCheck Co-Authored-By: Tejas Badadare <[email protected]> * chore(lazer/sui-js-sdk): add flat ESLint config using @cprussin/eslint-config Co-Authored-By: Tejas Badadare <[email protected]> * chore(lazer/sui-js-sdk): use CJS flat ESLint config via @cprussin/eslint-config Co-Authored-By: Tejas Badadare <[email protected]> * chore(lazer/sui-js-sdk): wire flat ESLint config via @cprussin/eslint-config path Co-Authored-By: Tejas Badadare <[email protected]> * chore(lazer/sui-js-sdk): fix type import by inferring config; ignore JS and eslint.config.js in lint Co-Authored-By: Tejas Badadare <[email protected]> * chore(lazer/sui-js-sdk): flat ESLint config with @cprussin/eslint-config and in-file ignores Co-Authored-By: Tejas Badadare <[email protected]> * chore(lazer/sui-js-sdk): configure ESLint via @cprussin/eslint-config; fix types/imports; remove deprecated .eslintignore Co-Authored-By: Tejas Badadare <[email protected]> * improve boilerplate * feat(lazer/sui-js-sdk): add runnable examples/SuiRelay.ts showing e2e Lazer update -> Sui PTB composition Co-Authored-By: Tejas Badadare <[email protected]> * refactor(lazer/sui-js-sdk): remove getLeEcdsaUpdate from API; docs: add runnable example instructions Co-Authored-By: Tejas Badadare <[email protected]> * feat(lazer/sui-js-sdk): add lazer.subscribe and signAndExecuteTransaction to SuiRelay example Co-Authored-By: Tejas Badadare <[email protected]> * feat(lazer/sui-js-sdk): SuiRelay example — add subscribe, signing; fix imports and README flags Co-Authored-By: Tejas Badadare <[email protected]> * chore(lazer/sui-js-sdk): rename example to FetchAndVerifyUpdate; update script and README Co-Authored-By: Tejas Badadare <[email protected]> * improve example script * docs * remove turbo json * import * fix scaffolding * fix scaffolding, lint, format * remove main and types exports * fix dual export, use array instead of buffer * lint * build:esm and build:cjs depend on ^build --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent 0e151e6 commit 2bc4785

File tree

13 files changed

+422
-7
lines changed

13 files changed

+422
-7
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
dist/
2+
node_modules/
3+
*.tsbuildinfo
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.turbo/
2+
node_modules/
3+
dist/
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# Pyth Lazer Sui JS SDK
2+
3+
This package provides utilities to create a Sui Programmable Transaction to parse & verify a Pyth Lazer price update on-chain.
4+
5+
## Build
6+
7+
From the repository root:
8+
9+
```sh
10+
pnpm turbo build -F @pythnetwork/pyth-lazer-sui-js
11+
```
12+
13+
## Quickstart
14+
15+
A runnable example is provided at `examples/FetchAndVerifyUpdate.ts`. It:
16+
17+
- connects to Lazer via `@pythnetwork/pyth-lazer-sdk`,
18+
- fetches a single `leEcdsa` payload,
19+
- composes a Sui transaction calling `parse_and_verify_le_ecdsa_update`.
20+
21+
### Run the example
22+
23+
Install `tsx` to run TypeScript scripts:
24+
25+
```sh
26+
npm install -g tsx
27+
```
28+
29+
Execute the example script:
30+
31+
```sh
32+
SUI_KEY=<YOUR_SUI_PRIVATE_KEY> pnpm -F @pythnetwork/pyth-lazer-sui-js example:fetch-and-verify --fullnodeUrl <SUI_FULLNODE_URL> --packageId <PYTH_LAZER_PACKAGE_ID> --stateObjectId <PYTH_LAZER_STATE_OBJECT_ID> --token <LAZER_TOKEN>
33+
```
34+
35+
The script's core logic is summarized below:
36+
37+
```ts
38+
import { SuiClient } from "@mysten/sui/client";
39+
import { Transaction } from "@mysten/sui/transactions";
40+
import { SuiLazerClient } from "@pythnetwork/pyth-lazer-sui-js";
41+
42+
// Prepare Mysten Sui client
43+
const provider = new SuiClient({ url: "<sui-fullnode-url>" });
44+
45+
// Create SDK client
46+
const client = new SuiLazerClient(provider);
47+
48+
// Obtain a Lazer leEcdsa payload using @pythnetwork/pyth-lazer-sdk.
49+
// See examples/FetchAndVerifyUpdate.ts for a runnable end-to-end example.
50+
const leEcdsa: Buffer = /* fetch via @pythnetwork/pyth-lazer-sdk */ Buffer.from(
51+
[],
52+
);
53+
54+
// Build transaction calling parse_and_verify_le_ecdsa_update
55+
const tx = new Transaction();
56+
const packageId = "<pyth_lazer_package_id>";
57+
const stateObjectId = "<pyth_lazer_state_object_id>";
58+
59+
const updateVal = client.addParseAndVerifyLeEcdsaUpdateCall({
60+
tx,
61+
packageId,
62+
stateObjectId,
63+
updateBytes: leEcdsa,
64+
});
65+
66+
// Sign and execute the transaction using your signer.
67+
```
68+
69+
## Notes
70+
71+
- FIXME: Automatic `packageId` management is coming soon. The Lazer contract doesn't support upgradeability yet.
72+
73+
## References
74+
75+
- Pyth Lazer Sui contract: `lazer/contracts/sui/`
76+
- Lazer JS SDK (data source): `lazer/sdk/js/`
77+
- Mysten Sui TS SDK docs: https://sdk.mystenlabs.com/typescript/transaction-building/basics
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { base as default } from "@cprussin/eslint-config";
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import { SuiClient } from "@mysten/sui/client";
2+
import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519";
3+
import { Transaction } from "@mysten/sui/transactions";
4+
import type { Request as SubscriptionRequest } from "@pythnetwork/pyth-lazer-sdk";
5+
import { PythLazerClient } from "@pythnetwork/pyth-lazer-sdk";
6+
import yargs from "yargs";
7+
import { hideBin } from "yargs/helpers";
8+
9+
import { addParseAndVerifyLeEcdsaUpdateCall } from "../src/client.js";
10+
11+
async function getOneLeEcdsaUpdate(urls: string[], token: string) {
12+
const lazer = await PythLazerClient.create({
13+
urls,
14+
token,
15+
numConnections: 1,
16+
});
17+
18+
const subscription: SubscriptionRequest = {
19+
subscriptionId: 1,
20+
type: "subscribe",
21+
priceFeedIds: [1],
22+
properties: ["price", "bestBidPrice", "bestAskPrice", "exponent"],
23+
formats: ["leEcdsa"],
24+
channel: "fixed_rate@200ms",
25+
deliveryFormat: "binary",
26+
jsonBinaryEncoding: "hex",
27+
};
28+
29+
lazer.subscribe(subscription);
30+
31+
return new Promise<Uint8Array>((resolve) => {
32+
lazer.addMessageListener((event) => {
33+
if (event.type === "binary" && event.value.leEcdsa) {
34+
const buf = event.value.leEcdsa;
35+
36+
// For the purposes of this example, we only need one update.
37+
lazer.shutdown();
38+
resolve(buf);
39+
}
40+
});
41+
});
42+
}
43+
44+
async function main() {
45+
const args = await yargs(hideBin(process.argv))
46+
.option("fullnodeUrl", {
47+
type: "string",
48+
description:
49+
"URL of the full Sui node RPC endpoint. e.g: https://fullnode.testnet.sui.io:443",
50+
demandOption: true,
51+
})
52+
.option("packageId", {
53+
type: "string",
54+
description: "Lazer contract package ID",
55+
demandOption: true,
56+
})
57+
.option("stateObjectId", {
58+
type: "string",
59+
description: "Lazer contract shared State object ID",
60+
demandOption: true,
61+
})
62+
.option("lazerUrls", {
63+
type: "array",
64+
string: true,
65+
description: "Lazer WebSocket URLs",
66+
default: [
67+
"wss://pyth-lazer-0.dourolabs.app/v1/stream",
68+
"wss://pyth-lazer-1.dourolabs.app/v1/stream",
69+
],
70+
})
71+
.option("lazerToken", {
72+
type: "string",
73+
description: "Lazer authentication token",
74+
demandOption: true,
75+
})
76+
.help()
77+
.parseAsync();
78+
79+
// Defined as a dependency in turbo.json
80+
// eslint-disable-next-line n/no-process-env
81+
if (process.env.SUI_KEY === undefined) {
82+
throw new Error(
83+
`SUI_KEY environment variable should be set to your Sui private key in hex format.`,
84+
);
85+
}
86+
87+
const provider = new SuiClient({ url: args.fullnodeUrl });
88+
89+
// Fetch the price update
90+
const updateBytes = await getOneLeEcdsaUpdate(
91+
args.lazerUrls,
92+
args.lazerToken,
93+
);
94+
95+
// Build the Sui transaction
96+
const tx = new Transaction();
97+
98+
// Add the parse and verify call
99+
addParseAndVerifyLeEcdsaUpdateCall({
100+
tx,
101+
packageId: args.packageId,
102+
stateObjectId: args.stateObjectId,
103+
updateBytes,
104+
});
105+
106+
// --- You can add more calls to the transaction that consume the parsed update here ---
107+
108+
const wallet = Ed25519Keypair.fromSecretKey(
109+
// eslint-disable-next-line n/no-process-env
110+
Buffer.from(process.env.SUI_KEY, "hex"),
111+
);
112+
const res = await provider.signAndExecuteTransaction({
113+
signer: wallet,
114+
transaction: tx,
115+
options: { showEffects: true, showEvents: true },
116+
});
117+
118+
// eslint-disable-next-line no-console
119+
console.log("Execution result:", JSON.stringify(res, undefined, 2));
120+
}
121+
122+
// eslint-disable-next-line unicorn/prefer-top-level-await
123+
main().catch((error: unknown) => {
124+
throw error;
125+
});
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
{
2+
"name": "@pythnetwork/pyth-lazer-sui-js",
3+
"version": "0.1.0",
4+
"description": "TypeScript SDK for the Pyth Lazer Sui contract",
5+
"license": "Apache-2.0",
6+
"type": "module",
7+
"engines": {
8+
"node": "22"
9+
},
10+
"files": [
11+
"dist"
12+
],
13+
"exports": {
14+
".": {
15+
"import": {
16+
"types": "./dist/esm/client.d.ts",
17+
"default": "./dist/esm/client.js"
18+
},
19+
"require": {
20+
"types": "./dist/cjs/client.d.ts",
21+
"default": "./dist/cjs/client.js"
22+
}
23+
}
24+
},
25+
"sideEffects": false,
26+
"scripts": {
27+
"build:cjs": "tsc --project tsconfig.build.json --verbatimModuleSyntax false --module commonjs --outDir ./dist/cjs && echo '{\"type\":\"commonjs\"}' > dist/cjs/package.json",
28+
"build:esm": "tsc --project tsconfig.build.json --outDir ./dist/esm && echo '{\"type\":\"module\"}' > dist/esm/package.json",
29+
"fix:format": "prettier --write .",
30+
"fix:lint": "eslint --fix .",
31+
"test:format": "prettier --check .",
32+
"test:lint": "eslint . --max-warnings 0",
33+
"test:types": "tsc",
34+
"example:fetch-and-verify": "tsx examples/fetch-and-verify-update.ts"
35+
},
36+
"dependencies": {
37+
"@mysten/sui": "catalog:",
38+
"@pythnetwork/pyth-lazer-sdk": "workspace:*",
39+
"@types/yargs": "catalog:",
40+
"yargs": "catalog:"
41+
},
42+
"devDependencies": {
43+
"@cprussin/eslint-config": "catalog:",
44+
"@cprussin/tsconfig": "catalog:",
45+
"@types/node": "catalog:",
46+
"eslint": "catalog:",
47+
"prettier": "catalog:",
48+
"typescript": "catalog:"
49+
},
50+
"publishConfig": {
51+
"access": "public"
52+
}
53+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { bcs } from "@mysten/sui/bcs";
2+
import { Transaction } from "@mysten/sui/transactions";
3+
import { SUI_CLOCK_OBJECT_ID } from "@mysten/sui/utils";
4+
5+
export function addParseAndVerifyLeEcdsaUpdateCall({
6+
tx,
7+
packageId,
8+
stateObjectId,
9+
updateBytes,
10+
}: {
11+
tx: Transaction;
12+
packageId: string;
13+
stateObjectId: string;
14+
updateBytes: Uint8Array;
15+
}) {
16+
const [updateObj] = tx.moveCall({
17+
target: `${packageId}::pyth_lazer::parse_and_verify_le_ecdsa_update`,
18+
arguments: [
19+
tx.object(stateObjectId),
20+
tx.object(SUI_CLOCK_OBJECT_ID),
21+
tx.pure(bcs.vector(bcs.U8).serialize(updateBytes).toBytes()),
22+
],
23+
});
24+
return updateObj;
25+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"extends": "./tsconfig.json",
3+
"compilerOptions": {
4+
"noEmit": false,
5+
"incremental": false,
6+
"declaration": true
7+
},
8+
"exclude": ["node_modules", "dist"]
9+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"extends": "@cprussin/tsconfig/base.json"
3+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"$schema": "https://turbo.build/schema.json",
3+
"extends": ["//"],
4+
"tasks": {
5+
"example:fetch-and-verify": {
6+
"env": ["SUI_KEY"]
7+
}
8+
}
9+
}

0 commit comments

Comments
 (0)