> 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/sdk-description.md).

# TypeScript SDK

## Hinkal 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 SDK allows wallets, dApps, and payment platforms to integrate protocol-level privacy on Ethereum, Solana, Tron, Polygon, Base, Arbitrum, and Optimism. 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                      |
| ----------- | --------- | -------------------------- |
| Node.js     | ✅         | v18+                       |
| Browser     | ✅         | React, Next.js, vanilla JS |

### Installation

Using npm:

```sh
npm install @hinkal/common
```

Or, yarn:

```sh
yarn add @hinkal/common
```

### Usage

#### Initialization

To begin using Hinkal in your application, you'll need to initialize the SDK with your preferred Web3 connection library. Hinkal supports multiple popular libraries out of the box, allowing seamless integration with your existing wallet connection setup.

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

Each provider exposes three prepare helpers:

* `prepare*Hinkal` — signs the Hinkal login message and initializes user keys (deterministic signers)
* `prepare*HinkalWithEnclaveSignIn` — signs in through the secure enclave and stabilizes identity for non-deterministic signers (smart contract wallets, some hardware wallets)
* `prepare*HinkalFromSignature` — initializes user keys from a previously stored signature

**ethers.js:**

```typescript
import { prepareEthersHinkal } from '@hinkal/common/providers/prepareEthersHinkal';
// signer: ethers.Signer
const hinkal = await prepareEthersHinkal(signer, hinkalConfig);
```

**wagmi:**

```typescript
import { prepareWagmiHinkal } from '@hinkal/common/providers/prepareWagmiHinkal';
// connector: wagmi.Connector
// wagmiConfig: wagmi.Config
const hinkal = await prepareWagmiHinkal(connector, wagmiConfig, hinkalConfig);
```

**Solana:**

```typescript
import { prepareSolanaHinkal } from '@hinkal/common/providers/prepareSolanaHinkal';
// connector: SolanaWallet
// ethereumAddress: optional linked EVM address
const hinkal = await prepareSolanaHinkal(connector, ethereumAddress, hinkalConfig);
```

**Tron:**

```typescript
import { prepareTronHinkal } from '@hinkal/common/providers/prepareTronHinkal';
// connector: TronWeb
const hinkal = await prepareTronHinkal(connector, hinkalConfig);
```

The same `WithEnclaveSignIn` and `FromSignature` variants are available for each provider (for example, `prepareWagmiHinkalWithEnclaveSignIn`, `prepareSolanaHinkalFromSignature`).

The `hinkalConfig` is defined as follows:

```typescript
type HinkalConfig = {
  /** Disables caching in browser localStorage, storing data only in memory. Front-end only. Defaults to false. */
  disableCaching?: boolean;

  /** If true, allows caching in a file locally. Node.js only. Defaults to false. */
  useFileCache?: boolean;

  /**
   * Path to the cache file used for storing temporary data. Node.js only.
   * It should be a valid file path string. Defaults to hinkalCache.json in the current working directory.
   */
  cacheFilePath?: string;

  /**
   * Indicator controlling whether the proof should be constructed remotely in secure enclave. Defaults to true.
   */
  generateProofRemotely?: boolean;

  /** Disables automatic merkle tree updates. Defaults to false. */
  disableMerkleTreeUpdates?: boolean;

  /** Override which Tron chain this Hinkal instance targets. */
  tronChainOverride?: number;
};
```

#### 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 `prepare*HinkalWithEnclaveSignIn` instead of `prepare*Hinkal`. 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 either `initUserKeysWithSignature` or `prepare*HinkalFromSignature`:

```typescript
function storeAndGetInitialSignature(
  authSignature: string,
  isSolanaLedger?: boolean,
  txMessageForSolanaLedger?: string,
): Promise<string>;
```

Parameters:

* `authSignature` — signature from the current login session
* `isSolanaLedger` — set to `true` for a Solana Ledger wallet. Defaults to `false`
* `txMessageForSolanaLedger` — base64-encoded transaction message used for Solana Ledger authentication. Required when `isSolanaLedger` is `true`

Typical flow with a stored signature:

```typescript
const initialSignature = await hinkal.storeAndGetInitialSignature(authSignature);
hinkal.initUserKeysWithSignature(initialSignature);
```

Call this once per session, after wallet connection and before fetching balances or submitting transactions.

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

You do not need enclave sign-in if your wallet produces deterministic signatures for the same login message on every session — in that case, `prepare*Hinkal` is sufficient. It is also not needed if you persist the signature yourself via `prepare*HinkalFromSignature`, or if you use seed-phrase-based login through `initUserKeysFromSeedPhrases`.

#### 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 initializing the Hinkal object and calling `initUserKeys` (or a prepare helper), fetch balances for a specific chain:

```typescript
function getTotalBalance(
  chainId: number,
  resetCacheBefore?: boolean,
  updateTokensListBefore?: boolean,
): Promise<TokenBalance[]>;
```

`TokenBalance` contains `chainId`, `erc20Address`, `balance`, and an optional `timestamp`.

