For the complete documentation index, see llms.txt. This page is also available as Markdown.

GO SDK

Hinkal Go SDK

Hinkal is a privacy middleware and smart-contract SDK for public blockchains that enables private transactions and settlement flows without changing wallets, custody, or chains.

The Go SDK is a native Go implementation of the Hinkal SDK. It is a port, not a binding: UTXO management, Merkle tree synchronization, note decryption, and transaction construction all run in pure Go, with no Node.js runtime and no WebAssembly. Zero-knowledge proofs are generated remotely, in the secure enclave.

The SDK allows backends and payment platforms to integrate protocol-level privacy on Ethereum, Solana, Tron, Polygon, Base, Arbitrum, BNB Chain, Tempo and Arc Testnet. It hides transaction history, wallet relationships, and asset flows on-chain while preserving public-chain finality and compliance.

With Hinkal SDK, developers can:

  • Enable private sends between public wallets

  • Perform private payments

  • Route transactions through Hinkal's privacy contracts without exposing sender, recipient, or amounts

  • Maintain non-custodial control with optional compliance visibility via viewing keys

Compatibility

Environment
Supported
Notes

Go

v1.25+

Linux / macOS

amd64, arm64

Browser

use the TypeScript SDK

Environment
Supported
Notes

Go

v1.25+

Linux / macOS

amd64, arm64

Browser

use the TypeScript SDK

Installation

Import the root package as hinkal:

Usage

Initialization

Initialization is explicit and always follows the same three steps:

  1. Build a signer - holds the key material and signs messages and transactions.

  2. Build a provider adapter for the target chain family, and attach the signer.

  3. Create the Hinkal object, attach the adapter, then derive the user keys.

Initializing the SDK creates a Hinkal object that encapsulates:

  • The user's private balances

  • Actions the user can perform, such as shielding (depositing), transfers, and swapping

  • Cryptographic keys for privacy-preserving operations

Four key-derivation methods are available:

  • InitUserKeys(ctx, mode) - signs the Hinkal login message and initializes user keys (deterministic signers)

  • InitUserKeysWithEnclaveSignature(ctx, mode) - signs in through the secure enclave and stabilizes identity for non-deterministic signers (smart contract wallets, some hardware wallets)

  • InitUserKeysWithSignature(signature) - initializes user keys from a previously stored signature

  • InitUserKeysFromSeedPhrases(words) - derives keys from a seed phrase, with no wallet signature involved

EVM:

Solana:

Tron:

A single Hinkal object can hold one adapter per chain family. Call InitProviderAdapter again with another adapter to add a chain, SwitchNetworkByChainID(chainID) to change the active EVM network, and ResetProviderAdapters() to drop them all. Call Destroy() on shutdown.

The config is defined as follows:

For a long-lived server process, enable the file cache so restarts do not re-scan the whole tree:

Identity persistence

When a user connects their wallet, they sign a fixed login message to authenticate with Hinkal. That signature defines their Hinkal identity. Their private balances, transaction ability, and all private operations depend on it.

Most wallets return the same signature every time for the same message. Some do not. Smart contract wallets, certain hardware wallets, and other non-deterministic signers may produce a different signature on each login, even for the same address and message.

When that happens, a returning user appears as a new account. Funds deposited in an earlier session remain tied to the original identity and are not accessible from the new one.

Recommended approach: use InitUserKeysWithEnclaveSignature instead of InitUserKeys. It signs the login message, stores the first signature server-side through the secure enclave, and always initializes with the original identity on later sessions. Solana Ledger wallets are handled automatically.

Manual approach: if you manage identity yourself, call StoreAndGetInitialSignature and then InitUserKeysWithSignature:

Parameters:

  • authSignature - signature from the current login session

  • isSolanaLedger - set to true for a Solana Ledger wallet

  • txMessageForSolanaLedger - base64-encoded transaction message used for Solana Ledger authentication. Required when isSolanaLedger is true

Typical flow with a stored signature:

Call this once per session, after the provider adapter is attached and before fetching balances or submitting transactions.

Two login messages exist. LoginMessageModeProtocol is the standard shielded-account message. LoginMessageModePrivateTransfer derives a separate identity used for private-send-only flows. They produce different accounts - pick one and stay on it.

