Zconnect as a standard Web wallet interface for Zcash

Motivation

The Zcash ecosystem currently has no dedicated Web wallet software, apart from a Metamask snap from the ChainSafe team, much less a standardized interface that all wallets could use as a signing gadget of sorts, similar to window.web3 or window.ethereum in Ethereum.

Zconnect

window.zconnect could function as the Zcash ecosystem analogous to window.web3/window.ethereum, exposed by Web extensions. Its interface can be limited to signing capabilities, with proof construction out-of-scope, and left to Web apps. Perhaps it could also perform trial decryption, to avoid exposing viewing keys to untrusted Web apps.

Zcash has already standardized the Partially Created Zcash Transaction format (PCZT), to transmit incomplete transactions to be passed around to different signers (such as hardware wallets). This will be the transaction format used by Zconnect.

The only thing left to spec out is the API. The rest of this post will be used as the grounds for discussion of a standardized window.zconnect API for Zcash Web apps to consume. The first candidate Web app is Zillions, which will use the Zillions Web wallet extension as the first implementer of Zconnect.

1 Like

Here’s a rough/draft API, designed with the help of Gemini. Take it as a first reference point, not as a final design lol

/**
 * Zconnect API for Zcash Web Applications.
 * 
 * Injected into the global `window` object by compatible Zcash web extensions.
 * This API delegates transaction signing and viewing key operations (trial decryption)
 * to the extension, protecting the user's spending and viewing keys from the dApp.
 */
declare global {
  interface Window {
    zconnect?: ZconnectProvider;
  }
}

/**
 * Standard Zcash network identifiers.
 */
export type ZcashNetwork = 'mainnet' | 'testnet' | 'regtest';

/**
 * A Zcash account authorized for use by the Web App.
 * Full Viewing Keys (FVK) and Spending Keys (SK) are strictly excluded.
 */
export interface ZcashAccount {
  /** The Unified Address (or legacy Sapling/Transparent address) */
  address: string;
}

/**
 * The primary interface for interacting with a Zcash wallet extension.
 */
export interface ZconnectProvider {
  /**
   * Prompts the user to authorize the Web App to connect to their wallet.
   * 
   * @returns A promise resolving to an array of connected accounts.
   */
  requestAccounts(): Promise<ZcashAccount[]>;

  /**
   * Retrieves already authorized accounts without prompting the user.
   * 
   * @returns A promise resolving to an array of connected accounts.
   */
  getAccounts(): Promise<ZcashAccount[]>;

  /**
   * Retrieves the current network the wallet is connected to.
   * 
   * @returns A promise resolving to the network identifier.
   */
  getNetwork(): Promise<ZcashNetwork>;

  /**
   * Requests the wallet to sign a Partially Created Zcash Transaction (PCZT).
   * 
   * The wallet is solely responsible for displaying transaction details to the user,
   * securing their approval, and applying the spending key signatures. The dApp is
   * responsible for all zero-knowledge proof construction prior to this call.
   * 
   * @param pczt Contiguous byte array representing the un-signed PCZT.
   * @returns A promise resolving to the signed PCZT byte array.
   */
  signPczt(pczt: Uint8Array): Promise<Uint8Array>;

  /**
   * Performs high-efficiency, zero-allocation trial decryption on Compact Blocks.
   * 
   * To minimize inter-process communication (IPC) overhead between the Web App 
   * and the Extension, inputs and outputs are packed into flat, contiguous 
   * `Uint8Array` buffers using strict byte strides.
   * 
   * ### Input Stride (152 bytes per output)
   * The `inputs` buffer must be concatenated chunks of 152 bytes formatted as:
   * - **Offset 0 (32 bytes):** Transaction ID.
   * - **Offset 32 (4 bytes):** Output Index (Little-Endian).
   * - **Offset 36 (32 bytes):** `cmu` (Sapling) or `cmx` (Orchard). The Zcash specification defines `cmx` as the $x$-coordinate of a Pallas curve point[cite: 1].
   * - **Offset 68 (32 bytes):** Ephemeral Public Key (`epk`). The specification defines this as 32 bytes[cite: 1].
   * - **Offset 100 (52 bytes):** Compact Ciphertext (the first 52 bytes of the encrypted note plaintext).
   * 
   * ### Output Stride (119 bytes per successful decryption)
   * The returned buffer contains concatenated chunks of 119 bytes for each *successful* hit:
   * - **Offset 0 (32 bytes):** Transaction ID (to map back to the block).
   * - **Offset 32 (4 bytes):** Output Index (Little-Endian).
   * - **Offset 36 (8 bytes):** Note Value in zatoshis (Little-Endian). The specification defines the value length as 64 bits[cite: 1].
   * - **Offset 44 (32 bytes):** `rseed` (for Orchard/Sapling v2) or `rcm` (for Sapling v1). The specification defines the `rseed` type as 32 bytes[cite: 1].
   * - **Offset 76 (11 bytes):** Diversifier. The specification defines the diversifier length as 88 bits[cite: 1].
   * - **Offset 87 (32 bytes):** Diversified Transmission Key (`pk_d`). The specification defines this as a 32-byte group element[cite: 1].
   * 
   * @param inputs Contiguous Uint8Array of encrypted compact outputs.
   * @returns A promise resolving to a contiguous Uint8Array of decrypted note data.
   */
  trialDecrypt(inputs: Uint8Array): Promise<Uint8Array>;

  /**
   * Subscribes to wallet state changes.
   * 
   * @param event The event name to listen for.
   * @param handler The callback function triggered on the event.
   */
  on(event: 'accountsChanged', handler: (accounts: ZcashAccount[]) => void): void;
  on(event: 'networkChanged', handler: (network: ZcashNetwork) => void): void;

  /**
   * Removes a previously attached event listener.
   * 
   * @param event The event name.
   * @param handler The specific callback function to remove.
   */
  removeListener(event: string, handler: (...args: any[]) => void): void;
}

We might also want to include an API dedicated to output ciphertexts, in order to recover the complete shielded history of an account (sent and received zec).

1 Like

Suggestion: instead of having the removeListener(), we can return the unsubscribe() function from the on() function. I believe this is a more modern way to handle events, and it avoid string literals.

const unsubscribe = window.zconnect.on(...)
// later...
unsubscribe();

If we want to be even stricter with the strings, we can create a custom method for each event

onAccountChanged(handler): unsubscribe
onNetworkChanged(handler): unsubscribe