> For the complete documentation index, see [llms.txt](https://hinkal-team.gitbook.io/hinkal/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://hinkal-team.gitbook.io/hinkal/hinkal-sdk/go-sdk.md).

# GO SDK

### Hinkal Go SDK

Hinkal is a privacy middleware and smart-contract SDK for public blockchains that enables confidential 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 confidential payouts and settlements
* 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

```bash
go get github.com/Hinkal-Protocol/hinkal-go
```

Import the root package as `hinkal`:

```go
import hinkal "github.com/Hinkal-Protocol/hinkal-go"
```

### 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 shielded 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:**

```go
import hinkal "github.com/Hinkal-Protocol/Hinkal-Protocol/libs/go"

signer, err := hinkal.NewPrivateKeyEVMSigner("0x<private-key>")
if err != nil {
	return err
}

adapter, err := hinkal.NewEthersProviderAdapter()
if err != nil {
	return err
}
adapter.InitSigner(signer)

chainID := hinkal.ChainIDs.Base
if err := adapter.Init(&chainID); err != nil {
	return err
}

h := hinkal.New(nil)
if err := h.InitProviderAdapter(ctx, adapter); err != nil {
	return err
}
if err := h.InitUserKeys(ctx, hinkal.LoginMessageModeProtocol); err != nil {
	return err
}
```

**Solana:**

```go
signer, err := hinkal.NewPrivateKeySolanaSigner("<base58-private-key>")
if err != nil {
	return err
}
pubKey, err := signer.GetPublicKey(ctx)
if err != nil {
	return err
}

adapter, err := hinkal.NewSolanaProviderAdapter(hinkal.ChainIDs.SolanaMainnet, pubKey.String())
if err != nil {
	return err
}
adapter.InitConnector(signer)

h := hinkal.New(nil)
if err := h.InitProviderAdapter(ctx, adapter); err != nil {
	return err
}
if err := h.InitUserKeysFromSeedPhrases(seedWords); err != nil {
	return err
}
```

**Tron:**

```go
signer, err := hinkal.NewPrivateKeyTronSigner("<private-key-hex>")
if err != nil {
	return err
}

adapter := hinkal.NewTronProviderAdapter(hinkal.ChainIDs.TronMainnet)
if err := adapter.InitConnector(ctx, signer); err != nil {
	return err
}

h := hinkal.New(nil)
if err := h.InitProviderAdapter(ctx, adapter); err != nil {
	return err
}
if err := h.InitUserKeys(ctx, hinkal.LoginMessageModeProtocol); err != nil {
	return err
}
```

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:

```go
type Config struct {
	// Constructs the proof remotely in the secure enclave instead of locally.
	// Defaults to false: the Go SDK proves in-process.
	GenerateProofRemotely *bool

	// Disables automatic merkle tree updates. Defaults to false.
	DisableMerkleTreeUpdates bool

	// Custom cache backend. Defaults to an in-memory cache.
	CacheDevice ICacheDevice

	// Path to the cache file used for storing temporary data.
	CacheFilePath string

	// If true, allows caching in a file locally. Defaults to false.
	UseFileCache bool

	// Disables caching entirely, storing data only in memory. Defaults to false.
	DisableCaching bool

	// Preloads a previously serialized cache.
	SerializedCache map[string]string

	// Override which Tron chain this Hinkal instance targets.
	TronChainOverride int

	// Decrypts notes across goroutines when computing balances. Defaults to false.
	AllowParallelBalanceLocalDecryption bool
}
```

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

```go
h := hinkal.New(&hinkal.Config{
	UseFileCache:                        true,
	CacheFilePath:                       "/var/lib/hinkal/cache.json",
	AllowParallelBalanceLocalDecryption: true,
})
```

#### 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 shielded 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.

```go
if err := h.InitUserKeysWithEnclaveSignature(ctx, hinkal.LoginMessageModeProtocol); err != nil {
	return err
}
```

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

```go
func StoreAndGetInitialSignature(
	ctx context.Context,
	authSignature string,
	isSolanaLedger bool,
	txMessageForSolanaLedger string,
) (string, error)
```

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:

```go
initialSignature, err := h.StoreAndGetInitialSignature(ctx, authSignature, false, "")
if err != nil {
	return err
}
h.InitUserKeysWithSignature(initialSignature)
```

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.

#### Shielded balance

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

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

```go
func GetTotalBalance(
	ctx context.Context,
	chainID int,
	userKeys *UserKeys,
	ethAddress string,
	resetCacheBefore bool,
	useBlockedUtxos bool,
) ([]TokenBalance, error)
```

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`.

```go
balances, err := h.GetTotalBalance(ctx, chainID, nil, ethAddress, true, false)
if err != nil {
	return err
}
for _, b := range balances {
	fmt.Printf("%s: %s\n", b.Token.Symbol, b.Balance)
}
```

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 shielded 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 shielded address using:

```go
func Deposit(
	ctx context.Context,
	chainID int,
	erc20Addresses []string,
	amountChanges []*big.Int,
	preEstimateGas bool,
	returnTxData bool,
) (TransactionRequest, string, error)
```

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.

```go
amount := big.NewInt(10_000) // 0.01 USDC at 6 decimals
_, txHash, err := h.Deposit(ctx, chainID, []string{usdc}, []*big.Int{amount}, true, false)
if err != nil {
	return err
}
if _, err := h.WaitForTransaction(ctx, chainID, txHash, 1); err != nil {
	return err
}
```

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 shielded balance

Private Send to Public Address allows you to send tokens from your private, shielded 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 shielded address to a public address using:

```go
func Withdraw(
	ctx context.Context,
	chainID int,
	erc20Addresses []string,
	deltaAmounts []*big.Int,
	recipientAddress string,
	isRelayerOff bool,
	feeToken string,
	feeStructureOverride *FeeStructure,
) (TransactionRequest, string, error)
```

where:

* `deltaAmounts` — signed amount changes. **Negative** for the shielded 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

```go
withdrawChange := new(big.Int).Neg(withdrawAmount)
_, txHash, err := h.Withdraw(ctx, chainID, []string{usdc}, []*big.Int{withdrawChange}, recipient, false, usdc, nil)
```

#### Private Send to Private Address: transferring funds from shielded balance

Private Send to Private Address enables fully confidential transfers between shielded 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 shielded balance to another shielded address using:

```go
func Transfer(
	ctx context.Context,
	chainID int,
	erc20Addresses []string,
	amountChanges []*big.Int,
	recipientAddress string,
	feeToken string,
	feeStructureOverride *FeeStructure,
) (string, error)
```

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)

```go
transferChange := new(big.Int).Neg(transferAmount)
txHash, err := h.Transfer(ctx, chainID, []string{usdc}, []*big.Int{transferChange}, recipientInfo, usdc, nil)
```

Validate a counterparty-supplied private address before using it:

```go
if !hinkal.IsValidPrivateAddress(recipientInfo) {
	return hinkal.ErrRecipientFormatIncorrect
}
```

#### 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:

```go
func DepositAndWithdraw(
	ctx context.Context,
	chainID int,
	erc20Address string,
	recipientAmounts []*big.Int,
	recipientAddresses []string,
	txCompletionTime *int,
	feeStructureOverride *FeeStructure,
	preEstimateGas bool,
) (DepositAndSendExtendedResult, error)
```

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:

```go
type DepositAndSendExtendedResult struct {
	DepositTxHash string `json:"depositTxHash"`
	ScheduleID    string `json:"scheduleId"`
}
```

* `DepositTxHash` — on-chain hash of the deposit transaction
* `ScheduleID` — relayer schedule identifier used to fetch withdrawal status

```go
result, err := h.DepositAndWithdraw(
	ctx, chainID, usdc,
	[]*big.Int{big.NewInt(10_000)},
	[]string{recipient},
	nil, nil, true,
)
```

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

```go
type BridgeRecipient struct {
	RecipientAddress    string              `json:"recipientAddress"`
	BridgeAmount        *big.Int            `json:"bridgeAmount"`
	Quote               BridgeQuote         `json:"quote"`
	TemporarySubAccount TemporarySubAccount `json:"temporarySubAccount"`
}
```

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

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

```go
result, err := h.BridgePrivateToPrivate(ctx, srcChain, srcToken, dstChain, dstToken, amount, recipient, 0.5, feeToken)
```

#### Checking scheduled send status

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

```go
func CheckSendTransactionStatus(
	ctx context.Context,
	scheduleID string,
) (ScheduledTransactionByIDResponse, error)
```

where:

* `scheduleID` — schedule identifier returned from `DepositAndWithdraw` or `DepositAndBridge`

The function returns:

```go
type ScheduledTransactionByIDResponse struct {
	ScheduleID   string                           `json:"scheduleId"`
	ChainID      int                              `json:"chainId"`
	Transactions []ScheduledTransactionItemStatus `json:"transactions"`
}

type ScheduledTransactionItemStatus struct {
	Status        ScheduledTransactionStatus `json:"status"`
	ScheduledTime string                     `json:"scheduledTime"`
	TxHash        *string                    `json:"txHash"`
}
```

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

```go
status, err := h.CheckSendTransactionStatus(ctx, result.ScheduleID)
if err != nil {
	return err
}
for _, tx := range status.Transactions {
	if tx.Status == hinkal.ScheduledTransactionStatusCompleted {
		log.Printf("delivered: %s", *tx.TxHash)
	}
}
```

#### Swapping tokens from the shielded balance

Swapping allows you to exchange tokens directly from your shielded 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 shielded balance, swapped through the specified protocol, and the resulting tokens are deposited back into your shielded balance — all in a single private transaction.

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

```go
func Swap(
	ctx context.Context,
	chainID int,
	erc20Addresses []string,
	deltaAmounts []*big.Int,
	externalActionID ExternalActionID,
	swapData string,
	feeToken string,
	feeStructureOverride *FeeStructure,
) (string, error)
```

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:

```go
func GetEvmSwapPrices(
	ctx context.Context,
	chainID int,
	inSwapAmount string,
	inSwapTokenAddress string,
	outSwapTokenAddress string,
) (*EVMSwapPrice, error)
```

```go
type EVMSwapPrice struct {
	OutSwapAmountValue *big.Int `json:"outSwapAmountValue"`
	LifiDataValue      string   `json:"lifiDataValue"`
}
```

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

```go
quote, err := h.GetEvmSwapPrices(ctx, chainID, inSwapAmount, inToken, outToken)
if err != nil {
	return err
}

deltaAmounts := []*big.Int{new(big.Int).Neg(inAmountWei), quote.OutSwapAmountValue}
txHash, err := h.Swap(
	ctx, chainID,
	[]string{inToken, outToken},
	deltaAmounts,
	hinkal.ExternalActionLifi,
	quote.LifiDataValue,
	inToken,
	nil,
)
```

**Solana** — OKX is the routing provider:

```go
func GetSolanaSwapPrices(
	ctx context.Context,
	chainID int,
	inSwapAmount string,
	inSwapTokenAddress string,
	outSwapTokenAddress string,
) (*SolanaSwapPrice, error)
```

```go
type SolanaSwapPrice struct {
	OutSwapAmountValue *big.Int `json:"outSwapAmountValue"`
	OKXDataValue       string   `json:"okxDataValue"`
}
```

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 shielded 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 shielded balance.

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

```go
func EmporiumOp(
	contract string,
	callDataString string,
	invokeWallet bool,
	value *big.Int,
) (string, error)
```

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

```go
erc20ABI, err := abi.JSON(strings.NewReader(erc20JSON))
if err != nil {
	return err
}

approveData, err := erc20ABI.Pack("approve", common.HexToAddress(swapRouter), amountIn)
if err != nil {
	return err
}

approveOp, err := h.EmporiumOp(usdc, "0x"+hex.EncodeToString(approveData), false, nil)
if err != nil {
	return err
}
```

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

```
(address endpoint, bool invokeWallet, uint256 value, bytes data)
```

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

```solidity
(bool success, bytes memory err) = endpoint.call{value: value}(data);
```

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:

```
flatFee + (amount × variableRate ÷ 10000)
```

`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.

```go
type FeeStructure struct {
	FeeToken     string   `json:"feeToken"`
	FlatFee      *big.Int `json:"flatFee"`
	VariableRate *big.Int `json:"variableRate"` // basis points
}
```

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

```go
feeStructure, err := h.GetFeeStructure(ctx, chainID, usdc, []string{usdc}, hinkal.ExternalActionTransact, nil, nil, nil)
if err != nil {
	return err
}

totalFee := hinkal.CalculateTotalFee(transferAmount, feeStructure)
depositAmount := new(big.Int).Add(transferAmount, totalFee)
```

`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 confidential 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.
