import type { PublicKey, TransactionInstruction, Transaction, VersionedTransaction } from "@solana/web3.js"; import type { ClientConfig } from "./client/config"; /** Solana transaction wire formats: legacy, v0 (lookup tables), or SIMD-0385 v1. */ export type SolanaTransactionVersion = 'legacy' | 0 | 1; /** * A SIMD-0385 v1 transaction as @solana/kit represents it: the exact signed * message bytes plus one signature slot per required signer keyed by address * (null / all-zero = unsigned). Kept structural so wallet providers can sign v1 * message bytes without routing them through web3.js's legacy/v0-only classes. */ export interface SolanaV1Transaction { readonly messageBytes: Uint8Array; readonly signatures: Readonly>; readonly lifetimeConstraint?: { readonly blockhash: string; readonly lastValidBlockHeight: bigint; }; } export interface AuthProvider { login(): Promise; signMessage(message: string): Promise; signTransaction(transaction: Transaction | VersionedTransaction): Promise; /** * Sign a SIMD-0385 v1 transaction. Optional: a provider that omits it can only * sign legacy/v0, and the write request advertises exactly that, so the server * never hands such a client a v1 transaction. */ signTransactionV1?(transaction: SolanaV1Transaction): Promise; /** * The wire formats this provider can sign. Defaults to legacy + v0, plus v1 * when `signTransactionV1` is implemented. Providers that delegate to an * external signer (a browser wallet, a hosted signer) report what THAT signer * advertises, since a v1 transaction it cannot sign is a guaranteed failure. */ supportedTransactionVersions?(): SolanaTransactionVersion[] | Promise; /** * Signs and submits a Solana transaction to the network. * * This method handles blockhash and transaction confirmation automatically - you do NOT need to * set recentBlockhash or lastValidBlockHeight on the transaction before calling this method. * The network/RPC URL is derived from the provider's configuration (set during initialization). * * @param transaction - The transaction to sign and submit (Transaction or VersionedTransaction) * @param feePayer - Optional fee payer public key. If not provided and the transaction doesn't * already have a feePayer set, the connected wallet address will be used. * Useful for co-signing scenarios where a different account pays the fees. * @returns The transaction signature */ signAndSubmitTransaction(transaction: Transaction | VersionedTransaction, feePayer?: PublicKey): Promise; restoreSession(): Promise; logout(): Promise; getNativeMethods(): Promise; } export interface User { /** * Universal stable identity of the user (resolves @user.id). For wallet/SIWS * logins this EQUALS `address`; for email/social (Bounded Better Auth) logins * it is the account identity and `address` is normally the default Turnkey * wallet. `address` may be null for explicit wallet opt-out and unsupported * login types. Derived from the * idToken's `custom:userId` claim. Used for ownership/membership/identity. * * Optional only for manually constructed users or invalid legacy tokens. New * code should prefer `user.id` as the principal. */ id?: string; /** * REAL onchain wallet address (the Solana wallet). Present for wallet/SIWS * logins and normally for email/social logins through the default Turnkey * wallet. `null` for EVM-wallet (SIWE) logins and explicit wallet opt-out, * where the session has no Solana address - narrow before any string or * PublicKey use. Kept for backwards compatibility: for wallet users * `id === address`. */ address: string | null; /** * Verified, lowercased email of the user — present only for email-login * (Bounded Better Auth) sessions; `null`/absent for wallet/SIWS sessions. * Resolves @user.email in offchain policies. */ email?: string | null; /** * True when this is a zero-friction GUEST (anonymous) session created via * `signInAnonymously()` — a durable device-keypair identity that owns data * but has no email/real credential yet. Mirrors Firebase's `user.isAnonymous` * / Supabase's `is_anonymous`. Flips to `false` once the guest is upgraded * (links an email/social credential), with `id` preserved across the upgrade. * Apps use this to decide whether to show an "upgrade / save your account" * prompt. `false`/absent for real (email/social/wallet) logins. */ isAnonymous?: boolean; /** * REAL onchain EVM wallet address (resolves @user.evmAddress). Present for EVM * wallet/SIWE logins; `null`/absent for Solana-wallet and email-only logins * that haven't linked an EVM wallet. Canonical LOWERCASE 0x-hex — the identity * layer matches case-sensitively, so it is normalized at the boundary. Derived * from the idToken's `custom:evmWalletAddress` claim. For EVM wallet logins * `id === evmAddress` and `address` (the Solana wallet) is null. */ evmAddress?: string | null; provider: AuthProvider; } export interface SolTransaction { appId: string; txArgs: any[]; lutKey: PublicKey | null; /** Complete set of LUT addresses to use (includes lutKey when present). When provided, takes precedence over lutKey. */ additionalLutAddresses?: string[]; network: string; preInstructions: TransactionInstruction[]; /** Base64-encoded VersionedTransaction prebuilt server-side for sponsorship and/or required co-signers. */ signedTransaction?: string; } /** * Execution-lane tag for a Solana write. Reported verbatim by the realtime * worker on its 202; the SDK never infers it from the app protocol. */ export type SolanaChainTag = 'solana_devnet' | 'solana_mainnet'; /** * The execution lane a write actually ran on — NOT the app's declared protocol. * Routing is per batch, so a batch touching only unflagged collections in a * `realtime_devnet` app commits directly offchain and reports the offchain lane. * Mirrors the CLI's `Chain` field ("reports which path ran"). * * `'unknown'` is never a lane the platform runs; it appears only when a worker * predating the batch-receipt contract returned a direct commit with no lane at * all. Discarding a committed write's receipt over a missing tag would be worse * than reporting the gap honestly. */ export type ChainTag = SolanaChainTag | 'realtime_offchain' | 'unknown' | (string & {}); /** * One requested write, keyed by its CONCRETE destination path (never by index). * A `null` document means the entry was deleted or is absent. * * Positional correlation is provably wrong for Bounded writes: the worker * processes deletes before upserts and returns the deleted document's PREVIOUS * value, a missing delete and a passthrough write emit no result entry at all, * and hooks run after the result array is captured. Only the path correlates. */ export type SetEntry = { path: string; document: Record | null; }; export type SetDocuments = SetEntry[]; /** * The lifetime the signed Solana transaction actually carries, used for * confirmation and for same-signature reconciliation. * * It starts as the worker's 202 fence, and is REPLACED by the blockhash the SDK * refreshed onto the transaction immediately before the wallet signed it (see * `handlePreBuiltTransaction`). It therefore always describes the transaction * that was signed, never a blockhash the transaction no longer carries - * `expired_not_landed` is only sound while those are the same thing. */ export type SolanaFence = { blockhash: string; lastValidBlockHeight: number; }; /** * Whether server state was actually observed after a write. * * A successful sync HTTP call is NOT evidence of synchronization: the sync * endpoint returns `null` for a document that is absent, lagging, or * read-denied. `observed: true` requires every requested path to have been read * back and validated; anything else reports `observed: false` with a reason. */ export type MirrorSync = { observed: true; documents: SetDocuments; } | { observed: false; reason: string; }; /** The write committed directly on the worker (no client-signed transaction). */ export type CommittedSetResult = { status: 'committed'; /** The caller's own proposed writes — an echo, never authoritative state. */ requestedDocuments: SetDocuments; /** Post-hook server state, when the worker reported it per concrete path. */ observation: MirrorSync; chain: ChainTag; /** The batch's own transaction id, verbatim from the worker receipt. Never lifted from a result document. */ transactionId: string | null; }; /** A Solana transaction that confirmed on-chain without error. */ export type SolanaConfirmedSetResult = { status: 'confirmed'; appId: string; requestedDocuments: SetDocuments; chain: SolanaChainTag; transactionId: string; signedTransaction: string; fence: SolanaFence; /** * The RPC response context slot observed at confirmation — deliberately NOT * called `slot`. It is not the slot the transaction landed in; polling the * mirror for a `_block_number` matching it would never succeed. */ confirmationContextSlot: number; mirrorSync: MirrorSync; }; export type ConfirmedSetResult = SolanaConfirmedSetResult; /** * Solana broadcast whose confirmation outcome is unknown. The signature is * derived from the signed bytes BEFORE broadcast, so it survives a lost send * response. Never auto-resubmit — reconcile this same signature instead. */ export type SolanaSubmittedSetResult = { status: 'submitted'; appId: string; requestedDocuments: SetDocuments; chain: SolanaChainTag; transactionId: string; signedTransaction: string; fence: SolanaFence; confirmation: { state: 'unknown'; reason: string; }; }; export type SubmittedSetResult = SolanaSubmittedSetResult; /** * The one retry-safe terminal state: the write provably did NOT land. Reached * only after observed blockhash expiry PLUS history proving the signature is * absent. */ export type SolanaExpiredNotLandedSetResult = { status: 'expired_not_landed'; appId: string; requestedDocuments: SetDocuments; chain: SolanaChainTag; transactionId: string; fence: SolanaFence; }; /** * `shouldSubmitTx: false`: the transaction was signed and NOT sent. There is no * signature to report and nothing to mirror, so `transactionId` is `null` (not * an empty string) and no synchronization is attempted. */ export type SolanaSignedSetResult = { status: 'signed'; appId: string; requestedDocuments: SetDocuments; chain: SolanaChainTag; transactionId: null; signedTransaction: string; fence: SolanaFence; }; /** * What every Bounded write resolves to. Discriminate on `status` first. * * NOTE ON DELIVERY: this is not exactly-once delivery. A direct write commits * and runs hooks before its HTTP response is produced, so a LOST response to a * direct write is ambiguous. The SDK never retries writes for that reason; * resolve an ambiguous direct write by reading the path back, not by resubmitting. */ export type SetResult = CommittedSetResult | SolanaConfirmedSetResult | SolanaSubmittedSetResult | SolanaExpiredNotLandedSetResult | SolanaSignedSetResult; /** * What the Solana pre-built write lane resolves to. Discriminated so the * identifier ESCAPES an unknown confirmation outcome: * previously every lane awaited a receipt and threw on timeout, so a landed * transaction surfaced as an error with no hash and invited a duplicate submit. * * Definitive failures — a wallet user rejection, pre-broadcast validation, * or an on-chain revert/err — still THROW. Only an ambiguous transport outcome * resolves as `submitted`. */ export type SolanaConfirmedTransactionResult = { outcome: 'confirmed'; lane: 'solana'; transactionSignature: string; /** Base64 signed transaction bytes. */ signedTransaction: string; fence: SolanaFence; /** RPC response context slot at confirmation, NOT the landing slot. */ confirmationContextSlot: number; }; export type SolanaSubmittedTransactionResult = { outcome: 'submitted'; lane: 'solana'; /** Derived from the signed bytes before broadcast, so a lost send cannot orphan it. */ transactionSignature: string; signedTransaction: string; fence: SolanaFence; reason: string; }; export type SolanaSignedTransactionResult = { outcome: 'signed'; lane: 'solana'; transactionSignature: null; signedTransaction: string; fence: SolanaFence; }; export type SolanaTransactionResult = SolanaConfirmedTransactionResult | SolanaSubmittedTransactionResult | SolanaSignedTransactionResult; export type TransactionResult = SolanaTransactionResult; /** * The result of polling an already-broadcast transaction. Solana is reconciled * by core against the configured RPC. */ export type TransactionReconciliation = /** A confirmation/status exists and reports success. */ { state: 'confirmed'; confirmationContextSlot?: number; } /** It landed and FAILED on-chain. The caller throws; this is not `submitted`. */ | { state: 'failed'; reason: string; } /** Expiry observed AND history proves the signature is absent. */ | { state: 'absent_after_expiry'; } /** Nothing conclusive. Stays `submitted`. */ | { state: 'unknown'; reason: string; }; /** * The Solana RPC surface the write + reconciliation lanes use. A real * `Connection` satisfies it structurally; tests inject a double through * `_overrides._solanaRpc` so the send-loss / confirm-timeout / exactly-one- * broadcast / expiry paths are exercised without a validator. */ export interface SolanaWriteRpc { /** * Optional only because `_solanaRpc` doubles predate it - a real `Connection` * always has it. The write lane needs it to give the wallet a blockhash that * is still alive, and REFUSES (before anything is signed) rather than falling * back to a stale one when an injected RPC cannot answer. */ getLatestBlockhash?(commitment?: any): Promise<{ blockhash: string; lastValidBlockHeight: number; }>; sendRawTransaction(rawTransaction: Uint8Array, options?: { skipPreflight?: boolean; maxRetries?: number; }): Promise; confirmTransaction(strategy: { signature: string; blockhash: string; lastValidBlockHeight: number; }, commitment?: any): Promise<{ value?: { err?: unknown; } | null; context?: { slot?: number; }; }>; getTransaction(signature: string, options?: any): Promise; /** * A real web3.js `Connection` exposes its URL here. When present, the SDK reads * transaction history as raw JSON-RPC instead of through `getTransaction`: * web3.js 1.98.x throws inside its response parser on a SIMD-0385 v1 * transaction, and a throw there must not be mistaken for "not landed". */ rpcEndpoint?: string; getSignatureStatuses?(signatures: string[], config?: any): Promise<{ value?: Array; context?: { slot?: number; }; }>; getBlockHeight?(commitment?: any): Promise; } export interface TransactionReceipt { } export interface SubscriptionOptions { /** * Structured MongoDB-style filter (same shape as GetOptions.filter), e.g. * `{ status: "open", amount: { $gt: 10 } }`. Deterministic; the live feed only * delivers documents matching the filter (and obeying the read rule). */ filter?: Record; /** * Sort spec applied server-side to the live feed (same shape/semantics as * GetOptions.sort), e.g. `{ createdAt: -1 }` (1 = asc, -1 = desc). The server * sorts the read-authorized result set before delivering it. */ sort?: Record; prompt?: string; /** * Include documents from sub-paths (nested collections) in the live feed, same * as GetOptions.includeSubPaths. The server applies it to both initial data and * deltas. */ includeSubPaths?: boolean; /** Relationship shape for joined data (e.g., { owner: {}, posts: { comments: {} } }) */ shape?: Record; /** Maximum number of items to return (opt-in pagination) */ limit?: number; /** Opaque cursor for cursor-based pagination (used with limit) */ cursor?: string; /** * Opt into immediate delivery of a short-lived cached subscription snapshot. * Cache entries are scoped to the opaque authenticated principal. Anonymous * subscriptions do not populate or read the response cache. */ cache?: boolean; /** Override the app ID for this subscription (instead of the configured default) */ appId?: string; onData?: (data: any) => void; onError?: (error: any) => void; /** * @internal Per-subscription auth override. The server `WalletClient` sets this * (via `subscribe()`) so the WS connection authenticates as that wallet's * explicit identity. Mirrors `RequestOverrides`; only the fields the subscribe * path consumes. App code never sets this — it's threaded internally. */ _overrides?: { _getAuthHeaders?: () => Promise>; _walletAddress?: string; _clearAuth?: () => Promise; /** * @internal Fully-resolved config for a scoped `createClient()` instance. The * subscription connects to THIS config's realtime endpoint (wsApiUrl) instead * of the module-global one. Threaded internally; app code never sets it. */ _config?: ClientConfig; /** * @internal Read-only scoped instance marker. The websocket connects * anonymously (no auth frame) and never reads or refreshes the ambient global * session. Threaded internally by the scoped read-only client. */ _readOnly?: boolean; }; } export type TransactionOptions = { shouldSubmit?: boolean; }; export interface OffchainInstruction { type: 'set' | 'delete'; path: string; data?: Record; } export interface OffchainTransaction { id: string; version: 1; feePayer: string; instructions: OffchainInstruction[]; message: string; createdAt: number; expiresAt: number; appId: string; nonce: string; } export interface SignedOffchainTransaction { transaction: OffchainTransaction; signature: string; }