For reactive UI updates, subscribe to balance changes with USD values:

```typescript
// Current state keyed by chainId
hinkal.privateBalancesWithUSD;

// Subscribe to updates; returns unsubscribe function
const unsubscribe = hinkal.onPrivateBalancesWithUSDChange((state) => {
  // state: Record<chainId, TokenBalanceWithUsd[]>
});

// Trigger a refresh after a transaction
hinkal.refreshBalance({ chainIdToUpdate: chainId, updateType: PrivateBalanceUpdateType.Fresh });
```

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

```typescript
function deposit(
  chainId: number,
  erc20Addresses: string[],
  amountChanges: bigint[],
  preEstimateGas?: boolean,
  returnTxData?: boolean,
): Promise<
  | ethers.TransactionResponse
  | ethers.TransactionRequest
  | string
  | TronWebTypes.Transaction<TronWebTypes.TriggerSmartContract>
>;
```

where:

* `chainId` — target chain
* `erc20Addresses` — token contract addresses to deposit
* `amountChanges` — corresponding deposit amounts in the token's smallest unit
* `preEstimateGas` — if true (default), gas is estimated before executing the deposit
* `returnTxData` — if true, returns unsigned transaction data without executing. Defaults to false

On Solana, use `depositSolana(chainId, erc20Address, amount)`.

To shield funds for another user's private address, use `depositForOther` (EVM/Tron) or `depositSolanaForOther` (Solana) with their `recipientInfo` string from `getRecipientInfo()`.

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

```typescript
function withdraw(
  chainId: number,
  erc20Addresses: string[],
  deltaAmounts: bigint[],
  recipientAddress: string,
  isRelayerOff: boolean,
  feeToken?: string,
  feeStructureOverride?: FeeStructure,
): Promise<ethers.TransactionResponse | string>;
```

where:

* `recipientAddress` — public address that receives the withdrawn funds
* `isRelayerOff` — when `false`, a relayer handles gas fees; when `true`, the user pays gas directly
* `feeToken` — optional token address used to pay protocol fees
* `feeStructureOverride` — optional custom fee structure

#### Private Send to Private Address: transfering 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:

```typescript
function transfer(
  chainId: number,
  erc20Addresses: string[],
  amountChanges: bigint[],
  recipientAddress: string,
  feeToken?: string,
  feeStructureOverride?: FeeStructure,
): Promise<string>;
```

where:

* `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)

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

```typescript
function depositAndWithdraw(
  chainId: number,
  erc20Address: string,
  recipientAmounts: bigint[],
  recipientAddresses: string[],
  txCompletionTime?: number,
  feeStructureOverride?: FeeStructure,
  preEstimateGas?: boolean,
): Promise<DepositAndSendExtendedResult>;
```

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
* `feeStructureOverride` — optional custom fee structure
* `preEstimateGas` — if true (default), gas is estimated before executing the deposit

The function returns:

```typescript
type DepositAndSendExtendedResult = {
  depositTxHash: string;
  scheduleId: string;
};
```

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

For cross-chain private sends, use `depositAndBridge(chainId, erc20Address, recipientBridges, ...)` with `BridgeRecipient` entries that include bridge quotes and destination addresses.

#### Checking scheduled send status

After `depositAndWithdraw` or `depositAndBridge`, fetch scheduled withdrawal status using the returned `scheduleId`:

```typescript
function checkSendTransactionStatus(
  scheduleId: string,
): Promise<ScheduledTransactionByIdResponse>;
```

where:

* `scheduleId` — schedule identifier returned from `depositAndWithdraw` or `depositAndBridge`

The function returns:

```typescript
type ScheduledTransactionByIdResponse = {
  scheduleId: string;
  chainId: number;
  transactions: ScheduledTransactionItemStatus[];
};

