diff --git a/.github/workflows/test-ts.yml b/.github/workflows/test-ts.yml
index 4372330..f4472b7 100644
--- a/.github/workflows/test-ts.yml
+++ b/.github/workflows/test-ts.yml
@@ -36,6 +36,10 @@ jobs:
run: npm test
working-directory: sdk/ts
- - name: Build demo
+ - name: Build EVM demo
run: npm --workspace @yellow-org/evm-deposit-demo run build
working-directory: sdk/ts
+
+ - name: Build Solana demo
+ run: npm --workspace @yellow-org/solana-deposit-demo run build
+ working-directory: sdk/ts
diff --git a/Makefile b/Makefile
index 222927e..d0c312b 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-.PHONY: build lint test generate devnet devnet-evm devnet-down ts-deps integration
+.PHONY: build lint test generate devnet devnet-evm devnet-sol devnet-down ts-deps integration
build:
go build ./...
@@ -27,6 +27,10 @@ devnet-evm:
docker compose -f devnet/docker-compose.yml up -d anvil
go run ./devnet/wait --networks anvil
+devnet-sol:
+ docker compose -f devnet/docker-compose.yml up -d solana
+ go run ./devnet/wait --networks solana
+
devnet-down:
docker compose -f devnet/docker-compose.yml down -v
@@ -34,7 +38,8 @@ 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.
+# per chain; the TS suite covers EVM and Solana deposits. See devnet/README.md.
integration: ts-deps
go test -tags integration ./pkg/blockchain/... -v
npm --prefix sdk/ts run test:integration:evm
+ npm --prefix sdk/ts run test:integration:sol
diff --git a/devnet/README.md b/devnet/README.md
index eef03ea..721e2b4 100644
--- a/devnet/README.md
+++ b/devnet/README.md
@@ -7,15 +7,15 @@ 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.
+The TypeScript SDK integration tests live under `sdk/ts/test` and run through
+the same `make integration` target.
## Run
```sh
make devnet # anvil + bitcoind + rippled + solana-test-validator; blocks until all answer RPC
npm --prefix sdk/ts ci
-make integration # Go blockchain integrations + TS EVM integration
+make integration # Go blockchain integrations + TS EVM and Solana integration
make devnet-down
```
@@ -47,8 +47,20 @@ wallet, the XRPL genesis master).
authority), deposits native SOL, then runs the quorum withdrawal. The Config
PDA is a singleton, so the signer set is **fixed** across runs and only the
withdrawalID is fresh — re-runs stay clean without a validator restart. The
- validator image is multi-arch (no `platform:` pin — the Agave validator needs
- AVX, which isn't emulable on Apple silicon).
+ TypeScript Solana integration test creates and funds local signers, submits
+ native SOL and SPL deposits, and verifies each returned transaction reference.
+ The validator image is multi-arch (no `platform:` pin — the Agave validator
+ needs AVX, which isn't emulable on Apple silicon).
+
+For focused local iteration:
+
+```sh
+make devnet-evm
+npm --prefix sdk/ts run test:integration:evm
+
+make devnet-sol
+npm --prefix sdk/ts run test:integration:sol
+```
## Optional overrides
@@ -59,6 +71,7 @@ Defaults target the devnet; override the endpoints if pointing elsewhere:
| `EVM_RPC_URL` / `EVM_DEPLOYER_KEY` | `http://127.0.0.1:8545` / anvil account 0 |
| `BTC_RPC_URL` / `BTC_RPC_USER` / `BTC_RPC_PASS` | `http://127.0.0.1:18443` / `sdk` / `sdk` |
| `XRPL_RPC_URL` | `http://127.0.0.1:5005` |
+| `SOL_RPC_URL` | `http://127.0.0.1:8899` |
## Notes
diff --git a/sdk/ts/README.md b/sdk/ts/README.md
index 87f1c59..989f863 100644
--- a/sdk/ts/README.md
+++ b/sdk/ts/README.md
@@ -1,16 +1,17 @@
# 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.
+TypeScript SDK for Clearnet integration. This package currently exposes EVM and
+Solana vault depositors. EVM supports native ETH and ERC-20 deposits. Solana
+supports native SOL and SPL token 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.
+The package is ESM-first. EVM callers use `viem` clients and primitives. Solana
+callers provide an SDK-owned signer adapter around their wallet or local keypair.
## Install
```sh
-npm install @yellow-org/clearnet-sdk viem
+npm install @yellow-org/clearnet-sdk viem @solana/web3.js
```
For local development in this repository:
@@ -20,7 +21,7 @@ cd sdk/ts
npm ci
```
-## Quick Start
+## EVM Quick Start
Native ETH deposits use `EVM_NATIVE_ASSET`, which is the EVM zero address. Amounts
must be `bigint` values in base units.
@@ -123,6 +124,71 @@ before submitting the custody `deposit(...)` transaction. A successful
If an ERC-20 approval fails before the deposit is submitted, `error.txRef` may
refer to the approval transaction.
+## Solana Deposits
+
+Solana deposits use `SolanaVaultDepositor`. The SDK builds the custody
+instruction and delegates signing/broadcast to a caller-provided `SolanaSigner`.
+The signer boundary is small so browser-wallet, Wallet Standard, and local
+keypair adapters can live outside the core SDK.
+
+```ts
+import {
+ SOLANA_NATIVE_ASSET,
+ SolanaVaultDepositor,
+} from "@yellow-org/clearnet-sdk";
+import {
+ Connection,
+ Keypair,
+ LAMPORTS_PER_SOL,
+ sendAndConfirmTransaction,
+} from "@solana/web3.js";
+import type { SolanaSigner } from "@yellow-org/clearnet-sdk";
+
+const rpcUrl = "http://127.0.0.1:8899";
+const keypair = Keypair.generate();
+const connection = new Connection(rpcUrl, "confirmed");
+
+const airdrop = await connection.requestAirdrop(
+ keypair.publicKey,
+ LAMPORTS_PER_SOL,
+);
+await connection.confirmTransaction(airdrop, "confirmed");
+
+const signer: SolanaSigner = {
+ publicKey: keypair.publicKey.toBase58(),
+ async signAndSend(transaction) {
+ return sendAndConfirmTransaction(connection, transaction, [keypair], {
+ commitment: "confirmed",
+ preflightCommitment: "confirmed",
+ });
+ },
+};
+
+const depositor = new SolanaVaultDepositor({
+ rpcUrl,
+ signer,
+ commitment: "confirmed",
+});
+
+const ref = await depositor.submitDeposit({
+ destination: {
+ account: "00000000000000000000000000000000000000a1",
+ ref: "0x3333333333333333333333333333333333333333333333333333333333333333",
+ },
+ asset: SOLANA_NATIVE_ASSET,
+ amount: 100_000_000n,
+});
+
+console.log(ref.raw); // Solana base58 signature
+console.log(ref.hash); // 0x + sha256(signature bytes)
+console.log(await depositor.verifyDeposit(ref, 0));
+```
+
+Native asset aliases are `SOL`, `sol`, `native`, and an empty string. For SPL
+deposits, pass the mint public key as `asset` and the amount in token base units.
+The SDK does not mint tokens or create token accounts. SPL callers must ensure
+the depositor ATA and vault ATA exist before submitting the deposit.
+
## Deposit References
Pass `destination.ref` to attach a 32-byte opaque sub-account reference to the
@@ -139,8 +205,9 @@ const ref = await depositor.submitDeposit({
});
```
-For EVM, the reference is passed to `Custody.deposit(...)` as `bytes32`. The SDK
-does not interpret it.
+For EVM, the reference is passed to `Custody.deposit(...)` as `bytes32`. For
+Solana, it is encoded into `deposit_sol` or `deposit_spl` as `[u8; 32]`. The SDK
+does not interpret it. Omitted references are sent as 32 zero bytes.
## Verify A Deposit
@@ -157,7 +224,9 @@ const status = await depositor.verifyDeposit(ref, 1);
| `absent` | The transaction is unknown or has a reverted receipt. |
`minConfirmations` accepts a non-negative safe integer `number` or a non-negative
-`bigint`.
+`bigint`. EVM treats it as an inclusive receipt confirmation count. Solana maps
+it onto the commitment ladder: `0` accepts `confirmed`; `>= 1` requires
+`finalized`.
## API Reference
@@ -208,6 +277,34 @@ type TxRef = {
For EVM, `hash` and `raw` are both the transaction hash.
+### `SolanaVaultDepositor`
+
+```ts
+new SolanaVaultDepositor(config: SolanaDepositorConfig)
+```
+
+Config fields:
+
+| Field | Type | Notes |
+|---|---|---|
+| `rpcUrl` | `string` | Used for signature-status verification and commitment waits. |
+| `signer` | `SolanaSigner` | Provides `publicKey` and `signAndSend(transaction)`. |
+| `programId` | `string` | Optional. Must be the default custody program ID in this version. |
+| `commitment` | `"processed" \| "confirmed" \| "finalized"` | Optional; defaults to `finalized`. |
+| `receiptTimeoutMs` | `number` | Optional default timeout for commitment waits. |
+
+Solana input fields:
+
+| Field | Type | Notes |
+|---|---|---|
+| `destination.account` | `string` | 20-byte Clearnet account as hex, optional `0x`, or URI-like value whose final path segment is that hex. |
+| `destination.ref` | `` `0x${string}` \| undefined `` | Optional 32-byte opaque reference. |
+| `asset` | `string` | Native alias (`SOL`, `sol`, `native`, or empty string) or SPL mint public key. |
+| `amount` | `bigint` | Positive base-unit amount that fits in `uint64`. |
+
+For Solana, `TxRef.raw` is the base58 signature and `TxRef.hash` is `0x` plus
+the SHA-256 digest of the signature bytes.
+
### `verifyDeposit(ref, minConfirmations)`
Returns `Promise<"absent" | "pending" | "confirmed">`.
@@ -221,6 +318,7 @@ npm run typecheck
npm test
npm run build
npm --workspace @yellow-org/evm-deposit-demo run build
+npm --workspace @yellow-org/solana-deposit-demo run build
```
Run the EVM integration test against local Anvil:
@@ -239,7 +337,26 @@ 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:
+Run the Solana integration test against the local validator:
+
+```sh
+# From the repository root:
+make devnet-sol
+
+# From sdk/ts:
+npm run test:integration:sol
+
+# From the repository root:
+make devnet-down
+```
+
+The Solana devnet preloads the custody program at
+`98eVpih8X9CAcgU9bzNB9V7VtkRrnFZUmqzEnsq7cfmg`. The integration test creates
+and funds local signers, creates SPL token accounts needed for the test, submits
+native SOL and SPL deposits, and verifies each returned transaction reference.
+
+To run the repository integration suite, including the TS EVM and Solana
+integration tests:
```sh
# From the repository root:
@@ -254,9 +371,10 @@ Start the browser demo from `sdk/ts`:
```sh
npm run demo:evm
+npm run demo:sol
```
-The demo expects:
+The EVM demo expects:
- an EIP-1193 wallet, such as MetaMask
- an RPC URL and chain ID that match the wallet's selected network
@@ -266,19 +384,26 @@ The demo expects:
`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.
+The Solana demo discovers wallets through Wallet Standard, uses
+`solana:signTransaction`, and broadcasts the signed transaction through the
+configured RPC URL. The selected wallet chain must be one the wallet advertises,
+such as `solana:localnet` for a local validator. The local devnet preloads the
+custody program, but the wallet must be funded and SPL token accounts must
+already exist for SPL deposits.
+
## 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_ADDRESS` | EVM address, Solana public key, Solana mint, program ID, or Clearnet account is invalid. |
+| `INVALID_AMOUNT` | `amount` is not a positive `bigint` or exceeds the chain limit (`uint256` for EVM, `uint64` for Solana). |
| `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`. |
+| `INVALID_TX_REF` | `ref.hash` is not bytes32, or Solana `ref.raw` is not a 64-byte signature. |
+| `MISSING_WALLET_ACCOUNT` | The EVM wallet account is missing/mismatched, or the Solana signer is missing. |
+| `CHAIN_MISMATCH` | EVM only: 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. |
diff --git a/sdk/ts/examples/solana-deposit/index.html b/sdk/ts/examples/solana-deposit/index.html
new file mode 100644
index 0000000..86c0f14
--- /dev/null
+++ b/sdk/ts/examples/solana-deposit/index.html
@@ -0,0 +1,210 @@
+
+
+
+
+
+ Solana Deposit Demo
+
+
+
+
+ Solana Deposit Demo
+
+
+
+
+
+
diff --git a/sdk/ts/examples/solana-deposit/package.json b/sdk/ts/examples/solana-deposit/package.json
new file mode 100644
index 0000000..2a088b9
--- /dev/null
+++ b/sdk/ts/examples/solana-deposit/package.json
@@ -0,0 +1,22 @@
+{
+ "name": "@yellow-org/solana-deposit-demo",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite --host 127.0.0.1",
+ "build": "vite build"
+ },
+ "dependencies": {
+ "@solana/wallet-standard-features": "^1.4.0",
+ "@solana/web3.js": "^1.98.4",
+ "@wallet-standard/app": "^1.1.1",
+ "@wallet-standard/base": "^1.1.1",
+ "@wallet-standard/features": "^1.1.1",
+ "@yellow-org/clearnet-sdk": "file:../.."
+ },
+ "devDependencies": {
+ "typescript": "^5.9.0",
+ "vite": "^7.0.0"
+ }
+}
diff --git a/sdk/ts/examples/solana-deposit/src/main.ts b/sdk/ts/examples/solana-deposit/src/main.ts
new file mode 100644
index 0000000..70fb875
--- /dev/null
+++ b/sdk/ts/examples/solana-deposit/src/main.ts
@@ -0,0 +1,336 @@
+import {
+ SOLANA_CUSTODY_PROGRAM_ID,
+ SolanaVaultDepositor,
+} from "@yellow-org/clearnet-sdk";
+import {
+ SolanaSignTransaction,
+ type SolanaSignTransactionFeature,
+} from "@solana/wallet-standard-features";
+import { Connection, PublicKey, Transaction } from "@solana/web3.js";
+import { getWallets } from "@wallet-standard/app";
+import type {
+ Wallet,
+ WalletAccount,
+ WalletWithFeatures,
+} from "@wallet-standard/base";
+import {
+ StandardConnect,
+ type StandardConnectFeature,
+} from "@wallet-standard/features";
+import type {
+ SolanaCommitment,
+ SolanaSigner,
+ TxRef,
+} from "@yellow-org/clearnet-sdk";
+
+type SolanaWalletChain =
+ | "solana:localnet"
+ | "solana:devnet"
+ | "solana:testnet"
+ | "solana:mainnet";
+
+type StandardSolanaWallet = WalletWithFeatures<
+ StandardConnectFeature & SolanaSignTransactionFeature
+>;
+
+const form = mustElement("deposit-form");
+const connectButton = mustElement("connect");
+const submitButton = mustElement("submit");
+const verifyButton = mustElement("verify");
+const logOutput = mustElement("log");
+
+let signer: BrowserSolanaSigner | 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 Solana browser wallet to the configured RPC.");
+
+async function connectWallet(): Promise {
+ setBusy(connectButton, true);
+ try {
+ const chain = readWalletChain();
+ const rpcUrl = readInput("rpc-url");
+ const commitment = readCommitment();
+ const wallet = requireWallet(chain);
+ const result = await wallet.features[StandardConnect].connect();
+ const account = firstSupportedAccount(result.accounts, chain);
+ signer = new BrowserSolanaSigner(wallet, account, rpcUrl, chain, commitment);
+ const balance = await signer.balance();
+ writeLog(
+ `Connected ${wallet.name} ${account.address}\nWallet balance: ${balance} lamports`,
+ );
+ } catch (error) {
+ writeError(error);
+ } finally {
+ setBusy(connectButton, false);
+ }
+}
+
+async function submitDeposit(): Promise {
+ if (signer === undefined) {
+ await connectWallet();
+ }
+ if (signer === undefined) {
+ return;
+ }
+
+ setBusy(submitButton, true);
+ try {
+ signer.assertMatches(readInput("rpc-url"), readWalletChain(), readCommitment());
+ const ref = readOptional("reference");
+ const depositor = new SolanaVaultDepositor({
+ rpcUrl: signer.rpcUrl,
+ signer,
+ programId: readInput("program-id"),
+ commitment: signer.commitment,
+ });
+
+ lastRef = await depositor.submitDeposit(
+ {
+ destination: {
+ account: readInput("account"),
+ ...(ref === undefined ? {} : { ref: ref as `0x${string}` }),
+ },
+ asset: readInput("asset"),
+ amount: BigInt(readInput("amount")),
+ },
+ {
+ onSubmitted(ref) {
+ lastRef = ref;
+ verifyButton.disabled = false;
+ writeLog(`Submitted ${ref.raw}\nhash: ${ref.hash}`);
+ },
+ },
+ );
+ verifyButton.disabled = false;
+ writeLog(`Confirmed ${lastRef.raw}\nhash: ${lastRef.hash}`);
+ } catch (error) {
+ const txRef = errorTxRef(error);
+ writeError(error, txRef === undefined ? undefined : `Submitted ${txRef.raw}`);
+ } finally {
+ setBusy(submitButton, false);
+ }
+}
+
+async function verifyLastTx(): Promise {
+ if (lastRef === undefined || signer === undefined) {
+ return;
+ }
+
+ setBusy(verifyButton, true);
+ try {
+ signer.assertMatches(readInput("rpc-url"), readWalletChain(), readCommitment());
+ const depositor = new SolanaVaultDepositor({
+ rpcUrl: signer.rpcUrl,
+ signer,
+ programId: readInput("program-id"),
+ commitment: signer.commitment,
+ });
+
+ const status = await depositor.verifyDeposit(lastRef, 0);
+ writeLog(`Verify ${lastRef.raw}\nstatus: ${status}`);
+ } catch (error) {
+ writeError(error);
+ } finally {
+ setBusy(verifyButton, false);
+ }
+}
+
+class BrowserSolanaSigner implements SolanaSigner {
+ constructor(
+ private readonly wallet: StandardSolanaWallet,
+ private readonly account: WalletAccount,
+ readonly rpcUrl: string,
+ private readonly chain: SolanaWalletChain,
+ readonly commitment: SolanaCommitment,
+ ) {}
+
+ get publicKey(): string {
+ return this.account.address;
+ }
+
+ async balance(): Promise {
+ return await this.connection().getBalance(new PublicKey(this.publicKey));
+ }
+
+ assertMatches(
+ rpcUrl: string,
+ chain: SolanaWalletChain,
+ commitment: SolanaCommitment,
+ ): void {
+ if (
+ rpcUrl !== this.rpcUrl ||
+ chain !== this.chain ||
+ commitment !== this.commitment
+ ) {
+ throw new Error("network settings changed after wallet connection; reconnect wallet");
+ }
+ }
+
+ async signAndSend(transaction: Transaction): Promise {
+ const latest = await this.connection().getLatestBlockhash(this.commitment);
+ transaction.recentBlockhash = latest.blockhash;
+ transaction.feePayer ??= new PublicKey(this.publicKey);
+ const [result] = await this.wallet.features[SolanaSignTransaction].signTransaction({
+ account: this.account,
+ chain: this.chain,
+ transaction: transaction.serialize({
+ requireAllSignatures: false,
+ verifySignatures: false,
+ }),
+ options: {
+ preflightCommitment: this.commitment,
+ },
+ });
+ if (result?.signedTransaction === undefined) {
+ throw new Error("wallet did not return a signed transaction");
+ }
+ return await this.connection().sendRawTransaction(result.signedTransaction, {
+ preflightCommitment: this.commitment,
+ });
+ }
+
+ private connection(): Connection {
+ return new Connection(this.rpcUrl, this.commitment);
+ }
+}
+
+function readCommitment(): SolanaCommitment {
+ const value = readInput("commitment");
+ if (value !== "confirmed" && value !== "finalized") {
+ throw new Error("commitment must be confirmed or finalized");
+ }
+ return value;
+}
+
+function readWalletChain(): SolanaWalletChain {
+ const value = readInput("wallet-chain");
+ if (
+ value !== "solana:localnet" &&
+ value !== "solana:devnet" &&
+ value !== "solana:testnet" &&
+ value !== "solana:mainnet"
+ ) {
+ throw new Error("wallet chain must be a supported Solana chain");
+ }
+ return value;
+}
+
+function requireWallet(chain: SolanaWalletChain): StandardSolanaWallet {
+ const wallet = getWallets()
+ .get()
+ .find((wallet) => supportsRequiredFeatures(wallet, chain));
+ if (wallet === undefined) {
+ throw new Error(
+ `No Wallet Standard Solana wallet found for ${chain}`,
+ );
+ }
+ return wallet;
+}
+
+function supportsRequiredFeatures(
+ wallet: Wallet,
+ chain: SolanaWalletChain,
+): wallet is StandardSolanaWallet {
+ return (
+ wallet.chains.includes(chain) &&
+ StandardConnect in wallet.features &&
+ SolanaSignTransaction in wallet.features
+ );
+}
+
+function firstSupportedAccount(
+ accounts: readonly WalletAccount[],
+ chain: SolanaWalletChain,
+): WalletAccount {
+ const account = accounts.find(
+ (account) =>
+ account.chains.includes(chain) &&
+ account.features.includes(SolanaSignTransaction),
+ );
+ if (account === undefined) {
+ throw new Error(`wallet did not return an account for ${chain}`);
+ }
+ return account;
+}
+
+function errorTxRef(error: unknown): TxRef | undefined {
+ if (error && typeof error === "object" && "txRef" in error) {
+ return (error as { txRef?: TxRef }).txRef;
+ }
+ return undefined;
+}
+
+function writeError(error: unknown, prefix?: string): void {
+ const code = errorCode(error);
+ const codeText = code === undefined ? "" : ` [${String(code)}]`;
+ writeLog(
+ [prefix, `${codeText} ${errorMessage(error)}`.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 readInput(id: string): string {
+ return mustElement(id).value.trim();
+}
+
+function readOptional(id: string): string | undefined {
+ const value = readInput(id);
+ return value === "" ? undefined : value;
+}
+
+function setBusy(button: HTMLButtonElement, busy: boolean): void {
+ button.disabled = busy;
+}
+
+function writeLog(message: string): void {
+ logOutput.value = message;
+}
+
+function mustElement(id: string): T {
+ const element = document.getElementById(id);
+ if (element === null) {
+ throw new Error(`missing #${id}`);
+ }
+ return element as T;
+}
+
+if (readInput("program-id") === "") {
+ mustElement("program-id").value = SOLANA_CUSTODY_PROGRAM_ID;
+}
diff --git a/sdk/ts/examples/solana-deposit/tsconfig.json b/sdk/ts/examples/solana-deposit/tsconfig.json
new file mode 100644
index 0000000..b5062d4
--- /dev/null
+++ b/sdk/ts/examples/solana-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
index c4533de..3d3c90c 100644
--- a/sdk/ts/package-lock.json
+++ b/sdk/ts/package-lock.json
@@ -12,6 +12,10 @@
"examples/*"
],
"dependencies": {
+ "@noble/hashes": "^2.2.0",
+ "@solana/web3.js": "^1.98.4",
+ "bs58": "^6.0.0",
+ "buffer": "^6.0.3",
"viem": "^2.39.0"
},
"devDependencies": {
@@ -33,12 +37,37 @@
"vite": "^7.0.0"
}
},
+ "examples/solana-deposit": {
+ "name": "@yellow-org/solana-deposit-demo",
+ "version": "0.0.0",
+ "dependencies": {
+ "@solana/wallet-standard-features": "^1.4.0",
+ "@solana/web3.js": "^1.98.4",
+ "@wallet-standard/app": "^1.1.1",
+ "@wallet-standard/base": "^1.1.1",
+ "@wallet-standard/features": "^1.1.1",
+ "@yellow-org/clearnet-sdk": "file:../.."
+ },
+ "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/@babel/runtime": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz",
+ "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
"node_modules/@esbuild/aix-ppc64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz",
@@ -515,7 +544,7 @@
"url": "https://paulmillr.com/funding/"
}
},
- "node_modules/@noble/hashes": {
+ "node_modules/@noble/curves/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==",
@@ -527,6 +556,18 @@
"url": "https://paulmillr.com/funding/"
}
},
+ "node_modules/@noble/hashes": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz",
+ "integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 20.19.0"
+ },
+ "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",
@@ -939,6 +980,18 @@
"url": "https://paulmillr.com/funding/"
}
},
+ "node_modules/@scure/bip32/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/@scure/bip39": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.6.0.tgz",
@@ -952,6 +1005,146 @@
"url": "https://paulmillr.com/funding/"
}
},
+ "node_modules/@scure/bip39/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/@solana/buffer-layout": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@solana/buffer-layout/-/buffer-layout-4.0.1.tgz",
+ "integrity": "sha512-E1ImOIAD1tBZFRdjeM4/pzTiTApC0AOBGwyAMS4fwIodCWArzJ3DWdoh8cKxeFM2fElkxBh2Aqts1BPC373rHA==",
+ "license": "MIT",
+ "dependencies": {
+ "buffer": "~6.0.3"
+ },
+ "engines": {
+ "node": ">=5.10"
+ }
+ },
+ "node_modules/@solana/wallet-standard-features": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/@solana/wallet-standard-features/-/wallet-standard-features-1.4.0.tgz",
+ "integrity": "sha512-f0tAdqwM2aL6CiFbIgt9h5zKFp+mgY/iNGwoxPMTj9VSTeQj7d1GGSmWhZw0XWoZ4N/1tnKTKmYFq+Dyq08jRw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@wallet-standard/base": "^1.1.0",
+ "@wallet-standard/features": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=22"
+ }
+ },
+ "node_modules/@solana/web3.js": {
+ "version": "1.98.4",
+ "resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-1.98.4.tgz",
+ "integrity": "sha512-vv9lfnvjUsRiq//+j5pBdXig0IQdtzA0BRZ3bXEP4KaIyF1CcaydWqgyzQgfZMNIsWNWmG+AUHwPy4AHOD6gpw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.25.0",
+ "@noble/curves": "^1.4.2",
+ "@noble/hashes": "^1.4.0",
+ "@solana/buffer-layout": "^4.0.1",
+ "@solana/codecs-numbers": "^2.1.0",
+ "agentkeepalive": "^4.5.0",
+ "bn.js": "^5.2.1",
+ "borsh": "^0.7.0",
+ "bs58": "^4.0.1",
+ "buffer": "6.0.3",
+ "fast-stable-stringify": "^1.0.0",
+ "jayson": "^4.1.1",
+ "node-fetch": "^2.7.0",
+ "rpc-websockets": "^9.0.2",
+ "superstruct": "^2.0.2"
+ }
+ },
+ "node_modules/@solana/web3.js/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/@solana/web3.js/node_modules/@solana/codecs-core": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/@solana/codecs-core/-/codecs-core-2.3.0.tgz",
+ "integrity": "sha512-oG+VZzN6YhBHIoSKgS5ESM9VIGzhWjEHEGNPSibiDTxFhsFWxNaz8LbMDPjBUE69r9wmdGLkrQ+wVPbnJcZPvw==",
+ "license": "MIT",
+ "dependencies": {
+ "@solana/errors": "2.3.0"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.3.3"
+ }
+ },
+ "node_modules/@solana/web3.js/node_modules/@solana/codecs-numbers": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/@solana/codecs-numbers/-/codecs-numbers-2.3.0.tgz",
+ "integrity": "sha512-jFvvwKJKffvG7Iz9dmN51OGB7JBcy2CJ6Xf3NqD/VP90xak66m/Lg48T01u5IQ/hc15mChVHiBm+HHuOFDUrQg==",
+ "license": "MIT",
+ "dependencies": {
+ "@solana/codecs-core": "2.3.0",
+ "@solana/errors": "2.3.0"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.3.3"
+ }
+ },
+ "node_modules/@solana/web3.js/node_modules/@solana/errors": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/@solana/errors/-/errors-2.3.0.tgz",
+ "integrity": "sha512-66RI9MAbwYV0UtP7kGcTBVLxJgUxoZGm8Fbc0ah+lGiAw17Gugco6+9GrJCV83VyF2mDWyYnYM9qdI3yjgpnaQ==",
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "^5.4.1",
+ "commander": "^14.0.0"
+ },
+ "bin": {
+ "errors": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": ">=20.18.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.3.3"
+ }
+ },
+ "node_modules/@solana/web3.js/node_modules/base-x": {
+ "version": "3.0.11",
+ "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.11.tgz",
+ "integrity": "sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/@solana/web3.js/node_modules/bs58": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz",
+ "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==",
+ "license": "MIT",
+ "dependencies": {
+ "base-x": "^3.0.2"
+ }
+ },
"node_modules/@standard-schema/spec": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
@@ -959,6 +1152,15 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@swc/helpers": {
+ "version": "0.5.23",
+ "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz",
+ "integrity": "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.8.0"
+ }
+ },
"node_modules/@types/chai": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
@@ -970,6 +1172,15 @@
"assertion-error": "^2.0.1"
}
},
+ "node_modules/@types/connect": {
+ "version": "3.4.38",
+ "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
+ "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@types/deep-eql": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
@@ -988,12 +1199,26 @@
"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/@types/uuid": {
+ "version": "10.0.0",
+ "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
+ "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
+ "license": "MIT"
+ },
+ "node_modules/@types/ws": {
+ "version": "7.4.7",
+ "resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.4.7.tgz",
+ "integrity": "sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@vitest/expect": {
"version": "4.1.9",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.9.tgz",
@@ -1107,6 +1332,39 @@
"url": "https://opencollective.com/vitest"
}
},
+ "node_modules/@wallet-standard/app": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@wallet-standard/app/-/app-1.1.1.tgz",
+ "integrity": "sha512-WDGwoByhP5gwHH01r5EaLgQdLVkACPCdOMQhmhn8rsm10h/siSgTorShzBxrn0ExSPof+Lu+C3TfgqBrPa1xoQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@wallet-standard/base": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=22"
+ }
+ },
+ "node_modules/@wallet-standard/base": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@wallet-standard/base/-/base-1.1.1.tgz",
+ "integrity": "sha512-gggIHTtxicF9XFMQ12DkfS6NAG92Ak795JeSA7f2whAQ6Y3AkMWWuCMxSZXG2NIPN42kEaZSNVjqMsJRaJRxMQ==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=22"
+ }
+ },
+ "node_modules/@wallet-standard/features": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@wallet-standard/features/-/features-1.1.1.tgz",
+ "integrity": "sha512-aCWYmVeSCGViyEU5k7GMoW8zxE4Gs+C1s1Pp2XLesvSNlnZ4PMES9HUnTB3hl0b3RVj7C61yze3IWyrncqg4MA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@wallet-standard/base": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=22"
+ }
+ },
"node_modules/@yellow-org/clearnet-sdk": {
"resolved": "",
"link": true
@@ -1115,6 +1373,10 @@
"resolved": "examples/evm-deposit",
"link": true
},
+ "node_modules/@yellow-org/solana-deposit-demo": {
+ "resolved": "examples/solana-deposit",
+ "link": true
+ },
"node_modules/abitype": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/abitype/-/abitype-1.2.3.tgz",
@@ -1136,6 +1398,18 @@
}
}
},
+ "node_modules/agentkeepalive": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz",
+ "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==",
+ "license": "MIT",
+ "dependencies": {
+ "humanize-ms": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 8.0.0"
+ }
+ },
"node_modules/assertion-error": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
@@ -1146,6 +1420,114 @@
"node": ">=12"
}
},
+ "node_modules/base-x": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/base-x/-/base-x-5.0.1.tgz",
+ "integrity": "sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==",
+ "license": "MIT"
+ },
+ "node_modules/base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/bn.js": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.3.tgz",
+ "integrity": "sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w==",
+ "license": "MIT"
+ },
+ "node_modules/borsh": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/borsh/-/borsh-0.7.0.tgz",
+ "integrity": "sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "bn.js": "^5.2.0",
+ "bs58": "^4.0.0",
+ "text-encoding-utf-8": "^1.0.2"
+ }
+ },
+ "node_modules/borsh/node_modules/base-x": {
+ "version": "3.0.11",
+ "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.11.tgz",
+ "integrity": "sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/borsh/node_modules/bs58": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz",
+ "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==",
+ "license": "MIT",
+ "dependencies": {
+ "base-x": "^3.0.2"
+ }
+ },
+ "node_modules/bs58": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/bs58/-/bs58-6.0.0.tgz",
+ "integrity": "sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==",
+ "license": "MIT",
+ "dependencies": {
+ "base-x": "^5.0.0"
+ }
+ },
+ "node_modules/buffer": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
+ "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.2.1"
+ }
+ },
+ "node_modules/bufferutil": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.1.0.tgz",
+ "integrity": "sha512-ZMANVnAixE6AWWnPzlW2KpUrxhm9woycYvPOo67jWHyFowASTEd9s+QN1EIMsSDtwhIxN4sWE1jotpuDUIgyIw==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "node-gyp-build": "^4.3.0"
+ },
+ "engines": {
+ "node": ">=6.14.2"
+ }
+ },
"node_modules/chai": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
@@ -1156,6 +1538,27 @@
"node": ">=18"
}
},
+ "node_modules/chalk": {
+ "version": "5.6.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
+ "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
+ "license": "MIT",
+ "engines": {
+ "node": "^12.17.0 || ^14.13 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/commander": {
+ "version": "14.0.2",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.2.tgz",
+ "integrity": "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ }
+ },
"node_modules/convert-source-map": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
@@ -1163,6 +1566,18 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/delay": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/delay/-/delay-5.0.0.tgz",
+ "integrity": "sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/es-module-lexer": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz",
@@ -1170,6 +1585,21 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/es6-promise": {
+ "version": "4.2.8",
+ "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz",
+ "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==",
+ "license": "MIT"
+ },
+ "node_modules/es6-promisify": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz",
+ "integrity": "sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es6-promise": "^4.0.3"
+ }
+ },
"node_modules/esbuild": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz",
@@ -1238,6 +1668,20 @@
"node": ">=12.0.0"
}
},
+ "node_modules/eyes": {
+ "version": "0.1.8",
+ "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz",
+ "integrity": "sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==",
+ "engines": {
+ "node": "> 0.1.90"
+ }
+ },
+ "node_modules/fast-stable-stringify": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fast-stable-stringify/-/fast-stable-stringify-1.0.0.tgz",
+ "integrity": "sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag==",
+ "license": "MIT"
+ },
"node_modules/fdir": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
@@ -1271,6 +1715,44 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
+ "node_modules/humanize-ms": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
+ "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.0.0"
+ }
+ },
+ "node_modules/ieee754": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/isomorphic-ws": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz",
+ "integrity": "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==",
+ "license": "MIT",
+ "peerDependencies": {
+ "ws": "*"
+ }
+ },
"node_modules/isows": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/isows/-/isows-1.0.7.tgz",
@@ -1286,6 +1768,50 @@
"ws": "*"
}
},
+ "node_modules/jayson": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/jayson/-/jayson-4.3.0.tgz",
+ "integrity": "sha512-AauzHcUcqs8OBnCHOkJY280VaTiCm57AbuO7lqzcw7JapGj50BisE3xhksye4zlTSR1+1tAz67wLTl8tEH1obQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/connect": "^3.4.33",
+ "@types/node": "^12.12.54",
+ "@types/ws": "^7.4.4",
+ "commander": "^2.20.3",
+ "delay": "^5.0.0",
+ "es6-promisify": "^5.0.0",
+ "eyes": "^0.1.8",
+ "isomorphic-ws": "^4.0.1",
+ "json-stringify-safe": "^5.0.1",
+ "stream-json": "^1.9.1",
+ "uuid": "^8.3.2",
+ "ws": "^7.5.10"
+ },
+ "bin": {
+ "jayson": "bin/jayson.js"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/jayson/node_modules/@types/node": {
+ "version": "12.20.55",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz",
+ "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==",
+ "license": "MIT"
+ },
+ "node_modules/jayson/node_modules/commander": {
+ "version": "2.20.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
+ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
+ "license": "MIT"
+ },
+ "node_modules/json-stringify-safe": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
+ "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==",
+ "license": "ISC"
+ },
"node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
@@ -1296,6 +1822,12 @@
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
"node_modules/nanoid": {
"version": "3.3.12",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
@@ -1315,6 +1847,38 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
+ "node_modules/node-fetch": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
+ "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-url": "^5.0.0"
+ },
+ "engines": {
+ "node": "4.x || >=6.0.0"
+ },
+ "peerDependencies": {
+ "encoding": "^0.1.0"
+ },
+ "peerDependenciesMeta": {
+ "encoding": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/node-gyp-build": {
+ "version": "4.8.4",
+ "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
+ "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
+ "license": "MIT",
+ "optional": true,
+ "bin": {
+ "node-gyp-build": "bin.js",
+ "node-gyp-build-optional": "optional.js",
+ "node-gyp-build-test": "build-test.js"
+ }
+ },
"node_modules/obug": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz",
@@ -1359,6 +1923,18 @@
}
}
},
+ "node_modules/ox/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/pathe": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
@@ -1460,6 +2036,71 @@
"fsevents": "~2.3.2"
}
},
+ "node_modules/rpc-websockets": {
+ "version": "9.3.9",
+ "resolved": "https://registry.npmjs.org/rpc-websockets/-/rpc-websockets-9.3.9.tgz",
+ "integrity": "sha512-2iQDaTB4g5fDB2ihrTFSJSibCEuxaRi1q7qTW7ZO9/M5/TC+ToHA4D9/ffNLEbAoHNNrcdeP05oATNk44SKZXA==",
+ "license": "LGPL-3.0-only",
+ "dependencies": {
+ "@swc/helpers": "^0.5.11",
+ "@types/uuid": "^10.0.0",
+ "@types/ws": "^8.2.2",
+ "buffer": "^6.0.3",
+ "eventemitter3": "^5.0.1",
+ "uuid": "^14.0.0",
+ "ws": "^8.5.0"
+ },
+ "funding": {
+ "type": "paypal",
+ "url": "https://paypal.me/kozjak"
+ },
+ "optionalDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": "^6.0.0"
+ }
+ },
+ "node_modules/rpc-websockets/node_modules/@types/ws": {
+ "version": "8.18.1",
+ "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
+ "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/rpc-websockets/node_modules/uuid": {
+ "version": "14.0.1",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.1.tgz",
+ "integrity": "sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==",
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
+ "license": "MIT",
+ "bin": {
+ "uuid": "dist-node/bin/uuid"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
"node_modules/siginfo": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
@@ -1491,6 +2132,35 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/stream-chain": {
+ "version": "2.2.5",
+ "resolved": "https://registry.npmjs.org/stream-chain/-/stream-chain-2.2.5.tgz",
+ "integrity": "sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/stream-json": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/stream-json/-/stream-json-1.9.1.tgz",
+ "integrity": "sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "stream-chain": "^2.2.5"
+ }
+ },
+ "node_modules/superstruct": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-2.0.2.tgz",
+ "integrity": "sha512-uV+TFRZdXsqXTL2pRvujROjdZQ4RAlBUS5BTh9IGm+jTqQntYThciG/qu57Gs69yjnVUSqdxF9YLmSnpupBW9A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/text-encoding-utf-8": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz",
+ "integrity": "sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg=="
+ },
"node_modules/tinybench": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
@@ -1535,11 +2205,22 @@
"node": ">=14.0.0"
}
},
+ "node_modules/tr46": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
+ "license": "MIT"
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD"
+ },
"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",
@@ -1553,9 +2234,35 @@
"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/utf-8-validate": {
+ "version": "6.0.6",
+ "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-6.0.6.tgz",
+ "integrity": "sha512-q3l3P9UtEEiAHcsgsqTgf9PPjctrDWoIXW3NpOHFdRDbLvu4DLIcxHangJ4RLrWkBcKjmcs/6NkerI8T/rE4LA==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "node-gyp-build": "^4.3.0"
+ },
+ "engines": {
+ "node": ">=6.14.2"
+ }
+ },
+ "node_modules/uuid": {
+ "version": "11.1.1",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz",
+ "integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==",
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
+ "license": "MIT",
+ "bin": {
+ "uuid": "dist/esm/bin/uuid"
+ }
+ },
"node_modules/viem": {
"version": "2.52.2",
"resolved": "https://registry.npmjs.org/viem/-/viem-2.52.2.tgz",
@@ -1586,6 +2293,18 @@
}
}
},
+ "node_modules/viem/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/vite": {
"version": "7.3.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.5.tgz",
@@ -1751,6 +2470,22 @@
}
}
},
+ "node_modules/webidl-conversions": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/whatwg-url": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+ "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
+ "license": "MIT",
+ "dependencies": {
+ "tr46": "~0.0.3",
+ "webidl-conversions": "^3.0.0"
+ }
+ },
"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",
diff --git a/sdk/ts/package.json b/sdk/ts/package.json
index e355521..35e1b2f 100644
--- a/sdk/ts/package.json
+++ b/sdk/ts/package.json
@@ -25,16 +25,27 @@
"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": "npm run test:evm && npm run test:sol",
+ "test:evm": "vitest run test/blockchain/evm/depositor.test.ts",
+ "test:sol": "vitest run test/blockchain/sol/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"
+ "test:integration:sol": "vitest run test/blockchain/sol/depositor.integration.test.ts",
+ "demo:evm": "npm --workspace @yellow-org/evm-deposit-demo run dev",
+ "demo:sol": "npm --workspace @yellow-org/solana-deposit-demo run dev"
},
"dependencies": {
+ "@noble/hashes": "^2.2.0",
+ "@solana/web3.js": "^1.98.4",
+ "bs58": "^6.0.0",
+ "buffer": "^6.0.3",
"viem": "^2.39.0"
},
"overrides": {
"esbuild": "^0.28.1",
- "ws": "^8.21.0"
+ "ws": "^8.21.0",
+ "jayson": {
+ "uuid": "11.1.1"
+ }
},
"devDependencies": {
"@types/node": "^24.0.0",
diff --git a/sdk/ts/src/blockchain/sol/constants.ts b/sdk/ts/src/blockchain/sol/constants.ts
new file mode 100644
index 0000000..59bf201
--- /dev/null
+++ b/sdk/ts/src/blockchain/sol/constants.ts
@@ -0,0 +1,31 @@
+export const SOLANA_CUSTODY_PROGRAM_ID =
+ "98eVpih8X9CAcgU9bzNB9V7VtkRrnFZUmqzEnsq7cfmg";
+
+export const SOLANA_NATIVE_ASSET = "SOL";
+
+export const SOLANA_SYSTEM_PROGRAM_ID =
+ "11111111111111111111111111111111";
+
+export const SOLANA_TOKEN_PROGRAM_ID =
+ "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
+
+export const SOLANA_ASSOCIATED_TOKEN_PROGRAM_ID =
+ "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL";
+
+export const DEFAULT_SOLANA_COMMITMENT = "finalized";
+
+export const DEFAULT_RECEIPT_TIMEOUT_MS = 60_000;
+
+export const POLL_INTERVAL_MS = 250;
+
+export const DEPOSIT_SOL_DISCRIMINATOR = [
+ 108, 81, 78, 117, 125, 155, 56, 200,
+] as const;
+
+export const DEPOSIT_SPL_DISCRIMINATOR = [
+ 224, 0, 198, 175, 198, 47, 105, 204,
+] as const;
+
+export const DEPOSITED_EVENT_DISCRIMINATOR = [
+ 111, 141, 26, 45, 161, 35, 100, 57,
+] as const;
diff --git a/sdk/ts/src/blockchain/sol/depositor.ts b/sdk/ts/src/blockchain/sol/depositor.ts
new file mode 100644
index 0000000..f726506
--- /dev/null
+++ b/sdk/ts/src/blockchain/sol/depositor.ts
@@ -0,0 +1,477 @@
+import bs58 from "bs58";
+import { sha256 } from "@noble/hashes/sha2.js";
+import {
+ Connection,
+ PublicKey,
+ SystemProgram,
+ Transaction,
+ TransactionInstruction,
+} from "@solana/web3.js";
+
+import { ClearnetSdkError } from "../../core/errors.js";
+import type {
+ DepositStatus,
+ SubmitDepositOptions,
+ TxRef,
+ VaultDepositor,
+} from "../../core/types.js";
+import {
+ DEFAULT_RECEIPT_TIMEOUT_MS,
+ DEPOSIT_SOL_DISCRIMINATOR,
+ DEPOSIT_SPL_DISCRIMINATOR,
+ POLL_INTERVAL_MS,
+ SOLANA_ASSOCIATED_TOKEN_PROGRAM_ID,
+ SOLANA_CUSTODY_PROGRAM_ID,
+ SOLANA_TOKEN_PROGRAM_ID,
+} from "./constants.js";
+import { encodeDepositData } from "./encoding.js";
+import type {
+ SolanaCommitment,
+ SolanaDepositorConfig,
+ SolanaSubmitDepositInput,
+ SolanaSigner,
+} from "./types.js";
+import {
+ bytes32Hex,
+ normalizeCommitment,
+ normalizeMinConfirmations,
+ publicKeyFromString,
+ requireAmount,
+ requireClearnetAccount,
+ requireDepositDestination,
+ requireProgramId,
+ requireReceiptTimeout,
+ requireReference,
+ requireRpcUrl,
+ requireSigner,
+ requireTxRef,
+ resolveMint,
+} from "./validation.js";
+
+type SignatureStatusValue = Awaited<
+ ReturnType
+>["value"][number];
+
+const SOLANA_CUSTODY_PUBLIC_KEY = new PublicKey(SOLANA_CUSTODY_PROGRAM_ID);
+const SOLANA_TOKEN_PROGRAM_PUBLIC_KEY = new PublicKey(SOLANA_TOKEN_PROGRAM_ID);
+const SOLANA_ASSOCIATED_TOKEN_PROGRAM_PUBLIC_KEY = new PublicKey(
+ SOLANA_ASSOCIATED_TOKEN_PROGRAM_ID,
+);
+const VAULT_SEED = new TextEncoder().encode("vault");
+const EVENT_AUTHORITY_SEED = new TextEncoder().encode("__event_authority");
+
+export class SolanaVaultDepositor
+ implements VaultDepositor
+{
+ private readonly signer: SolanaSigner;
+ private readonly depositor: PublicKey;
+ private readonly programId: PublicKey;
+ private readonly commitment: SolanaCommitment;
+ private readonly receiptTimeoutMs: number;
+ private readonly connection: Connection;
+ private readonly vault: PublicKey;
+ private readonly eventAuthority: PublicKey;
+
+ constructor(config: SolanaDepositorConfig) {
+ const rpcUrl = requireRpcUrl(config.rpcUrl);
+ this.signer = requireSigner(config.signer);
+ this.depositor = publicKeyFromString(this.signer.publicKey, "signer.publicKey");
+ this.programId = requireProgramId(config.programId);
+ this.vault = vaultPda(this.programId);
+ this.eventAuthority = eventAuthorityPda(this.programId);
+ this.commitment = normalizeCommitment(config.commitment);
+ this.receiptTimeoutMs =
+ config.receiptTimeoutMs === undefined
+ ? DEFAULT_RECEIPT_TIMEOUT_MS
+ : requireReceiptTimeout(config.receiptTimeoutMs);
+ this.connection = new Connection(rpcUrl, {
+ commitment: this.commitment,
+ fetch: (input, init) => globalThis.fetch(input, init),
+ });
+ }
+
+ async submitDeposit(
+ input: SolanaSubmitDepositInput,
+ options: SubmitDepositOptions = {},
+ ): Promise {
+ const waitOptions = requireSubmitDepositOptions(options);
+ const fields =
+ input && typeof input === "object"
+ ? (input as Partial)
+ : {};
+ const destination = requireDepositDestination(fields.destination);
+ const account = requireClearnetAccount(destination.account);
+ const reference = requireReference(destination.ref);
+ const amount = requireAmount(fields.amount);
+ const mint = resolveMint(fields.asset);
+ validateWaitOptions(waitOptions);
+ const transaction = new Transaction();
+ transaction.feePayer = this.depositor;
+ transaction.add(
+ mint === undefined
+ ? this.depositSolInstruction(account, reference, amount)
+ : this.depositSplInstruction(mint, account, reference, amount),
+ );
+
+ const signature = await this.signAndSend(transaction);
+ const ref = txRef(signature);
+ waitOptions.onSubmitted?.(ref);
+ await this.waitForCommitment(signature, ref, waitOptions);
+ return ref;
+ }
+
+ async verifyDeposit(
+ ref: TxRef,
+ minConfirmations: bigint | number,
+ ): Promise {
+ requireTxRef(ref);
+ const minConf = normalizeMinConfirmations(minConfirmations);
+ const status = await this.getSignatureStatus(ref.raw, ref);
+ return mapStatus(status, minConf);
+ }
+
+ private depositSolInstruction(
+ account: Uint8Array,
+ reference: Uint8Array,
+ amount: bigint,
+ ): TransactionInstruction {
+ return new TransactionInstruction({
+ programId: this.programId,
+ keys: [
+ { pubkey: this.depositor, isSigner: true, isWritable: true },
+ { pubkey: this.vault, isSigner: false, isWritable: true },
+ { pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
+ {
+ pubkey: this.eventAuthority,
+ isSigner: false,
+ isWritable: false,
+ },
+ { pubkey: this.programId, isSigner: false, isWritable: false },
+ ],
+ data: encodeDepositData(
+ DEPOSIT_SOL_DISCRIMINATOR,
+ account,
+ reference,
+ amount,
+ ),
+ });
+ }
+
+ private depositSplInstruction(
+ mint: PublicKey,
+ account: Uint8Array,
+ reference: Uint8Array,
+ amount: bigint,
+ ): TransactionInstruction {
+ return new TransactionInstruction({
+ programId: this.programId,
+ keys: [
+ { pubkey: this.depositor, isSigner: true, isWritable: true },
+ { pubkey: mint, isSigner: false, isWritable: false },
+ {
+ pubkey: associatedTokenAddress(this.depositor, mint),
+ isSigner: false,
+ isWritable: true,
+ },
+ { pubkey: this.vault, isSigner: false, isWritable: false },
+ {
+ pubkey: associatedTokenAddress(this.vault, mint),
+ isSigner: false,
+ isWritable: true,
+ },
+ {
+ pubkey: SOLANA_TOKEN_PROGRAM_PUBLIC_KEY,
+ isSigner: false,
+ isWritable: false,
+ },
+ {
+ pubkey: SOLANA_ASSOCIATED_TOKEN_PROGRAM_PUBLIC_KEY,
+ isSigner: false,
+ isWritable: false,
+ },
+ {
+ pubkey: this.eventAuthority,
+ isSigner: false,
+ isWritable: false,
+ },
+ { pubkey: this.programId, isSigner: false, isWritable: false },
+ ],
+ data: encodeDepositData(
+ DEPOSIT_SPL_DISCRIMINATOR,
+ account,
+ reference,
+ amount,
+ ),
+ });
+ }
+
+ private async signAndSend(transaction: Transaction): Promise {
+ try {
+ return await this.signer.signAndSend(transaction);
+ } catch (error) {
+ if (error instanceof ClearnetSdkError) {
+ throw error;
+ }
+ throw new ClearnetSdkError("RPC_ERROR", "sol: sign and send", {
+ cause: error,
+ });
+ }
+ }
+
+ private async waitForCommitment(
+ signature: string,
+ ref: TxRef,
+ options: SubmitDepositOptions,
+ ): Promise {
+ const timeoutMs = requireReceiptTimeout(
+ options.receiptTimeoutMs ?? this.receiptTimeoutMs,
+ );
+ const deadline = Date.now() + timeoutMs;
+ for (;;) {
+ if (options.signal?.aborted === true) {
+ throw new ClearnetSdkError("RECEIPT_TIMEOUT", "sol: receipt aborted", {
+ txRef: ref,
+ });
+ }
+ const status = await waitWithControls(
+ () => this.getSignatureStatus(signature, ref),
+ remainingMs(deadline, ref),
+ options.signal,
+ ref,
+ );
+ if (status?.err != null) {
+ throw new ClearnetSdkError("TX_REVERTED", "sol: transaction failed", {
+ txRef: ref,
+ });
+ }
+ if (statusSatisfiesCommitment(status, this.commitment)) {
+ return;
+ }
+ if (Date.now() >= deadline) {
+ throw new ClearnetSdkError("RECEIPT_TIMEOUT", "sol: receipt timeout", {
+ txRef: ref,
+ });
+ }
+ await sleep(Math.min(POLL_INTERVAL_MS, remainingMs(deadline, ref)), options.signal, ref);
+ }
+ }
+
+ private async getSignatureStatus(
+ signature: string,
+ txRef: TxRef | undefined,
+ ): Promise {
+ try {
+ const out = await this.connection.getSignatureStatuses([signature], {
+ searchTransactionHistory: true,
+ });
+ return out.value[0] ?? null;
+ } catch (error) {
+ throw new ClearnetSdkError(
+ "RPC_ERROR",
+ "sol: signature status",
+ txRef === undefined ? { cause: error } : { txRef, cause: error },
+ );
+ }
+ }
+}
+
+export function vaultPda(programId = SOLANA_CUSTODY_PUBLIC_KEY): PublicKey {
+ return PublicKey.findProgramAddressSync([VAULT_SEED], programId)[0];
+}
+
+export function eventAuthorityPda(
+ programId = SOLANA_CUSTODY_PUBLIC_KEY,
+): PublicKey {
+ return PublicKey.findProgramAddressSync([EVENT_AUTHORITY_SEED], programId)[0];
+}
+
+function associatedTokenAddress(owner: PublicKey, mint: PublicKey): PublicKey {
+ return PublicKey.findProgramAddressSync(
+ [
+ owner.toBytes(),
+ SOLANA_TOKEN_PROGRAM_PUBLIC_KEY.toBytes(),
+ mint.toBytes(),
+ ],
+ SOLANA_ASSOCIATED_TOKEN_PROGRAM_PUBLIC_KEY,
+ )[0];
+}
+
+function txRef(signature: string): TxRef {
+ let signatureBytes: Uint8Array;
+ try {
+ signatureBytes = bs58.decode(signature);
+ } catch (error) {
+ throw new ClearnetSdkError("INVALID_TX_REF", "Solana signature must be base58", {
+ cause: error,
+ });
+ }
+ if (signatureBytes.length !== 64) {
+ throw new ClearnetSdkError(
+ "INVALID_TX_REF",
+ "Solana signature must decode to 64 bytes",
+ );
+ }
+ return { hash: bytes32Hex(sha256(signatureBytes)), raw: signature };
+}
+
+function mapStatus(
+ status: SignatureStatusValue,
+ minConfirmations: bigint,
+): DepositStatus {
+ if (status == null || status.err != null) {
+ return "absent";
+ }
+ switch (status.confirmationStatus) {
+ case "finalized":
+ return "confirmed";
+ case "confirmed":
+ return minConfirmations === 0n ? "confirmed" : "pending";
+ default:
+ return "pending";
+ }
+}
+
+function statusSatisfiesCommitment(
+ status: SignatureStatusValue,
+ commitment: SolanaCommitment,
+): boolean {
+ if (status == null || status.err != null) {
+ return false;
+ }
+ if (commitment === "processed") {
+ return true;
+ }
+ if (commitment === "confirmed") {
+ return (
+ status.confirmationStatus === "confirmed" ||
+ status.confirmationStatus === "finalized"
+ );
+ }
+ return status.confirmationStatus === "finalized";
+}
+
+function validateWaitOptions(options: SubmitDepositOptions): void {
+ if (options.receiptTimeoutMs !== undefined) {
+ requireReceiptTimeout(options.receiptTimeoutMs);
+ }
+ if (options.signal?.aborted === true) {
+ throw new ClearnetSdkError("RECEIPT_TIMEOUT", "sol: receipt aborted");
+ }
+}
+
+function requireSubmitDepositOptions(options: unknown): SubmitDepositOptions {
+ if (options === null || typeof options !== "object") {
+ throw new ClearnetSdkError(
+ "RECEIPT_TIMEOUT",
+ "submit options must be an object",
+ );
+ }
+ return options;
+}
+
+function remainingMs(deadline: number, ref: TxRef): number {
+ const remaining = deadline - Date.now();
+ if (remaining <= 0) {
+ throw new ClearnetSdkError("RECEIPT_TIMEOUT", "sol: receipt timeout", {
+ txRef: ref,
+ });
+ }
+ return remaining;
+}
+
+async function waitWithControls(
+ wait: () => Promise,
+ timeoutMs: number,
+ signal: AbortSignal | undefined,
+ ref: TxRef,
+): Promise {
+ if (signal?.aborted === true) {
+ throw new ClearnetSdkError("RECEIPT_TIMEOUT", "sol: receipt aborted", {
+ txRef: ref,
+ });
+ }
+
+ let timeoutId: ReturnType | undefined;
+ let abortHandler: (() => void) | undefined;
+
+ const timeoutPromise = new Promise((_, reject) => {
+ timeoutId = setTimeout(() => {
+ reject(
+ new ClearnetSdkError("RECEIPT_TIMEOUT", "sol: receipt timeout", {
+ txRef: ref,
+ }),
+ );
+ }, timeoutMs);
+ });
+
+ const abortPromise =
+ signal === undefined
+ ? undefined
+ : new Promise((_, reject) => {
+ abortHandler = () => {
+ reject(
+ new ClearnetSdkError("RECEIPT_TIMEOUT", "sol: receipt aborted", {
+ 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);
+ }
+ }
+}
+
+async function sleep(
+ ms: number,
+ signal: AbortSignal | undefined,
+ txRef: TxRef,
+): Promise {
+ await new Promise((resolve, reject) => {
+ if (signal?.aborted === true) {
+ reject(
+ new ClearnetSdkError("RECEIPT_TIMEOUT", "sol: receipt aborted", {
+ txRef,
+ }),
+ );
+ return;
+ }
+ let abortHandler: (() => void) | undefined;
+ let timeout: ReturnType | undefined;
+ const cleanup = () => {
+ if (timeout !== undefined) {
+ clearTimeout(timeout);
+ }
+ if (signal !== undefined && abortHandler !== undefined) {
+ signal.removeEventListener("abort", abortHandler);
+ }
+ };
+ timeout = setTimeout(() => {
+ cleanup();
+ resolve();
+ }, ms);
+ if (signal !== undefined) {
+ abortHandler = () => {
+ cleanup();
+ reject(
+ new ClearnetSdkError("RECEIPT_TIMEOUT", "sol: receipt aborted", {
+ txRef,
+ }),
+ );
+ };
+ signal.addEventListener("abort", abortHandler, { once: true });
+ }
+ });
+}
diff --git a/sdk/ts/src/blockchain/sol/encoding.ts b/sdk/ts/src/blockchain/sol/encoding.ts
new file mode 100644
index 0000000..ddf8899
--- /dev/null
+++ b/sdk/ts/src/blockchain/sol/encoding.ts
@@ -0,0 +1,15 @@
+import { Buffer } from "buffer";
+
+export function encodeDepositData(
+ discriminator: readonly number[],
+ account: Uint8Array,
+ reference: Uint8Array,
+ amount: bigint,
+): Buffer {
+ const data = new Uint8Array(8 + 20 + 32 + 8);
+ data.set(discriminator, 0);
+ data.set(account, 8);
+ data.set(reference, 28);
+ new DataView(data.buffer).setBigUint64(60, amount, true);
+ return Buffer.from(data);
+}
diff --git a/sdk/ts/src/blockchain/sol/index.ts b/sdk/ts/src/blockchain/sol/index.ts
new file mode 100644
index 0000000..1100ddc
--- /dev/null
+++ b/sdk/ts/src/blockchain/sol/index.ts
@@ -0,0 +1,17 @@
+export {
+ eventAuthorityPda,
+ SolanaVaultDepositor,
+ vaultPda,
+} from "./depositor.js";
+export {
+ SOLANA_CUSTODY_PROGRAM_ID,
+ SOLANA_NATIVE_ASSET,
+} from "./constants.js";
+export type {
+ SolanaAsset,
+ SolanaCommitment,
+ SolanaDepositDestination,
+ SolanaDepositorConfig,
+ SolanaSigner,
+ SolanaSubmitDepositInput,
+} from "./types.js";
diff --git a/sdk/ts/src/blockchain/sol/types.ts b/sdk/ts/src/blockchain/sol/types.ts
new file mode 100644
index 0000000..94e4c45
--- /dev/null
+++ b/sdk/ts/src/blockchain/sol/types.ts
@@ -0,0 +1,41 @@
+import type { Transaction } from "@solana/web3.js";
+
+import type {
+ Bytes32Hex,
+ DepositDestination,
+ SubmitDepositInput,
+} from "../../core/types.js";
+
+export type SolanaAsset = string;
+
+export type SolanaCommitment = "processed" | "confirmed" | "finalized";
+
+export interface SolanaDepositDestination extends DepositDestination {
+ account: string;
+ ref?: Bytes32Hex;
+}
+
+export interface SolanaSubmitDepositInput extends SubmitDepositInput {
+ asset: SolanaAsset;
+ amount: bigint;
+ destination: SolanaDepositDestination;
+}
+
+export interface SolanaSigner {
+ publicKey: string;
+ /**
+ * Signs and submits a @solana/web3.js v1 Transaction.
+ *
+ * Implementations must set transaction.recentBlockhash, usually from
+ * getLatestBlockhash, before signing.
+ */
+ signAndSend(transaction: Transaction): Promise;
+}
+
+export interface SolanaDepositorConfig {
+ rpcUrl: string;
+ signer: SolanaSigner;
+ programId?: string;
+ commitment?: SolanaCommitment;
+ receiptTimeoutMs?: number;
+}
diff --git a/sdk/ts/src/blockchain/sol/validation.ts b/sdk/ts/src/blockchain/sol/validation.ts
new file mode 100644
index 0000000..ac0361b
--- /dev/null
+++ b/sdk/ts/src/blockchain/sol/validation.ts
@@ -0,0 +1,245 @@
+import { Buffer } from "buffer";
+
+import bs58 from "bs58";
+import { sha256 } from "@noble/hashes/sha2.js";
+import { PublicKey } from "@solana/web3.js";
+
+import { ClearnetSdkError } from "../../core/errors.js";
+import type { Bytes32Hex, DepositDestination, TxRef } from "../../core/types.js";
+import {
+ DEFAULT_SOLANA_COMMITMENT,
+ SOLANA_CUSTODY_PROGRAM_ID,
+} from "./constants.js";
+import type { SolanaCommitment, SolanaSigner } from "./types.js";
+
+const UINT64_MAX = (1n << 64n) - 1n;
+const BYTES32_HEX_PATTERN = /^0x[a-fA-F0-9]{64}$/;
+
+export function requireRpcUrl(rpcUrl: unknown): string {
+ if (typeof rpcUrl !== "string" || rpcUrl.trim() === "") {
+ throw new ClearnetSdkError("RPC_ERROR", "rpcUrl is required");
+ }
+ return rpcUrl;
+}
+
+export function requireSigner(signer: unknown): SolanaSigner {
+ if (!signer || typeof signer !== "object") {
+ throw new ClearnetSdkError(
+ "MISSING_WALLET_ACCOUNT",
+ "Solana signer is required",
+ );
+ }
+ const candidate = signer as Partial;
+ publicKeyFromString(candidate.publicKey, "signer.publicKey");
+ if (typeof candidate.signAndSend !== "function") {
+ throw new ClearnetSdkError(
+ "MISSING_WALLET_ACCOUNT",
+ "Solana signer.signAndSend is required",
+ );
+ }
+ return candidate as SolanaSigner;
+}
+
+export function requireProgramId(value: unknown): PublicKey {
+ const programId =
+ value === undefined
+ ? new PublicKey(SOLANA_CUSTODY_PROGRAM_ID)
+ : publicKeyFromString(value, "programId");
+ if (programId.toBase58() !== SOLANA_CUSTODY_PROGRAM_ID) {
+ throw new ClearnetSdkError(
+ "INVALID_ADDRESS",
+ `programId must be ${SOLANA_CUSTODY_PROGRAM_ID}`,
+ );
+ }
+ return programId;
+}
+
+export function normalizeCommitment(
+ commitment: SolanaCommitment | undefined,
+): SolanaCommitment {
+ if (commitment === undefined) {
+ return DEFAULT_SOLANA_COMMITMENT;
+ }
+ if (
+ commitment !== "processed" &&
+ commitment !== "confirmed" &&
+ commitment !== "finalized"
+ ) {
+ throw new ClearnetSdkError(
+ "RPC_ERROR",
+ "commitment must be processed, confirmed, or finalized",
+ );
+ }
+ return commitment;
+}
+
+export function requireReceiptTimeout(value: number): number {
+ if (!Number.isSafeInteger(value) || value <= 0) {
+ throw new ClearnetSdkError(
+ "RECEIPT_TIMEOUT",
+ "receiptTimeoutMs must be a positive safe integer",
+ );
+ }
+ return value;
+}
+
+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 > UINT64_MAX) {
+ throw new ClearnetSdkError("INVALID_AMOUNT", "amount must fit in uint64");
+ }
+ return amount;
+}
+
+export function requireDepositDestination(
+ destination: unknown,
+): DepositDestination {
+ if (!destination || typeof destination !== "object") {
+ throw new ClearnetSdkError(
+ "INVALID_ADDRESS",
+ "destination.account must be a 20-byte hex address",
+ );
+ }
+ return destination as DepositDestination;
+}
+
+export function requireClearnetAccount(account: unknown): Uint8Array {
+ if (typeof account !== "string") {
+ throw new ClearnetSdkError(
+ "INVALID_ADDRESS",
+ "destination.account must be a 20-byte hex address",
+ );
+ }
+ const segment = account.slice(account.lastIndexOf("/") + 1);
+ const hex = segment.toLowerCase().replace(/^0x/, "");
+ if (!/^[a-f0-9]+$/.test(hex) || hex.length !== 40) {
+ throw new ClearnetSdkError(
+ "INVALID_ADDRESS",
+ "destination.account must be a 20-byte hex address",
+ );
+ }
+ return Uint8Array.from(Buffer.from(hex, "hex"));
+}
+
+export function requireReference(reference: unknown): Uint8Array {
+ if (reference === undefined || reference === "") {
+ return new Uint8Array(32);
+ }
+ if (typeof reference !== "string" || !BYTES32_HEX_PATTERN.test(reference)) {
+ throw new ClearnetSdkError(
+ "INVALID_REFERENCE",
+ "destination.ref must be a 32-byte hex value",
+ );
+ }
+ return Uint8Array.from(Buffer.from(reference.slice(2), "hex"));
+}
+
+export function resolveMint(asset: unknown): PublicKey | undefined {
+ if (
+ asset === "" ||
+ asset === "native" ||
+ asset === "SOL" ||
+ asset === "sol"
+ ) {
+ return undefined;
+ }
+ return publicKeyFromString(asset, "asset");
+}
+
+export function publicKeyFromString(value: unknown, field: string): PublicKey {
+ if (typeof value !== "string") {
+ throw new ClearnetSdkError(
+ "INVALID_ADDRESS",
+ `${field} must be a valid Solana public key`,
+ );
+ }
+ try {
+ return new PublicKey(value);
+ } catch (error) {
+ throw new ClearnetSdkError(
+ "INVALID_ADDRESS",
+ `${field} must be a valid Solana public key`,
+ { cause: error },
+ );
+ }
+}
+
+export function requireTxRef(ref: unknown): Uint8Array {
+ if (!ref || typeof ref !== "object") {
+ throw new ClearnetSdkError(
+ "INVALID_TX_REF",
+ "ref.raw must be a Solana signature",
+ );
+ }
+ const fields = ref as Partial;
+ if (typeof fields.hash !== "string" || !BYTES32_HEX_PATTERN.test(fields.hash)) {
+ throw new ClearnetSdkError(
+ "INVALID_TX_REF",
+ "ref.hash must be a 32-byte hex value",
+ );
+ }
+ if (typeof fields.raw !== "string") {
+ throw new ClearnetSdkError(
+ "INVALID_TX_REF",
+ "ref.raw must be a Solana signature",
+ );
+ }
+ let signature: Uint8Array;
+ try {
+ signature = bs58.decode(fields.raw);
+ } catch (error) {
+ throw new ClearnetSdkError(
+ "INVALID_TX_REF",
+ "ref.raw must be a Solana signature",
+ { cause: error },
+ );
+ }
+ if (signature.length !== 64) {
+ throw new ClearnetSdkError(
+ "INVALID_TX_REF",
+ "ref.raw must decode to a 64-byte Solana signature",
+ );
+ }
+ if (fields.hash.toLowerCase() !== bytes32Hex(sha256(signature))) {
+ throw new ClearnetSdkError(
+ "INVALID_TX_REF",
+ "ref.hash must match the Solana signature hash",
+ );
+ }
+ return signature;
+}
+
+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 bytes32Hex(bytes: Uint8Array): Bytes32Hex {
+ const hex = [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
+ return `0x${hex}`;
+}
diff --git a/sdk/ts/src/core/types.ts b/sdk/ts/src/core/types.ts
index cd5175e..7826058 100644
--- a/sdk/ts/src/core/types.ts
+++ b/sdk/ts/src/core/types.ts
@@ -1,13 +1,14 @@
import type {
Account,
Address,
- Hash,
PublicClient,
WalletClient,
} from "viem";
+export type Bytes32Hex = `0x${string}`;
+
export interface TxRef {
- hash: Hash;
+ hash: Bytes32Hex;
raw: string;
}
@@ -15,7 +16,7 @@ export type DepositStatus = "absent" | "pending" | "confirmed";
export interface DepositDestination {
account: string;
- ref?: Hash;
+ ref?: Bytes32Hex;
}
export interface EvmDepositDestination extends DepositDestination {
diff --git a/sdk/ts/src/index.ts b/sdk/ts/src/index.ts
index c7c73cf..4457f59 100644
--- a/sdk/ts/src/index.ts
+++ b/sdk/ts/src/index.ts
@@ -1,4 +1,5 @@
export type {
+ Bytes32Hex,
DepositDestination,
DepositStatus,
EvmDepositDestination,
@@ -13,3 +14,18 @@ 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";
+export {
+ eventAuthorityPda,
+ SOLANA_CUSTODY_PROGRAM_ID,
+ SOLANA_NATIVE_ASSET,
+ SolanaVaultDepositor,
+ vaultPda,
+} from "./blockchain/sol/index.js";
+export type {
+ SolanaAsset,
+ SolanaCommitment,
+ SolanaDepositDestination,
+ SolanaDepositorConfig,
+ SolanaSigner,
+ SolanaSubmitDepositInput,
+} from "./blockchain/sol/index.js";
diff --git a/sdk/ts/test/blockchain/sol/depositor.integration.test.ts b/sdk/ts/test/blockchain/sol/depositor.integration.test.ts
new file mode 100644
index 0000000..61cdcc0
--- /dev/null
+++ b/sdk/ts/test/blockchain/sol/depositor.integration.test.ts
@@ -0,0 +1,267 @@
+import {
+ Connection,
+ Keypair,
+ LAMPORTS_PER_SOL,
+ PublicKey,
+ sendAndConfirmTransaction,
+ Transaction,
+} from "@solana/web3.js";
+import bs58 from "bs58";
+import { beforeAll, describe, expect, it } from "vitest";
+
+import {
+ SOLANA_NATIVE_ASSET,
+ SolanaVaultDepositor,
+ vaultPda,
+} from "../../../src/index.js";
+import type { SolanaSigner } from "../../../src/index.js";
+import { DEPOSITED_EVENT_DISCRIMINATOR } from "../../../src/blockchain/sol/constants.js";
+import { bytes32Hex } from "../../../src/blockchain/sol/validation.js";
+import {
+ createAssociatedTokenAccountIdempotent,
+ createMint,
+ getAssociatedTokenAddress,
+ mintTo,
+ tokenBalance,
+} from "./spl-test-helpers.js";
+
+const RPC_URL = process.env.SOL_RPC_URL ?? "http://127.0.0.1:8899";
+const ACCOUNT = "00000000000000000000000000000000000000a1";
+const REFERENCE =
+ "0x3333333333333333333333333333333333333333333333333333333333333333";
+const connection = new Connection(RPC_URL, "confirmed");
+const DEPOSITED_EVENT_SIZE = 8 + 32 + 20 + 32 + 32 + 8;
+
+describe("SolanaVaultDepositor validator integration", () => {
+ beforeAll(async () => {
+ const version = await connection.getVersion();
+ expect(version["solana-core"]).toBeTruthy();
+ }, 60_000);
+
+ it("deposits native SOL and verifies the deposit tx", async () => {
+ const depositorKeypair = Keypair.generate();
+ await airdrop(depositorKeypair.publicKey, LAMPORTS_PER_SOL);
+ const signer = new KeypairSolanaSigner(depositorKeypair);
+ const depositor = new SolanaVaultDepositor({
+ rpcUrl: RPC_URL,
+ signer,
+ commitment: "confirmed",
+ });
+ const vault = vaultPda();
+ const amount = 100_000_000n;
+ const beforeBalance = BigInt(await connection.getBalance(vault));
+
+ const ref = await depositor.submitDeposit({
+ asset: SOLANA_NATIVE_ASSET,
+ amount,
+ destination: { account: ACCOUNT, ref: REFERENCE },
+ });
+
+ const afterBalance = await waitForLamports(vault, beforeBalance + amount);
+ expect(afterBalance - beforeBalance).toBe(amount);
+ await expect(depositor.verifyDeposit(ref, 0)).resolves.toBe("confirmed");
+ await expectDepositedEvent(ref.raw, {
+ depositor: depositorKeypair.publicKey,
+ account: ACCOUNT,
+ reference: REFERENCE,
+ mint: PublicKey.default,
+ amount,
+ });
+ }, 120_000);
+
+ it("deposits SPL tokens and verifies the deposit tx", async () => {
+ const payer = Keypair.generate();
+ await airdrop(payer.publicKey, LAMPORTS_PER_SOL);
+ const signer = new KeypairSolanaSigner(payer);
+ const depositor = new SolanaVaultDepositor({
+ rpcUrl: RPC_URL,
+ signer,
+ commitment: "confirmed",
+ });
+ const mint = await createMint(connection, payer, payer.publicKey, 0);
+ const depositorAta = await createAssociatedTokenAccountIdempotent(
+ connection,
+ payer,
+ mint,
+ payer.publicKey,
+ );
+ const amount = 25n;
+ await mintTo(connection, payer, mint, depositorAta, payer, amount);
+ const vaultTokenAccount = await createAssociatedTokenAccountIdempotent(
+ connection,
+ payer,
+ mint,
+ vaultPda(),
+ );
+ const vaultAta = getAssociatedTokenAddress(mint, vaultPda());
+ expect(vaultTokenAccount.toBase58()).toBe(vaultAta.toBase58());
+ const beforeBalance = await tokenBalance(connection, vaultAta);
+ expect(beforeBalance).toBe(0n);
+
+ const ref = await depositor.submitDeposit({
+ asset: mint.toBase58(),
+ amount,
+ destination: { account: ACCOUNT, ref: REFERENCE },
+ });
+
+ const afterBalance = await waitForTokenBalance(vaultAta, beforeBalance + amount);
+ expect(afterBalance - beforeBalance).toBe(amount);
+ await expect(depositor.verifyDeposit(ref, 0)).resolves.toBe("confirmed");
+ await expectDepositedEvent(ref.raw, {
+ depositor: payer.publicKey,
+ account: ACCOUNT,
+ reference: REFERENCE,
+ mint,
+ amount,
+ });
+ }, 120_000);
+});
+
+class KeypairSolanaSigner implements SolanaSigner {
+ readonly publicKey: string;
+
+ constructor(private readonly keypair: Keypair) {
+ this.publicKey = keypair.publicKey.toBase58();
+ }
+
+ async signAndSend(transaction: Transaction): Promise {
+ return sendAndConfirmTransaction(connection, transaction, [this.keypair], {
+ commitment: "confirmed",
+ preflightCommitment: "confirmed",
+ });
+ }
+}
+
+async function airdrop(pubkey: PublicKey, lamports: number): Promise {
+ const signature = await connection.requestAirdrop(pubkey, lamports);
+ await connection.confirmTransaction(signature, "confirmed");
+}
+
+interface ExpectedDepositedEvent {
+ depositor: PublicKey;
+ account: string;
+ reference: string;
+ mint: PublicKey;
+ amount: bigint;
+}
+
+interface DepositedEvent {
+ depositor: PublicKey;
+ account: Uint8Array;
+ reference: Uint8Array;
+ mint: PublicKey;
+ amount: bigint;
+}
+
+async function expectDepositedEvent(
+ signature: string,
+ expected: ExpectedDepositedEvent,
+): Promise {
+ const event = await readDepositedEvent(signature);
+ expect(event.depositor.toBase58()).toBe(expected.depositor.toBase58());
+ expect(stripHexPrefix(bytes32Hex(event.account))).toBe(
+ stripHexPrefix(expected.account),
+ );
+ expect(stripHexPrefix(bytes32Hex(event.reference))).toBe(
+ stripHexPrefix(expected.reference),
+ );
+ expect(event.mint.toBase58()).toBe(expected.mint.toBase58());
+ expect(event.amount).toBe(expected.amount);
+}
+
+async function readDepositedEvent(signature: string): Promise {
+ const deadline = Date.now() + 30_000;
+ for (;;) {
+ const transaction = await connection.getTransaction(signature, {
+ commitment: "confirmed",
+ maxSupportedTransactionVersion: 0,
+ });
+ const innerInstructions = transaction?.meta?.innerInstructions ?? [];
+ for (const group of innerInstructions) {
+ for (const instruction of group.instructions) {
+ const event = decodeDepositedEvent(bs58.decode(instruction.data));
+ if (event !== undefined) {
+ return event;
+ }
+ }
+ }
+ if (Date.now() >= deadline) {
+ throw new Error(`Deposited event not found in ${signature}`);
+ }
+ await sleep(250);
+ }
+}
+
+function decodeDepositedEvent(data: Uint8Array): DepositedEvent | undefined {
+ const eventOffset = findBytes(data, DEPOSITED_EVENT_DISCRIMINATOR);
+ if (eventOffset < 0 || data.length < eventOffset + DEPOSITED_EVENT_SIZE) {
+ return undefined;
+ }
+ let cursor = eventOffset + DEPOSITED_EVENT_DISCRIMINATOR.length;
+ const depositor = new PublicKey(data.subarray(cursor, cursor + 32));
+ cursor += 32;
+ const account = data.slice(cursor, cursor + 20);
+ cursor += 20;
+ const reference = data.slice(cursor, cursor + 32);
+ cursor += 32;
+ const mint = new PublicKey(data.subarray(cursor, cursor + 32));
+ cursor += 32;
+ const amountBytes = data.subarray(cursor, cursor + 8);
+ const amount = new DataView(
+ amountBytes.buffer,
+ amountBytes.byteOffset,
+ amountBytes.byteLength,
+ ).getBigUint64(0, true);
+ return { depositor, account, reference, mint, amount };
+}
+
+function findBytes(data: Uint8Array, needle: readonly number[]): number {
+ for (let offset = 0; offset <= data.length - needle.length; offset += 1) {
+ if (needle.every((byte, index) => data[offset + index] === byte)) {
+ return offset;
+ }
+ }
+ return -1;
+}
+
+function stripHexPrefix(value: string): string {
+ return value.startsWith("0x") ? value.slice(2) : value;
+}
+
+async function waitForLamports(
+ pubkey: PublicKey,
+ target: bigint,
+): Promise {
+ const deadline = Date.now() + 30_000;
+ for (;;) {
+ const balance = BigInt(await connection.getBalance(pubkey));
+ if (balance >= target) {
+ return balance;
+ }
+ if (Date.now() >= deadline) {
+ throw new Error(`timed out waiting for ${pubkey.toBase58()} balance`);
+ }
+ await sleep(250);
+ }
+}
+
+async function waitForTokenBalance(
+ address: PublicKey,
+ target: bigint,
+): Promise {
+ const deadline = Date.now() + 30_000;
+ for (;;) {
+ const balance = await tokenBalance(connection, address);
+ if (balance >= target) {
+ return balance;
+ }
+ if (Date.now() >= deadline) {
+ throw new Error(`timed out waiting for ${address.toBase58()} token balance`);
+ }
+ await sleep(250);
+ }
+}
+
+async function sleep(ms: number): Promise {
+ await new Promise((resolve) => setTimeout(resolve, ms));
+}
diff --git a/sdk/ts/test/blockchain/sol/depositor.test.ts b/sdk/ts/test/blockchain/sol/depositor.test.ts
new file mode 100644
index 0000000..6989eb7
--- /dev/null
+++ b/sdk/ts/test/blockchain/sol/depositor.test.ts
@@ -0,0 +1,648 @@
+import bs58 from "bs58";
+import { sha256 } from "@noble/hashes/sha2.js";
+import {
+ PublicKey,
+ SystemProgram,
+ Transaction,
+ type TransactionInstruction,
+} from "@solana/web3.js";
+import { afterEach, describe, expect, expectTypeOf, it, vi } from "vitest";
+
+import {
+ ClearnetSdkError,
+ eventAuthorityPda as sdkEventAuthorityPda,
+ SOLANA_CUSTODY_PROGRAM_ID,
+ SOLANA_NATIVE_ASSET,
+ SolanaVaultDepositor,
+ vaultPda as sdkVaultPda,
+} from "../../../src/index.js";
+import {
+ DEPOSIT_SOL_DISCRIMINATOR,
+ DEPOSIT_SPL_DISCRIMINATOR,
+ SOLANA_ASSOCIATED_TOKEN_PROGRAM_ID,
+ SOLANA_TOKEN_PROGRAM_ID,
+} from "../../../src/blockchain/sol/constants.js";
+import { bytes32Hex } from "../../../src/blockchain/sol/validation.js";
+import type {
+ Bytes32Hex,
+ DepositStatus,
+ SolanaSigner,
+ SolanaSubmitDepositInput,
+ SubmitDepositOptions,
+ TxRef,
+ VaultDepositor,
+} from "../../../src/index.js";
+
+const RPC_URL = "http://127.0.0.1:8899";
+const EXPECTED_PROGRAM_ID = "98eVpih8X9CAcgU9bzNB9V7VtkRrnFZUmqzEnsq7cfmg";
+const PROGRAM_ID = new PublicKey(EXPECTED_PROGRAM_ID);
+const DEPOSITOR = publicKey(11);
+const MINT = publicKey(22);
+const ACCOUNT = "0x1111111111111111111111111111111111111111";
+const ACCOUNT_URI = `yellow://local/user/${ACCOUNT.slice(2)}`;
+const REFERENCE =
+ "0x2222222222222222222222222222222222222222222222222222222222222222" as Bytes32Hex;
+const SIGNATURE = bs58.encode(Uint8Array.from({ length: 64 }, (_, i) => i + 1));
+
+const SYSTEM_PROGRAM_ID = SystemProgram.programId.toBase58();
+const TOKEN_PROGRAM_ID = SOLANA_TOKEN_PROGRAM_ID;
+const ASSOCIATED_TOKEN_PROGRAM_ID = SOLANA_ASSOCIATED_TOKEN_PROGRAM_ID;
+const VAULT_PDA = sdkVaultPda(PROGRAM_ID).toBase58();
+const EVENT_AUTHORITY_PDA = sdkEventAuthorityPda(PROGRAM_ID).toBase58();
+
+interface MockSigner extends SolanaSigner {
+ signAndSend: ReturnType<
+ typeof vi.fn<(transaction: Transaction) => Promise>
+ >;
+}
+
+describe("SolanaVaultDepositor", () => {
+ afterEach(() => {
+ vi.useRealTimers();
+ vi.unstubAllGlobals();
+ });
+
+ it("matches the public depositor and result type contracts", () => {
+ expectTypeOf().toMatchTypeOf<
+ VaultDepositor
+ >();
+ expectTypeOf().toEqualTypeOf<{ hash: Bytes32Hex; raw: string }>();
+ expectTypeOf().toEqualTypeOf<
+ "absent" | "pending" | "confirmed"
+ >();
+ expect(SOLANA_NATIVE_ASSET).toBe("SOL");
+ expect(SOLANA_CUSTODY_PROGRAM_ID).toBe(EXPECTED_PROGRAM_ID);
+ });
+
+ it("submits native SOL with the deposit_sol layout and Go-compatible tx ref", async () => {
+ stubSignatureStatus({ confirmationStatus: "finalized" });
+ const signer = createSigner();
+ const depositor = createDepositor(signer);
+ const onSubmitted = vi.fn();
+
+ const ref = await depositor.submitDeposit(
+ {
+ asset: SOLANA_NATIVE_ASSET,
+ amount: 10n,
+ destination: { account: ACCOUNT_URI, ref: REFERENCE },
+ },
+ { onSubmitted },
+ );
+
+ expect(ref).toEqual(txRefForSignature(SIGNATURE));
+ expect(onSubmitted).toHaveBeenCalledExactlyOnceWith(ref);
+
+ const tx = signedTransaction(signer);
+ expect(tx.feePayer?.toBase58()).toBe(DEPOSITOR.toBase58());
+ expect(tx.instructions).toHaveLength(1);
+
+ const instruction = tx.instructions[0]!;
+ expect(instruction.programId.toBase58()).toBe(EXPECTED_PROGRAM_ID);
+ expect(metas(instruction)).toEqual([
+ meta(DEPOSITOR, true, true),
+ meta(VAULT_PDA, false, true),
+ meta(SYSTEM_PROGRAM_ID, false, false),
+ meta(EVENT_AUTHORITY_PDA, false, false),
+ meta(PROGRAM_ID, false, false),
+ ]);
+ expect([...instruction.data]).toEqual([
+ ...DEPOSIT_SOL_DISCRIMINATOR,
+ ...hexBytes(ACCOUNT),
+ ...hexBytes(REFERENCE),
+ ...u64(10n),
+ ]);
+ });
+
+ it("submits SPL tokens with ATA derivation and the deposit_spl layout", async () => {
+ stubSignatureStatus({ confirmationStatus: "finalized" });
+ const signer = createSigner();
+ const depositor = createDepositor(signer);
+
+ const ref = await depositor.submitDeposit({
+ asset: MINT.toBase58(),
+ amount: 25n,
+ destination: { account: ACCOUNT },
+ });
+
+ expect(ref).toEqual(txRefForSignature(SIGNATURE));
+
+ const instruction = signedTransaction(signer).instructions[0]!;
+ expect(instruction.programId.toBase58()).toBe(EXPECTED_PROGRAM_ID);
+ expect(metas(instruction)).toEqual([
+ meta(DEPOSITOR, true, true),
+ meta(MINT, false, false),
+ meta(ata(DEPOSITOR, MINT), false, true),
+ meta(VAULT_PDA, false, false),
+ meta(ata(VAULT_PDA, MINT), false, true),
+ meta(TOKEN_PROGRAM_ID, false, false),
+ meta(ASSOCIATED_TOKEN_PROGRAM_ID, false, false),
+ meta(EVENT_AUTHORITY_PDA, false, false),
+ meta(PROGRAM_ID, false, false),
+ ]);
+ expect([...instruction.data]).toEqual([
+ ...DEPOSIT_SPL_DISCRIMINATOR,
+ ...hexBytes(ACCOUNT),
+ ...new Uint8Array(32),
+ ...u64(25n),
+ ]);
+ });
+
+ it("rejects invalid deposit input before signing", async () => {
+ const fetch = vi.fn();
+ vi.stubGlobal("fetch", fetch);
+ const signer = createSigner();
+ const depositor = createDepositor(signer);
+
+ await expect(
+ depositor.submitDeposit(null as unknown as SolanaSubmitDepositInput),
+ ).rejects.toMatchObject({ code: "INVALID_ADDRESS" });
+ await expect(
+ depositor.submitDeposit(
+ {
+ asset: SOLANA_NATIVE_ASSET,
+ amount: 1n,
+ destination: { account: ACCOUNT },
+ },
+ null as unknown as SubmitDepositOptions,
+ ),
+ ).rejects.toMatchObject({ code: "RECEIPT_TIMEOUT" });
+ await expect(
+ depositor.submitDeposit({
+ asset: SOLANA_NATIVE_ASSET,
+ amount: 1n,
+ destination: null as unknown as SolanaSubmitDepositInput["destination"],
+ }),
+ ).rejects.toMatchObject({ code: "INVALID_ADDRESS" });
+ await expect(
+ depositor.submitDeposit({
+ asset: SOLANA_NATIVE_ASSET,
+ amount: 1n,
+ destination: { account: "0x1234" },
+ }),
+ ).rejects.toMatchObject({ code: "INVALID_ADDRESS" });
+ await expect(
+ depositor.submitDeposit({
+ asset: "not-base58",
+ amount: 1n,
+ destination: { account: ACCOUNT },
+ }),
+ ).rejects.toMatchObject({ code: "INVALID_ADDRESS" });
+ await expect(
+ depositor.submitDeposit({
+ asset: SOLANA_NATIVE_ASSET,
+ amount: 0n,
+ destination: { account: ACCOUNT },
+ }),
+ ).rejects.toMatchObject({ code: "INVALID_AMOUNT" });
+ await expect(
+ depositor.submitDeposit({
+ asset: SOLANA_NATIVE_ASSET,
+ amount: 1 as unknown as bigint,
+ destination: { account: ACCOUNT },
+ }),
+ ).rejects.toMatchObject({ code: "INVALID_AMOUNT" });
+ await expect(
+ depositor.submitDeposit({
+ asset: SOLANA_NATIVE_ASSET,
+ amount: 1n << 64n,
+ destination: { account: ACCOUNT },
+ }),
+ ).rejects.toMatchObject({ code: "INVALID_AMOUNT" });
+ await expect(
+ depositor.submitDeposit({
+ asset: SOLANA_NATIVE_ASSET,
+ amount: 1n,
+ destination: { account: ACCOUNT, ref: "invoice-1" as Bytes32Hex },
+ }),
+ ).rejects.toMatchObject({ code: "INVALID_REFERENCE" });
+
+ expect(signer.signAndSend).not.toHaveBeenCalled();
+ expect(fetch).not.toHaveBeenCalled();
+ });
+
+ it("requires the default program ID for v1", () => {
+ const signer = createSigner();
+
+ expect(() =>
+ new SolanaVaultDepositor({
+ rpcUrl: RPC_URL,
+ signer,
+ programId: publicKey(33).toBase58(),
+ }),
+ ).toThrow(ClearnetSdkError);
+ expect(() =>
+ new SolanaVaultDepositor({ rpcUrl: "", signer }),
+ ).toThrow(ClearnetSdkError);
+ expect(() =>
+ new SolanaVaultDepositor({
+ rpcUrl: RPC_URL,
+ signer: undefined as unknown as SolanaSigner,
+ }),
+ ).toThrow(ClearnetSdkError);
+ expect(() =>
+ new SolanaVaultDepositor({
+ rpcUrl: RPC_URL,
+ signer: {
+ publicKey: "not-base58",
+ signAndSend: async () => SIGNATURE,
+ },
+ }),
+ ).toThrow(ClearnetSdkError);
+ expect(() =>
+ new SolanaVaultDepositor({
+ rpcUrl: RPC_URL,
+ signer: { publicKey: DEPOSITOR.toBase58() } as unknown as SolanaSigner,
+ }),
+ ).toThrow(ClearnetSdkError);
+ expect(() =>
+ new SolanaVaultDepositor({
+ rpcUrl: RPC_URL,
+ signer,
+ commitment: "recent" as never,
+ }),
+ ).toThrow(ClearnetSdkError);
+ expect(() =>
+ new SolanaVaultDepositor({
+ rpcUrl: RPC_URL,
+ signer,
+ receiptTimeoutMs: 0,
+ }),
+ ).toThrow(ClearnetSdkError);
+ });
+
+ it("wraps signer failures before a tx ref exists", async () => {
+ const cause = new Error("wallet rejected");
+ stubSignatureStatus({ confirmationStatus: "finalized" });
+ const signer = createSigner();
+ signer.signAndSend.mockRejectedValue(cause);
+ const depositor = createDepositor(signer);
+ const onSubmitted = vi.fn();
+
+ await expect(
+ depositor.submitDeposit(
+ {
+ asset: SOLANA_NATIVE_ASSET,
+ amount: 1n,
+ destination: { account: ACCOUNT },
+ },
+ { onSubmitted },
+ ),
+ ).rejects.toMatchObject({
+ code: "RPC_ERROR",
+ cause,
+ txRef: undefined,
+ });
+ expect(onSubmitted).not.toHaveBeenCalled();
+ });
+
+ it("rejects invalid signer-returned signatures before submission callback", async () => {
+ stubSignatureStatus({ confirmationStatus: "finalized" });
+ const signer = createSigner();
+ signer.signAndSend.mockResolvedValue("not a base58 signature");
+ const depositor = createDepositor(signer);
+ const onSubmitted = vi.fn();
+
+ await expect(
+ depositor.submitDeposit(
+ {
+ asset: SOLANA_NATIVE_ASSET,
+ amount: 1n,
+ destination: { account: ACCOUNT },
+ },
+ { onSubmitted },
+ ),
+ ).rejects.toMatchObject({ code: "INVALID_TX_REF" });
+ expect(onSubmitted).not.toHaveBeenCalled();
+ });
+
+ it("attaches txRef when a post-broadcast status lookup fails", async () => {
+ const rpcError = new Error("node offline");
+ stubRpcFailure(rpcError);
+ const signer = createSigner();
+ const depositor = createDepositor(signer);
+ const expectedRef = txRefForSignature(SIGNATURE);
+
+ await expect(
+ depositor.submitDeposit({
+ asset: SOLANA_NATIVE_ASSET,
+ amount: 1n,
+ destination: { account: ACCOUNT },
+ }),
+ ).rejects.toMatchObject({
+ code: "RPC_ERROR",
+ txRef: expectedRef,
+ cause: rpcError,
+ });
+ });
+
+ it("attaches txRef when a submitted transaction reports an execution error", async () => {
+ stubSignatureStatus({
+ confirmationStatus: "confirmed",
+ err: { InstructionError: [0, "Custom"] },
+ });
+ const signer = createSigner();
+ const depositor = createDepositor(signer);
+ const expectedRef = txRefForSignature(SIGNATURE);
+
+ await expect(
+ depositor.submitDeposit({
+ asset: SOLANA_NATIVE_ASSET,
+ amount: 1n,
+ destination: { account: ACCOUNT },
+ }),
+ ).rejects.toMatchObject({
+ code: "TX_REVERTED",
+ txRef: expectedRef,
+ });
+ });
+
+ it("attaches txRef when a submitted transaction times out", async () => {
+ vi.useFakeTimers();
+ stubSignatureStatus(null);
+ const signer = createSigner();
+ const depositor = createDepositor(signer);
+ const expectedRef = txRefForSignature(SIGNATURE);
+
+ const promise = depositor.submitDeposit(
+ {
+ asset: SOLANA_NATIVE_ASSET,
+ amount: 1n,
+ destination: { account: ACCOUNT },
+ },
+ { receiptTimeoutMs: 1_000 },
+ );
+ const assertion = expect(promise).rejects.toMatchObject({
+ code: "RECEIPT_TIMEOUT",
+ txRef: expectedRef,
+ });
+ await vi.advanceTimersByTimeAsync(1_000);
+ await assertion;
+ });
+
+ it("attaches txRef when a submitted transaction is aborted while waiting", async () => {
+ vi.useFakeTimers();
+ stubSignatureStatus(null);
+ const signer = createSigner();
+ const depositor = createDepositor(signer);
+ const controller = new AbortController();
+ const expectedRef = txRefForSignature(SIGNATURE);
+
+ const promise = depositor.submitDeposit(
+ {
+ asset: SOLANA_NATIVE_ASSET,
+ amount: 1n,
+ destination: { account: ACCOUNT },
+ },
+ {
+ signal: controller.signal,
+ receiptTimeoutMs: 10_000,
+ onSubmitted() {
+ setTimeout(() => controller.abort(), 1);
+ },
+ },
+ );
+ const assertion = expect(promise).rejects.toMatchObject({
+ code: "RECEIPT_TIMEOUT",
+ txRef: expectedRef,
+ });
+ await vi.advanceTimersByTimeAsync(1);
+ await assertion;
+ });
+
+ it("rejects invalid wait options before signing", async () => {
+ const signer = createSigner();
+ const depositor = createDepositor(signer);
+ const controller = new AbortController();
+ controller.abort();
+
+ await expect(
+ depositor.submitDeposit(
+ {
+ asset: SOLANA_NATIVE_ASSET,
+ amount: 1n,
+ destination: { account: ACCOUNT },
+ },
+ { receiptTimeoutMs: 0 },
+ ),
+ ).rejects.toMatchObject({ code: "RECEIPT_TIMEOUT" });
+ await expect(
+ depositor.submitDeposit(
+ {
+ asset: SOLANA_NATIVE_ASSET,
+ amount: 1n,
+ destination: { account: ACCOUNT },
+ },
+ { signal: controller.signal },
+ ),
+ ).rejects.toMatchObject({ code: "RECEIPT_TIMEOUT" });
+ expect(signer.signAndSend).not.toHaveBeenCalled();
+ });
+
+ it("bounds a hung post-submit status lookup", async () => {
+ vi.useFakeTimers();
+ vi.stubGlobal("fetch", vi.fn(() => new Promise(() => undefined)));
+ const signer = createSigner();
+ const depositor = createDepositor(signer);
+ const expectedRef = txRefForSignature(SIGNATURE);
+
+ const promise = depositor.submitDeposit(
+ {
+ asset: SOLANA_NATIVE_ASSET,
+ amount: 1n,
+ destination: { account: ACCOUNT },
+ },
+ { receiptTimeoutMs: 1_000 },
+ );
+ const assertion = expect(promise).rejects.toMatchObject({
+ code: "RECEIPT_TIMEOUT",
+ txRef: expectedRef,
+ });
+ await vi.advanceTimersByTimeAsync(1_000);
+ await assertion;
+ });
+
+ it("maps Solana signature statuses to the shared deposit status", async () => {
+ const depositor = createDepositor(createSigner());
+
+ stubSignatureStatus({ confirmationStatus: "confirmed" });
+ await expect(depositor.verifyDeposit(txRefForSignature(SIGNATURE), 0)).resolves.toBe(
+ "confirmed",
+ );
+
+ stubSignatureStatus({ confirmationStatus: "confirmed" });
+ await expect(depositor.verifyDeposit(txRefForSignature(SIGNATURE), 1)).resolves.toBe(
+ "pending",
+ );
+
+ stubSignatureStatus({ confirmationStatus: "finalized" });
+ await expect(depositor.verifyDeposit(txRefForSignature(SIGNATURE), 1n)).resolves.toBe(
+ "confirmed",
+ );
+
+ stubSignatureStatus({ confirmationStatus: "processed" });
+ await expect(depositor.verifyDeposit(txRefForSignature(SIGNATURE), 0)).resolves.toBe(
+ "pending",
+ );
+
+ stubSignatureStatus(null);
+ await expect(depositor.verifyDeposit(txRefForSignature(SIGNATURE), 0)).resolves.toBe(
+ "absent",
+ );
+
+ stubSignatureStatus({ confirmationStatus: "finalized", err: { InstructionError: [0, "Custom"] } });
+ await expect(depositor.verifyDeposit(txRefForSignature(SIGNATURE), 0)).resolves.toBe(
+ "absent",
+ );
+ });
+
+ it("validates tx refs and confirmation depths before RPC", async () => {
+ const fetch = vi.fn();
+ vi.stubGlobal("fetch", fetch);
+ const depositor = createDepositor(createSigner());
+
+ await expect(
+ depositor.verifyDeposit({ hash: txRefForSignature(SIGNATURE).hash, raw: "bad sig" }, 0),
+ ).rejects.toMatchObject({ code: "INVALID_TX_REF" });
+ await expect(
+ depositor.verifyDeposit({ hash: "0x1234" as Bytes32Hex, raw: SIGNATURE }, 0),
+ ).rejects.toMatchObject({ code: "INVALID_TX_REF" });
+ await expect(
+ depositor.verifyDeposit(
+ {
+ hash: txRefForSignature(bs58.encode(new Uint8Array(64).fill(9))).hash,
+ raw: SIGNATURE,
+ },
+ 0,
+ ),
+ ).rejects.toMatchObject({ code: "INVALID_TX_REF" });
+ await expect(
+ depositor.verifyDeposit(txRefForSignature(SIGNATURE), -1),
+ ).rejects.toMatchObject({ code: "INVALID_CONFIRMATIONS" });
+ await expect(
+ depositor.verifyDeposit(txRefForSignature(SIGNATURE), 1.5),
+ ).rejects.toMatchObject({ code: "INVALID_CONFIRMATIONS" });
+
+ expect(fetch).not.toHaveBeenCalled();
+ });
+
+ it("preserves txRef when verifyDeposit status lookup fails", async () => {
+ const rpcError = new Error("node offline");
+ stubRpcFailure(rpcError);
+ const depositor = createDepositor(createSigner());
+ const ref = txRefForSignature(SIGNATURE);
+
+ await expect(depositor.verifyDeposit(ref, 0)).rejects.toMatchObject({
+ code: "RPC_ERROR",
+ txRef: ref,
+ cause: rpcError,
+ });
+ });
+});
+
+function createDepositor(signer: SolanaSigner): SolanaVaultDepositor {
+ return new SolanaVaultDepositor({ rpcUrl: RPC_URL, signer });
+}
+
+function createSigner(): MockSigner {
+ return {
+ publicKey: DEPOSITOR.toBase58(),
+ signAndSend: vi.fn<(transaction: Transaction) => Promise>()
+ .mockResolvedValue(SIGNATURE),
+ };
+}
+
+function signedTransaction(signer: MockSigner): Transaction {
+ const call = signer.signAndSend.mock.calls[0];
+ if (call === undefined) {
+ throw new Error("signAndSend was not called");
+ }
+ return call[0];
+}
+
+function publicKey(seed: number): PublicKey {
+ return new PublicKey(Uint8Array.from({ length: 32 }, (_, i) => seed + i));
+}
+
+function ata(owner: PublicKey | string, mint: PublicKey): string {
+ const ownerKey = typeof owner === "string" ? new PublicKey(owner) : owner;
+ return PublicKey.findProgramAddressSync(
+ [
+ ownerKey.toBytes(),
+ new PublicKey(TOKEN_PROGRAM_ID).toBytes(),
+ mint.toBytes(),
+ ],
+ new PublicKey(ASSOCIATED_TOKEN_PROGRAM_ID),
+ )[0].toBase58();
+}
+
+function meta(
+ pubkey: PublicKey | string,
+ isSigner: boolean,
+ isWritable: boolean,
+): { pubkey: string; isSigner: boolean; isWritable: boolean } {
+ return {
+ pubkey: typeof pubkey === "string" ? pubkey : pubkey.toBase58(),
+ isSigner,
+ isWritable,
+ };
+}
+
+function metas(instruction: TransactionInstruction): ReturnType[] {
+ return instruction.keys.map((key) =>
+ meta(key.pubkey, key.isSigner, key.isWritable),
+ );
+}
+
+function hexBytes(value: string): Uint8Array {
+ const hex = value.toLowerCase().replace(/^0x/, "");
+ return Uint8Array.from(Buffer.from(hex, "hex"));
+}
+
+function u64(value: bigint): Uint8Array {
+ const bytes = new Uint8Array(8);
+ new DataView(bytes.buffer).setBigUint64(0, value, true);
+ return bytes;
+}
+
+function txRefForSignature(signature: string): TxRef {
+ const signatureBytes = bs58.decode(signature);
+ return { hash: bytes32Hex(sha256(signatureBytes)), raw: signature };
+}
+
+function stubSignatureStatus(
+ value: null | { confirmationStatus: string; err?: unknown },
+): void {
+ const response = {
+ jsonrpc: "2.0",
+ id: "1",
+ result: {
+ context: { slot: 1 },
+ value: [
+ value === null
+ ? null
+ : {
+ slot: 1,
+ confirmations:
+ value.confirmationStatus === "finalized" ? null : 1,
+ err: value.err ?? null,
+ confirmationStatus: value.confirmationStatus,
+ },
+ ],
+ },
+ };
+ vi.stubGlobal(
+ "fetch",
+ vi.fn().mockImplementation(() =>
+ Promise.resolve(
+ new Response(JSON.stringify(response), {
+ status: 200,
+ headers: { "content-type": "application/json" },
+ }),
+ ),
+ ),
+ );
+}
+
+function stubRpcFailure(error: Error): void {
+ vi.stubGlobal("fetch", vi.fn().mockRejectedValue(error));
+}
diff --git a/sdk/ts/test/blockchain/sol/spl-test-helpers.ts b/sdk/ts/test/blockchain/sol/spl-test-helpers.ts
new file mode 100644
index 0000000..2e9f51b
--- /dev/null
+++ b/sdk/ts/test/blockchain/sol/spl-test-helpers.ts
@@ -0,0 +1,149 @@
+import { Buffer } from "buffer";
+
+import {
+ Connection,
+ Keypair,
+ PublicKey,
+ sendAndConfirmTransaction,
+ SystemProgram,
+ Transaction,
+ TransactionInstruction,
+} from "@solana/web3.js";
+
+import {
+ SOLANA_ASSOCIATED_TOKEN_PROGRAM_ID,
+ SOLANA_TOKEN_PROGRAM_ID,
+} from "../../../src/blockchain/sol/constants.js";
+
+const MINT_ACCOUNT_SIZE = 82;
+const INITIALIZE_MINT2_INSTRUCTION = 20;
+const CREATE_IDEMPOTENT_ATA_INSTRUCTION = 1;
+const MINT_TO_INSTRUCTION = 7;
+const TOKEN_AMOUNT_OFFSET = 1;
+const TOKEN_AMOUNT_BYTES = 8;
+const TOKEN_AMOUNT_LENGTH = TOKEN_AMOUNT_OFFSET + TOKEN_AMOUNT_BYTES;
+
+const TOKEN_PROGRAM_ID = new PublicKey(SOLANA_TOKEN_PROGRAM_ID);
+const ASSOCIATED_TOKEN_PROGRAM_ID = new PublicKey(
+ SOLANA_ASSOCIATED_TOKEN_PROGRAM_ID,
+);
+
+export function getAssociatedTokenAddress(
+ mint: PublicKey,
+ owner: PublicKey,
+): PublicKey {
+ return PublicKey.findProgramAddressSync(
+ [owner.toBuffer(), TOKEN_PROGRAM_ID.toBuffer(), mint.toBuffer()],
+ ASSOCIATED_TOKEN_PROGRAM_ID,
+ )[0];
+}
+
+export async function createMint(
+ connection: Connection,
+ payer: Keypair,
+ mintAuthority: PublicKey,
+ decimals: number,
+): Promise {
+ const mint = Keypair.generate();
+ const lamports =
+ await connection.getMinimumBalanceForRentExemption(MINT_ACCOUNT_SIZE);
+ const transaction = new Transaction().add(
+ SystemProgram.createAccount({
+ fromPubkey: payer.publicKey,
+ newAccountPubkey: mint.publicKey,
+ lamports,
+ space: MINT_ACCOUNT_SIZE,
+ programId: TOKEN_PROGRAM_ID,
+ }),
+ new TransactionInstruction({
+ programId: TOKEN_PROGRAM_ID,
+ keys: [{ pubkey: mint.publicKey, isSigner: false, isWritable: true }],
+ data: Buffer.concat([
+ Buffer.from([INITIALIZE_MINT2_INSTRUCTION, decimals]),
+ mintAuthority.toBuffer(),
+ Buffer.from([0]),
+ ]),
+ }),
+ );
+ await sendAndConfirmTransaction(connection, transaction, [payer, mint], {
+ commitment: "confirmed",
+ });
+ return mint.publicKey;
+}
+
+export async function createAssociatedTokenAccountIdempotent(
+ connection: Connection,
+ payer: Keypair,
+ mint: PublicKey,
+ owner: PublicKey,
+): Promise {
+ const ata = getAssociatedTokenAddress(mint, owner);
+ const transaction = new Transaction().add(
+ new TransactionInstruction({
+ programId: ASSOCIATED_TOKEN_PROGRAM_ID,
+ keys: [
+ { pubkey: payer.publicKey, isSigner: true, isWritable: true },
+ { pubkey: ata, isSigner: false, isWritable: true },
+ { pubkey: owner, isSigner: false, isWritable: false },
+ { pubkey: mint, isSigner: false, isWritable: false },
+ { pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
+ { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
+ ],
+ data: Buffer.from([CREATE_IDEMPOTENT_ATA_INSTRUCTION]),
+ }),
+ );
+ await sendAndConfirmTransaction(connection, transaction, [payer], {
+ commitment: "confirmed",
+ });
+ return ata;
+}
+
+export async function mintTo(
+ connection: Connection,
+ payer: Keypair,
+ mint: PublicKey,
+ destination: PublicKey,
+ authority: Keypair,
+ amount: bigint,
+): Promise {
+ const data = Buffer.alloc(TOKEN_AMOUNT_LENGTH);
+ data[0] = MINT_TO_INSTRUCTION;
+ data.writeBigUInt64LE(amount, TOKEN_AMOUNT_OFFSET);
+ const transaction = new Transaction().add(
+ new TransactionInstruction({
+ programId: TOKEN_PROGRAM_ID,
+ keys: [
+ { pubkey: mint, isSigner: false, isWritable: true },
+ { pubkey: destination, isSigner: false, isWritable: true },
+ { pubkey: authority.publicKey, isSigner: true, isWritable: false },
+ ],
+ data,
+ }),
+ );
+ await sendAndConfirmTransaction(
+ connection,
+ transaction,
+ uniqueSigners([payer, authority]),
+ { commitment: "confirmed" },
+ );
+}
+
+export async function tokenBalance(
+ connection: Connection,
+ ata: PublicKey,
+): Promise {
+ const account = await connection.getAccountInfo(ata);
+ if (account === null) {
+ return 0n;
+ }
+ const balance = await connection.getTokenAccountBalance(ata);
+ return BigInt(balance.value.amount);
+}
+
+function uniqueSigners(signers: Keypair[]): Keypair[] {
+ const out = new Map();
+ for (const signer of signers) {
+ out.set(signer.publicKey.toBase58(), signer);
+ }
+ return [...out.values()];
+}