diff --git a/.github/workflows/ci-lazer-stellar.yml b/.github/workflows/ci-lazer-stellar.yml index d23a252d..eb0426ef 100644 --- a/.github/workflows/ci-lazer-stellar.yml +++ b/.github/workflows/ci-lazer-stellar.yml @@ -31,3 +31,19 @@ jobs: run: cargo clippy --release --target wasm32v1-none --locked -- --deny warnings - name: Build wasm run: cargo build --release --target wasm32v1-none --locked + + lazer-stellar-client: + name: Lazer Stellar Demo Client + runs-on: ubuntu-22.04 + defaults: + run: + working-directory: lazer/stellar/client + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + - run: npm ci + # Static checks only: the demo itself needs a Lazer token, a funded Stellar + # account and a live network, so it cannot run in CI. + - run: npm test diff --git a/lazer/stellar/README.md b/lazer/stellar/README.md index a2c42a4e..6e10dcf0 100644 --- a/lazer/stellar/README.md +++ b/lazer/stellar/README.md @@ -17,6 +17,10 @@ integration on Stellar: This is an example, not a production library — it tracks exactly one feed and keeps only the most recent price. The main implementation lives in [`src/lib.rs`](./src/lib.rs). +The [`client/`](./client) directory holds a standalone Node client that drives the whole flow in one +command: it fetches a freshly signed update from Pyth Lazer, submits it to a deployed instance of +this contract, and reads the stored price back. + ## Prerequisites - A Rust toolchain (minimum **1.84**) with the `wasm32v1-none` target: @@ -24,6 +28,8 @@ recent price. The main implementation lives in [`src/lib.rs`](./src/lib.rs). rustup target add wasm32v1-none ``` - The [Stellar CLI](https://developers.stellar.org/docs/tools/developer-tools/cli/install-cli). +- Node.js 20 or newer, to run the demo client. +- A Pyth Lazer access token, exported as `PYTH_API_KEY`. ## Build @@ -48,7 +54,7 @@ mainnet id as `--lazer` if you deploy there instead: Configure a funded testnet identity once: ```bash -stellar keys generate deployer --network testnet +stellar keys generate deployer --network testnet --fund ``` Build and deploy the example, passing the constructor args (verifier address, feed id, freshness @@ -65,22 +71,81 @@ stellar contract deploy \ --freshness_threshold_us 60000000 ``` -Then submit a signed Lazer update and read it back: +The command prints the deployed contract id (`C...`). Export it: + +```bash +export EXAMPLE_CONTRACT_ADDRESS= +``` + +## Run the demo client + +The client fetches a signed update, calls `update_price`, and reads `get_price` back — one command, +no monorepo checkout required: + +```bash +cd client +npm install +export PYTH_API_KEY= +npm run demo -- --network testnet --contract-id "$EXAMPLE_CONTRACT_ADDRESS" +``` + +With no `--secret` and no `STELLAR_WALLET_SECRET`, the client generates a throwaway keypair and +friendbot-funds it on testnet. On mainnet a funded account is required: ```bash +export STELLAR_WALLET_SECRET= +npm run demo -- --network mainnet --contract-id "$EXAMPLE_CONTRACT_ADDRESS" +``` + +Run `npm run demo -- --help` for the full option list (`--feed-id`, `--channel`, +`--lazer-endpoint`, `--secret`). + +See [`client/README.md`](./client/README.md) for the sample output and the failure modes. + +## Invoking the contract directly + +The client is the supported way to produce a payload, because the update has to be signed by Lazer +and fresh. If you want to drive the contract from the Stellar CLI instead, take the `Update hex` +line the client prints and pass it straight through — but note the payload goes stale within the +contract's `freshness_threshold_us`, so fetch and submit in the same breath: + +```bash +UPDATE_HEX=$(curl -sS -X POST https://pyth-lazer-0.dourolabs.app/v1/latest_price \ + -H "Authorization: Bearer $PYTH_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{"priceFeedIds":[1],"properties":["price","exponent","feedUpdateTimestamp"],"formats":["leEcdsa"],"jsonBinaryEncoding":"hex","channel":"fixed_rate@200ms"}' \ + | python3 -c 'import json,sys; print(json.load(sys.stdin)["leEcdsa"]["data"])') + stellar contract invoke \ - --id \ + --id "$EXAMPLE_CONTRACT_ADDRESS" \ --source deployer \ --network testnet \ - -- update_price --payload + -- update_price --payload "$UPDATE_HEX" stellar contract invoke \ - --id \ + --id "$EXAMPLE_CONTRACT_ADDRESS" \ --source deployer \ --network testnet \ -- get_price ``` +`properties` must list `price`, `exponent` **and** `feedUpdateTimestamp`: `update_price` reads all +three, and a property that is not requested decodes to `None` on-chain, failing the call with +`PriceMissing` / `ExponentMissing` / `TimestampMissing`. `formats` must be `leEcdsa`, which is the +signature format the Soroban verifier accepts. + +## Failure modes worth knowing + +`update_price` is deliberately strict. The two you will hit while experimenting: + +- **`PriceStale` (error #1)** — the update's feed timestamp lags ledger time by more than + `freshness_threshold_us`. Fetch a new update. +- **`PriceOutdated` (error #9)** — the stored price is at least as new as the update you submitted. + Updates are strictly monotonic, so **replaying a cached payload always fails**. Re-running the + demo works because it fetches a fresh update every run. + +The client maps these codes to a plain-English explanation before dumping the raw diagnostics. + ## Additional Resources - The Pyth Lazer consumer guide on [docs.pyth.network/lazer](https://docs.pyth.network/lazer). diff --git a/lazer/stellar/client/.gitignore b/lazer/stellar/client/.gitignore new file mode 100644 index 00000000..c2658d7d --- /dev/null +++ b/lazer/stellar/client/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/lazer/stellar/client/.prettierignore b/lazer/stellar/client/.prettierignore new file mode 100644 index 00000000..3c3629e6 --- /dev/null +++ b/lazer/stellar/client/.prettierignore @@ -0,0 +1 @@ +node_modules diff --git a/lazer/stellar/client/.prettierrc.json b/lazer/stellar/client/.prettierrc.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/lazer/stellar/client/.prettierrc.json @@ -0,0 +1 @@ +{} diff --git a/lazer/stellar/client/README.md b/lazer/stellar/client/README.md new file mode 100644 index 00000000..1331e8c3 --- /dev/null +++ b/lazer/stellar/client/README.md @@ -0,0 +1,92 @@ +# Pyth Lazer Stellar demo client + +A standalone Node client for the [example consumer contract](../README.md). One command runs the +whole consumer story: + +1. Fetch a freshly signed Pyth Lazer price update over REST, carrying the three properties the + contract needs (`price`, `exponent`, `feedUpdateTimestamp`). +2. Print the update hex and the off-chain price, for comparison. +3. Submit it to the deployed example consumer's `update_price`. +4. Read `get_price` back and print the stored price in human units. + +It depends only on `@stellar/stellar-sdk` and talks to Lazer with plain `fetch`, so it runs outside +the Pyth monorepos. + +## Usage + +```bash +npm install +export PYTH_API_KEY= +npm run demo -- --network testnet --contract-id +``` + +Deploy the example consumer first — see the [parent README](../README.md). + +| Option | Default | Notes | +| ------------------ | ---------------------------------------------------- | --------------------------------------------------- | +| `--contract-id` | _required_ | Deployed example consumer contract id. | +| `--network` | `testnet` | `testnet` or `mainnet`. | +| `--secret` | `$STELLAR_WALLET_SECRET` | Stellar secret key. Required on mainnet. | +| `--feed-id` | `1` (BTC/USD) | Must match the feed the contract was deployed with. | +| `--channel` | `fixed_rate@200ms` | Lazer channel. | +| `--lazer-endpoint` | `https://pyth-lazer-0.dourolabs.app/v1/latest_price` | Lazer `latest_price` REST endpoint. | + +On testnet, an account that is missing or unfunded is topped up from friendbot automatically — with +no secret at all the client generates a throwaway keypair. Mainnet has no friendbot, so +`STELLAR_WALLET_SECRET` (or `--secret`) must hold a funded account. The client prints the signing +account's public key and XLM balance before it spends anything. + +On mainnet the client bids an inclusion fee of `1000000` stroops (0.1 XLM). Mainnet ledgers run +close to full and a base-fee bid gets evicted, so the transaction would expire without ever reaching +a ledger. + +## Sample output + +``` +=== Pyth Lazer Stellar demo (testnet) === + Example consumer: CCEZSNTTVRIYXYHBKD2OMBRQI3RP6ZNXFWQ5ZG5ZDTQ7JZ4DTZ6FR72Q + Explorer: https://stellar.expert/explorer/testnet/contract/CCEZSNTTVRIYXYHBKD2OMBRQI3RP6ZNXFWQ5ZG5ZDTQ7JZ4DTZ6FR72Q + +=== Signing account (from --secret / $STELLAR_WALLET_SECRET) === + Public key: GBTKQ2WYNYE5TJW3N5LH7PGGERX2EIBDIIN7X6LFHMVQQZCZT36KA3PC + XLM balance: 9999.9995630 + +=== Fetching signed Lazer update (feed 1, fixed_rate@200ms) === + Update size: 112 bytes + Update hex: e4bd474d77cb5c62...c04301cd995b0600 + Off-chain price: 75549.21621105 (raw 7554921621105, exponent -8) + Feed timestamp: 1789565987800000 us (2026-09-16T13:39:47.800Z) + +=== Submitting update_price to the example consumer === + Transaction hash: 10c8e82aed30bfb2f3de6848f8729c4288a2c0e791fc280299700070682ffcb9 + Explorer: https://stellar.expert/explorer/testnet/tx/10c8e82aed30bfb2f3de6848f8729c4288a2c0e791fc280299700070682ffcb9 + Status: SUCCESS (ledger 4708481) + +=== Reading get_price back from the contract === + Stored price: 75549.21621105 + Raw price: 7554921621105 (exponent -8) + Feed timestamp: 1789565987800000 us (2026-09-16T13:39:47.800Z) + +✅ Demo complete. +``` + +## Failure modes + +The client decodes the contract's error codes and leads with what they mean: + +- **`PriceStale` (#1)** — the update lagged ledger time by more than the contract's + `freshness_threshold_us`. Fetch a new update. +- **`PriceOutdated` (#9)** — the stored price is at least as new as this update. `update_price` is + strictly monotonic, so a replayed payload always fails. The client fetches a fresh update on every + run, so re-running is safe. +- **`FeedMissing` (#2)** — `--feed-id` does not match the feed the contract was deployed with. +- **`TimestampMissing` / `PriceMissing` / `ExponentMissing` (#3 / #4 / #5)** — a required property + was absent from the payload. The client always requests all three, so this only shows up against + a payload produced elsewhere. + +## Development + +```bash +npm run test # prettier --check + tsc --noEmit +npm run fix:format # prettier --write +``` diff --git a/lazer/stellar/client/package-lock.json b/lazer/stellar/client/package-lock.json new file mode 100644 index 00000000..5604cc1f --- /dev/null +++ b/lazer/stellar/client/package-lock.json @@ -0,0 +1,1384 @@ +{ + "name": "pyth-lazer-stellar-example-client", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "pyth-lazer-stellar-example-client", + "version": "1.0.0", + "license": "Apache-2.0", + "dependencies": { + "@stellar/stellar-sdk": "^14.0.0" + }, + "devDependencies": { + "@types/node": "^22.10.0", + "prettier": "^3.4.1", + "tsx": "^4.19.2", + "typescript": "^5.7.2" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@noble/curves": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", + "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@stellar/js-xdr": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@stellar/js-xdr/-/js-xdr-3.1.2.tgz", + "integrity": "sha512-VVolPL5goVEIsvuGqDc5uiKxV03lzfWdvYg1KikvwheDmTBO68CKDji3bAZ/kppZrx5iTA8z3Ld5yuytcvhvOQ==", + "license": "Apache-2.0" + }, + "node_modules/@stellar/stellar-base": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@stellar/stellar-base/-/stellar-base-14.1.0.tgz", + "integrity": "sha512-A8kFli6QGy22SRF45IjgPAJfUNGjnI+R7g4DF5NZYVsD1kGf7B4ITyc4OPclLV9tqNI4/lXxafGEw0JEUbHixw==", + "deprecated": "This package is now rolled into @stellar/stellar-sdk. Please use @stellar/stellar-sdk to continue receiving updates and support.", + "license": "Apache-2.0", + "dependencies": { + "@noble/curves": "^1.9.6", + "@stellar/js-xdr": "^3.1.2", + "base32.js": "^0.1.0", + "bignumber.js": "^9.3.1", + "buffer": "^6.0.3", + "sha.js": "^2.4.12" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@stellar/stellar-sdk": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@stellar/stellar-sdk/-/stellar-sdk-14.6.1.tgz", + "integrity": "sha512-A1rQWDLdUasXkMXnYSuhgep+3ZZzyuXJKdt5/KAIc0gkmSp906HTvUpbT4pu+bVr41tu0+J4Ugz9J4BQAGGytg==", + "license": "Apache-2.0", + "dependencies": { + "@stellar/stellar-base": "^14.1.0", + "axios": "^1.13.3", + "bignumber.js": "^9.3.1", + "commander": "^14.0.2", + "eventsource": "^2.0.2", + "feaxios": "^0.0.23", + "randombytes": "^2.1.0", + "toml": "^3.0.0", + "urijs": "^1.19.1" + }, + "bin": { + "stellar-js": "bin/stellar-js" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@types/node": { + "version": "22.20.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.3.tgz", + "integrity": "sha512-DZmzkmwHzXrLPAXPyKNDzlIwMMUZCVacoD25ywdy5YTKGbOx/2ld+Q38Im2zJ0vBuZP5Prd3VZutKZyXwkOS8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axios": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.20.0.tgz", + "integrity": "sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.6", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/base32.js": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/base32.js/-/base32.js-0.1.0.tgz", + "integrity": "sha512-n3TkB02ixgBOhTvANakDb4xaMXnYUVkNoRFJjQflcqMQhyEKxEHdj3E6N8t8sUQ0mjH/3/JxzlXuz3ul/J90pQ==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "node_modules/eventsource": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-2.0.2.tgz", + "integrity": "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/feaxios": { + "version": "0.0.23", + "resolved": "https://registry.npmjs.org/feaxios/-/feaxios-0.0.23.tgz", + "integrity": "sha512-eghR0A21fvbkcQBgZuMfQhrXxJzC0GNUGC9fXhBge33D+mFDTwl0aJ35zoQQn575BhyjQitRc5N4f+L4cP708g==", + "license": "MIT", + "dependencies": { + "is-retry-allowed": "^3.0.0" + } + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-retry-allowed": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-3.0.0.tgz", + "integrity": "sha512-9xH0xvoggby+u0uGF7cZXdrutWiBiaFG8ZT4YFPXL8NzkyAwX3AKGLeFQLvzDpM430+nDFBZ1LHkie/8ocL06A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "license": "MIT" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/prettier": { + "version": "3.9.7", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.7.tgz", + "integrity": "sha512-T3mF5P7HFzZVLDQuCUNtJj6WK1pcpLMweMNUVwGb01bLNkH9AkKxtZvmInw8K6bnn2O3d6/5RHl6zSHiBAD0Og==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/sha.js": { + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", + "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", + "license": "(MIT AND BSD-3-Clause)", + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.0" + }, + "bin": { + "sha.js": "bin.js" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/to-buffer": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", + "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==", + "license": "MIT", + "dependencies": { + "isarray": "^2.0.5", + "safe-buffer": "^5.2.1", + "typed-array-buffer": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/toml": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz", + "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==", + "license": "MIT" + }, + "node_modules/tsx": { + "version": "4.23.13", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.13.tgz", + "integrity": "sha512-BL5MGkRln6aDYhb0xbQlEAGw743BaZYWdbWtdJOBriYJboKgUUYCadFp2/FpBBZquBC/ezNBn7wMMPx7FDZUDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/urijs": { + "version": "1.19.11", + "resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.11.tgz", + "integrity": "sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ==", + "license": "MIT" + }, + "node_modules/which-typed-array": { + "version": "1.1.22", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.22.tgz", + "integrity": "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==", + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + } + } +} diff --git a/lazer/stellar/client/package.json b/lazer/stellar/client/package.json new file mode 100644 index 00000000..7e2bb6b0 --- /dev/null +++ b/lazer/stellar/client/package.json @@ -0,0 +1,27 @@ +{ + "name": "pyth-lazer-stellar-example-client", + "version": "1.0.0", + "description": "One-command demo client for the Pyth Lazer Stellar example consumer contract", + "type": "module", + "private": true, + "scripts": { + "demo": "tsx src/demo.ts", + "test": "npm run test:format && npm run test:types", + "fix:format": "prettier --write .", + "test:format": "prettier --check .", + "test:types": "tsc --noEmit" + }, + "license": "Apache-2.0", + "engines": { + "node": ">=20" + }, + "dependencies": { + "@stellar/stellar-sdk": "^14.0.0" + }, + "devDependencies": { + "@types/node": "^22.10.0", + "prettier": "^3.4.1", + "tsx": "^4.19.2", + "typescript": "^5.7.2" + } +} diff --git a/lazer/stellar/client/src/demo.ts b/lazer/stellar/client/src/demo.ts new file mode 100644 index 00000000..8557b78e --- /dev/null +++ b/lazer/stellar/client/src/demo.ts @@ -0,0 +1,434 @@ +/** + * One-command demo of the Pyth Lazer Stellar example consumer. + * + * Fetches a freshly signed Pyth Lazer price update over REST, submits it to a + * deployed instance of the example consumer contract, and reads the stored + * price back in human units. + * + * Deliberately self-contained: it depends only on `@stellar/stellar-sdk` and + * talks to Lazer with plain `fetch`, so it runs outside the Pyth monorepos. + */ + +import { parseArgs } from "node:util"; + +import { + BASE_FEE, + Contract, + Keypair, + Networks, + nativeToScVal, + scValToNative, + TransactionBuilder, +} from "@stellar/stellar-sdk"; +import { Api, Server } from "@stellar/stellar-sdk/rpc"; + +const NETWORKS = { + mainnet: { + explorer: "public", + friendbot: undefined, + horizonUrl: "https://horizon.stellar.org", + // Inclusion bid, not the resource fee: `prepareTransaction` adds the Soroban + // resource fee on top but leaves this untouched. Mainnet ledgers run close to + // full, so a BASE_FEE bid gets evicted and the transaction expires without + // ever reaching a ledger. + inclusionFee: "1000000", + networkPassphrase: Networks.PUBLIC, + rpcUrl: "https://mainnet.sorobanrpc.com", + }, + testnet: { + explorer: "testnet", + friendbot: "https://friendbot.stellar.org", + horizonUrl: "https://horizon-testnet.stellar.org", + inclusionFee: BASE_FEE, + networkPassphrase: Networks.TESTNET, + rpcUrl: "https://soroban-testnet.stellar.org", + }, +} as const; + +type NetworkName = keyof typeof NETWORKS; + +const DEFAULT_LAZER_ENDPOINT = + "https://pyth-lazer-0.dourolabs.app/v1/latest_price"; +const DEFAULT_FEED_ID = "1"; // BTC/USD +const DEFAULT_CHANNEL = "fixed_rate@200ms"; + +/** + * The example consumer reads `price`, `exponent` and `feed_update_timestamp` + * off the verified payload. A property that is not requested decodes to `None` + * and `update_price` fails with `ExponentMissing` / `TimestampMissing`, so all + * three are mandatory here. + */ +const REQUIRED_PROPERTIES = [ + "price", + "exponent", + "feedUpdateTimestamp", +] as const; + +/** Contract error codes from the example consumer's `Error` enum (src/error.rs). */ +const CONTRACT_ERRORS: Record = { + 1: "PriceStale — the update is older than the contract's freshness_threshold_us. Fetch a fresh update and submit it immediately.", + 2: "FeedMissing — the payload does not carry the feed id this contract was deployed with. Pass --feed-id matching the contract's configured feed.", + 3: "TimestampMissing — the payload has no feedUpdateTimestamp. Request the 'feedUpdateTimestamp' property from Lazer.", + 4: "PriceMissing — the payload has no price. Request the 'price' property from Lazer.", + 5: "ExponentMissing — the payload has no exponent. Request the 'exponent' property from Lazer.", + 6: "PriceNotInitialized — no update has ever been stored. Run update_price first.", + 7: "Overflow — ledger timestamp overflowed when converted to microseconds.", + 8: "ParseError — the verifier rejected or could not parse the update.", + 9: "PriceOutdated — the stored price is at least as new as this update. update_price is strictly monotonic; fetch a NEW update rather than replaying a cached payload.", +}; + +const USAGE = ` +Usage: npm run demo -- --contract-id [options] + +Options: + --contract-id Deployed example consumer contract id. Required. + --network testnet | mainnet. Default: testnet. + --secret Stellar secret key. Defaults to $STELLAR_WALLET_SECRET. + On testnet a throwaway key is generated and friendbot-funded + when neither is given. Required on mainnet. + --feed-id Pyth Lazer price feed id. Default: ${DEFAULT_FEED_ID} (BTC/USD). + Must match the feed the contract was deployed with. + --channel Lazer channel. Default: ${DEFAULT_CHANNEL}. + --lazer-endpoint Lazer latest_price REST endpoint. + Default: ${DEFAULT_LAZER_ENDPOINT} + --help Show this message. + +Environment: + PYTH_API_KEY Pyth Lazer bearer token. Required. + (PYTH_LAZER_TOKEN is accepted as an alias.) + STELLAR_WALLET_SECRET Stellar secret key used to sign, unless --secret is given. +`.trim(); + +const { values } = parseArgs({ + options: { + channel: { type: "string", default: DEFAULT_CHANNEL }, + "contract-id": { type: "string" }, + "feed-id": { type: "string", default: DEFAULT_FEED_ID }, + help: { type: "boolean", default: false }, + "lazer-endpoint": { type: "string", default: DEFAULT_LAZER_ENDPOINT }, + network: { type: "string", default: "testnet" }, + secret: { type: "string" }, + }, + strict: true, +}); + +if (values.help) { + console.log(USAGE); + process.exit(0); +} + +/** Abort with a message the operator can act on, without a stack trace. */ +function fail(message: string): never { + console.error(`\n❌ ${message}`); + process.exit(1); +} + +function isNetworkName(name: string): name is NetworkName { + return name in NETWORKS; +} + +if (!isNetworkName(values.network)) { + fail( + `Unknown --network '${values.network}'. Expected one of: ${Object.keys(NETWORKS).join(", ")}.`, + ); +} +const networkName: NetworkName = values.network; +const network = NETWORKS[networkName]; + +const contractId = values["contract-id"]; +if (!contractId) { + fail( + `--contract-id is required: the id of a deployed example consumer contract.\n\n${USAGE}`, + ); +} + +const feedId = Number(values["feed-id"]); +if (!Number.isInteger(feedId) || feedId < 0) { + fail(`--feed-id must be a non-negative integer, got '${values["feed-id"]}'.`); +} + +const lazerToken = + process.env["PYTH_API_KEY"] ?? process.env["PYTH_LAZER_TOKEN"]; +if (!lazerToken) { + fail( + "Set PYTH_API_KEY to your Pyth Lazer access token (it doubles as the Lazer bearer token).", + ); +} + +/** + * Render a Lazer fixed-point price (`price * 10^exponent`) as a decimal string. + * Done on the integer, not via floating point, so no precision is lost. + */ +function formatScaled(price: bigint, exponent: number): string { + if (exponent >= 0) { + return (price * 10n ** BigInt(exponent)).toString(); + } + const decimals = -exponent; + const negative = price < 0n; + const digits = (negative ? -price : price) + .toString() + .padStart(decimals + 1, "0"); + const whole = digits.slice(0, digits.length - decimals); + const fraction = digits.slice(digits.length - decimals); + return `${negative ? "-" : ""}${whole}.${fraction}`; +} + +function formatMicros(timestampUs: bigint): string { + return new Date(Number(timestampUs / 1000n)).toISOString(); +} + +/** + * Soroban surfaces a contract error as `Error(Contract, #N)` inside a wall of + * simulation diagnostics. Pull out N and lead with what it means, so a failed + * demo opens with the actionable line instead of XDR. + */ +function explainContractError(error: unknown): string { + const text = error instanceof Error ? error.message : String(error); + const match = /Error\(Contract, #(\d+)\)/.exec(text); + if (!match?.[1]) { + return text; + } + const code = Number(match[1]); + const meaning = CONTRACT_ERRORS[code] ?? "unknown error code."; + return `contract error #${code}: ${meaning}\n\n Full diagnostics:\n${text}`; +} + +type LazerResponse = { + parsed?: { + timestampUs: string; + priceFeeds: { + priceFeedId: number; + price: string; + exponent: number; + feedUpdateTimestamp: number | string; + }[]; + }; + leEcdsa?: { encoding: string; data: string }; +}; + +/** Fetch a freshly signed update for `feedId`, carrying all three required properties. */ +async function fetchLazerUpdate(): Promise<{ + hex: string; + parsed: LazerResponse["parsed"]; +}> { + const response = await fetch(values["lazer-endpoint"], { + body: JSON.stringify({ + channel: values.channel, + formats: ["leEcdsa"], + jsonBinaryEncoding: "hex", + priceFeedIds: [feedId], + properties: REQUIRED_PROPERTIES, + }), + headers: { + Authorization: `Bearer ${lazerToken}`, + "Content-Type": "application/json", + }, + method: "POST", + }); + + if (!response.ok) { + const body = await response.text(); + fail( + `Lazer request failed: ${response.status} ${response.statusText}\n ${body}\n\n Check PYTH_API_KEY, and that feed id ${feedId} and channel '${values.channel}' are available to your token.`, + ); + } + + const body = (await response.json()) as LazerResponse; + const hex = body.leEcdsa?.data; + if (!hex) { + fail( + `Lazer returned no 'leEcdsa' payload. Soroban needs the leEcdsa format.\n Response: ${JSON.stringify(body)}`, + ); + } + return { hex, parsed: body.parsed }; +} + +// --- Step 1: resolve the signing account ------------------------------------- + +const server = new Server(network.rpcUrl); + +console.log(`=== Pyth Lazer Stellar demo (${networkName}) ===`); +console.log(` Example consumer: ${contractId}`); +console.log( + ` Explorer: https://stellar.expert/explorer/${network.explorer}/contract/${contractId}`, +); + +/** Native XLM balance as a decimal string, or `undefined` if the account is unfunded. */ +async function nativeBalance(publicKey: string): Promise { + const response = await fetch(`${network.horizonUrl}/accounts/${publicKey}`); + if (response.status === 404) { + return undefined; + } + if (!response.ok) { + fail( + `Horizon could not read account ${publicKey}: ${response.status} ${response.statusText}`, + ); + } + const account = (await response.json()) as { + balances: { asset_type: string; balance: string }[]; + }; + return ( + account.balances.find((b) => b.asset_type === "native")?.balance ?? "0" + ); +} + +async function friendbotFund(publicKey: string): Promise { + const funded = await fetch( + `${network.friendbot}?addr=${encodeURIComponent(publicKey)}`, + ); + if (!funded.ok) { + fail( + `Friendbot could not fund ${publicKey}: ${funded.status} ${funded.statusText}\n ${await funded.text()}`, + ); + } +} + +const secret = values.secret ?? process.env["STELLAR_WALLET_SECRET"]; +let keypair: Keypair; +if (secret) { + try { + keypair = Keypair.fromSecret(secret); + } catch { + fail( + "The Stellar secret key is not a valid S... key. Check --secret / $STELLAR_WALLET_SECRET.", + ); + } + console.log( + `\n=== Signing account (from --secret / $STELLAR_WALLET_SECRET) ===`, + ); +} else if (networkName === "mainnet") { + fail( + "Mainnet has no friendbot, so a funded account is required. Set STELLAR_WALLET_SECRET or pass --secret.", + ); +} else { + keypair = Keypair.random(); + console.log(`\n=== Signing account (generated) ===`); +} + +console.log(` Public key: ${keypair.publicKey()}`); + +// Report the balance before spending anything, so the operator can see exactly +// which account pays for the demo. +let balance = await nativeBalance(keypair.publicKey()); +if (balance === undefined) { + if (networkName === "mainnet") { + fail( + `Account ${keypair.publicKey()} does not exist on mainnet. Fund it with XLM before running the demo.`, + ); + } + // A key that is only funded on mainnet is still unfunded here, so friendbot + // covers both the generated key and a provided-but-unfunded one. + console.log(` Unfunded on testnet — requesting friendbot airdrop…`); + await friendbotFund(keypair.publicKey()); + balance = await nativeBalance(keypair.publicKey()); +} +console.log(` XLM balance: ${balance ?? "unknown"}`); + +// --- Step 2: fetch a freshly signed Lazer update ----------------------------- + +console.log( + `\n=== Fetching signed Lazer update (feed ${feedId}, ${values.channel}) ===`, +); +const { hex, parsed } = await fetchLazerUpdate(); +const update = Buffer.from(hex, "hex"); +console.log(` Update size: ${update.length} bytes`); +console.log(` Update hex: ${hex}`); + +const parsedFeed = parsed?.priceFeeds.find((f) => f.priceFeedId === feedId); +if (parsedFeed) { + const offChainTs = BigInt(parsedFeed.feedUpdateTimestamp); + console.log( + ` Off-chain price: ${formatScaled(BigInt(parsedFeed.price), parsedFeed.exponent)} (raw ${parsedFeed.price}, exponent ${parsedFeed.exponent})`, + ); + console.log( + ` Feed timestamp: ${offChainTs} us (${formatMicros(offChainTs)})`, + ); +} + +// --- Step 3: submit it to the example consumer ------------------------------- + +console.log(`\n=== Submitting update_price to the example consumer ===`); +const contract = new Contract(contractId); +const account = await server.getAccount(keypair.publicKey()); +const tx = new TransactionBuilder(account, { + fee: network.inclusionFee, + networkPassphrase: network.networkPassphrase, +}) + .addOperation( + contract.call("update_price", nativeToScVal(update, { type: "bytes" })), + ) + .setTimeout(60) + .build(); + +let prepared; +try { + prepared = await server.prepareTransaction(tx); +} catch (error) { + fail(`update_price failed in simulation — ${explainContractError(error)}`); +} +prepared.sign(keypair); + +const sendResult = await server.sendTransaction(prepared); +console.log(` Transaction hash: ${sendResult.hash}`); +console.log( + ` Explorer: https://stellar.expert/explorer/${network.explorer}/tx/${sendResult.hash}`, +); +if (sendResult.status === "ERROR") { + fail( + `Transaction submission failed: ${JSON.stringify(sendResult.errorResult)}\n ${explainContractError(JSON.stringify(sendResult.errorResult))}`, + ); +} + +const txResult = await server.pollTransaction(sendResult.hash); +if (txResult.status === Api.GetTransactionStatus.NOT_FOUND) { + fail( + `Transaction ${sendResult.hash} was dropped before reaching a ledger, so nothing executed and no fee was charged.\n The ledger was most likely full and the inclusion fee of ${network.inclusionFee} stroops was outbid. Retry; raise the inclusion fee if it keeps happening.`, + ); +} +if (txResult.status !== Api.GetTransactionStatus.SUCCESS) { + fail( + `update_price did not succeed: ${txResult.status}\n ${explainContractError(JSON.stringify(txResult))}`, + ); +} +console.log(` Status: SUCCESS (ledger ${txResult.ledger})`); + +// --- Step 4: read the stored price back -------------------------------------- + +console.log(`\n=== Reading get_price back from the contract ===`); +const readAccount = await server.getAccount(keypair.publicKey()); +const readTx = new TransactionBuilder(readAccount, { + fee: network.inclusionFee, + networkPassphrase: network.networkPassphrase, +}) + .addOperation(contract.call("get_price")) + .setTimeout(60) + .build(); + +// `get_price` only reads, so simulating it is the whole call: no fee, no ledger. +const simulation = await server.simulateTransaction(readTx); +if (Api.isSimulationError(simulation)) { + fail(`get_price failed — ${explainContractError(simulation.error)}`); +} +if (!simulation.result?.retval) { + fail("get_price returned no value."); +} + +const stored = scValToNative(simulation.result.retval) as { + price: bigint; + exponent: number; + timestamp_us: bigint; +}; + +console.log(` Stored price: ${formatScaled(stored.price, stored.exponent)}`); +console.log(` Raw price: ${stored.price} (exponent ${stored.exponent})`); +console.log( + ` Feed timestamp: ${stored.timestamp_us} us (${formatMicros(stored.timestamp_us)})`, +); + +console.log(`\n✅ Demo complete.`); +console.log( + ` Contract: https://stellar.expert/explorer/${network.explorer}/contract/${contractId}`, +); +console.log( + ` Update tx: https://stellar.expert/explorer/${network.explorer}/tx/${sendResult.hash}`, +); diff --git a/lazer/stellar/client/tsconfig.json b/lazer/stellar/client/tsconfig.json new file mode 100644 index 00000000..85bb73a8 --- /dev/null +++ b/lazer/stellar/client/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "moduleResolution": "bundler", + "types": ["node"], + "strict": true, + "noUncheckedIndexedAccess": true, + "noEmit": true, + "skipLibCheck": true, + "verbatimModuleSyntax": true + }, + "include": ["src"], + "exclude": ["node_modules"] +}