diff --git a/.github/workflows/main-pr.yml b/.github/workflows/main-pr.yml index b8d7f2a..5e5e7ed 100644 --- a/.github/workflows/main-pr.yml +++ b/.github/workflows/main-pr.yml @@ -12,6 +12,10 @@ jobs: name: Unit uses: ./.github/workflows/test-go.yml + typescript: + name: TypeScript + uses: ./.github/workflows/test-ts.yml + integration: name: Integration uses: ./.github/workflows/test-integration.yml diff --git a/.github/workflows/main-push.yml b/.github/workflows/main-push.yml index 08b4b7e..69ea8e7 100644 --- a/.github/workflows/main-push.yml +++ b/.github/workflows/main-push.yml @@ -12,6 +12,10 @@ jobs: name: Unit uses: ./.github/workflows/test-go.yml + typescript: + name: TypeScript + uses: ./.github/workflows/test-ts.yml + integration: name: Integration uses: ./.github/workflows/test-integration.yml diff --git a/.github/workflows/test-integration.yml b/.github/workflows/test-integration.yml index 6455ec3..1734413 100644 --- a/.github/workflows/test-integration.yml +++ b/.github/workflows/test-integration.yml @@ -24,6 +24,13 @@ jobs: go-version-file: go.mod cache: true + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + cache-dependency-path: sdk/ts/package-lock.json + - name: Bring up devnet run: make devnet diff --git a/.github/workflows/test-ts.yml b/.github/workflows/test-ts.yml new file mode 100644 index 0000000..4372330 --- /dev/null +++ b/.github/workflows/test-ts.yml @@ -0,0 +1,41 @@ +name: Test (TypeScript) + +on: + workflow_call: + +permissions: + contents: read + +jobs: + unit: + name: TypeScript Unit + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + cache-dependency-path: sdk/ts/package-lock.json + + - name: Install + run: npm ci + working-directory: sdk/ts + + - name: Typecheck + run: npm run typecheck + working-directory: sdk/ts + + - name: Build package + run: npm run build + working-directory: sdk/ts + + - name: Unit tests + run: npm test + working-directory: sdk/ts + + - name: Build demo + run: npm --workspace @yellow-org/evm-deposit-demo run build + working-directory: sdk/ts diff --git a/.gitignore b/.gitignore index 0665444..0c71044 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,11 @@ # Go workspace files go.work go.work.sum + +# Node.js dependencies and build output +node_modules/ +dist/ +examples/*/dist/ +sdk/ts/node_modules/ +sdk/ts/dist/ +sdk/ts/examples/*/dist/ diff --git a/Makefile b/Makefile index 9a2a228..222927e 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: build lint test generate devnet devnet-down integration +.PHONY: build lint test generate devnet devnet-evm devnet-down ts-deps integration build: go build ./... @@ -23,10 +23,18 @@ devnet: docker compose -f devnet/docker-compose.yml up -d go run ./devnet/wait +devnet-evm: + docker compose -f devnet/docker-compose.yml up -d anvil + go run ./devnet/wait --networks anvil + devnet-down: docker compose -f devnet/docker-compose.yml down -v -# Build-tagged blockchain flow tests (deposit + withdrawal per chain). Every -# test self-provisions against the devnet — no setup, no env. See devnet/README.md. -integration: +ts-deps: + npm --prefix sdk/ts ci + +# Blockchain flow tests against the devnet. Go tests cover deposit + withdrawal +# per chain; the TS suite covers EVM deposits. See devnet/README.md. +integration: ts-deps go test -tags integration ./pkg/blockchain/... -v + npm --prefix sdk/ts run test:integration:evm diff --git a/devnet/README.md b/devnet/README.md index c201cf1..eef03ea 100644 --- a/devnet/README.md +++ b/devnet/README.md @@ -7,26 +7,31 @@ withdrawal test runs the whole *k-of-n* quorum in-process — it holds N local `sign.KeySigner`s and drives `Pack → Validate → Sign → Merge → Submit → VerifyExecution` itself, so no p2p mesh is needed. +The TypeScript EVM SDK integration test lives under `sdk/ts/test` and runs +through the same `make integration` target. + ## Run ```sh make devnet # anvil + bitcoind + rippled + solana-test-validator; blocks until all answer RPC -make integration # go test -tags integration ./pkg/blockchain/... +npm --prefix sdk/ts ci +make integration # Go blockchain integrations + TS EVM integration make devnet-down ``` `make devnet` returns only once every node answers (the `devnet/wait` probe). -`make integration` then needs **no setup and no env** — every test -self-provisions against the devnet and is **idempotent**: each run uses fresh -keys / accounts / a freshly-deployed contract, so re-running is a clean run. -Only each node's funder persists (anvil account 0, the bitcoind coinbase +After Node dependencies are installed, `make integration` needs **no env**. The +tests self-provision against the devnet and are **idempotent**: each run uses +fresh keys / accounts / a freshly-deployed contract, so re-running is a clean +run. Only each node's funder persists (anvil account 0, the bitcoind coinbase wallet, the XRPL genesis master). ## What each test provisions - **EVM** — deploys a fresh `Custody` vault over N freshly-generated signer keys (funded from anvil account 0), deposits native ETH, then runs the quorum - withdrawal. + withdrawal. The TypeScript EVM integration test also deploys fresh `Custody` + and `MockERC20` contracts and runs native ETH + ERC-20 deposit coverage. - **BTC** — creates a legacy wallet, mines to maturity, generates a fresh vault + depositor, watch-imports their addresses, funds the depositor, deposits to the per-account P2WSH address, then runs the quorum withdrawal (mining to diff --git a/devnet/wait/main.go b/devnet/wait/main.go index 5acc53f..aa65579 100644 --- a/devnet/wait/main.go +++ b/devnet/wait/main.go @@ -6,9 +6,11 @@ package main import ( "bytes" "context" + "flag" "fmt" "net/http" "os" + "strings" "time" ) @@ -21,6 +23,13 @@ type probe struct { } func main() { + networks := flag.String("networks", "", "comma-separated devnet networks to wait for: anvil,bitcoind,rippled,solana") + flag.Parse() + if flag.NArg() > 0 { + fmt.Fprintf(os.Stderr, "devnet: unexpected positional arguments: %s\n", strings.Join(flag.Args(), " ")) + os.Exit(2) + } + probes := []probe{ {name: "anvil", url: envOr("EVM_RPC_URL", "http://127.0.0.1:8545"), body: `{"jsonrpc":"2.0","id":1,"method":"eth_blockNumber","params":[]}`}, @@ -33,9 +42,15 @@ func main() { body: `{"jsonrpc":"2.0","id":1,"method":"getHealth"}`}, } + selected, err := selectProbes(probes, *networks, flagWasSet("networks")) + if err != nil { + fmt.Fprintf(os.Stderr, "devnet: %v\n", err) + os.Exit(2) + } + deadline := time.Now().Add(90 * time.Second) client := &http.Client{Timeout: 3 * time.Second} - for _, p := range probes { + for _, p := range selected { if err := waitOne(client, p, deadline); err != nil { fmt.Fprintf(os.Stderr, "devnet: %s not ready: %v\n", p.name, err) os.Exit(1) @@ -44,6 +59,45 @@ func main() { } } +func selectProbes(probes []probe, networks string, specified bool) ([]probe, error) { + if !specified { + return probes, nil + } + + byName := make(map[string]probe, len(probes)) + names := make([]string, 0, len(probes)) + for _, p := range probes { + byName[p.name] = p + names = append(names, p.name) + } + + parts := strings.Split(networks, ",") + selected := make([]probe, 0, len(parts)) + for _, part := range parts { + name := strings.TrimSpace(part) + if name == "" { + return nil, fmt.Errorf("--networks must contain non-empty names from: %s", strings.Join(names, ",")) + } + p, ok := byName[name] + if !ok { + return nil, fmt.Errorf("unsupported network %q; supported values: %s", name, strings.Join(names, ",")) + } + selected = append(selected, p) + } + + return selected, nil +} + +func flagWasSet(name string) bool { + wasSet := false + flag.Visit(func(f *flag.Flag) { + if f.Name == name { + wasSet = true + } + }) + return wasSet +} + func waitOne(client *http.Client, p probe, deadline time.Time) error { var last error for time.Now().Before(deadline) { diff --git a/devnet/wait/main_test.go b/devnet/wait/main_test.go new file mode 100644 index 0000000..db99ac1 --- /dev/null +++ b/devnet/wait/main_test.go @@ -0,0 +1,73 @@ +package main + +import "testing" + +func TestSelectProbesDefaultAll(t *testing.T) { + probes := testProbes() + + got, err := selectProbes(probes, "", false) + if err != nil { + t.Fatalf("selectProbes returned error: %v", err) + } + if len(got) != len(probes) { + t.Fatalf("got %d probes, want %d", len(got), len(probes)) + } + for i := range probes { + if got[i].name != probes[i].name { + t.Fatalf("probe %d = %q, want %q", i, got[i].name, probes[i].name) + } + } +} + +func TestSelectProbesSubset(t *testing.T) { + got, err := selectProbes(testProbes(), "anvil,solana", true) + if err != nil { + t.Fatalf("selectProbes returned error: %v", err) + } + + want := []string{"anvil", "solana"} + assertProbeNames(t, got, want) +} + +func TestSelectProbesTrimsNames(t *testing.T) { + got, err := selectProbes(testProbes(), " anvil , rippled ", true) + if err != nil { + t.Fatalf("selectProbes returned error: %v", err) + } + + want := []string{"anvil", "rippled"} + assertProbeNames(t, got, want) +} + +func TestSelectProbesRejectsEmptyName(t *testing.T) { + if _, err := selectProbes(testProbes(), "anvil,", true); err == nil { + t.Fatal("expected empty network name to fail") + } +} + +func TestSelectProbesRejectsUnsupportedName(t *testing.T) { + if _, err := selectProbes(testProbes(), "anvil,ethereum", true); err == nil { + t.Fatal("expected unsupported network name to fail") + } +} + +func testProbes() []probe { + return []probe{ + {name: "anvil"}, + {name: "bitcoind"}, + {name: "rippled"}, + {name: "solana"}, + } +} + +func assertProbeNames(t *testing.T, probes []probe, want []string) { + t.Helper() + if len(probes) != len(want) { + t.Fatalf("got %d probes, want %d", len(probes), len(want)) + } + for i := range want { + if probes[i].name != want[i] { + t.Fatalf("probe %d = %q, want %q", i, probes[i].name, want[i]) + } + } +} diff --git a/pkg/p2p/pubsub/pubsub_test.go b/pkg/p2p/pubsub/pubsub_test.go index 07249c8..4071cd8 100644 --- a/pkg/p2p/pubsub/pubsub_test.go +++ b/pkg/p2p/pubsub/pubsub_test.go @@ -59,9 +59,7 @@ func TestPubSub_FinalizedWithdrawal(t *testing.T) { if fw.WithdrawalID != want.WithdrawalID || fw.EntryIndex != want.EntryIndex { t.Fatalf("delivered %+v, want %+v", fw.Header(), want.Header()) } - if m := follower.Metrics().Snapshot(); m.Delivered != 1 { - t.Errorf("Delivered = %d, want 1", m.Delivered) - } + waitDelivered(t, follower, 1) return case <-ticker.C: continue @@ -91,3 +89,22 @@ func connect(t *testing.T, from, to host.Host) { t.Fatalf("connect: %v", err) } } + +func waitDelivered(t *testing.T, follower *Follower[core.FinalizedWithdrawal, *core.FinalizedWithdrawal], want uint64) { + t.Helper() + deadline := time.After(time.Second) + ticker := time.NewTicker(10 * time.Millisecond) + defer ticker.Stop() + + for { + if got := follower.Metrics().Snapshot().Delivered; got == want { + return + } + select { + case <-deadline: + got := follower.Metrics().Snapshot().Delivered + t.Fatalf("Delivered = %d, want %d", got, want) + case <-ticker.C: + } + } +} diff --git a/sdk/ts/LICENSE b/sdk/ts/LICENSE new file mode 100644 index 0000000..0d9ae32 --- /dev/null +++ b/sdk/ts/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Yellow Network + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/sdk/ts/README.md b/sdk/ts/README.md new file mode 100644 index 0000000..87f1c59 --- /dev/null +++ b/sdk/ts/README.md @@ -0,0 +1,288 @@ +# Clearnet TypeScript SDK + +TypeScript SDK for Clearnet integration. This package currently exposes +the EVM vault depositor, with support for native ETH deposits and ERC-20 +deposits. Deposits credit a `destination` made of an account and an optional +ADR-015 opaque reference. + +The package is ESM-first and uses `viem` for EVM clients and primitives. + +## Install + +```sh +npm install @yellow-org/clearnet-sdk viem +``` + +For local development in this repository: + +```sh +cd sdk/ts +npm ci +``` + +## Quick Start + +Native ETH deposits use `EVM_NATIVE_ASSET`, which is the EVM zero address. Amounts +must be `bigint` values in base units. + +```ts +import { + ClearnetSdkError, + EVM_NATIVE_ASSET, + EvmVaultDepositor, +} from "@yellow-org/clearnet-sdk"; +import { + createPublicClient, + createWalletClient, + custom, + http, + parseEther, +} from "viem"; +import type { Address, EIP1193Provider } from "viem"; + +declare const window: Window & { ethereum?: EIP1193Provider }; + +const chainId = 31_337; +const rpcUrl = "http://127.0.0.1:8545"; +// Replace with the Custody contract deployed on the configured chain. +const custodyAddress = "0x0000000000000000000000000000000000001000" as Address; + +if (window.ethereum === undefined) { + throw new Error("No EIP-1193 wallet found"); +} + +const accounts = (await window.ethereum.request({ + method: "eth_requestAccounts", +})) as Address[]; +const walletAccount = accounts[0]; +if (walletAccount === undefined) { + throw new Error("Wallet did not return an account"); +} + +const publicClient = createPublicClient({ + transport: http(rpcUrl), +}); +const walletClient = createWalletClient({ + account: walletAccount, + transport: custom(window.ethereum), +}); + +const depositor = new EvmVaultDepositor({ + publicClient, + walletClient, + walletAccount, + custodyAddress, + chainId, +}); + +try { + const ref = await depositor.submitDeposit( + { + destination: { account: walletAccount }, + asset: EVM_NATIVE_ASSET, + amount: parseEther("0.01"), + }, + { + onSubmitted(submittedRef) { + console.log("deposit submitted", submittedRef.hash); + }, + }, + ); + + console.log("deposit mined", ref.hash); + console.log("status", await depositor.verifyDeposit(ref, 1)); +} catch (error) { + if (error instanceof ClearnetSdkError) { + console.error(error.code, error.txRef?.hash); + } + throw error; +} +``` + +The SDK checks the configured public RPC chain and wallet chain before signing +or submitting a deposit. If either chain does not match `chainId`, it throws +`CHAIN_MISMATCH`. + +## ERC-20 Deposits + +For ERC-20 deposits, pass the token contract address as `asset`. + +```ts +import { parseUnits } from "viem"; + +const ref = await depositor.submitDeposit({ + destination: { account: walletAccount }, + asset: "0x0000000000000000000000000000000000003000", + amount: parseUnits("25", 18), +}); +``` + +The SDK submits an exact-amount `approve(custodyAddress, amount)` transaction +before submitting the custody `deposit(...)` transaction. A successful +`submitDeposit` call returns the deposit transaction hash, not the approval hash. +If an ERC-20 approval fails before the deposit is submitted, `error.txRef` may +refer to the approval transaction. + +## Deposit References + +Pass `destination.ref` to attach a 32-byte opaque sub-account reference to the +deposit. Omit it when there is no sub-account reference. + +```ts +const ref = await depositor.submitDeposit({ + destination: { + account: walletAccount, + ref: "0x3333333333333333333333333333333333333333333333333333333333333333", + }, + asset: EVM_NATIVE_ASSET, + amount: parseEther("0.01"), +}); +``` + +For EVM, the reference is passed to `Custody.deposit(...)` as `bytes32`. The SDK +does not interpret it. + +## Verify A Deposit + +```ts +const status = await depositor.verifyDeposit(ref, 1); +``` + +`verifyDeposit` returns: + +| Status | Meaning | +|---|---| +| `confirmed` | The transaction has a successful receipt and at least `minConfirmations` confirmations. | +| `pending` | The transaction is known but is not confirmed enough yet. | +| `absent` | The transaction is unknown or has a reverted receipt. | + +`minConfirmations` accepts a non-negative safe integer `number` or a non-negative +`bigint`. + +## API Reference + +### `EvmVaultDepositor` + +```ts +new EvmVaultDepositor(config: EvmDepositorConfig) +``` + +Config fields: + +| Field | Type | Notes | +|---|---|---| +| `publicClient` | `viem` `PublicClient` | Used for chain checks, receipt waits, and verification. | +| `walletClient` | `viem` `WalletClient` | Used to submit approval and deposit transactions. | +| `walletAccount` | `Account \| Address` | Must match `walletClient.account` when the wallet client has one. | +| `custodyAddress` | `Address` | The deployed custody contract address. | +| `chainId` | `number` | Positive safe integer; must match the public RPC and wallet chain. | +| `receiptTimeoutMs` | `number` | Optional default timeout for receipt waits. | + +### `submitDeposit(input, options?)` + +Input fields: + +| Field | Type | Notes | +|---|---|---| +| `destination.account` | `Address` | Clearnet account credited by the custody deposit. | +| `destination.ref` | `Hash \| undefined` | Optional 32-byte opaque reference. Omitted values are sent as `bytes32(0)`. | +| `asset` | `Address` | Use `EVM_NATIVE_ASSET` for native ETH, or an ERC-20 token address. | +| `amount` | `bigint` | Positive base-unit amount that fits in `uint256`. | + +Options: + +| Field | Notes | +|---|---| +| `signal` | Aborts the receipt wait. | +| `receiptTimeoutMs` | Overrides the receipt wait timeout for this call. | +| `onSubmitted` | Called with the deposit `TxRef` after the deposit transaction is submitted. | + +Returns: + +```ts +type TxRef = { + hash: `0x${string}`; + raw: string; +}; +``` + +For EVM, `hash` and `raw` are both the transaction hash. + +### `verifyDeposit(ref, minConfirmations)` + +Returns `Promise<"absent" | "pending" | "confirmed">`. + +## Local Development + +Run package checks from `sdk/ts`: + +```sh +npm run typecheck +npm test +npm run build +npm --workspace @yellow-org/evm-deposit-demo run build +``` + +Run the EVM integration test against local Anvil: + +```sh +# From the repository root: +make devnet-evm + +# From sdk/ts: +npm run test:integration:evm + +# From the repository root: +make devnet-down +``` + +The integration test deploys fresh `Custody` and `MockERC20` contracts on each +run. + +To run the repository integration suite, including this TS EVM integration test: + +```sh +# From the repository root: +make devnet +make integration +make devnet-down +``` + +## Demo + +Start the browser demo from `sdk/ts`: + +```sh +npm run demo:evm +``` + +The demo expects: + +- an EIP-1193 wallet, such as MetaMask +- an RPC URL and chain ID that match the wallet's selected network +- a deployed `Custody` contract address +- a funded wallet account on that network + +`make devnet-evm` starts Anvil on `http://127.0.0.1:8545` with chain ID `31337`, +but it does not predeploy `Custody` for the browser demo. + +## Troubleshooting + +Errors thrown by the SDK use `ClearnetSdkError` with a stable `code`. + +| Code | Common cause | +|---|---| +| `INVALID_ADDRESS` | `account`, `asset`, `custodyAddress`, or `walletAccount` is not a valid EVM address. | +| `INVALID_AMOUNT` | `amount` is not a positive `bigint` or does not fit in `uint256`. | +| `INVALID_CONFIRMATIONS` | `minConfirmations` is negative, fractional, or an unsafe number. | +| `INVALID_REFERENCE` | `destination.ref` is not a 32-byte hex value. | +| `INVALID_TX_REF` | `ref.hash` is missing or is not a 32-byte EVM transaction hash. | +| `MISSING_WALLET_ACCOUNT` | The wallet account is missing or does not match `walletClient.account`. | +| `CHAIN_MISMATCH` | The public RPC or wallet chain does not match `chainId`. | +| `TX_REVERTED` | A submitted approval or deposit transaction reverted. | +| `RECEIPT_TIMEOUT` | Waiting for a receipt timed out or was aborted. | +| `RPC_ERROR` | The public RPC or wallet provider returned an unexpected error. | + +When a transaction may already have been submitted, `ClearnetSdkError` can include +`txRef`. Use that hash to let a user inspect or retry verification of the +submitted transaction. diff --git a/sdk/ts/examples/evm-deposit/index.html b/sdk/ts/examples/evm-deposit/index.html new file mode 100644 index 0000000..6d95baf --- /dev/null +++ b/sdk/ts/examples/evm-deposit/index.html @@ -0,0 +1,198 @@ + + + + + + EVM Deposit Demo + + + +
+

