# Wallet execution (v1)

> Run a v1 deposit plan. EIP-1193 requests on EVM, base64 transactions on Solana, deposit addresses, and server-side broadcast.

> API v1 is deprecated. It stays supported, but new integrations should use the [Khalani API](/). See [Migrate to v2](/v1/migrate/).

`POST /v1/deposit/build` returns a deposit plan. Switch on its `kind`:

- **`CONTRACT_CALL`**: the user's wallet runs the actions in `approvals[]`, in order. You submit the hash of the action marked `deposit: true`. This page covers EVM and Solana wallets.
- **`TRANSFER`**: the user sends a plain transfer to a deposit address. See [Deposit addresses](#deposit-addresses).

## What a wallet action holds

| Field | On | Meaning |
|---|---|---|
| `type` | Every action | `eip1193_request` (EVM) or `solana_sendTransaction` (Solana). |
| `request` | EVM | An EIP-1193 request: `wallet_switchEthereumChain` or `eth_sendTransaction`. Forward it unchanged. |
| `transaction` | Solana | A base64-encoded transaction to sign and send. |
| `waitForReceipt` | EVM, optional | `true` on approvals. Wait for the receipt before the next action. |
| `deposit` | Optional | `true` on the one action that is the deposit. Submit its hash. |

A chain-switch action has neither `waitForReceipt` nor `deposit`. Treat a missing flag as `false`.

## EVM: forward EIP-1193 requests

An EVM plan is a list of EIP-1193 requests, so any EIP-1193 wallet can run it. Pass each `request` to `provider.request` as it is. A typical native-route plan is a chain switch, an approval when the allowance is short, and the deposit.

```ts title="v1-wallet-evm.ts"
// API v1 (deprecated): run the EVM wallet actions from POST /v1/deposit/build with any EIP-1193
// wallet, then submit the deposit hash. Each request is forwarded to the wallet unchanged.
import { type Hex, isHex } from "viem";

const API = "https://api.hyperstream.dev";

export type Eip1193Provider = {
	request(args: { method: string; params?: readonly unknown[] }): Promise<unknown>;
};

/** The quote and route the user chose, and the wallet that sends the deposit. */
export type Selection = { quoteId: string; routeId: string; from: string };

type EvmAction = {
	type: "eip1193_request";
	request: { method: string; params?: readonly unknown[] };
	waitForReceipt?: boolean;
	deposit?: boolean;
};
type SolanaAction = { type: "solana_sendTransaction"; transaction: string; deposit?: boolean };
type DepositPlan =
	| { kind: "CONTRACT_CALL"; approvals?: Array<EvmAction | SolanaAction>; expiresAt?: number; buildId?: string }
	| { kind: "TRANSFER"; depositAddress: string; amount: string; token: string; chainId: number }
	| { kind: "PERMIT2" };
type Receipt = { status: Hex };

/** Builds, executes and submits one EVM deposit. Returns the v1 order id. */
export async function depositWithEvmWallet(provider: Eip1193Provider, selection: Selection): Promise<string> {
	const plan = await call<DepositPlan>("POST", "/v1/deposit/build", selection);
	if (plan.kind !== "CONTRACT_CALL") throw new Error(`Not a wallet deposit: ${plan.kind}`);

	let depositTxHash: Hex | undefined;
	for (const action of plan.approvals ?? []) {
		if (action.type !== "eip1193_request") throw new Error("A Solana action in an EVM plan");
		// wallet_switchEthereumChain resolves to null; eth_sendTransaction to the transaction hash.
		const result = await provider.request(action.request);
		if (action.request.method !== "eth_sendTransaction") continue;
		if (typeof result !== "string" || !isHex(result)) throw new Error("The wallet returned no transaction hash");
		if (action.waitForReceipt) await waitForReceipt(provider, result);
		if (action.deposit) depositTxHash = result;
	}
	if (!depositTxHash) throw new Error("The plan had no deposit action");

	// Submit once. buildId is present only for a quote made without fromAddress.
	const submitted = await call<{ orderId: string; txHash: string }>("PUT", "/v1/deposit/submit", {
		txHash: depositTxHash,
		quoteId: selection.quoteId,
		routeId: selection.routeId,
		...(plan.buildId ? { buildId: plan.buildId } : {}),
	});
	return submitted.orderId;
}

async function waitForReceipt(provider: Eip1193Provider, hash: Hex): Promise<void> {
	for (;;) {
		const receipt = (await provider.request({ method: "eth_getTransactionReceipt", params: [hash] })) as Receipt | null;
		if (receipt) {
			if (receipt.status !== "0x1") throw new Error(`Transaction ${hash} reverted`);
			return;
		}
		await new Promise((resolve) => setTimeout(resolve, 2_000));
	}
}

async function call<T>(method: "POST" | "PUT", path: string, body: unknown): Promise<T> {
	const res = await fetch(`${API}${path}`, {
		method,
		headers: { "Content-Type": "application/json" },
		body: JSON.stringify(body),
	});
	const text = await res.text();
	if (!res.ok) throw new Error(`${res.status} ${path}: ${text} (x-request-id ${res.headers.get("x-request-id")})`);
	return JSON.parse(text) as T;
}
```

- **Forward every field.** The deposit's `eth_sendTransaction` carries `value` and often `gas`. Dropping `value` makes the deposit revert, because it pays the relay fee.
- **Wait where the plan says so.** An approval has `waitForReceipt: true`. Sending the deposit before the approval confirms makes it revert.
- **A chain switch can fail** when the wallet does not know the chain. Ask the user to add it with `wallet_addEthereumChain`, using the RPC and explorer from [`GET /v1/chains`](/v1/reference/chains/), then run the plan again.
- **Show a stepper.** Tell the user how many signatures to expect (approve, then deposit), so no prompt arrives without context.

## Solana: sign base64 transactions

A Solana plan holds base64-encoded transactions. The example below targets an injected Solana wallet provider, such as the one a browser wallet extension exposes, whose `signAndSendTransaction` resolves to `{ signature }`. It uses `@solana/web3.js`, which you install yourself; it is not type-checked with these docs.

```ts
import { VersionedTransaction } from "@solana/web3.js";

const API = "https://api.hyperstream.dev";

// An injected Solana wallet provider, connected to the account in `from`.
type SolanaProvider = {
  signAndSendTransaction(tx: VersionedTransaction): Promise<{ signature: string }>;
};
type SolanaAction = { type: "solana_sendTransaction"; transaction: string; deposit?: boolean };
type DepositPlan =
  | { kind: "CONTRACT_CALL"; approvals?: SolanaAction[]; buildId?: string }
  | { kind: "TRANSFER" | "PERMIT2" };

export async function depositWithSolanaWallet(
  provider: SolanaProvider,
  selection: { quoteId: string; routeId: string; from: string },
): Promise<string> {
  const plan = await call<DepositPlan>("POST", "/v1/deposit/build", selection);
  if (plan.kind !== "CONTRACT_CALL") throw new Error(`Not a wallet deposit: ${plan.kind}`);

  let depositSignature: string | undefined;
  for (const action of plan.approvals ?? []) {
    if (action.type !== "solana_sendTransaction") throw new Error("Expected Solana actions");
    // Handles legacy and v0 transactions. The build refreshed the blockhash: send right away.
    const tx = VersionedTransaction.deserialize(base64ToBytes(action.transaction));
    const { signature } = await provider.signAndSendTransaction(tx);
    if (action.deposit) depositSignature = signature;
  }
  if (!depositSignature) throw new Error("The plan had no deposit action");

  const submitted = await call<{ orderId: string }>("PUT", "/v1/deposit/submit", {
    txHash: depositSignature,
    quoteId: selection.quoteId,
    routeId: selection.routeId,
    ...(plan.buildId ? { buildId: plan.buildId } : {}),
  });
  return submitted.orderId;
}

function base64ToBytes(base64: string): Uint8Array {
  return Uint8Array.from(atob(base64), (char) => char.charCodeAt(0));
}

async function call<T>(method: "POST" | "PUT", path: string, body: unknown): Promise<T> {
  const res = await fetch(`${API}${path}`, {
    method,
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(body),
  });
  const text = await res.text();
  if (!res.ok) throw new Error(`${res.status} ${path}: ${text}`);
  return JSON.parse(text) as T;
}
```

- **Sign right after the build.** The API puts a fresh blockhash into Solana transactions when it builds them, and a blockhash expires after roughly a minute. A plan that sat too long fails to send. Build again.
- **Submit the signature as `txHash`.** A Solana signature is base58, 87 or 88 characters.

## Deposit addresses

A `TRANSFER` plan has no wallet actions. It names an address and an amount:

```json
{
  "kind": "TRANSFER",
  "orderId": "9f2c4e1a7b3d5f60812a4c6e8b0d2f41",
  "depositAddress": "0x7d2c…e41a",
  "amount": "10000000",
  "token": "0xaf88d065e77c8cc2239327c5edb3a432268e5831",
  "chainId": 42161,
  "expiresAt": 1792443600
}
```

Send exactly `amount` of `token` to `depositAddress` on `chainId`, before `expiresAt`. Any wallet can send it. Then:

- Without `submitTxHashRequested` (the native route on an EVM source): do not call submit. The order already exists, and the deposit address detects the transfer itself. For a quote made without a wallet, submit returns `400`. Track the order with `GET /v1/orders/{refundTo}?orderIds={orderId}`, where the address is the order's refund address (`refundTo`, or else `recipient`, or else `from`). `GET /v1/orders/by-id/{orderId}` does not serve these 32-character ids.
- With `submitTxHashRequested: true` (a TRON or Bitcoin source on `HyperstreamUTXO`): after the transfer, call `PUT /v1/deposit/submit` with its hash as `txHash`, written as 64 hex characters without `0x`. The response carries the `orderId` to track.

## Server-side broadcast

If the user's wallet can sign but not broadcast, send the signed transaction instead of a hash, and the API broadcasts it:

```json
{ "signedTransaction": "0x02f8…", "quoteId": "3f6c2a1e-8b4d-4e7a-9c15-2d0b7e9a4f61", "routeId": "Hyperstream" }
```

Use `0x`-prefixed hex for EVM and base64 for Solana. Send either `txHash` or `signedTransaction`, never both. A broadcast the chain rejects returns `400 BroadcastException`. Check the balance and nonce, then sign again and resubmit. See [Deposit](/v1/reference/deposit/) for every field.
