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
+
+
+
+
+
+
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,
+ },
+});