You do not need enclave sign-in if your signer produces deterministic signatures for the same login message on every session. The bundled NewPrivateKeyEVMSigner, NewPrivateKeyTronSigner, and NewPrivateKeySolanaSigner are deterministic, so InitUserKeys is sufficient. It is also not needed if you persist the signature yourself via InitUserKeysWithSignature, or if you use seed-phrase-based login through InitUserKeysFromSeedPhrases.

Security

The stored signature is protected at every stage. Before leaving the client, the signature is encrypted with hybrid encryption. The payload is encrypted with a symmetric key, and that key is encrypted with the enclave's public key.

Inside the secure enclave, Google Cloud KMS decrypts the symmetric key. Only then is the signature decrypted. The plaintext signature never leaves the enclave unprotected.

At rest, only the encrypted signature and encrypted key are stored in the database. A caller cannot retrieve a stored signature by wallet address alone. Each request must include any valid signature that proves wallet ownership.

Requests that fail this check are rejected. The first signature stored for a given address is never replaced. Later logins only use a fresh signature to authenticate retrieval of the original.

Private Balance

Private balances are encrypted token holdings stored within the Hinkal protocol. Unlike regular blockchain balances that are publicly visible, Private balances are hidden from external observers.

After the provider adapter is attached and the user keys are initialized, fetch balances for a specific chain:

where:

  • userKeys - pass nil to use the keys held by the Hinkal object

  • ethAddress - the connected address; pass "" on Solana

  • resetCacheBefore - re-syncs the Merkle tree and re-decrypts notes before summing. Use true right after a transaction, false for cheap reads

  • useBlockedUtxos - includes notes that are still pending or blocked

TokenBalance contains a resolved Token (ERC20Token, carrying chain id, address, symbol, and decimals), a *big.Int Balance in the token's smallest unit, and an optional Timestamp.

The Go SDK has no reactive balance subscription. Server-side callers poll GetTotalBalance after a transaction confirms.

Use GetStuckShieldedBalances to list notes that failed to settle, and WithdrawStuckUtxos(ctx, chainID, erc20Address, recipientAddress) to recover them.

Shielding: depositing funds to the private balance

Shielding is the process of moving your tokens from a public blockchain address into a private, encrypted balance. Once shielded, your tokens are no longer visible on-chain to external observers. This provides privacy for your holdings and subsequent transactions.

A user can deposit funds to their private address using:

where:

  • chainID - target chain

  • erc20Addresses - token contract addresses to deposit

  • amountChanges - corresponding deposit amounts in the token's smallest unit, positive

  • preEstimateGas - if true, gas is estimated before executing the deposit

  • returnTxData - if true, returns unsigned transaction data without executing

The first return value is the unsigned TransactionRequest, populated when returnTxData is true. The second is the transaction hash.

On Solana, use DepositSolana(ctx, chainID, erc20Address, amount, returnTxData).

To shield funds for another user's private address, use DepositForOther (EVM/Tron) or DepositSolanaForOther (Solana) with their recipientInfo string from GetRecipientInfo().

ProoflessDeposit shields directly to a set of stealth addresses without generating a proof, for high-throughput payout systems that construct recipients themselves.

Private Send to Public Address: withdrawing funds from the private balance

Private Send to Public Address allows you to send tokens from your private balance directly to any public blockchain address. The sender's identity is not exposed during this transaction. The recipient receives the funds at their public address, where the tokens become visible on-chain. This is useful when you need to interact with public DeFi protocols, send funds to exchanges, or transfer to any public wallet while maintaining privacy for your shielded balance.

A user can withdraw funds from their private address to a public address using:

where:

  • deltaAmounts - signed amount changes. Negative for the private balance being spent

  • recipientAddress - public address that receives the withdrawn funds

  • isRelayerOff - when false, a relayer handles gas fees; when true, the user pays gas directly

  • feeToken - token address used to pay protocol fees

  • feeStructureOverride - optional custom fee structure. Pass nil to let the SDK fetch one

Private Send to Private Address: transferring funds from private balance

Private Send to Private Address enables fully private transfers between private balances. Both the sender and recipient remain anonymous, and the transaction amount is hidden from external observers. This is the most private way to transfer tokens, as neither party's identity nor the transaction details are exposed on-chain.

A user can transfer tokens from their private balance to another private address using:

