Openfort
Openfort gives your users a secure embedded wallet from a simple email login - no seed phrase, with private keys protected by Openfort's Shield key-management service and never exposed, not even to Openfort. Because Hinkal only needs a standard signer to operate, an Openfort wallet plugs in directly: the same wallet a user signs in with can hold a private, shielded balance and run confidential deposits, withdrawals, transfers, swaps, and private sends, while the keys stay protected by Openfort the entire time.
Openfort's role is unchanged. It authenticates the user, manages the embedded wallet and its recovery, and produces signatures. Hinkal uses those signatures to authorize private operations and never takes custody of the key.
Compatibility
Browser
✅
React, Next.js
Libraries
✅
@openfort/react v1.6+
How the integration works
Three parties are involved:
The user's Openfort wallet - produces every signature, with keys protected by Openfort Shield.
The Hinkal API - an HTTP service running inside a secure enclave. It decrypts the user's shielded balance, generates the zero-knowledge proofs, and builds the on-chain transactions. The wallet's private key never reaches it.
The relayer - broadcasts private transactions, so the user's public wallet is never the on-chain sender of a confidential operation.
Installation
npm install @openfort/react ethers
# or
yarn add @openfort/react ethers1. Get a signer from Openfort
Every Hinkal request is authorized by the user's wallet. Openfort is headless: create a client, authenticate the user (email OTP), provision an EVM EOA embedded wallet, then take its EIP-1193 provider and wrap it in ethers.
activeWallet.getEthereumProvider() returns the wallet's EIP-1193 provider; wrapping it in an ethers.BrowserProvider yields a Signer. From here, every Hinkal call is identical to any other wallet.
Notes specific to Openfort
Shield key required.
shieldConfiguration.shieldPublishableKeybacks key recovery -configurefails without it. Use a real per-user recovery secret in production; a fixed password is for demos only.EOA account type. The wallet is provisioned as
AccountTypeEnum.EOA, giving a standard EIP-1193 provider and ethers signer.Per-chain signer. Openfort builds the signer for one chain at a time. To switch chains, rebuild the signer with the new chain rather than switching an existing one.
Gas pre-fill required. Openfort routes
eth_estimateGasto its backend, which returns 400 for EOA wallets. Before sending any transaction the user broadcasts (deposit, ERC-20 approval, private-send deposit), pre-fillgasLimit, fee fields, and nonce from your own RPC so ethers skips estimation:
2. How requests are authorized
Hinkal authorizes requests with a session. The user signs one message with their Openfort wallet to open the session; from then on, requests are authenticated by a session key - not by a wallet popup per request. The enclave reconstructs the same data and verifies it server-side before doing anything.
Open a session (sign once)
The client generates a secp256k1 session keypair and a sessionId, then has the Openfort wallet sign the session message. The signed message is exactly:
The session has two modes. In EIP712 mode (useEIP712: true) each transaction is additionally authorized with a per-tx typed-data signature from the wallet (next section). In Normal mode (useEIP712: false) the session message carries a fourth line - This signature can also be used to submit transactions. - and that single signature authorizes every transaction for the session's lifetime; the per-transaction wallet signature is skipped.
Per-request session signature (GET requests and Normal-mode POSTs)
Every GET request and every Normal-mode POST is signed with the session private key and sent in a header. The enclave checks it against the session's registered public key:
Transaction signature (EIP-712 typed data - EIP712 mode)
The two POST auth modes are mutually exclusive:
Mode
Body contains signature?
x-hinkal-request-signature header?
EIP712
✅ wallet typed-data signature
❌ not sent
Normal
❌ not present
✅ session key signs body
In EIP712 mode each transaction is authorized with a typed-data signature that commits to the exact operation. The enclave rebuilds the identical structure and verifies it.
The domain is:
Each operation has its own primary type. The full set:
When signing, include only the primary type you are using plus the types it references. Append feeToken (address), feeStructure (FeeStructure), and/or txCompletionTime (uint256) to the primary type's fields only when those values are present. A deposit, for example:
Two rules the enclave enforces - the signature must match exactly:
Sort
tokenAmountsby token address (checksummed, ascending) before signing - and sortrecipientsthe same way for a private send. The enclave re-sorts before verifying; an unsorted array produces a non-matching signature.The nonce is single-use and expires after 60 seconds. Generate a fresh UUID for every signed request; a reused or expired nonce is rejected server-side.
3. Read the shielded balance
The shielded balance is the user's private, encrypted holdings. Reading it uses the session: query parameters plus the session-key request-signature header.
The enclave decrypts the user's shielded outputs internally and returns the balances.
4. Shield - deposit (public → private)
A deposit moves funds from the user's public Openfort wallet into their shielded balance.
Authorize the deposit.
POST /depositwith the auth fields and details.The enclave returns an unsigned transaction (
txData).The user's Openfort wallet signs and broadcasts it - a deposit is the public on-ramp into privacy, sent from the user's own address. Pre-fill gas (section 1) before sending.
After confirmation, the funds are in the user's shielded balance.
5. Unshield - withdraw (private → public)
A withdrawal sends funds from the shielded balance to any public address. The user does not broadcast it: the enclave builds and proves the transaction and the relayer broadcasts it, so the user's wallet is never the on-chain sender.
Authorize the withdrawal.
POST /withdraw.The response is the resulting
txHash.
6. Transfer (private → private)
A transfer moves funds from the user's shielded balance to another user's shielded balance. Both sides stay private - there is no public on-chain link between sender and recipient, and the amount is not visible. The user does not broadcast it: the enclave proves the transaction and the relayer broadcasts it.
The recipient is identified by their recipient info - a private identifier the recipient obtains from GET /recipient-info(authorized by their own session) and shares with the sender. It is not a public address.
The sender then authorizes the Transfer and submits:
The enclave proves and the relayer broadcasts.
7. Swap (within the shielded balance)
A swap exchanges one token for another inside the shielded balance. It is a two-call flow: first fetch a quote, then execute. The Swap typed data commits to the quote via externalActionId and swapData.
As with withdrawals and transfers, the enclave proves and the relayer broadcasts.
8. Private send (one deposit → many private payouts)
A private send deposits public funds once and has the enclave pay out privately to one or more recipients - the on-chain link between sender and recipients is never visible. The PrivateSend typed data commits to the token and the full recipient set.
POST /private-sendwith the token, recipients, and amounts → returns an order (orderId,approvalAddress,serializedTx,amountIn,amountOut,fee).Approve the ERC-20 spend if
approvalAddressis set.Sign and broadcast the deposit with the user's Openfort wallet.
Poll
GET /private-send/{orderId}until every scheduled payout completes.
amountIn is what leaves the wallet; amountOut is what recipients collectively receive; the difference is the protocol fee. Amounts are in the token's smallest unit. The user broadcasts the single deposit; the enclave then proves and the relayer broadcasts each private payout.
Summary
Open session
POST /create-session
Wallet message signature
-
Read balance
GET /balance
Session key (request signature)
-
Deposit (shield)
POST /deposit
Deposit typed data / session key
The user's wallet
Withdraw (unshield)
POST /withdraw
Withdraw typed data / session key
Relayer
Transfer (private → private)
GET /recipient-info + POST /transfer
Transfer typed data / session key
Relayer
Swap
GET /get-swap-data + POST /swap
Swap typed data / session key
Relayer
Private send
POST /private-send + poll
PrivateSend typed data / session key
User (deposit) + relayer (payouts)
Last updated