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
Go
✅
v1.25+
Linux / macOS
✅
amd64, arm64
Browser
❌
use the TypeScript SDK
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:
Build a signer - holds the key material and signs messages and transactions.
Build a provider adapter for the target chain family, and attach the signer.
Create the
Hinkalobject, 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 signatureInitUserKeysFromSeedPhrases(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 sessionisSolanaLedger- set totruefor a Solana Ledger wallettxMessageForSolanaLedger- base64-encoded transaction message used for Solana Ledger authentication. Required whenisSolanaLedgeristrue
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- passnilto use the keys held by theHinkalobjectethAddress- the connected address; pass""on SolanaresetCacheBefore- re-syncs the Merkle tree and re-decrypts notes before summing. Usetrueright after a transaction,falsefor cheap readsuseBlockedUtxos- 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 chainerc20Addresses- token contract addresses to depositamountChanges- corresponding deposit amounts in the token's smallest unit, positivepreEstimateGas- iftrue, gas is estimated before executing the depositreturnTxData- iftrue, 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 spentrecipientAddress- public address that receives the withdrawn fundsisRelayerOff- whenfalse, a relayer handles gas fees; whentrue, the user pays gas directlyfeeToken- token address used to pay protocol feesfeeStructureOverride- optional custom fee structure. Passnilto 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 withWithdrawrecipientAddress- recipient's private address string fromGetRecipientInfo(). Pass it as-is; do not reformat. It is a comma-separated string with five components:stealthAddress- recipient's stealth address (hex,0xprefix, 64–66 characters)H0[0]- first coordinate of the H0 elliptic-curve pointH0[1]- second coordinate of the H0 elliptic-curve pointH1[1]- second coordinate of the H1 elliptic-curve pointencryptionKey- recipient's encryption public key (hex,0xprefix, 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 unitrecipientAddresses- public addresses that receive the fundstxCompletionTime- optional Unix timestamp in seconds by which all scheduled withdrawals must complete. Passnilfor the default schedulefeeStructureOverride- optional custom fee structurepreEstimateGas- iftrue, gas is estimated before executing the deposit
The function returns:
DepositTxHash- on-chain hash of the deposit transactionScheduleID- 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 fromDepositAndWithdraworDepositAndBridge
The function returns:
where:
ScheduleIDis the schedule identifierChainIDis the chain on which the scheduled transactions are executedTransactionsis the list of scheduled withdrawals, one entry per recipientStatusindicates the current state of a scheduled withdrawalScheduledTimeis the planned execution time in ISO 8601 formatTxHashis the on-chain transaction hash once the withdrawal is sent on-chain, ornilbefore submission
Possible values for ScheduledTransactionStatus are:
ScheduledTransactionStatusPending(pending) - the withdrawal is scheduled and waiting for its execution timeScheduledTransactionStatusProcessing(processing) - the relayer is currently submitting the withdrawal transaction on-chainScheduledTransactionStatusWaitingForRelayer(waiting_for_relayer) - the relayer is busy; the withdrawal is queued to startScheduledTransactionStatusSentOnChain(sent_on_chain) - the withdrawal was submitted on-chain andTxHashis availableScheduledTransactionStatusCompleted(completed) - the withdrawal was confirmed on-chainScheduledTransactionStatusFailed(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 receivedexternalActionID- routing provider:hinkal.ExternalActionLifion EVM chains,hinkal.ExternalActionOkxon SolanaswapData- encoded swap parameters from the quotefeeToken- fee-payment token addressfeeStructureOverride- 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 addresscallDataString(required) - pre-encoded calldata. Go has no runtime ABI encoder equivalent to the TypeScriptfunc/argsform, so encode it withgo-ethereum'sabi.PackfirstinvokeWallet(optional) - execute from a persistent wallet address (stateful interactions)value(optional) - native value sent with the call. Passnilfor 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 publicactionPrivateWalletexecutor - that path is currently used internally byBridgePrivateToPrivate. 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:
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