type ScheduledTransactionItemStatus = {
  status: ScheduledTransactionStatus;
  scheduledTime: string;
  txHash: string | null;
};
```

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 `null` before submission

Possible values for `ScheduledTransactionStatus` are:

* `pending` — the withdrawal is scheduled and waiting for its execution time
* `processing` — the relayer is currently submitting the withdrawal transaction on-chain
* `waiting_for_relayer` — the relayer is busy; the withdrawal is queued to start
* `sent_on_chain` — the withdrawal was submitted on-chain and `txHash` is available
* `completed` — the withdrawal was confirmed on-chain
* `failed` — the withdrawal transaction failed

#### 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 integrated DEX protocols (Uniswap, 1Inch, Odos) 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:

```typescript
function swap(
  chainId: number,
  erc20Addresses: string[],
  deltaAmounts: bigint[],
  externalActionId: ExternalActionId,
  swapData: string,
  feeToken?: string,
  feeStructureOverride?: FeeStructure,
): Promise<string>;
```

where:

* `externalActionId` — swap protocol to use (`ExternalActionId.Uniswap`, `ExternalActionId.OneInch`, `ExternalActionId.Odos`, etc.)
* `swapData` — encoded swap parameters for the chosen protocol
* `feeToken` — optional fee-payment token address
* `feeStructureOverride` — optional custom fee structure

**Getting swap quotes and calldata:**

Fetch quotes from all supported aggregators for a chain in one call.

**EVM chains:**

```typescript
function getEvmSwapPrices(
  chainId: number,
  inSwapAmount: string,
  inSwapTokenAddress: string,
  outSwapTokenAddress: string,
): Promise<EvmSwapPrices>;
```

```typescript
type EvmSwapPrices = {
  uniswap: { tokenPrice: bigint; poolFee: string } | null;
  odos: { outSwapAmountValue: bigint; odosDataValue: string } | null;
  oneInch: { outSwapAmountValue: bigint; oneInchDataValue: string } | null;
};
```

Pass the swap calldata from the chosen quote to `swap` as `swapData`, with the matching `externalActionId`:

* **Uniswap** — `uniswap.poolFee` with `ExternalActionId.Uniswap` (`uniswap.tokenPrice` is the quoted output amount)
* **Odos** — `odos.odosDataValue` with `ExternalActionId.Odos`
* **1Inch** — `oneInch.oneInchDataValue` with `ExternalActionId.OneInch`

**Solana:**

```typescript
function getSolanaSwapPrices(
  chainId: number,
  inSwapAmount: string,
  inSwapTokenAddress: string,
  outSwapTokenAddress: string,
): Promise<SolanaSwapPrices>;
```

```typescript
type SolanaSwapPrices = {
  okx: { outSwapAmountValue: bigint; okxDataValue: string } | null;
};
```

Pass `okx.okxDataValue` to `swap` as `swapData` with `ExternalActionId.Okx`.

#### 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. This means you can use DeFi protocols, NFT marketplaces, or any other smart contract while maintaining full anonymity.

```typescript
function actionPrivateWallet(
  chainId: number,
  erc20Addresses: string[],
  deltaAmounts: bigint[],
  onChainCreation: boolean[],
  ops: string[],
  feeToken?: string,
  feeStructureOverride?: FeeStructure,
): Promise<string>;
```

Parameters:

* `chainId` — blockchain network identifier
* `erc20Addresses` — token contract addresses involved in the action
* `deltaAmounts` — token amount changes
* `onChainCreation` — `true` for tokens received, `false` for tokens spent
* `ops` — encoded user operations (see below)
* `feeToken` — optional fee-payment token address
* `feeStructureOverride` — optional custom fee structure

User operations (`ops`) tell the Emporium contract what to execute. Generate them with `emporiumOp`:

```typescript
function emporiumOp(
  contract: ethers.Contract | string,
  func?: string,
  args?: unknown[],
  callDataString?: string,
  invokeWallet?: boolean,
  value?: bigint,
): string;
```

Arguments:

1. `contract` *(required)* — target address or contract instance
2. `func` *(optional)* — function name to call
3. `args` *(optional)* — function arguments
4. `callDataString` *(optional)* — pre-encoded calldata; do not combine with `func` and `args`
5. `invokeWallet` *(optional)* — execute from a persistent wallet address (stateful interactions)
6. `value` *(optional)* — ETH/value sent with the call

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

```solidity
(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 (e.g., when and what swaps have been performed from his account in the past). It only needs to know how much token to swap and the exchange rate.

```typescript
const operations = [
  hinkal.emporiumOp(usdcContractInstance, 'approve', [swapRouterAddress, amountIn]),
  hinkal.emporiumOp(swapRouterContractInstance, 'exactInputSingle', [swapSingleParams]),
  hinkal.emporiumOp(wethContractInstance, 'withdraw', [amountOut]),
];
```

In this example:

* First operation approves the swap router to spend USDC tokens
* Second operation executes the swap from USDC to WETH
* Third operation unwraps WETH to ETH

**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: 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.

```typescript
const operations = [
  hinkal.emporiumOp(lpTokenInstance, 'approve', [gaugeAddressInstance, amount], undefined, true),
  hinkal.emporiumOp(gaugeAddressInstance, 'deposit', [amount, invokeWalletAddress], undefined, true),
];
```

In this example:

* First operation approves the gauge contract to spend LP tokens, executed from the persistent wallet
* Second operation deposits LP tokens into the gauge, with the wallet address as the recipient for reward tracking

### Supported Chains

Hinkal SDK is available on the following blockchain networks:

| Chain           | Chain ID   | Status |
| --------------- | ---------- | ------ |
| Ethereum        | 1          | ✅ Live |
| Arbitrum        | 42161      | ✅ Live |
| Optimism        | 10         | ✅ Live |
| Polygon         | 137        | ✅ Live |
| Base            | 8453       | ✅ Live |
| Solana          | 501        | ✅ Live |
| Tron            | 728126428  | ✅ Live |
| Tempo           | 4217       | ✅ Live |
| Arc Testnet     | 5042002    | ✅ Live |
| Sepolia Testnet | 11155111   | ✅ Live |
| Tron Nile       | 3448148188 | ✅ Live |

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