where:

  • amountChanges - negative amounts, as with Withdraw

  • recipientAddress - recipient's private address string from GetRecipientInfo(). Pass it as-is; do not reformat. It is a comma-separated string with five components:

    • stealthAddress - recipient's stealth address (hex, 0x prefix, 64–66 characters)

    • H0[0] - first coordinate of the H0 elliptic-curve point

    • H0[1] - second coordinate of the H0 elliptic-curve point

    • H1[1] - second coordinate of the H1 elliptic-curve point

    • encryptionKey - recipient's encryption public key (hex, 0x prefix, 66 characters)

Validate a counterparty-supplied private address before using it:

Private Send from Public to Public addresses

Private Send from Public to Public addresses enables you to transfer tokens between two public addresses while using Hinkal's privacy infrastructure. The tokens are first shielded from the sender's public address, then unshielded to the recipient's public address either immediately or after some interval. This ensures there is no traceable connection between the sender and recipient on-chain, providing transaction privacy even when both parties use public addresses.

A user can perform a private transfer between public addresses using:

where:

  • erc20Address - token contract address (single-token transfers only)

  • recipientAmounts - amounts to send to each recipient in the token's smallest unit

  • recipientAddresses - public addresses that receive the funds

  • txCompletionTime - optional Unix timestamp in seconds by which all scheduled withdrawals must complete. Pass nil for the default schedule

  • feeStructureOverride - optional custom fee structure

  • preEstimateGas - if true, gas is estimated before executing the deposit

The function returns:

  • DepositTxHash - on-chain hash of the deposit transaction

  • ScheduleID - relayer schedule identifier used to fetch withdrawal status

For cross-chain private sends, use DepositAndBridge(ctx, chainID, erc20Address, recipients, txCompletionTime, feeStructureOverride, preEstimateGas) with BridgeRecipient entries that include bridge quotes and destination addresses:

NearDepositAndBridge covers NEAR Intents routes. Quotes come from hinkal.GetNearIntentsQuote, supported assets from hinkal.GetNearIntentsTokens.

BridgePrivateToPrivate moves a private balance from one chain to another without ever unshielding:

Checking scheduled send status

After DepositAndWithdraw or DepositAndBridge, fetch scheduled withdrawal status using the returned ScheduleID:

where:

  • scheduleID - schedule identifier returned from DepositAndWithdraw or DepositAndBridge

The function returns:

where:

  • ScheduleID is the schedule identifier

  • ChainID is the chain on which the scheduled transactions are executed

  • Transactions is the list of scheduled withdrawals, one entry per recipient

  • Status indicates the current state of a scheduled withdrawal

  • ScheduledTime is the planned execution time in ISO 8601 format

  • TxHash is the on-chain transaction hash once the withdrawal is sent on-chain, or nil before submission

Possible values for ScheduledTransactionStatus are:

  • ScheduledTransactionStatusPending (pending) - the withdrawal is scheduled and waiting for its execution time

  • ScheduledTransactionStatusProcessing (processing) - the relayer is currently submitting the withdrawal transaction on-chain

  • ScheduledTransactionStatusWaitingForRelayer (waiting_for_relayer) - the relayer is busy; the withdrawal is queued to start

  • ScheduledTransactionStatusSentOnChain (sent_on_chain) - the withdrawal was submitted on-chain and TxHash is available

  • ScheduledTransactionStatusCompleted (completed) - the withdrawal was confirmed on-chain

  • ScheduledTransactionStatusFailed (failed) - the withdrawal transaction failed

Swapping tokens from the private balance

Swapping allows you to exchange tokens directly from your private balance without revealing your identity. The swap is executed through an integrated routing provider while keeping your transaction private. Your tokens are withdrawn from your private balance, swapped through the specified protocol, and the resulting tokens are deposited back into your private balance - all in a single private transaction.

A user can swap tokens directly from their private balance using:

where:

  • erc20Addresses - [inputToken, outputToken]

  • deltaAmounts - [-inputAmount, +expectedOutputAmount]: negative for the token spent, positive for the token received

  • externalActionID - routing provider: hinkal.ExternalActionLifi on EVM chains, hinkal.ExternalActionOkx on Solana

  • swapData - encoded swap parameters from the quote

  • feeToken - fee-payment token address

  • feeStructureOverride - optional custom fee structure

Getting swap quotes and calldata:

EVM chains - LiFi is the routing provider:

