Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
b1625c0
feat(lazer/sui-js-sdk): initial Sui TS SDK for parse_and_verify_le_ec…
devin-ai-integration[bot] Aug 29, 2025
7ce2510
chore(lazer/sui-js-sdk): fix tsconfig extends to repo root; enable es…
devin-ai-integration[bot] Aug 29, 2025
026afdd
chore(lazer/sui-js-sdk): add flat ESLint config using @cprussin/eslin…
devin-ai-integration[bot] Aug 29, 2025
243d606
chore(lazer/sui-js-sdk): use CJS flat ESLint config via @cprussin/esl…
devin-ai-integration[bot] Aug 29, 2025
07cf308
chore(lazer/sui-js-sdk): wire flat ESLint config via @cprussin/eslint…
devin-ai-integration[bot] Aug 29, 2025
774257c
chore(lazer/sui-js-sdk): fix type import by inferring config; ignore …
devin-ai-integration[bot] Aug 29, 2025
203d237
chore(lazer/sui-js-sdk): flat ESLint config with @cprussin/eslint-con…
devin-ai-integration[bot] Aug 29, 2025
6e8f0a6
chore(lazer/sui-js-sdk): configure ESLint via @cprussin/eslint-config…
devin-ai-integration[bot] Aug 29, 2025
4f3d45f
improve boilerplate
tejasbadadare Aug 29, 2025
cf0edef
feat(lazer/sui-js-sdk): add runnable examples/SuiRelay.ts showing e2e…
devin-ai-integration[bot] Aug 29, 2025
70714bd
refactor(lazer/sui-js-sdk): remove getLeEcdsaUpdate from API; docs: a…
devin-ai-integration[bot] Aug 29, 2025
00ae520
feat(lazer/sui-js-sdk): add lazer.subscribe and signAndExecuteTransac…
devin-ai-integration[bot] Aug 29, 2025
e2460ad
feat(lazer/sui-js-sdk): SuiRelay example — add subscribe, signing; fi…
devin-ai-integration[bot] Aug 29, 2025
cc1cd1b
chore(lazer/sui-js-sdk): rename example to FetchAndVerifyUpdate; upda…
devin-ai-integration[bot] Aug 29, 2025
acdf31d
improve example script
tejasbadadare Aug 30, 2025
3e3b1c0
docs
tejasbadadare Sep 4, 2025
3e59238
remove turbo json
tejasbadadare Sep 4, 2025
1abfa5e
import
tejasbadadare Sep 4, 2025
ceaa78b
fix scaffolding
tejasbadadare Sep 4, 2025
4d26a50
fix scaffolding, lint, format
tejasbadadare Sep 4, 2025
31b9d43
remove main and types exports
tejasbadadare Sep 4, 2025
b372753
fix dual export, use array instead of buffer
tejasbadadare Sep 5, 2025
88a5353
lint
tejasbadadare Sep 8, 2025
e20d12e
build:esm and build:cjs depend on ^build
tejasbadadare Sep 8, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions lazer/contracts/sui/sdk/js/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
lib/
node_modules/
*.tsbuildinfo
69 changes: 69 additions & 0 deletions lazer/contracts/sui/sdk/js/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Pyth Lazer Sui JS SDK

This package provides utilities to create a Sui Programmable Transaction to parse & verify a Pyth Lazer price update on-chain.

## Build

From the repository root:

```sh
pnpm turbo build -F @pythnetwork/pyth-lazer-sui-js
```

## Quickstart
A runnable example is provided at `examples/FetchAndVerifyUpdate.ts`. It:
- connects to Lazer via `@pythnetwork/pyth-lazer-sdk`,
- fetches a single `leEcdsa` payload,
- composes a Sui transaction calling `parse_and_verify_le_ecdsa_update`.

### Run the example
Install `tsx` to run TypeScript scripts:
```sh
npm install -g tsx
```

Execute the example script:
```sh
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>
```