EVM Deposit Demo

+
+
+ Network +
+ + +
+ +
+ +
+ Deposit +
+ + + + + +
+
+ +
+ + + +
+
+ +
+ + + diff --git a/sdk/ts/examples/evm-deposit/package.json b/sdk/ts/examples/evm-deposit/package.json new file mode 100644 index 0000000..5028d37 --- /dev/null +++ b/sdk/ts/examples/evm-deposit/package.json @@ -0,0 +1,18 @@ +{ + "name": "@yellow-org/evm-deposit-demo", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite --host 127.0.0.1", + "build": "vite build" + }, + "dependencies": { + "@yellow-org/clearnet-sdk": "file:../..", + "viem": "^2.39.0" + }, + "devDependencies": { + "typescript": "^5.9.0", + "vite": "^7.0.0" + } +} diff --git a/sdk/ts/examples/evm-deposit/src/main.ts b/sdk/ts/examples/evm-deposit/src/main.ts new file mode 100644 index 0000000..784d259 --- /dev/null +++ b/sdk/ts/examples/evm-deposit/src/main.ts @@ -0,0 +1,336 @@ +import { EVM_NATIVE_ASSET, EvmVaultDepositor } from "@yellow-org/clearnet-sdk"; +import { + createPublicClient, + createWalletClient, + custom, + formatEther, + getAddress, + http, + parseUnits, +} from "viem"; +import type { TxRef } from "@yellow-org/clearnet-sdk"; +import type { Address, Hash } from "viem"; + +interface Eip1193Provider { + request(args: { method: string; params?: unknown[] }): Promise; +} + +declare global { + interface Window { + ethereum?: Eip1193Provider; + } +} + +const form = mustElement("deposit-form"); +const connectButton = mustElement("connect"); +const submitButton = mustElement("submit"); +const verifyButton = mustElement("verify"); +const logOutput = mustElement("log"); + +let walletAccount: Address | undefined; +let lastRef: TxRef | undefined; + +connectButton.addEventListener("click", () => { + void connectWallet(); +}); + +form.addEventListener("submit", (event) => { + event.preventDefault(); + void submitDeposit(); +}); + +verifyButton.addEventListener("click", () => { + void verifyLastTx(); +}); + +writeLog("Connect a wallet to the configured local Anvil network."); + +async function connectWallet(): Promise { + const provider = requireProvider(); + setBusy(connectButton, true); + try { + const chainId = readChainId(); + const rpcUrl = readInput("rpc-url"); + await requireConfiguredRpcChain(rpcUrl, chainId); + await ensureWalletChain(provider, chainId, rpcUrl); + const accounts = await provider.request({ method: "eth_requestAccounts" }); + if (!Array.isArray(accounts) || typeof accounts[0] !== "string") { + throw new Error("wallet did not return an account"); + } + walletAccount = getAddress(accounts[0]); + setInput("account", walletAccount); + const balanceMessage = await walletBalanceMessage( + provider, + walletAccount, + readInput("rpc-url"), + ); + writeLog(`Connected ${walletAccount}\n${balanceMessage}`); + } catch (error) { + writeError(error); + } finally { + setBusy(connectButton, false); + } +} + +async function submitDeposit(): Promise { + if (walletAccount === undefined) { + await connectWallet(); + } + if (walletAccount === undefined) { + return; + } + + const provider = requireProvider(); + const chainId = readChainId(); + const rpcUrl = readInput("rpc-url"); + const custodyAddress = getAddress(readInput("custody-address")); + const account = getAddress(readInput("account")); + const asset = getAddress(readInput("asset")); + const ref = readOptionalHash("reference"); + const amount = parseUnits(readInput("amount"), readInt("decimals")); + const publicClient = createPublicClient({ transport: http(rpcUrl) }); + const walletClient = createWalletClient({ + account: walletAccount, + transport: custom(provider), + }); + const depositor = new EvmVaultDepositor({ + publicClient, + walletClient, + walletAccount, + custodyAddress, + chainId, + }); + + setBusy(submitButton, true); + try { + await requireConfiguredRpcChain(rpcUrl, chainId); + lastRef = await depositor.submitDeposit( + { + destination: { + account, + ...(ref === undefined ? {} : { ref }), + }, + asset, + amount, + }, + { + onSubmitted(ref) { + lastRef = ref; + verifyButton.disabled = false; + writeLog(`Submitted ${ref.hash}`); + }, + }, + ); + verifyButton.disabled = false; + writeLog(`Mined ${lastRef.hash}\nraw: ${lastRef.raw}`); + } catch (error) { + const txHash = errorTxHash(error); + writeError(error, txHash === undefined ? undefined : `Submitted ${txHash}`); + } finally { + setBusy(submitButton, false); + } +} + +async function verifyLastTx(): Promise { + if (lastRef === undefined) { + return; + } + const chainId = readChainId(); + const rpcUrl = readInput("rpc-url"); + await requireConfiguredRpcChain(rpcUrl, chainId); + const publicClient = createPublicClient({ + transport: http(rpcUrl), + }); + const provider = requireProvider(); + const walletClient = createWalletClient({ + account: walletAccount, + transport: custom(provider), + }); + const depositor = new EvmVaultDepositor({ + publicClient, + walletClient, + walletAccount: walletAccount ?? EVM_NATIVE_ASSET, + custodyAddress: getAddress(readInput("custody-address")), + chainId, + }); + + setBusy(verifyButton, true); + try { + const status = await depositor.verifyDeposit(lastRef, 1); + writeLog(`Verify ${lastRef.hash}\nstatus: ${status}`); + } catch (error) { + writeError(error); + } finally { + setBusy(verifyButton, false); + } +} + +async function ensureWalletChain( + provider: Eip1193Provider, + chainId: number, + rpcUrl: string, +): Promise { + const hexChainId = `0x${chainId.toString(16)}`; + try { + await provider.request({ + method: "wallet_switchEthereumChain", + params: [{ chainId: hexChainId }], + }); + } catch (error) { + if (errorCode(error) !== 4902) { + throw error; + } + await provider.request({ + method: "wallet_addEthereumChain", + params: [ + { + chainId: hexChainId, + chainName: "Anvil", + nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 }, + rpcUrls: [rpcUrl], + }, + ], + }); + } +} + +async function requireConfiguredRpcChain( + rpcUrl: string, + chainId: number, +): Promise { + const publicClient = createPublicClient({ transport: http(rpcUrl) }); + const rpcChainId = await publicClient.getChainId(); + if (rpcChainId !== chainId) { + throw new Error( + `RPC URL reports chain ${rpcChainId}, but Chain ID is ${chainId}. Update both fields to the same network.`, + ); + } +} + +function requireProvider(): Eip1193Provider { + if (window.ethereum === undefined) { + throw new Error("No EIP-1193 wallet found"); + } + return window.ethereum; +} + +function errorTxHash(error: unknown): Hash | undefined { + if (error && typeof error === "object" && "txRef" in error) { + const txRef = (error as { txRef?: TxRef }).txRef; + return txRef?.hash; + } + return undefined; +} + +async function walletBalanceMessage( + provider: Eip1193Provider, + account: Address, + rpcUrl: string, +): Promise { + const publicClient = createPublicClient({ transport: http(rpcUrl) }); + const [walletBalanceHex, configuredRpcBalance] = await Promise.all([ + provider.request({ + method: "eth_getBalance", + params: [account, "latest"], + }), + publicClient.getBalance({ address: account }), + ]); + const walletBalance = + typeof walletBalanceHex === "string" ? BigInt(walletBalanceHex) : undefined; + const walletText = + walletBalance === undefined ? "unknown" : `${formatEther(walletBalance)} ETH`; + const configuredText = `${formatEther(configuredRpcBalance)} ETH`; + if (walletBalance !== configuredRpcBalance) { + return [ + `Wallet RPC balance: ${walletText}`, + `Configured RPC balance: ${configuredText}`, + "Wallet network RPC does not match the RPC URL above.", + ].join("\n"); + } + return `Wallet balance: ${walletText}`; +} + +function writeError(error: unknown, prefix?: string): void { + const message = errorMessage(error); + const code = errorCode(error); + const codeText = code === undefined ? "" : ` [${String(code)}]`; + writeLog([prefix, `${codeText} ${message}`.trim()].filter(Boolean).join("\n")); +} + +function errorCode(error: unknown): number | string | undefined { + if (error && typeof error === "object" && "code" in error) { + const code = (error as { code?: unknown }).code; + if (typeof code === "number" || typeof code === "string") { + return code; + } + } + return undefined; +} + +function errorMessage(error: unknown): string { + if (error instanceof Error) { + return error.message; + } + if (error && typeof error === "object" && "message" in error) { + const message = (error as { message?: unknown }).message; + if (typeof message === "string") { + return message; + } + } + return String(error); +} + +function writeLog(message: string): void { + logOutput.value = message; +} + +function setBusy(button: HTMLButtonElement, busy: boolean): void { + button.disabled = busy; +} + +function readInput(id: string): string { + const value = mustElement(id).value.trim(); + if (value.length === 0) { + throw new Error(`${id} is required`); + } + return value; +} + +function readOptionalHash(id: string): Hash | undefined { + const value = mustElement(id).value.trim(); + if (value.length === 0) { + return undefined; + } + if (!/^0x[a-fA-F0-9]{64}$/.test(value)) { + throw new Error(`${id} must be a 32-byte hex value`); + } + return value as Hash; +} + +function readInt(id: string): number { + const value = Number(readInput(id)); + if (!Number.isSafeInteger(value) || value < 0) { + throw new Error(`${id} must be a non-negative integer`); + } + return value; +} + +function readChainId(): number { + const chainId = readInt("chain-id"); + if (chainId <= 0) { + throw new Error("chain-id must be positive"); + } + return chainId; +} + +function setInput(id: string, value: string): void { + mustElement(id).value = value; +} + +function mustElement(id: string): T { + const element = document.getElementById(id); + if (element === null) { + throw new Error(`missing element #${id}`); + } + return element as T; +} diff --git a/sdk/ts/examples/evm-deposit/tsconfig.json b/sdk/ts/examples/evm-deposit/tsconfig.json new file mode 100644 index 0000000..b5062d4 --- /dev/null +++ b/sdk/ts/examples/evm-deposit/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.test.json", + "compilerOptions": { + "noEmit": true + }, + "include": ["src/**/*.ts"] +} diff --git a/sdk/ts/package-lock.json b/sdk/ts/package-lock.json new file mode 100644 index 0000000..c4533de --- /dev/null +++ b/sdk/ts/package-lock.json @@ -0,0 +1,1793 @@ +{ + "name": "@yellow-org/clearnet-sdk", + "version": "0.2.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@yellow-org/clearnet-sdk", + "version": "0.2.0", + "license": "MIT", + "workspaces": [ + "examples/*" + ], + "dependencies": { + "viem": "^2.39.0" + }, + "devDependencies": { + "@types/node": "^24.0.0", + "typescript": "^5.9.0", + "vite": "^7.0.0", + "vitest": "^4.0.0" + } + }, + "examples/evm-deposit": { + "name": "@yellow-org/evm-deposit-demo", + "version": "0.0.0", + "dependencies": { + "@yellow-org/clearnet-sdk": "file:../..", + "viem": "^2.39.0" + }, + "devDependencies": { + "typescript": "^5.9.0", + "vite": "^7.0.0" + } + }, + "node_modules/@adraffy/ens-normalize": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.1.tgz", + "integrity": "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==", + "license": "MIT" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@noble/ciphers": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", + "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/curves": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz", + "integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==", + "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/@rollup/rollup-android-arm-eabi": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.0.tgz", + "integrity": "sha512-IPIQ55ythEHkfEd9jMEi32OQ7SxURsGA43JI22lj01OLZNt2NUbJX8YUHxkVWyQ6daHPNn0truF5nSj3DQp6YQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.0.tgz", + "integrity": "sha512-M6s9cr10MibETyo8JsOkq+Lo1+lU6hcvb1MApnUql5qte/5hMEgzlN8/ReIKNfRV8rrqX50W1BX9zoUhC192RA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.0.tgz", + "integrity": "sha512-BqCoMoIbn0keKys+dEAdBa70EtOwV1bEsQCUgU9FdiZmmMge/Zk7LlkYGqbrdHR+Frnt0E1FOanly+rlwvvQzw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.0.tgz", + "integrity": "sha512-SIMzST3VFNXDAbeIWDWiFCNM5qncUBDWaEV7NfE7oZbDt2mgfW4MvbKdbYiGOLoM32gbTv608UMd0XktEYSD7w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.0.tgz", + "integrity": "sha512-ezjfSQMP7ArdUsbBwbQIfwAlhE84I2iVnzQNCFSveqV42q+BmKlzVpf7mxv5EchLcoWU4y6/heFzVg1F+hodUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.0.tgz", + "integrity": "sha512-9+qTWGW9AZRhnUgwtTwzNwcPlL87ngkeN0LA+q1bADvmY9aNvWaF2TFW8BZgnQPYxpDI7+rMVLivcd4V737TAQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.0.tgz", + "integrity": "sha512-T1dMEQhXA/jkJ/jyMIw9IovK8bSUq7A8kLIlvZTb/6YIVsp2zLavr4F3oyllHWo7eIVJRyE5n3tUjQJEbE1IuQ==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.0.tgz", + "integrity": "sha512-2as0LgT7qQpyceQq6VUJYnumUMUrgGQCWIiDIN9DE0/tglsk6o66uCB4f3djRawAltvfCNLyZZrsqbPA6inCsA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.0.tgz", + "integrity": "sha512-bVURMg+6eNN9C/yc0aVjooZcwTTtYF4YW3xta5pP0//r3o1V8gXEHXWCndj47w/HhwsFroZrFhR+6uQP5T0n0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.0.tgz", + "integrity": "sha512-Ful8pM/2yYI83PViWdFdpZhdI8HJ5qsXANe5atypbHDf+KIBBDsZsbyy8hbXnULVvW9NsTh5DHwbcBftyLTfiw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.0.tgz", + "integrity": "sha512-9Gp/DgrkzfUBmNPVTyPTvay+4xEP7M/clXpj3efXBcm6uTIVIgDg4rqUpqKXvLEuFRVuEpSAOkhgNeecvaZ4Cg==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.0.tgz", + "integrity": "sha512-m9tsJz54LUXkSYM8+8PG81B9IKK5r+2T0clMq4QrS16xFosufU7firBDAZEsDheDs7wTlP7h3++S7lMsU955HA==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.0.tgz", + "integrity": "sha512-3UvJ5PNVU16aJf6M3tFI24pWzAl2/ynfbyRN3ICyQajK1lSkrnVYNnLz3v04J32qKa0FczJc22zeToc0lr2A3w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.0.tgz", + "integrity": "sha512-vRWUAbYLGHBZS6Q8Msb2sfnf1fvJf+47t8l/TwOerM2qArzy+IeNMTHrYLHXh95h8MoatPHI5hhSZNs+mGXKPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.0.tgz", + "integrity": "sha512-c00T5SYENHAt86cfW47URaP3Us5vLC/4QO7GYud1G5VNRffCwwCuBspwqYrriuJB+5m0WFzClCn9wed0FBjKvg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.0.tgz", + "integrity": "sha512-krrCDilhXOwFkSkO3Wm9I/f9H0L92XHHwy2fwxjukxIbh0dem8gZqOW5Y8BsHrpJv5qwlRBV+Wl4ZFyRWhUpwg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.0.tgz", + "integrity": "sha512-7pfYFSTc4/rUC/FtAI0Qp6QthDBCIi6/AuP1xYqFk5vanI6KnL5dWKP60OM/05LOsbwTmIcvr6eXC4CJuJ75IA==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.0.tgz", + "integrity": "sha512-7SDIalKeIpG0Ifogbbdn58HmSotYMlf23K3dCJEmiVd9Fg36Vmni82iPQec27N3wY4Bvbxftkxz6vSx9OcouTg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.0.tgz", + "integrity": "sha512-eRZevouTH2i1HeAVLqJuLnt256krQkGY0TN6WsTmsIhuzbh457HuWDMakKwmi0Cjadux983CoSr8Lim2QhUIFw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.0.tgz", + "integrity": "sha512-3oVS7FLGa4U1qcvao9ylGxrjXZyUQqR8UwxEcnUEyPX53O/C/mKDZegNXTdHCP+h3e6ta/f1EN38Yif1mmZHYg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.0.tgz", + "integrity": "sha512-yTB9TgfWj5wHe5QgktAgXTLLot1gvEjl1NiPPAUiCs4oPrIWFl5V4nC3GrkNdj9LaAU4s94nVrGbGOCqUpyWsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.0.tgz", + "integrity": "sha512-5LOhoaesY3doG1c+ac/2JtgREpKoJr5bUHH8tKY0V8di7+uSV6BwLs2PlR0/yzefGOkR+wE7ZolZphHCsyG5Rw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.0.tgz", + "integrity": "sha512-yYkWHhmbhRTWTnWos5HC4GcPQfjlzzCNbM9e/+GXrLuaBXYA3qSDR9f0Vgufd5S8yX81U8jPKp7ZnAjZFMtRnw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.0.tgz", + "integrity": "sha512-SoTb6lPg25xZlA2ibwQ++ahCCnH+FP0qmEuafMJ4gznZKOlXioKEAeJLgCrqjM98ACziXM9V1amFjICVL4IFoA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.0.tgz", + "integrity": "sha512-5L+T1fMX4RIEBoZzT0+sQ0PhTS36NULFmMXtl1TZo44TMAROIMHbZufSOjVWt/Y622BtxgxtaNOokbTDvfsrZA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@scure/base": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz", + "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.7.0.tgz", + "integrity": "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==", + "license": "MIT", + "dependencies": { + "@noble/curves": "~1.9.0", + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.6.0.tgz", + "integrity": "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.13.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.2.tgz", + "integrity": "sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.9.tgz", + "integrity": "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.9", + "@vitest/utils": "4.1.9", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.9.tgz", + "integrity": "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.9.tgz", + "integrity": "sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.9.tgz", + "integrity": "sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.9", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.9.tgz", + "integrity": "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.9", + "@vitest/utils": "4.1.9", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.9.tgz", + "integrity": "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.9.tgz", + "integrity": "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.9", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@yellow-org/clearnet-sdk": { + "resolved": "", + "link": true + }, + "node_modules/@yellow-org/evm-deposit-demo": { + "resolved": "examples/evm-deposit", + "link": true + }, + "node_modules/abitype": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.2.3.tgz", + "integrity": "sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/wevm" + }, + "peerDependencies": { + "typescript": ">=5.0.4", + "zod": "^3.22.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "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/isows": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.7.tgz", + "integrity": "sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "peerDependencies": { + "ws": "*" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/obug": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz", + "integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/ox": { + "version": "0.14.29", + "resolved": "https://registry.npmjs.org/ox/-/ox-0.14.29.tgz", + "integrity": "sha512-M5j87Ec4V99MQdRct/g09eWXW60g6zhHTUs1lr4deUtrPDnezBdCJTgKd7pxqTpSZBFveV0ALi9jMMuT1qKyNg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "dependencies": { + "@adraffy/ens-normalize": "^1.11.0", + "@noble/ciphers": "^1.3.0", + "@noble/curves": "1.9.1", + "@noble/hashes": "^1.8.0", + "@scure/bip32": "^1.7.0", + "@scure/bip39": "^1.6.0", + "abitype": "^1.2.3", + "eventemitter3": "5.0.1" + }, + "peerDependencies": { + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.0.tgz", + "integrity": "sha512-nc72Wgq62I7rtDV4izT5/aaS0zxy3kttkinf9586ApknY3jZO9NYsmtc24fUckA0X7Q2v+ML4a15pdUlV5V/jA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.0", + "@rollup/rollup-android-arm64": "4.62.0", + "@rollup/rollup-darwin-arm64": "4.62.0", + "@rollup/rollup-darwin-x64": "4.62.0", + "@rollup/rollup-freebsd-arm64": "4.62.0", + "@rollup/rollup-freebsd-x64": "4.62.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.0", + "@rollup/rollup-linux-arm-musleabihf": "4.62.0", + "@rollup/rollup-linux-arm64-gnu": "4.62.0", + "@rollup/rollup-linux-arm64-musl": "4.62.0", + "@rollup/rollup-linux-loong64-gnu": "4.62.0", + "@rollup/rollup-linux-loong64-musl": "4.62.0", + "@rollup/rollup-linux-ppc64-gnu": "4.62.0", + "@rollup/rollup-linux-ppc64-musl": "4.62.0", + "@rollup/rollup-linux-riscv64-gnu": "4.62.0", + "@rollup/rollup-linux-riscv64-musl": "4.62.0", + "@rollup/rollup-linux-s390x-gnu": "4.62.0", + "@rollup/rollup-linux-x64-gnu": "4.62.0", + "@rollup/rollup-linux-x64-musl": "4.62.0", + "@rollup/rollup-openbsd-x64": "4.62.0", + "@rollup/rollup-openharmony-arm64": "4.62.0", + "@rollup/rollup-win32-arm64-msvc": "4.62.0", + "@rollup/rollup-win32-ia32-msvc": "4.62.0", + "@rollup/rollup-win32-x64-gnu": "4.62.0", + "@rollup/rollup-win32-x64-msvc": "4.62.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/viem": { + "version": "2.52.2", + "resolved": "https://registry.npmjs.org/viem/-/viem-2.52.2.tgz", + "integrity": "sha512-HSU12p5aD/kAPZfrlbCUqdiP4P/c6hQ9AhfTS51VbLUQIjkWd1d5EjrCx/SCxZ0zhZVRn4Iv5X5WDqXPG8Ubew==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "dependencies": { + "@noble/curves": "1.9.1", + "@noble/hashes": "1.8.0", + "@scure/bip32": "1.7.0", + "@scure/bip39": "1.6.0", + "abitype": "1.2.3", + "isows": "1.0.7", + "ox": "0.14.29", + "ws": "8.20.1" + }, + "peerDependencies": { + "typescript": ">=5.0.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vite": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.5.tgz", + "integrity": "sha512-KuOaNhcnGFN2zIPGA7wRmzF+lJA1sea7rHq17aiJ++9lzY1WWG6Jpwqwe1KNbRVPIqHmr8GLYx7jbrQcN/7/ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.9.tgz", + "integrity": "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.9", + "@vitest/mocker": "4.1.9", + "@vitest/pretty-format": "4.1.9", + "@vitest/runner": "4.1.9", + "@vitest/snapshot": "4.1.9", + "@vitest/spy": "4.1.9", + "@vitest/utils": "4.1.9", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.9", + "@vitest/browser-preview": "4.1.9", + "@vitest/browser-webdriverio": "4.1.9", + "@vitest/coverage-istanbul": "4.1.9", + "@vitest/coverage-v8": "4.1.9", + "@vitest/ui": "4.1.9", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/sdk/ts/package.json b/sdk/ts/package.json new file mode 100644 index 0000000..e355521 --- /dev/null +++ b/sdk/ts/package.json @@ -0,0 +1,45 @@ +{ + "name": "@yellow-org/clearnet-sdk", + "version": "0.2.0", + "description": "TypeScript SDK for Clearnet integration.", + "type": "module", + "license": "MIT", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "files": [ + "dist", + "README.md", + "LICENSE" + ], + "sideEffects": false, + "workspaces": [ + "examples/*" + ], + "scripts": { + "build": "tsc -p tsconfig.json", + "prepack": "npm run build", + "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.test.json --noEmit", + "test": "vitest run test/blockchain/evm/depositor.test.ts", + "test:integration:evm": "vitest run test/blockchain/evm/depositor.integration.test.ts", + "demo:evm": "npm --workspace @yellow-org/evm-deposit-demo run dev" + }, + "dependencies": { + "viem": "^2.39.0" + }, + "overrides": { + "esbuild": "^0.28.1", + "ws": "^8.21.0" + }, + "devDependencies": { + "@types/node": "^24.0.0", + "typescript": "^5.9.0", + "vite": "^7.0.0", + "vitest": "^4.0.0" + } +} diff --git a/sdk/ts/src/blockchain/evm/abi.ts b/sdk/ts/src/blockchain/evm/abi.ts new file mode 100644 index 0000000..2dfa819 --- /dev/null +++ b/sdk/ts/src/blockchain/evm/abi.ts @@ -0,0 +1,97 @@ +export const custodyAbi = [ + { + type: "constructor", + inputs: [ + { name: "initialSigners", type: "address[]", internalType: "address[]" }, + { name: "initialThreshold", type: "uint256", internalType: "uint256" }, + ], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "deposit", + inputs: [ + { name: "account", type: "address", internalType: "address" }, + { name: "asset", type: "address", internalType: "address" }, + { name: "amount", type: "uint256", internalType: "uint256" }, + { name: "depositReference", type: "bytes32", internalType: "bytes32" }, + ], + outputs: [], + stateMutability: "payable", + }, + { + type: "event", + name: "Deposited", + inputs: [ + { + name: "account", + type: "address", + indexed: true, + internalType: "address", + }, + { + name: "depositReference", + type: "bytes32", + indexed: true, + internalType: "bytes32", + }, + { + name: "depositor", + type: "address", + indexed: false, + internalType: "address", + }, + { name: "asset", type: "address", indexed: false, internalType: "address" }, + { name: "amount", type: "uint256", indexed: false, internalType: "uint256" }, + ], + anonymous: false, + }, +] as const; + +export const erc20Abi = [ + { + type: "constructor", + inputs: [ + { name: "_name", type: "string", internalType: "string" }, + { name: "_symbol", type: "string", internalType: "string" }, + ], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "allowance", + inputs: [ + { name: "owner", type: "address", internalType: "address" }, + { name: "spender", type: "address", internalType: "address" }, + ], + outputs: [{ name: "", type: "uint256", internalType: "uint256" }], + stateMutability: "view", + }, + { + type: "function", + name: "approve", + inputs: [ + { name: "spender", type: "address", internalType: "address" }, + { name: "value", type: "uint256", internalType: "uint256" }, + ], + outputs: [{ name: "", type: "bool", internalType: "bool" }], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "balanceOf", + inputs: [{ name: "", type: "address", internalType: "address" }], + outputs: [{ name: "", type: "uint256", internalType: "uint256" }], + stateMutability: "view", + }, + { + type: "function", + name: "mint", + inputs: [ + { name: "to", type: "address", internalType: "address" }, + { name: "amount", type: "uint256", internalType: "uint256" }, + ], + outputs: [], + stateMutability: "nonpayable", + }, +] as const; diff --git a/sdk/ts/src/blockchain/evm/constants.ts b/sdk/ts/src/blockchain/evm/constants.ts new file mode 100644 index 0000000..50ec213 --- /dev/null +++ b/sdk/ts/src/blockchain/evm/constants.ts @@ -0,0 +1,4 @@ +import { zeroAddress } from "viem"; + +export const EVM_NATIVE_ASSET = zeroAddress; +export const DEFAULT_RECEIPT_TIMEOUT_MS = 120_000; diff --git a/sdk/ts/src/blockchain/evm/depositor.ts b/sdk/ts/src/blockchain/evm/depositor.ts new file mode 100644 index 0000000..334b069 --- /dev/null +++ b/sdk/ts/src/blockchain/evm/depositor.ts @@ -0,0 +1,385 @@ +import type { Address, Hash, TransactionReceipt } from "viem"; + +import { ClearnetSdkError } from "../../core/errors.js"; +import type { + DepositStatus, + EvmDepositorConfig, + EvmSubmitDepositInput, + SubmitDepositOptions, + TxRef, + VaultDepositor, +} from "../../core/types.js"; +import { custodyAbi, erc20Abi } from "./abi.js"; +import { + DEFAULT_RECEIPT_TIMEOUT_MS, + EVM_NATIVE_ASSET, +} from "./constants.js"; +import { + isTransactionNotFound, + requireDepositDestination, + normalizeMinConfirmations, + requireAddress, + requireAmount, + requireChainId, + requireTxRef, + requireWalletAccount, + txRef, + walletAccountAddress, + type ValidatedDepositDestination, +} from "./validation.js"; + +type AsyncValidation = Promise; + +export class EvmVaultDepositor implements VaultDepositor { + private readonly config: EvmDepositorConfig; + private readonly initialPublicChainValidation: AsyncValidation; + private readonly initialWriteChainValidation: AsyncValidation; + + constructor(config: EvmDepositorConfig) { + requireAddress(config.custodyAddress, "custodyAddress"); + const walletAccount = requireWalletAccount(config.walletAccount); + const clientAccount = config.walletClient.account; + if ( + clientAccount !== undefined && + walletAccountAddress(clientAccount).toLowerCase() !== + walletAccountAddress(walletAccount).toLowerCase() + ) { + throw new ClearnetSdkError( + "MISSING_WALLET_ACCOUNT", + "walletClient account does not match walletAccount", + ); + } + requireChainId(config.chainId); + if (config.receiptTimeoutMs !== undefined) { + requireReceiptTimeout(config.receiptTimeoutMs); + } + this.config = { ...config }; + this.initialPublicChainValidation = captureValidation( + this.checkPublicChain(), + ); + this.initialWriteChainValidation = captureValidation( + this.checkInitialWriteChain(), + ); + } + + async submitDeposit( + input: EvmSubmitDepositInput, + options: SubmitDepositOptions = {}, + ): Promise { + const destination = requireDepositDestination(input.destination); + const asset = requireAddress(input.asset, "asset"); + const amount = requireAmount(input.amount); + await this.ensureWriteChain(); + + if (asset === EVM_NATIVE_ASSET) { + return this.submitNativeDeposit(destination, amount, options); + } + return this.submitErc20Deposit(destination, asset, amount, options); + } + + async verifyDeposit( + ref: TxRef, + minConfirmations: bigint | number, + ): Promise { + const hash = requireTxRef(ref); + const minConf = normalizeMinConfirmations(minConfirmations); + await this.ensurePublicChain(); + + let receipt: TransactionReceipt; + try { + receipt = await this.config.publicClient.getTransactionReceipt({ hash }); + } catch (error) { + if (!isTransactionNotFound(error)) { + throw new ClearnetSdkError("RPC_ERROR", "evm: tx receipt", { + cause: error, + }); + } + return this.pendingOrAbsent(hash); + } + + if (receipt.status !== "success") { + return "absent"; + } + + let headBlockNumber: bigint; + try { + headBlockNumber = await this.config.publicClient.getBlockNumber({ + cacheTime: 0, + }); + } catch (error) { + throw new ClearnetSdkError("RPC_ERROR", "evm: block number", { + cause: error, + }); + } + + const confirmations = + headBlockNumber >= receipt.blockNumber + ? headBlockNumber - receipt.blockNumber + 1n + : 0n; + return confirmations >= minConf ? "confirmed" : "pending"; + } + + private async submitNativeDeposit( + destination: ValidatedDepositDestination, + amount: bigint, + options: SubmitDepositOptions, + ): Promise { + const hash = await this.writeContractRpc(() => + this.config.walletClient.writeContract({ + address: this.config.custodyAddress, + abi: custodyAbi, + functionName: "deposit", + args: [destination.account, EVM_NATIVE_ASSET, amount, destination.ref], + value: amount, + account: this.config.walletAccount, + chain: this.config.walletClient.chain ?? null, + }), + ); + const ref = txRef(hash); + options.onSubmitted?.(ref); + await this.waitForSuccessfulReceipt(hash, ref, options); + return ref; + } + + private async submitErc20Deposit( + destination: ValidatedDepositDestination, + asset: Address, + amount: bigint, + options: SubmitDepositOptions, + ): Promise { + const approvalHash = await this.writeContractRpc(() => + this.config.walletClient.writeContract({ + address: asset, + abi: erc20Abi, + functionName: "approve", + args: [this.config.custodyAddress, amount], + account: this.config.walletAccount, + chain: this.config.walletClient.chain ?? null, + }), + ); + await this.waitForSuccessfulReceipt(approvalHash, txRef(approvalHash), options); + + const depositHash = await this.writeContractRpc(() => + this.config.walletClient.writeContract({ + address: this.config.custodyAddress, + abi: custodyAbi, + functionName: "deposit", + args: [destination.account, asset, amount, destination.ref], + account: this.config.walletAccount, + chain: this.config.walletClient.chain ?? null, + }), + ); + const ref = txRef(depositHash); + options.onSubmitted?.(ref); + await this.waitForSuccessfulReceipt(depositHash, ref, options); + return ref; + } + + private async pendingOrAbsent(hash: Hash): Promise { + try { + await this.config.publicClient.getTransaction({ hash }); + return "pending"; + } catch (error) { + if (isTransactionNotFound(error)) { + return "absent"; + } + throw new ClearnetSdkError("RPC_ERROR", "evm: tx lookup", { + cause: error, + }); + } + } + + private async ensureWriteChain(): Promise { + await throwValidationError(this.initialWriteChainValidation); + await this.checkPublicChain(); + await this.checkWalletChain(); + } + + private async ensurePublicChain(): Promise { + await throwValidationError(this.initialPublicChainValidation); + await this.checkPublicChain(); + } + + private async checkInitialWriteChain(): Promise { + await throwValidationError(this.initialPublicChainValidation); + await this.checkWalletChain(); + } + + private async checkWalletChain(): Promise { + const walletClient = this.config.walletClient as { + getChainId?: () => Promise; + }; + if (typeof walletClient.getChainId !== "function") { + return; + } + let walletChainId: number; + try { + walletChainId = await walletClient.getChainId(); + } catch (error) { + throw new ClearnetSdkError("RPC_ERROR", "evm: wallet chain id", { + cause: error, + }); + } + if (walletChainId !== this.config.chainId) { + throw new ClearnetSdkError( + "CHAIN_MISMATCH", + `wallet chain ${walletChainId} does not match expected chain ${this.config.chainId}`, + ); + } + } + + private async checkPublicChain(): Promise { + let publicChainId: number; + try { + publicChainId = await this.config.publicClient.getChainId(); + } catch (error) { + throw new ClearnetSdkError("RPC_ERROR", "evm: public chain id", { + cause: error, + }); + } + if (publicChainId !== this.config.chainId) { + throw new ClearnetSdkError( + "CHAIN_MISMATCH", + `public chain ${publicChainId} does not match expected chain ${this.config.chainId}`, + ); + } + } + + private async writeContractRpc(write: () => Promise): Promise { + try { + return await write(); + } catch (error) { + if (error instanceof ClearnetSdkError) { + throw error; + } + throw new ClearnetSdkError("RPC_ERROR", "evm: write contract", { + cause: error, + }); + } + } + + private async waitForSuccessfulReceipt( + hash: Hash, + ref: TxRef | undefined, + options: SubmitDepositOptions, + ): Promise { + const timeoutMs = requireReceiptTimeout( + options.receiptTimeoutMs ?? this.config.receiptTimeoutMs ?? DEFAULT_RECEIPT_TIMEOUT_MS, + ); + let receipt: TransactionReceipt; + try { + receipt = await waitWithControls( + () => this.config.publicClient.waitForTransactionReceipt({ hash }), + timeoutMs, + options.signal, + ref, + ); + } catch (error) { + if (error instanceof ClearnetSdkError) { + throw error; + } + throw new ClearnetSdkError("RPC_ERROR", "evm: wait receipt", { + cause: error, + ...(ref !== undefined ? { txRef: ref } : {}), + }); + } + + if (receipt.status !== "success") { + throw new ClearnetSdkError( + "TX_REVERTED", + `transaction reverted (tx=${hash})`, + ref !== undefined ? { txRef: ref } : {}, + ); + } + } +} + +function captureValidation(validation: Promise): AsyncValidation { + return validation.then( + () => undefined, + (error: unknown) => + error instanceof ClearnetSdkError + ? error + : new ClearnetSdkError("RPC_ERROR", "evm: validation", { + cause: error, + }), + ); +} + +async function throwValidationError(validation: AsyncValidation): Promise { + const error = await validation; + if (error !== undefined) { + throw error; + } +} + +async function waitWithControls( + wait: () => Promise, + timeoutMs: number, + signal: AbortSignal | undefined, + ref: TxRef | undefined, +): Promise { + if (signal?.aborted) { + throw new ClearnetSdkError( + "RECEIPT_TIMEOUT", + "receipt wait aborted", + ref !== undefined ? { txRef: ref } : {}, + ); + } + + let timeoutId: ReturnType | undefined; + let abortHandler: (() => void) | undefined; + + const timeoutPromise = new Promise((_, reject) => { + timeoutId = setTimeout(() => { + reject( + new ClearnetSdkError( + "RECEIPT_TIMEOUT", + `receipt wait timed out after ${timeoutMs}ms`, + ref !== undefined ? { txRef: ref } : {}, + ), + ); + }, timeoutMs); + }); + + const abortPromise = + signal === undefined + ? undefined + : new Promise((_, reject) => { + abortHandler = () => { + reject( + new ClearnetSdkError( + "RECEIPT_TIMEOUT", + "receipt wait aborted", + ref !== undefined ? { txRef: ref } : {}, + ), + ); + }; + signal.addEventListener("abort", abortHandler, { once: true }); + }); + + try { + return await Promise.race( + abortPromise === undefined + ? [wait(), timeoutPromise] + : [wait(), timeoutPromise, abortPromise], + ); + } finally { + if (timeoutId !== undefined) { + clearTimeout(timeoutId); + } + if (signal !== undefined && abortHandler !== undefined) { + signal.removeEventListener("abort", abortHandler); + } + } +} + +function requireReceiptTimeout(timeoutMs: number): number { + if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0) { + throw new ClearnetSdkError( + "RECEIPT_TIMEOUT", + "receiptTimeoutMs must be a positive safe integer", + ); + } + return timeoutMs; +} diff --git a/sdk/ts/src/blockchain/evm/validation.ts b/sdk/ts/src/blockchain/evm/validation.ts new file mode 100644 index 0000000..de6a949 --- /dev/null +++ b/sdk/ts/src/blockchain/evm/validation.ts @@ -0,0 +1,162 @@ +import { isAddress, zeroHash } from "viem"; +import type { Account, Address, Hash } from "viem"; + +import { ClearnetSdkError } from "../../core/errors.js"; +import type { TxRef } from "../../core/types.js"; + +const UINT256_MAX = (1n << 256n) - 1n; +const HASH_PATTERN = /^0x[a-fA-F0-9]{64}$/; + +export interface ValidatedDepositDestination { + account: Address; + ref: Hash; +} + +export function requireAddress(value: unknown, field: string): Address { + if (typeof value !== "string" || !isAddress(value)) { + throw new ClearnetSdkError( + "INVALID_ADDRESS", + `${field} must be a valid EVM address`, + ); + } + return value; +} + +export function requireWalletAccount(account: Account | Address): Account | Address { + if (typeof account === "string") { + return requireAddress(account, "walletAccount"); + } + if (!account || typeof account !== "object") { + throw new ClearnetSdkError( + "MISSING_WALLET_ACCOUNT", + "walletAccount is required", + ); + } + requireAddress(account.address, "walletAccount.address"); + return account; +} + +export function walletAccountAddress(account: Account | Address): Address { + return typeof account === "string" ? account : account.address; +} + +export function requireAmount(amount: unknown): bigint { + if (typeof amount !== "bigint") { + throw new ClearnetSdkError( + "INVALID_AMOUNT", + "amount must be a bigint in base units", + ); + } + if (amount <= 0n) { + throw new ClearnetSdkError( + "INVALID_AMOUNT", + "amount must be greater than zero", + ); + } + if (amount > UINT256_MAX) { + throw new ClearnetSdkError( + "INVALID_AMOUNT", + "amount must fit in uint256", + ); + } + return amount; +} + +export function requireDepositDestination( + destination: unknown, +): ValidatedDepositDestination { + if (!destination || typeof destination !== "object") { + throw new ClearnetSdkError( + "INVALID_ADDRESS", + "destination.account must be a valid EVM address", + ); + } + const fields = destination as Record<"account" | "ref", unknown>; + return { + account: requireAddress(fields.account, "destination.account"), + ref: requireReference(fields.ref), + }; +} + +export function requireChainId(chainId: number): number { + if (!Number.isSafeInteger(chainId) || chainId <= 0) { + throw new ClearnetSdkError( + "CHAIN_MISMATCH", + "chainId must be a positive safe integer", + ); + } + return chainId; +} + +export function requireReference(reference: unknown): Hash { + if (reference === undefined || reference === "") { + return zeroHash; + } + if (typeof reference !== "string" || !HASH_PATTERN.test(reference)) { + throw new ClearnetSdkError( + "INVALID_REFERENCE", + "destination.ref must be a 32-byte hex value", + ); + } + return reference as Hash; +} + +export function requireTxRef(ref: unknown): Hash { + if (!ref || typeof ref !== "object" || !("hash" in ref)) { + throw new ClearnetSdkError( + "INVALID_TX_REF", + "ref.hash must be a 32-byte EVM transaction hash", + ); + } + const hash = (ref as Record<"hash", unknown>).hash; + if (typeof hash !== "string" || !HASH_PATTERN.test(hash)) { + throw new ClearnetSdkError( + "INVALID_TX_REF", + "ref.hash must be a 32-byte EVM transaction hash", + ); + } + return hash as Hash; +} + +export function txRef(hash: Hash): TxRef { + return { hash, raw: hash }; +} + +export function normalizeMinConfirmations(value: bigint | number): bigint { + if (typeof value === "bigint") { + if (value < 0n) { + throw new ClearnetSdkError( + "INVALID_CONFIRMATIONS", + "minConfirmations must be non-negative", + ); + } + return value; + } + if (!Number.isSafeInteger(value) || value < 0) { + throw new ClearnetSdkError( + "INVALID_CONFIRMATIONS", + "minConfirmations must be a non-negative safe integer", + ); + } + return BigInt(value); +} + +export function isTransactionNotFound(error: unknown): boolean { + const name = getErrorField(error, "name"); + if ( + name === "TransactionNotFoundError" || + name === "TransactionReceiptNotFoundError" + ) { + return true; + } + const message = getErrorField(error, "message").toLowerCase(); + return message.includes("not found") && message.includes("transaction"); +} + +function getErrorField(error: unknown, field: "name" | "message"): string { + if (error && typeof error === "object" && field in error) { + const value = (error as Record)[field]; + return typeof value === "string" ? value : ""; + } + return ""; +} diff --git a/sdk/ts/src/core/errors.ts b/sdk/ts/src/core/errors.ts new file mode 100644 index 0000000..5d6f79b --- /dev/null +++ b/sdk/ts/src/core/errors.ts @@ -0,0 +1,44 @@ +import type { TxRef } from "./types.js"; + +export type ClearnetSdkErrorCode = + | "INVALID_ADDRESS" + | "INVALID_AMOUNT" + | "INVALID_CONFIRMATIONS" + | "INVALID_REFERENCE" + | "INVALID_TX_REF" + | "MISSING_WALLET_ACCOUNT" + | "CHAIN_MISMATCH" + | "TX_REVERTED" + | "RECEIPT_TIMEOUT" + | "RPC_ERROR"; + +interface ClearnetSdkErrorOptions { + txRef?: TxRef; + cause?: unknown; +} + +export class ClearnetSdkError extends Error { + readonly code: ClearnetSdkErrorCode; + readonly txRef?: TxRef; + override cause?: unknown; + + constructor( + code: ClearnetSdkErrorCode, + message: string, + options: ClearnetSdkErrorOptions = {}, + ) { + super(message, causeOptions(options.cause)); + this.name = "ClearnetSdkError"; + this.code = code; + if (options.txRef !== undefined) { + this.txRef = options.txRef; + } + if (options.cause !== undefined) { + this.cause = options.cause; + } + } +} + +function causeOptions(cause: unknown): ErrorOptions | undefined { + return cause === undefined ? undefined : { cause }; +} diff --git a/sdk/ts/src/core/types.ts b/sdk/ts/src/core/types.ts new file mode 100644 index 0000000..cd5175e --- /dev/null +++ b/sdk/ts/src/core/types.ts @@ -0,0 +1,59 @@ +import type { + Account, + Address, + Hash, + PublicClient, + WalletClient, +} from "viem"; + +export interface TxRef { + hash: Hash; + raw: string; +} + +export type DepositStatus = "absent" | "pending" | "confirmed"; + +export interface DepositDestination { + account: string; + ref?: Hash; +} + +export interface EvmDepositDestination extends DepositDestination { + account: Address; +} + +export interface SubmitDepositInput { + asset: string; + amount: bigint; + destination: DepositDestination; +} + +export interface EvmSubmitDepositInput extends SubmitDepositInput { + asset: Address; + destination: EvmDepositDestination; +} + +export interface SubmitDepositOptions { + signal?: AbortSignal; + receiptTimeoutMs?: number; + onSubmitted?: (ref: TxRef) => void; +} + +export interface VaultDepositor< + TInput extends SubmitDepositInput = SubmitDepositInput, +> { + submitDeposit(input: TInput, options?: SubmitDepositOptions): Promise; + verifyDeposit( + ref: TxRef, + minConfirmations: bigint | number, + ): Promise; +} + +export interface EvmDepositorConfig { + publicClient: PublicClient; + walletClient: WalletClient; + walletAccount: Account | Address; + custodyAddress: Address; + chainId: number; + receiptTimeoutMs?: number; +} diff --git a/sdk/ts/src/index.ts b/sdk/ts/src/index.ts new file mode 100644 index 0000000..c7c73cf --- /dev/null +++ b/sdk/ts/src/index.ts @@ -0,0 +1,15 @@ +export type { + DepositDestination, + DepositStatus, + EvmDepositDestination, + EvmDepositorConfig, + EvmSubmitDepositInput, + SubmitDepositInput, + SubmitDepositOptions, + TxRef, + VaultDepositor, +} from "./core/types.js"; +export { ClearnetSdkError } from "./core/errors.js"; +export type { ClearnetSdkErrorCode } from "./core/errors.js"; +export { EvmVaultDepositor } from "./blockchain/evm/depositor.js"; +export { EVM_NATIVE_ASSET } from "./blockchain/evm/constants.js"; diff --git a/sdk/ts/test/blockchain/evm/depositor.integration.test.ts b/sdk/ts/test/blockchain/evm/depositor.integration.test.ts new file mode 100644 index 0000000..4bbf90a --- /dev/null +++ b/sdk/ts/test/blockchain/evm/depositor.integration.test.ts @@ -0,0 +1,252 @@ +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; + +import { beforeAll, describe, expect, it } from "vitest"; +import { + createPublicClient, + createWalletClient, + defineChain, + getAddress, + http, + parseAbiItem, + parseEther, + parseEventLogs, + zeroAddress, +} from "viem"; +import type { Address, Hash, Hex, Log } from "viem"; +import { privateKeyToAccount } from "viem/accounts"; + +import { EvmVaultDepositor } from "../../../src/blockchain/evm/depositor.js"; +import { custodyAbi, erc20Abi } from "../../../src/blockchain/evm/abi.js"; + +const RPC_URL = process.env.EVM_RPC_URL ?? "http://127.0.0.1:8545"; +const CHAIN_ID = 31_337; +const DEPLOYER_PRIVATE_KEY = + "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"; +const ACCOUNT_PRIVATE_KEYS = [ + DEPLOYER_PRIVATE_KEY, + "0x0000000000000000000000000000000000000000000000000000000000000001", + "0x0000000000000000000000000000000000000000000000000000000000000002", +] as const; +const DEPOSIT_REFERENCE = + "0x3333333333333333333333333333333333333333333333333333333333333333" as Hash; +const anvil = defineChain({ + id: CHAIN_ID, + name: "Anvil", + nativeCurrency: { decimals: 18, name: "Ether", symbol: "ETH" }, + rpcUrls: { default: { http: [RPC_URL] } }, +}); +const deployer = privateKeyToAccount(DEPLOYER_PRIVATE_KEY); +const publicClient = createPublicClient({ + chain: anvil, + transport: http(RPC_URL), +}); +const walletClient = createWalletClient({ + account: deployer, + chain: anvil, + transport: http(RPC_URL), +}); + +type AnvilPublicClient = typeof publicClient; +type AnvilWalletClient = typeof walletClient; + +describe("EvmVaultDepositor Anvil integration", () => { + beforeAll(async () => { + const chainId = await publicClient.getChainId(); + expect(chainId).toBe(CHAIN_ID); + }); + + it("deposits native ETH and verifies the deposit tx", async () => { + const custodyAddress = await deployCustody(publicClient, walletClient); + const depositor = new EvmVaultDepositor({ + publicClient, + walletClient, + walletAccount: deployer, + custodyAddress, + chainId: CHAIN_ID, + }); + const amount = parseEther("0.01"); + const beforeBalance = await publicClient.getBalance({ + address: custodyAddress, + }); + + const ref = await depositor.submitDeposit({ + destination: { account: deployer.address, ref: DEPOSIT_REFERENCE }, + asset: zeroAddress, + amount, + }); + const afterBalance = await publicClient.getBalance({ + address: custodyAddress, + }); + const receipt = await publicClient.getTransactionReceipt({ + hash: ref.hash, + }); + + expect(afterBalance - beforeBalance).toBe(amount); + expect( + hasDepositedLog( + receipt.logs, + custodyAddress, + deployer.address, + DEPOSIT_REFERENCE, + zeroAddress, + amount, + ), + ).toBe(true); + await expect(depositor.verifyDeposit(ref, 1)).resolves.toBe("confirmed"); + }); + + it("approves an exact ERC-20 amount, deposits, and verifies the deposit tx", async () => { + const custodyAddress = await deployCustody(publicClient, walletClient); + const tokenAddress = await deployMockErc20(publicClient, walletClient); + const depositor = new EvmVaultDepositor({ + publicClient, + walletClient, + walletAccount: deployer, + custodyAddress, + chainId: CHAIN_ID, + }); + const amount = parseEther("25"); + + await mine( + publicClient, + await walletClient.writeContract({ + address: tokenAddress, + abi: erc20Abi, + functionName: "mint", + args: [deployer.address, amount], + }), + ); + const beforeBalance = await publicClient.readContract({ + address: tokenAddress, + abi: erc20Abi, + functionName: "balanceOf", + args: [custodyAddress], + }); + const startBlock = await publicClient.getBlockNumber(); + + const ref = await depositor.submitDeposit({ + destination: { account: deployer.address, ref: DEPOSIT_REFERENCE }, + asset: tokenAddress, + amount, + }); + const allowance = await publicClient.readContract({ + address: tokenAddress, + abi: erc20Abi, + functionName: "allowance", + args: [deployer.address, custodyAddress], + }); + const afterBalance = await publicClient.readContract({ + address: tokenAddress, + abi: erc20Abi, + functionName: "balanceOf", + args: [custodyAddress], + }); + const receipt = await publicClient.getTransactionReceipt({ + hash: ref.hash, + }); + const approvalLogs = await publicClient.getLogs({ + address: tokenAddress, + event: parseAbiItem( + "event Approval(address indexed owner, address indexed spender, uint256 value)", + ), + args: { owner: deployer.address, spender: custodyAddress }, + fromBlock: startBlock, + toBlock: receipt.blockNumber, + }); + const approvalLog = approvalLogs.find( + (log) => log.args.value === amount && log.transactionHash !== ref.hash, + ); + if (approvalLog === undefined) { + throw new Error("expected exact approval log before deposit"); + } + const approvalReceipt = await publicClient.getTransactionReceipt({ + hash: approvalLog.transactionHash, + }); + + expect(allowance).toBe(0n); + expect(approvalReceipt.status).toBe("success"); + expect(approvalReceipt.blockNumber < receipt.blockNumber).toBe(true); + expect(afterBalance - beforeBalance).toBe(amount); + expect( + hasDepositedLog( + receipt.logs, + custodyAddress, + deployer.address, + DEPOSIT_REFERENCE, + tokenAddress, + amount, + ), + ).toBe(true); + await expect(depositor.verifyDeposit(ref, 1)).resolves.toBe("confirmed"); + }); +}); + +async function deployCustody( + publicClient: AnvilPublicClient, + walletClient: AnvilWalletClient, +): Promise
{ + const signers = ACCOUNT_PRIVATE_KEYS.map((key) => + getAddress(privateKeyToAccount(key).address), + ).sort((left, right) => left.toLowerCase().localeCompare(right.toLowerCase())); + const hash = await walletClient.deployContract({ + abi: custodyAbi, + bytecode: artifactBytecode("Custody.bin"), + args: [signers, 2n], + }); + const receipt = await mine(publicClient, hash); + if (receipt.contractAddress === null || receipt.contractAddress === undefined) { + throw new Error("Custody deployment did not return a contract address"); + } + return receipt.contractAddress; +} + +async function deployMockErc20( + publicClient: AnvilPublicClient, + walletClient: AnvilWalletClient, +): Promise
{ + const hash = await walletClient.deployContract({ + abi: erc20Abi, + bytecode: artifactBytecode("MockERC20.bin"), + args: ["Mock Token", "MOCK"], + }); + const receipt = await mine(publicClient, hash); + if (receipt.contractAddress === null || receipt.contractAddress === undefined) { + throw new Error("MockERC20 deployment did not return a contract address"); + } + return receipt.contractAddress; +} + +async function mine(publicClient: AnvilPublicClient, hash: Hex) { + return publicClient.waitForTransactionReceipt({ hash }); +} + +function artifactBytecode(fileName: "Custody.bin" | "MockERC20.bin"): Hex { + const contents = readFileSync( + resolve("../../pkg/blockchain/evm/artifacts", fileName), + "utf8", + ).trim(); + return contents.startsWith("0x") ? (contents as Hex) : `0x${contents}`; +} + +function hasDepositedLog( + logs: readonly Log[], + custodyAddress: Address, + account: Address, + reference: Hash, + asset: Address, + amount: bigint, +): boolean { + return parseEventLogs({ + abi: custodyAbi, + eventName: "Deposited", + logs: [...logs], + }).some( + (log) => + log.address.toLowerCase() === custodyAddress.toLowerCase() && + log.args.account.toLowerCase() === account.toLowerCase() && + log.args.depositReference.toLowerCase() === reference.toLowerCase() && + log.args.asset.toLowerCase() === asset.toLowerCase() && + log.args.amount === amount, + ); +} diff --git a/sdk/ts/test/blockchain/evm/depositor.test.ts b/sdk/ts/test/blockchain/evm/depositor.test.ts new file mode 100644 index 0000000..aa2df67 --- /dev/null +++ b/sdk/ts/test/blockchain/evm/depositor.test.ts @@ -0,0 +1,445 @@ +import { describe, expect, expectTypeOf, it, vi } from "vitest"; +import { zeroAddress, zeroHash } from "viem"; +import type { + Address, + Hash, + PublicClient, + TransactionReceipt, + WalletClient, +} from "viem"; + +import { + ClearnetSdkError, + EVM_NATIVE_ASSET, + EvmVaultDepositor, +} from "../../../src/index.js"; +import type { + DepositStatus, + EvmSubmitDepositInput, + TxRef, + VaultDepositor, +} from "../../../src/index.js"; + +const CHAIN_ID = 31_337; +const CUSTODY_ADDRESS = + "0x0000000000000000000000000000000000001000" as Address; +const ACCOUNT = "0x0000000000000000000000000000000000002000" as Address; +const TOKEN = "0x0000000000000000000000000000000000003000" as Address; +const DEPOSIT_HASH = + "0x1111111111111111111111111111111111111111111111111111111111111111" as Hash; +const APPROVAL_HASH = + "0x2222222222222222222222222222222222222222222222222222222222222222" as Hash; +const DEPOSIT_REFERENCE = + "0x3333333333333333333333333333333333333333333333333333333333333333" as Hash; + +interface ClientMocks { + publicClient: PublicClient; + walletClient: WalletClient; + publicMock: { + getChainId: ReturnType; + waitForTransactionReceipt: ReturnType; + getTransactionReceipt: ReturnType; + getTransaction: ReturnType; + getBlockNumber: ReturnType; + }; + walletMock: { + getChainId: ReturnType; + writeContract: ReturnType; + }; +} + +describe("EvmVaultDepositor", () => { + it("matches the public depositor and result type contracts", () => { + expectTypeOf().toMatchTypeOf< + VaultDepositor + >(); + expectTypeOf().toEqualTypeOf<{ hash: Hash; raw: string }>(); + expectTypeOf().toEqualTypeOf< + "absent" | "pending" | "confirmed" + >(); + }); + + it("exports the native zero-address constant", () => { + expect(EVM_NATIVE_ASSET).toBe(zeroAddress); + }); + + it("submits a native ETH deposit with matching value and returns the deposit hash", async () => { + const clients = createClients(); + clients.walletMock.writeContract.mockResolvedValueOnce(DEPOSIT_HASH); + + const depositor = createDepositor(clients); + const onSubmitted = vi.fn(); + const ref = await depositor.submitDeposit( + { + destination: { account: ACCOUNT, ref: DEPOSIT_REFERENCE }, + asset: zeroAddress, + amount: 10n, + }, + { onSubmitted }, + ); + + expect(ref).toEqual({ hash: DEPOSIT_HASH, raw: DEPOSIT_HASH }); + expect(onSubmitted).toHaveBeenCalledExactlyOnceWith(ref); + expect(clients.walletMock.writeContract).toHaveBeenCalledExactlyOnceWith( + expect.objectContaining({ + address: CUSTODY_ADDRESS, + functionName: "deposit", + args: [ACCOUNT, zeroAddress, 10n, DEPOSIT_REFERENCE], + value: 10n, + account: ACCOUNT, + chain: null, + }), + ); + expect(clients.publicMock.waitForTransactionReceipt).toHaveBeenCalledWith({ + hash: DEPOSIT_HASH, + }); + }); + + it("approves an exact ERC-20 amount before depositing and returns the deposit hash", async () => { + const clients = createClients(); + clients.walletMock.writeContract + .mockResolvedValueOnce(APPROVAL_HASH) + .mockResolvedValueOnce(DEPOSIT_HASH); + + const depositor = createDepositor(clients); + const onSubmitted = vi.fn(); + const ref = await depositor.submitDeposit( + { destination: { account: ACCOUNT }, asset: TOKEN, amount: 25n }, + { onSubmitted }, + ); + + expect(ref).toEqual({ hash: DEPOSIT_HASH, raw: DEPOSIT_HASH }); + expect(onSubmitted).toHaveBeenCalledExactlyOnceWith(ref); + expect(clients.walletMock.writeContract).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + address: TOKEN, + functionName: "approve", + args: [CUSTODY_ADDRESS, 25n], + account: ACCOUNT, + chain: null, + }), + ); + expect(clients.walletMock.writeContract).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + address: CUSTODY_ADDRESS, + functionName: "deposit", + args: [ACCOUNT, TOKEN, 25n, zeroHash], + account: ACCOUNT, + chain: null, + }), + ); + expect(clients.publicMock.waitForTransactionReceipt).toHaveBeenNthCalledWith( + 1, + { hash: APPROVAL_HASH }, + ); + expect(clients.publicMock.waitForTransactionReceipt).toHaveBeenNthCalledWith( + 2, + { hash: DEPOSIT_HASH }, + ); + }); + + it("throws TX_REVERTED with txRef when the deposit receipt fails", async () => { + const clients = createClients({ + waitReceipt: receipt({ status: "reverted" }), + }); + clients.walletMock.writeContract.mockResolvedValueOnce(DEPOSIT_HASH); + const depositor = createDepositor(clients); + + await expect( + depositor.submitDeposit({ + destination: { account: ACCOUNT }, + asset: zeroAddress, + amount: 1n, + }), + ).rejects.toMatchObject({ + code: "TX_REVERTED", + txRef: { hash: DEPOSIT_HASH, raw: DEPOSIT_HASH }, + }); + }); + + it("throws TX_REVERTED when the ERC-20 approval receipt fails", async () => { + const clients = createClients({ + waitReceipt: receipt({ status: "reverted" }), + }); + clients.walletMock.writeContract.mockResolvedValueOnce(APPROVAL_HASH); + const depositor = createDepositor(clients); + + await expect( + depositor.submitDeposit({ + destination: { account: ACCOUNT }, + asset: TOKEN, + amount: 1n, + }), + ).rejects.toMatchObject({ + code: "TX_REVERTED", + txRef: { hash: APPROVAL_HASH, raw: APPROVAL_HASH }, + }); + expect(clients.walletMock.writeContract).toHaveBeenCalledTimes(1); + }); + + it("throws RECEIPT_TIMEOUT with txRef after a submitted deposit times out", async () => { + const clients = createClients({ + waitReceiptPromise: new Promise(() => undefined), + }); + clients.walletMock.writeContract.mockResolvedValueOnce(DEPOSIT_HASH); + const depositor = createDepositor(clients); + + await expect( + depositor.submitDeposit( + { destination: { account: ACCOUNT }, asset: zeroAddress, amount: 1n }, + { receiptTimeoutMs: 1 }, + ), + ).rejects.toMatchObject({ + code: "RECEIPT_TIMEOUT", + txRef: { hash: DEPOSIT_HASH, raw: DEPOSIT_HASH }, + }); + }); + + it("validates deposit reference before chain checks or signing", async () => { + const clients = createClients(); + const depositor = createDepositor(clients); + + await expect( + depositor.submitDeposit({ + destination: { account: ACCOUNT, ref: "invoice-1" as Hash }, + asset: zeroAddress, + amount: 1n, + }), + ).rejects.toMatchObject({ code: "INVALID_REFERENCE" }); + expect(clients.walletMock.writeContract).not.toHaveBeenCalled(); + }); + + it("rejects invalid input before signing", async () => { + const clients = createClients(); + const depositor = createDepositor(clients); + + await expect( + depositor.submitDeposit({ + destination: { account: ACCOUNT }, + asset: "not-an-address" as Address, + amount: 1n, + }), + ).rejects.toMatchObject({ code: "INVALID_ADDRESS" }); + await expect( + depositor.submitDeposit({ + destination: { account: "not-an-address" as Address }, + asset: zeroAddress, + amount: 1n, + }), + ).rejects.toMatchObject({ code: "INVALID_ADDRESS" }); + await expect( + depositor.submitDeposit({ + destination: { account: ACCOUNT }, + asset: zeroAddress, + amount: 0n, + }), + ).rejects.toMatchObject({ code: "INVALID_AMOUNT" }); + expect(clients.walletMock.writeContract).not.toHaveBeenCalled(); + }); + + it("fails chain mismatch before signing", async () => { + const clients = createClients({ publicChainId: 1 }); + const depositor = createDepositor(clients); + + await expect( + depositor.submitDeposit({ + destination: { account: ACCOUNT }, + asset: zeroAddress, + amount: 1n, + }), + ).rejects.toMatchObject({ code: "CHAIN_MISMATCH" }); + expect(clients.walletMock.writeContract).not.toHaveBeenCalled(); + }); + + it("requires a wallet account in constructor", () => { + const clients = createClients(); + + expect( + () => + new EvmVaultDepositor({ + publicClient: clients.publicClient, + walletClient: clients.walletClient, + walletAccount: undefined as unknown as Address, + custodyAddress: CUSTODY_ADDRESS, + chainId: CHAIN_ID, + }), + ).toThrow(ClearnetSdkError); + }); + + it("rejects a wallet client account that cannot sign for walletAccount", () => { + const clients = createClients({ walletAccount: TOKEN }); + + expect( + () => + new EvmVaultDepositor({ + publicClient: clients.publicClient, + walletClient: clients.walletClient, + walletAccount: ACCOUNT, + custodyAddress: CUSTODY_ADDRESS, + chainId: CHAIN_ID, + }), + ).toThrow(ClearnetSdkError); + }); + + it("accepts equivalent wallet client and walletAccount address casing", () => { + const lowerAccount = ACCOUNT.toLowerCase() as Address; + const clients = createClients({ walletAccount: lowerAccount }); + + expect(() => createDepositor(clients)).not.toThrow(); + }); + + it("maps known successful receipts to confirmed or pending with inclusive confirmations", async () => { + const clients = createClients({ + txReceipt: receipt({ status: "success", blockNumber: 10n }), + headBlock: 10n, + }); + const depositor = createDepositor(clients); + + await expect( + depositor.verifyDeposit({ hash: DEPOSIT_HASH, raw: DEPOSIT_HASH }, 1), + ).resolves.toBe("confirmed"); + await expect( + depositor.verifyDeposit({ hash: DEPOSIT_HASH, raw: DEPOSIT_HASH }, 2), + ).resolves.toBe("pending"); + expect(clients.publicMock.getBlockNumber).toHaveBeenCalledWith({ + cacheTime: 0, + }); + }); + + it("maps failed receipts to absent", async () => { + const clients = createClients({ + txReceipt: receipt({ status: "reverted", blockNumber: 10n }), + }); + const depositor = createDepositor(clients); + + await expect( + depositor.verifyDeposit({ hash: DEPOSIT_HASH, raw: DEPOSIT_HASH }, 1n), + ).resolves.toBe("absent"); + }); + + it("maps missing receipt to pending when the transaction is known", async () => { + const clients = createClients({ + txReceiptError: transactionNotFound("TransactionReceiptNotFoundError"), + pendingTransactionKnown: true, + }); + const depositor = createDepositor(clients); + + await expect( + depositor.verifyDeposit({ hash: DEPOSIT_HASH, raw: DEPOSIT_HASH }, 1), + ).resolves.toBe("pending"); + }); + + it("maps missing receipt to absent when the transaction is unknown", async () => { + const clients = createClients({ + txReceiptError: transactionNotFound("TransactionReceiptNotFoundError"), + pendingTransactionKnown: false, + }); + const depositor = createDepositor(clients); + + await expect( + depositor.verifyDeposit({ hash: DEPOSIT_HASH, raw: DEPOSIT_HASH }, 1), + ).resolves.toBe("absent"); + }); + + it("throws RPC_ERROR with cause for real verify RPC failures", async () => { + const rpcError = new Error("node offline"); + const clients = createClients({ txReceiptError: rpcError }); + const depositor = createDepositor(clients); + + await expect( + depositor.verifyDeposit({ hash: DEPOSIT_HASH, raw: DEPOSIT_HASH }, 1), + ).rejects.toMatchObject({ code: "RPC_ERROR", cause: rpcError }); + }); + + it("validates tx refs and confirmation depths", async () => { + const depositor = createDepositor(createClients()); + + await expect( + depositor.verifyDeposit({ hash: "0x1234" as Hash, raw: "0x1234" }, 1), + ).rejects.toMatchObject({ code: "INVALID_TX_REF" }); + await expect( + depositor.verifyDeposit({ hash: DEPOSIT_HASH, raw: DEPOSIT_HASH }, -1), + ).rejects.toMatchObject({ code: "INVALID_CONFIRMATIONS" }); + await expect( + depositor.verifyDeposit({ hash: DEPOSIT_HASH, raw: DEPOSIT_HASH }, 1.5), + ).rejects.toMatchObject({ code: "INVALID_CONFIRMATIONS" }); + }); +}); + +function createDepositor(clients: ClientMocks): EvmVaultDepositor { + return new EvmVaultDepositor({ + publicClient: clients.publicClient, + walletClient: clients.walletClient, + walletAccount: ACCOUNT, + custodyAddress: CUSTODY_ADDRESS, + chainId: CHAIN_ID, + }); +} + +function createClients(options: { + publicChainId?: number; + walletChainId?: number; + walletAccount?: Address; + waitReceipt?: TransactionReceipt; + waitReceiptPromise?: Promise; + txReceipt?: TransactionReceipt; + txReceiptError?: unknown; + headBlock?: bigint; + pendingTransactionKnown?: boolean; +} = {}): ClientMocks { + const publicMock = { + getChainId: vi.fn().mockResolvedValue(options.publicChainId ?? CHAIN_ID), + waitForTransactionReceipt: vi + .fn() + .mockImplementation(() => + options.waitReceiptPromise === undefined + ? Promise.resolve(options.waitReceipt ?? receipt()) + : options.waitReceiptPromise, + ), + getTransactionReceipt: vi.fn().mockImplementation(() => { + if (options.txReceiptError !== undefined) { + return Promise.reject(options.txReceiptError); + } + return Promise.resolve(options.txReceipt ?? receipt()); + }), + getTransaction: vi.fn().mockImplementation(() => { + if (options.pendingTransactionKnown === false) { + return Promise.reject(transactionNotFound("TransactionNotFoundError")); + } + return Promise.resolve({ hash: DEPOSIT_HASH }); + }), + getBlockNumber: vi.fn().mockResolvedValue(options.headBlock ?? 1n), + }; + const walletMock = { + ...(options.walletAccount !== undefined + ? { account: { address: options.walletAccount } } + : {}), + getChainId: vi.fn().mockResolvedValue(options.walletChainId ?? CHAIN_ID), + writeContract: vi.fn(), + }; + + return { + publicClient: publicMock as unknown as PublicClient, + walletClient: walletMock as unknown as WalletClient, + publicMock, + walletMock, + }; +} + +function receipt(options: { + status?: TransactionReceipt["status"]; + blockNumber?: bigint; +} = {}): TransactionReceipt { + return { + status: options.status ?? "success", + blockNumber: options.blockNumber ?? 1n, + } as TransactionReceipt; +} + +function transactionNotFound(name: string): Error { + const error = new Error("transaction not found"); + error.name = name; + return error; +} diff --git a/sdk/ts/tsconfig.json b/sdk/ts/tsconfig.json new file mode 100644 index 0000000..004107a --- /dev/null +++ b/sdk/ts/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022", "DOM"], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "exactOptionalPropertyTypes": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + "isolatedModules": true, + "verbatimModuleSyntax": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "dist", + "rootDir": "src", + "skipLibCheck": true + }, + "include": ["src/**/*.ts"] +} diff --git a/sdk/ts/tsconfig.test.json b/sdk/ts/tsconfig.test.json new file mode 100644 index 0000000..5d9c3a6 --- /dev/null +++ b/sdk/ts/tsconfig.test.json @@ -0,0 +1,13 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "baseUrl": ".", + "noEmit": true, + "rootDir": ".", + "types": ["node", "vitest/globals"], + "paths": { + "@yellow-org/clearnet-sdk": ["./src/index.ts"] + } + }, + "include": ["src/**/*.ts", "test/**/*.ts", "examples/**/*.ts"] +} diff --git a/sdk/ts/vitest.config.ts b/sdk/ts/vitest.config.ts new file mode 100644 index 0000000..2fe0342 --- /dev/null +++ b/sdk/ts/vitest.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + globals: true, + restoreMocks: true, + testTimeout: 120_000, + }, +});