TypeScript SDK
TypeScript SDK for the Hashi protocol. Hashi is a decentralized Bitcoin collateralization primitive on Sui. Orchestrate native BTC directly from smart contracts—without centralized balance sheets.
Not production-ready. This SDK is pre-1.0 and under active development. The API may change without notice and only Sui devnet is wired up. Do not use it in production environments yet.
End-user actions only: deposit, request withdrawal, cancel withdrawal. Operator/committee/relayer calls are intentionally not part of this surface — those tools should import the generated bindings under src/contracts/hashi/ directly.
Install
pnpm add @mysten-incubation/hashi @mysten/sui
@mysten/sui is a peer dependency.
Setup
The SDK attaches to any Sui client via $extend. After extension, every Hashi method lives under client.hashi.*.
import { SuiGrpcClient } from "@mysten/sui/grpc";
import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519";
import { hashi } from "@mysten-incubation/hashi";
const client = new SuiGrpcClient({
network: "devnet",
baseUrl: "https://fullnode.devnet.sui.io:443",
}).$extend(hashi({ network: "devnet" }));
const signer = Ed25519Keypair.fromSecretKey(/* … */);
Network support. Only Sui devnet is currently wired up (Bitcoin signet by default). Testnet and mainnet are not yet deployed;
hashi({ network: "testnet" })will throw until those land. To target a custom or local deployment, passhashiObjectId,packageId, andbitcoinNetworkexplicitly.
Optional client options.
hashi({ ... })also acceptsbtcRpcUrl— a Bitcoin Core JSON-RPC URL, required for theclient.hashi.bitcoin.*lookups — andgraphqlUrl, which overrides the Sui GraphQL endpoint used bytransactionHistory(defaults tohttps://fullnode.{network}.sui.io:443/graphql).
Quickstart: Deposit BTC → mint hBTC
- Derive the unique P2TR Bitcoin deposit address for your Sui address.
- Send BTC to that address from any wallet.
- Submit the funding
txid+voutto Hashi for committee confirmation.
The committee watches the Bitcoin chain, confirms the funding tx after bitcoinConfirmationThreshold blocks, and mints hBTC to the recipient address.
const recipient = signer.toSuiAddress();
// 1. Get the deposit address.
const btcAddress = await client.hashi.generateDepositAddress({
suiAddress: recipient,
});
// 2. Send BTC to `btcAddress` from any wallet, then collect the
// funding tx's display-order txid and the vout that paid the
// deposit address. (Display-order = the form mempool.space and
// `bitcoin-cli` show — the SDK reverses internally.)
// 3. Record the deposit on Sui.
const result = await client.hashi.deposit({
signer,
txid: "0x<64-hex display-order txid>",
utxos: [{ vout: 0, amountSats: 100_000n }],
recipient,
});
if (result.$kind !== "Transaction") {
throw new Error(`deposit failed: ${JSON.stringify(result.FailedTransaction)}`);
}
// `hBTC` lands in `recipient`'s balance once the committee confirms.
A single funding tx may pay the deposit address on multiple outputs — pass them all in utxos and they're batched into one atomic Sui PTB.
Quickstart: Request withdrawal (burn hBTC → receive BTC)
Burns amountSats of hBTC from the signer's balance and enqueues a request for the committee to send BTC to bitcoinAddress. The address is decoded client-side as bech32 (P2WPKH) or bech32m (P2TR) and must match the client's configured Bitcoin network.
const result = await client.hashi.requestWithdrawal({
signer,
amountSats: 50_000n,
bitcoinAddress: "tb1q…", // P2WPKH on signet/testnet, or `tb1p…` for P2TR
});
if (result.$kind !== "Transaction") {
throw new Error(`request failed: ${JSON.stringify(result.FailedTransaction)}`);
}
// Pull the request id out of the WithdrawalRequestedEvent — needed if
// you later want to cancel.
const evt = result.Transaction.events?.find((e) =>
e.eventType.endsWith("::withdrawal_queue::WithdrawalRequestedEvent"),
);
const requestId = (evt as { json?: { request_id?: string } } | undefined)?.json?.request_id;
Quickstart: Cancel a pending withdrawal
Returns the locked hBTC to the signer. Only the original requester can cancel, only while the request is still Requested or Approved (not after committee commitment), and only after withdrawalCancellationCooldownMs has elapsed since the request. All three are enforced on-chain.
await client.hashi.cancelWithdrawal({ signer, requestId });
Tracking a deposit or withdrawal
deposit and requestWithdrawal resolve to a transaction execution result whose Transaction.digest identifies the submitted Sui tx. Pass that digest to the view.*Status readers for a one-shot check, or to the waitFor* helpers to poll until a terminal state.
const digest = result.Transaction?.digest;
// One-shot status check — returns null if the digest has no Hashi event.
const deposit = await client.hashi.view.depositStatus(digest);
// deposit?.status: "pending" | "confirmed" | "expired" | "unknown"
const withdrawal = await client.hashi.view.withdrawalStatus(digest);
// withdrawal?.status: "Requested" | "Approved" | "Processing"
// | "Signed" | "Confirmed" | "cancelled"
To block until the committee finishes, use the polling helpers. They resolve on a terminal state — deposits on confirmed/expired, withdrawals on Confirmed/cancelled:
const info = await client.hashi.waitForDeposit(digest, {
intervalMs: 15_000, // default
signal: AbortSignal.timeout(600_000), // optional cancellation
});
const wInfo = await client.hashi.waitForWithdrawal(digest);
// wInfo.btcTxid is populated once the committee commits the Bitcoin tx.
Checking hBTC balance
const { totalBalance, coinObjectCount } = await client.hashi.view.balance(signer.toSuiAddress());
// totalBalance — hBTC in satoshis; coinObjectCount — number of coin objects held.
Transaction history
view.transactionHistory returns a unified, newest-first list of deposits and withdrawals for a Sui address. Each item is a discriminated union — switch on kind:
const history = await client.hashi.view.transactionHistory(signer.toSuiAddress());
for (const item of history) {
if (item.kind === "deposit") {
console.log(item.btcTxid, item.amountSats, item.approved);
} else {
console.log(item.requestId, item.btcAmountSats, item.status);
}
}
Confirmed requests come from the on-chain index; in-flight deposits are discovered via the Sui GraphQL endpoint (graphqlUrl). If GraphQL is unavailable the call still returns the confirmed set.
Detecting already-used UTXOs
Before submitting a deposit, check whether its outputs were already recorded — re-submitting a consumed UTXO aborts on-chain.
const results = await client.hashi.view.findUsedUtxos([
{ txid: "0x<64-hex display-order txid>", vout: 0 },
]);
// results[0]: { utxoId, inActivePool, inSpentPool, isUsed }
Estimating fees
// Withdrawal: worst-case BTC network fee + on-chain minimum. Pass a sender
// to also get a dry-run gas estimate.
const fees = await client.hashi.view.withdrawalFees(signer.toSuiAddress());
// { worstCaseNetworkFeeSats, withdrawalMinimumSats, gasEstimateMist }
// Deposit: dry-run gas estimate only.
const { gasEstimateMist } = await client.hashi.view.depositGasEstimate(signer.toSuiAddress());
Gas estimation is best-effort — a failed simulation yields 0n rather than throwing.
Reading governance state
Governance parameters — the pause flag, deposit/withdrawal minimums, confirmation threshold, and cancellation cooldown — are read through client.hashi.view, the same namespace as the balance, status, history, and fee readers above. Prefer view.all() when you need 2+ values — single round-trip, internally consistent snapshot.
const snap = await client.hashi.view.all();
// { paused, bitcoinDepositMinimum, bitcoinWithdrawalMinimum,
// bitcoinConfirmationThreshold, withdrawalCancellationCooldownMs,
// worstCaseNetworkFee, ... }
Errors
Direct methods throw typed errors before signing whenever a precondition can be checked client-side. instanceof to distinguish:
| Error | Thrown when |
|---|---|
InvalidParamsError | txid/recipient not 0x-prefixed 32-byte hex, or utxos empty/duplicate |
InvalidBitcoinAddressError | bitcoinAddress fails bech32(m) decode or HRP mismatches the BTC network |
HashiPausedError | Governance has paused the operation (deposit or withdraw) |
AmountBelowMinimumError | A UTXO or withdrawal amount is below the on-chain minimum |
HashiFetchError | The Hashi shared object can't be read or has an unexpected shape |
HashiConfigError | A governance config entry is missing or malformed |
Advanced: composable transactions
The direct methods (deposit, requestWithdrawal, cancelWithdrawal) sign and execute in one call. For sponsored transactions, dry-runs, or bundling Hashi calls into a larger PTB, use the tx.* builders — they return an unsigned Transaction and leave signing to the caller.
const tx = client.hashi.tx.deposit({ txid, utxos, recipient });
// …add more commands to `tx`, then sign and execute via your usual path.
Move-call thunks are also available under client.hashi.call.* for direct composition into hand-built PTBs.
Bitcoin RPC (optional)
When the client is constructed with a btcRpcUrl, the client.hashi.bitcoin.* namespace reads the Bitcoin chain directly — useful for finding which output of a funding tx paid your deposit address, and for checking confirmations before submitting a deposit.
const client = new SuiGrpcClient({
network: "devnet",
baseUrl: "https://fullnode.devnet.sui.io:443",
}).$extend(hashi({ network: "devnet", btcRpcUrl: "http://user:pass@127.0.0.1:8332" }));
// Which output(s) of the funding tx paid the deposit address?
const output = await client.hashi.bitcoin.lookupVout(btcTxid, btcAddress);
const outputs = await client.hashi.bitcoin.lookupAllVouts(btcTxid, btcAddress);
// each result: { vout, amountSats }
// Confirmation count — 0 while the tx is still in the mempool.
const confirmations = await client.hashi.bitcoin.confirmations(btcTxid);
Calling any bitcoin.* method without btcRpcUrl configured throws.
Bitcoin address derivation
Each Sui address maps to a unique P2TR Bitcoin deposit address with two script-path leaves: an immediate 2-of-2 leaf multi_a(2, guardian, derive(mpc_master, sui_address)), and a delayed MPC-only recovery leaf and_v(v:older(delay), pk(derive(mpc_master, sui_address))). The MPC child-key derivation replicates fastcrypto_tbls::threshold_schnorr::key_derivation::derive_verifying_key; the guardian's BTC key is read from the on-chain guardian_btc_public_key config, and generateDepositAddress throws HashiConfigError until the deployment publishes it. See the Hashi address-scheme docs for the full design.