The script's core logic is summarized below:
```ts
import { SuiClient } from "@mysten/sui/client";
import { Transaction } from "@mysten/sui/transactions";
import { SuiLazerClient } from "@pythnetwork/pyth-lazer-sui-js";

// Prepare Mysten Sui client
const provider = new SuiClient({ url: "<sui-fullnode-url>" });

// Create SDK client
const client = new SuiLazerClient(provider);

// Obtain a Lazer leEcdsa payload using @pythnetwork/pyth-lazer-sdk.
// See examples/FetchAndVerifyUpdate.ts for a runnable end-to-end example.
const leEcdsa: Buffer = /* fetch via @pythnetwork/pyth-lazer-sdk */ Buffer.from([]);

// Build transaction calling parse_and_verify_le_ecdsa_update
const tx = new Transaction();
const packageId = "<pyth_lazer_package_id>";
const stateObjectId = "<pyth_lazer_state_object_id>";

const updateVal = client.addParseAndVerifyLeEcdsaUpdateCall({
tx,
packageId,
stateObjectId,
updateBytes: leEcdsa,
});

// Sign and execute the transaction using your signer.
```

## Notes

- FIXME: Automatic `packageId` management is coming soon. The Lazer contract doesn't support upgradeability yet.

## References

- Pyth Lazer Sui contract: `lazer/contracts/sui/`
- Lazer JS SDK (data source): `lazer/sdk/js/`
- Mysten Sui TS SDK docs: https://sdk.mystenlabs.com/typescript/transaction-building/basics
8 changes: 8 additions & 0 deletions lazer/contracts/sui/sdk/js/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { base } from "@cprussin/eslint-config";

export default [
...base,
{
ignores: ["eslint.config.js", "lib", "src/**/*.js"],
},
];
117 changes: 117 additions & 0 deletions lazer/contracts/sui/sdk/js/examples/FetchAndVerifyUpdate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { SuiClient } from "@mysten/sui/client";
import { Transaction } from "@mysten/sui/transactions";
import { SuiLazerClient } from "../src/client";
import { PythLazerClient, Request } from "@pythnetwork/pyth-lazer-sdk";
import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519";
import yargs from "yargs";
import { hideBin } from "yargs/helpers";

async function getOneLeEcdsaUpdate(urls: string[], token: string | undefined) {
const config: Parameters<typeof PythLazerClient.create>[0] = {
urls,
token: token ?? "",
numConnections: 1,
};
const lazer = await PythLazerClient.create(config);

const subscription: Request = {
subscriptionId: 1,
type: "subscribe",
priceFeedIds: [1],
properties: [
"price",
"bestBidPrice",
"bestAskPrice",
"exponent",
],
formats: ["leEcdsa"],
channel: "fixed_rate@200ms",
deliveryFormat: "binary",
jsonBinaryEncoding: "hex",
};

lazer.subscribe(subscription)

return new Promise<Buffer>((resolve, _) => {
lazer.addMessageListener((event) => {
if (event.type === "binary" && event.value.leEcdsa) {
const buf = event.value.leEcdsa;

// For the purposes of this example, we only need one update.
lazer.shutdown();
resolve(buf);
}
});
});
}

async function main() {
const args = await yargs(hideBin(process.argv))
.option("fullnodeUrl", {
type: "string",
description: "URL of the full Sui node RPC endpoint. e.g: https://fullnode.testnet.sui.io:443",
demandOption: true,
})
.option("packageId", {
type: "string",
description: "Lazer contract package ID",
demandOption: true,
})
.option("stateObjectId", {
type: "string",
description: "Lazer contract shared State object ID",
demandOption: true,
})
.option("lazerUrls", {
type: "string",
description: "Comma-separated Lazer WebSocket URLs",
default: "wss://pyth-lazer-0.dourolabs.app/v1/stream,wss://pyth-lazer-1.dourolabs.app/v1/stream",
})
.option("token", {
type: "string",
description: "Lazer authentication token",
})
.help()
.parseAsync();

if (process.env.SUI_KEY === undefined) {
throw new Error(`SUI_KEY environment variable should be set to your Sui private key in hex format.`);
}

const lazerUrls = args.lazerUrls.split(",");

const provider = new SuiClient({ url: args.fullnodeUrl });
const client = new SuiLazerClient(provider);

// Fetch the price update
const updateBytes = await getOneLeEcdsaUpdate(lazerUrls, args.token);

// Build the Sui transaction
const tx = new Transaction();

// Add the parse and verify call
client.addParseAndVerifyLeEcdsaUpdateCall({
tx,
packageId: args.packageId,
stateObjectId: args.stateObjectId,
updateBytes,
});

// You can add more calls to the transaction that consume the parsed update here

const wallet = Ed25519Keypair.fromSecretKey(
Buffer.from(process.env.SUI_KEY, "hex"),
);
const res = await provider.signAndExecuteTransaction({
signer: wallet,
transaction: tx,
options: { showEffects: true, showEvents: true },
});

console.log("Execution result:", JSON.stringify(res, null, 2));
}

