@hinkal/react-native
v0.3.4
Published
Prebundled Hinkal SDK for React Native — no Metro config required.
Readme
Hinkal React Native 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.
@hinkal/react-native is a prebundled build of the Hinkal SDK for React Native and Expo. It includes the polyfills, shims, and worker runtime required on mobile — no custom Metro configuration is needed.
The SDK allows mobile wallets, dApps, and payment apps to integrate protocol-level privacy on Ethereum, Solana, Tron, Polygon, Base, Arbitrum, and Optimism.
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 | | ------------ | --------- | ------------------ | | React Native | ✅ | v0.74+ | | Expo | ✅ | dev client or bare |
Installation
npm install @hinkal/react-nativeOr, yarn:
yarn add @hinkal/react-nativeUsage
HinkalProvider
Wrap your app with HinkalProvider before using any SDK function. It runs the React Native bootstrap and mounts the hidden WebView worker host.
import { WagmiProvider } from 'wagmi';
import { HinkalProvider } from '@hinkal/react-native';
import { wagmiConfig } from './wagmiConfig';
export default function App() {
return (
<WagmiProvider config={wagmiConfig}>
<HinkalProvider>
<YourApp />
</HinkalProvider>
</WagmiProvider>
);
}Initialization
After the wallet is connected, initialize a Hinkal instance with your preferred provider helper.
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
wagmi:
import { prepareWagmiHinkal } from '@hinkal/react-native';
// connector: wagmi.Connector
// wagmiConfig: wagmi.Config
const hinkal = await prepareWagmiHinkal(connector, wagmiConfig, hinkalConfig);ethers.js:
import { prepareEthersHinkal } from '@hinkal/react-native';
const hinkal = await prepareEthersHinkal(signer, hinkalConfig);Solana:
import { prepareSolanaHinkal } from '@hinkal/react-native';
// connector: SolanaWallet
// ethereumAddress: optional linked EVM address
const hinkal = await prepareSolanaHinkal(connector, ethereumAddress, hinkalConfig);Tron:
import { prepareTronHinkal } from '@hinkal/react-native';
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:
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.
* 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:
function storeAndGetInitialSignature(
authSignature: string,
isSolanaLedger?: boolean,
txMessageForSolanaLedger?: string,
): Promise<string>;Parameters:
authSignature— signature from the current login sessionisSolanaLedger— set totruefor a Solana Ledger wallet. Defaults tofalsetxMessageForSolanaLedger— base64-encoded transaction message used for Solana Ledger authentication. Required whenisSolanaLedgeristrue
Typical flow with a stored signature:
const initialSignature = await hinkal.storeAndGetInitialSignature(authSignature);
hinkal.initUserKeysWithSignature(initialSignature);Call this once per session, after wallet connection and before fetching balances or submitting transactions.
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.
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 initializing the Hinkal object and calling initUserKeys (or a prepare helper), fetch balances for a specific chain:
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:
// 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 moves tokens from a public blockchain address into a private, encrypted balance. Once shielded, tokens are no longer visible on-chain to external observers.
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 chainerc20Addresses— token contract addresses to depositamountChanges— corresponding deposit amounts in the token's smallest unitpreEstimateGas— if true (default), gas is estimated before executing the depositreturnTxData— 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 sends tokens from a shielded balance to any public blockchain address without exposing the sender.
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 fundsisRelayerOff— whenfalse, a relayer handles gas fees; whentrue, the user pays gas directlyfeeToken— optional token address used to pay protocol feesfeeStructureOverride— optional custom fee structure
Private Send to Private Address: transferring funds from shielded balance
Private Send to Private Address enables fully confidential transfers between shielded balances.
function transfer(
chainId: number,
erc20Addresses: string[],
amountChanges: bigint[],
recipientAddress: string,
feeToken?: string,
feeStructureOverride?: FeeStructure,
): Promise<string>;where:
recipientAddress— 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)
Private Send from Public to Public addresses
Private Send from Public to Public transfers tokens between two public addresses through Hinkal's privacy infrastructure. Tokens are shielded from the sender, then withdrawn to recipient public addresses on a relayer schedule.
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 unitrecipientAddresses— public addresses that receive the fundstxCompletionTime— optional Unix timestamp in seconds by which all scheduled withdrawals must completefeeStructureOverride— optional custom fee structurepreEstimateGas— if true (default), gas is estimated before executing the deposit
The function returns:
type DepositAndSendExtendedResult = {
depositTxHash: string;
scheduleId: string;
};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:
function checkSendTransactionStatus(scheduleId: string): Promise<ScheduledTransactionByIdResponse>;Possible values for ScheduledTransactionStatus:
pending— scheduled, waiting for execution timeprocessing— relayer is submitting the withdrawal on-chainwaiting_for_relayer— relayer is busy; withdrawal is queuedsent_on_chain— submitted on-chain;txHashis availablecompleted— confirmed on-chainfailed— withdrawal transaction failed
Swapping tokens from the shielded balance
function swap(
chainId: number,
erc20Addresses: string[],
deltaAmounts: bigint[],
externalActionId: ExternalActionId,
swapData: string,
feeToken?: string,
feeStructureOverride?: FeeStructure,
): Promise<string>;Getting swap quotes and calldata:
EVM chains:
function getEvmSwapPrices(
chainId: number,
inSwapAmount: string,
inSwapTokenAddress: string,
outSwapTokenAddress: string,
): Promise<EvmSwapPrice | null>;type EvmSwapPrice = {
outSwapAmountValue: bigint;
lifiDataValue: string;
};Returns null if no quote could be fetched. Pass the swap calldata from the quote to swap as swapData, with ExternalActionId.Lifi (outSwapAmountValue is the quoted output amount):
- LI.FI —
lifiDataValuewithExternalActionId.Lifi
Solana:
function getSolanaSwapPrices(
chainId: number,
inSwapAmount: string,
inSwapTokenAddress: string,
outSwapTokenAddress: string,
): Promise<SolanaSwapPrice | null>;type SolanaSwapPrice = {
outSwapAmountValue: bigint;
okxDataValue: string;
};Returns null if no quote could be fetched. Pass okxDataValue to swap as swapData with ExternalActionId.Okx.
Interacting with smart contracts privately
function actionPrivateWallet(
chainId: number,
erc20Addresses: string[],
deltaAmounts: bigint[],
onChainCreation: boolean[],
ops: string[],
feeToken?: string,
feeStructureOverride?: FeeStructure,
): Promise<string>;Generate user operations with emporiumOp:
function emporiumOp(
contract: ethers.Contract | string,
func?: string,
args?: unknown[],
callDataString?: string,
invokeWallet?: boolean,
value?: bigint,
): string;Stateless interactions (swaps, simple staking) use default invokeWallet: false.
Stateful interactions (reward tracking, voting power) require invokeWallet: true so the call runs from a persistent wallet address.
const operations = [
hinkal.emporiumOp(usdcContractInstance, 'approve', [swapRouterAddress, amountIn]),
hinkal.emporiumOp(swapRouterContractInstance, 'exactInputSingle', [swapSingleParams]),
];Supported Chains
| 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 |
References
Wallet: Hinkal Wallet
Application: Hinkal Pay
Docs: Hinkal Documentation
