-
Notifications
You must be signed in to change notification settings - Fork 300
feat(pyth-lazer-sui-js): Init Lazer Sui SDK #3017
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 18 commits
b1625c0
7ce2510
026afdd
243d606
07cf308
774257c
203d237
6e8f0a6
4f3d45f
cf0edef
70714bd
00ae520
e2460ad
cc1cd1b
acdf31d
3e3b1c0
3e59238
1abfa5e
ceaa78b
4d26a50
31b9d43
b372753
88a5353
e20d12e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
lib/ | ||
node_modules/ | ||
*.tsbuildinfo |
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 |
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"], | ||
}, | ||
]; |
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] = { | ||
tejasbadadare marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
urls, | ||
token: token ?? "", | ||
numConnections: 1, | ||
}; | ||
const lazer = await PythLazerClient.create(config); | ||
|
||
const subscription: Request = { | ||
tejasbadadare marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
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(","); | ||
tejasbadadare marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
||
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); | ||
}); |
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", | ||
tejasbadadare marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
"sideEffects": false, | ||
"scripts": { | ||
"build": "tsc -p tsconfig.json", | ||
tejasbadadare marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
"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" | ||
}, | ||
tejasbadadare marked this conversation as resolved.
Show resolved
Hide resolved
|
||
"dependencies": { | ||
"@mysten/sui": "^1.3.0", | ||
tejasbadadare marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
"@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" | ||
} | ||
} |
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; | ||
|
||
|
||
export type ObjectId = string; | ||
tejasbadadare marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
||
export class SuiLazerClient { | ||
tejasbadadare marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
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; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from "./client"; | ||
tejasbadadare marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
{ | ||
"extends": "../../../../../tsconfig.base.json", | ||
tejasbadadare marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
"compilerOptions": { | ||
"esModuleInterop": true, | ||
"skipLibCheck": true, | ||
"declaration": true, | ||
"outDir": "./lib", | ||
"rootDir": "src", | ||
"module": "commonjs", | ||
"target": "esnext", | ||
"strict": true | ||
}, | ||
"include": [ | ||
"src" | ||
], | ||
"exclude": [ | ||
"node_modules", | ||
"lib" | ||
] | ||
} |
Uh oh!
There was an error while loading. Please reload this page.