main().catch((e) => {
console.error(e);
process.exit(1);
});
39 changes: 39 additions & 0 deletions lazer/contracts/sui/sdk/js/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"name": "@pythnetwork/pyth-lazer-sui-js",
"version": "0.1.0",
"description": "TypeScript SDK for the Pyth Lazer Sui contract",
"license": "Apache-2.0",
"type": "module",
"main": "lib/index.js",
"module": "lib/index.js",
"types": "lib/index.d.ts",
"sideEffects": false,
"scripts": {
"build": "tsc -p tsconfig.json",
"clean": "rimraf lib",
"fix": "pnpm prettier --write src && pnpm eslint --fix src --ext .ts",
"lint": "eslint src --ext .ts",
"typecheck": "tsc -p tsconfig.json --noEmit",
"prepublishOnly": "pnpm build",
"example:fetch-and-verify": "tsx examples/FetchAndVerifyUpdate.ts"
},
"dependencies": {
"@mysten/sui": "^1.3.0",
"@pythnetwork/pyth-lazer-sdk": "workspace:*",
"@types/yargs": "^17.0.33",
"buffer": "^6.0.3",
"yargs": "^18.0.0"
},
"devDependencies": {
"@cprussin/eslint-config": "catalog:",
"@cprussin/tsconfig": "catalog:",
"@types/node": "^18.11.18",
"eslint": "catalog:",
"prettier": "catalog:",
"rimraf": "^5.0.5",
"typescript": "^5.3.3"
},
"publishConfig": {
"access": "public"
}
}
32 changes: 32 additions & 0 deletions lazer/contracts/sui/sdk/js/src/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { bcs } from "@mysten/sui/bcs";
import { SuiClient } from "@mysten/sui/client";
import { Transaction } from "@mysten/sui/transactions";
import { SUI_CLOCK_OBJECT_ID } from "@mysten/sui/utils";

const MAX_ARGUMENT_SIZE = 16 * 1024;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Super minor and not worth changing, but one thing I really like to do with consts like this is break them down with units so it's super easy to understand where the numbers come from, e.g. something like this:

const ONE_KIBIBYTE_IN_BYTES = 1024;
const MAX_ARGUMENT_SIZE_IN_BYTES = 16 * ONE_KIBIBYTE_IN_BYTES;

(I'm assuming that's where 1024 comes from but I don't know where 16 comes from)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually realized this might be an old requirement imposed by WH, idt we need it anymore


export type ObjectId = string;

export class SuiLazerClient {
constructor(public provider: SuiClient) {}

addParseAndVerifyLeEcdsaUpdateCall(opts: {
tx: Transaction;
packageId: string;
stateObjectId: ObjectId;
updateBytes: Buffer;
}) {
const { tx, packageId, stateObjectId, updateBytes } = opts;
const [updateObj] = tx.moveCall({
target: `${packageId}::pyth_lazer::parse_and_verify_le_ecdsa_update`,
arguments: [
tx.object(stateObjectId),
tx.object(SUI_CLOCK_OBJECT_ID),
tx.pure(
bcs.vector(bcs.U8).serialize([...updateBytes], { maxSize: MAX_ARGUMENT_SIZE }).toBytes(),
),
],
});
return updateObj;
}
}
1 change: 1 addition & 0 deletions lazer/contracts/sui/sdk/js/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./client";
20 changes: 20 additions & 0 deletions lazer/contracts/sui/sdk/js/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"extends": "../../../../../tsconfig.base.json",
"compilerOptions": {
"esModuleInterop": true,
"skipLibCheck": true,
"declaration": true,
"outDir": "./lib",
"rootDir": "src",
"module": "commonjs",
"target": "esnext",
"strict": true
},
"include": [
"src"
],
"exclude": [
"node_modules",
"lib"
]
}
Loading