Pass LifiDataValue to Swap as swapData, with hinkal.ExternalActionLifi:

Solana - OKX is the routing provider:

Pass OKXDataValue to Swap as swapData with hinkal.ExternalActionOkx.

hinkal.GetLifiPrice(ctx, inToken, outToken, amount, slippage, fromAddress, toAddress) is exported directly for callers that need to control slippage and routing addresses themselves.

Interacting with smart contracts privately

The SDK lets you interact with any smart contract on the blockchain while keeping your identity private. When you initiate a private wallet action, your funds are first unshielded from your Hinkal private balance to an intermediary called an Emporium contract. The Emporium then executes your desired actions on-chain (such as swaps, staking, or other DeFi interactions) without exposing who initiated them. After the operations complete, the resulting tokens are automatically shielded back into your Hinkal private balance.

User operations tell the Emporium contract what to execute. Generate them with EmporiumOp:

Arguments:

  • contract (required) - target contract address

  • callDataString (required) - pre-encoded calldata. Go has no runtime ABI encoder equivalent to the TypeScript func/args form, so encode it with go-ethereum's abi.Pack first

  • invokeWallet (optional) - execute from a persistent wallet address (stateful interactions)

  • value (optional) - native value sent with the call. Pass nil for zero

When the Emporium contract executes a user operation, it receives the data in this format:

This enables the Emporium contract to execute generic calls using user operations:

User operations can be categorized into two types based on whether the target protocol needs to track the caller account's history.

Stateless interactions are operations where the resulting token amount changes depend only on the calldata provided. Two different accounts executing the same calldata should receive the same result, regardless of their transaction history. Examples include token swaps, liquidity provision, and simple staking operations.

For example, consider exchanging USDC for ETH using a DEX. To perform a swap, the DEX does not need to know historical data about the caller. It only needs to know how much token to swap and the exchange rate. Three operations are needed: approve the swap router to spend USDC, execute the swap from USDC to WETH, then unwrap WETH to ETH. Leave invokeWallet as false.

Stateful interactions are operations where the target protocol needs to track the account's history for future calculations, such as staking rewards, voting power, or checkpoints. In these cases, set invokeWallet to true to ensure the operation is executed from a persistent wallet address that the protocol can track.

Consider a scenario where you have already staked Curve LP tokens and want to claim your rewards. The gauge contract needs to track your staking history to calculate accumulated rewards, so it must recognize the same wallet address across multiple interactions. Approve the gauge from the persistent wallet, then deposit with the wallet address as the reward recipient.

The Go SDK exposes user-operation construction through EmporiumOp, but does not yet expose a public actionPrivateWallet executor - that path is currently used internally by BridgePrivateToPrivate. Use the TypeScript SDK for arbitrary private contract interaction until the Go executor is published.

Fees

The relayer fee is a flat fee plus a variable fee:

VariableRate is in basis points - 10 means 0.1%. The flat fee covers the on-chain gas cost of the transaction, estimated by the relayer per chain, converted to USD, then priced in the chosen fee token.

Fetch a quote before sending, and use it to size the transaction:

hinkal.CalculateWithdrawalAmount(amountWithFee, feeStructure) performs the inverse: given the negative amount change the user spends, it returns what the recipient actually receives after the flat and variable fees.

On Solana, pass a *hinkal.SolanaGasEstimateParams as the last argument to GetFeeStructure so the relayer can price compute units.

Supported Chains

Hinkal Go SDK is available on the following blockchain networks:

Chain
Chain ID
Status

Ethereum

1

✅ Live

Arbitrum

42161

✅ Live

Polygon

137

✅ Live

Base

8453

✅ Live

Tempo

4217

✅ Live

BNB

56

✅ Live

Solana

501

✅ Live

Tron

728126428

✅ Live

Arc Testnet

5042002

✅ Live

Tron Nile

3448148188

✅ Live

Each chain supports the full suite of Hinkal privacy features including shielding, transfers, and private interactions with DeFi protocols.

Query the live list at runtime with h.GetSupportedChains(), and check a single chain with h.IsSelectedNetworkSupported(chainID).

Avalanche (43114), Cronos (25), Monad (143), Plasma (9745), Ink (57073), and HyperEVM (999) are bridge destinations only - reachable as DepositAndBridge targets, with no Hinkal contracts deployed.

Last updated