Integrate Bitcoin Staking
Understand Bitcoin Staking
Before you start, please make sure you have read the Bitcoin Staking chapter and understand BitHive's Bitcoin staking workflow.
BitHive provides various tools to facilitate the implementation of Bitcoin Staking by yourself.
In this section, we introduce how to stake and unstake BTC with BitHive Relayer API @bithive/relayer-api
. This approach is recommended if you'd like to integrate with minimal efforts. You can also construct and sign transactions with BitHive SDK, which is easier to customize and will be introduced later.
Below we illustrate the implementation on BTC Testnet4. You can have a try the features on BitHive Testnet4 dApp.
You can find the working examples in the BitHive Relayer API Example repository.
Installation
Install @bithive/relayer-api
to your project with package management tools like npm
, yarn
, pnpm
, etc. Use Node.js v20 or above.
pnpm install @bithive/relayer-api
Stake Bitcoin
(1) The stake.ts
example illustrates how to sign and send Bitcoin staking transactions with @bithive/relayer-api
.
stake.ts
Click to toggle the code
import { createRelayerClient } from '@bithive/relayer-api';
// Create a relayer client for Bitcoin testnet4
const relayer = createRelayerClient({ url: "https://relayer-testnet4.bithive.fi" });
// Bitcoin provider interface that is supported by most
// Bitcoin wallets like UniSat Wallet, OKX Wallet, etc.
interface BitcoinProvider {
signPsbt: (psbt: string, options?: any) => Promise<string>;
signMessage: (message: string) => Promise<string>;
};
/**
* Stake BTC to BitHive
* @param provider BTC provider with `signPsbt` interface
* @param publicKey User public key (compressed)
* @param address User address. Supported Address Types:
* - Native Segwit (P2WPKH)
* - Nested Segwit (P2SH-P2WPKH)
* - Taproot (P2TR)
* - Legacy (P2PKH)
* @param amount Bitcoin amount (in sats) that is within the valid scope. e.g. between 0.00005 and 0.01 BTC. 5000 means 0.00005 BTC
* @param options Optional: specify the fee (in sats) or fee rate (in sat/vB) for the staking transaction. If not specified, the fee will be calculated automatically.
* @returns Staking tx hash
*/
async function stake(
provider: BitcoinProvider,
publicKey: string,
address: string,
amount: number,
options?: {
fee?: number;
feeRate?: number;
},
) {
// 1. Build the PSBT that is ready for signing
const { psbt: unsignedPsbt } = await relayer.deposit.buildUnsignedPsbt({
publicKey,
address,
amount,
...options,
});
// 2. Sign and finalize the PSBT with wallet
if (!provider.signPsbt) {
throw Error('signPsbt is not supported');
}
const signedPsbt = await provider.signPsbt(unsignedPsbt);
// 3. Submit the finalized PSBT for broadcasting and relaying
const { txHash } = await relayer.deposit.submitFinalizedPsbt({
psbt: signedPsbt,
});
return txHash;
}
async function main() {
// Stake 0.00005 BTC
const txHash = await stake(window.unisat, await window.unisat.getPublicKey(), await window.unisat.getAccounts()[0], 5000);
console.log({ txHash });
}
main().catch(console.error);
The example stake()
function contains 3 steps to submit the Bitcoin staking transaction:
- Call the
relayer.deposit.buildUnsignedPsbt
function to construct the deposit PSBT; - Sign the PSBT with BTC wallets that support signing PSBT, e.g. UniSat Wallet, OKX Wallet, etc.;
- Send the signed PSBT to the relayer with
relayer.deposit.submitFinalizedPsbt
function. The relayer will broadcast the transaction to Bitcoin network, and submit the confirmed Bitcoin transaction hash to BitHive contract on NEAR.
The deposit BTC script is as below:
OP_IF
{{sequence}}
OP_CHECKSEQUENCEVERIFY
OP_DROP
{{user pubkey}}
OP_CHECKSIG
OP_ELSE
OP_2
{{user pubkey}}
{{bithive contract pubkey}}
OP_2
OP_CHECKMULTISIG
OP_ENDIF
You can find a working Bitcoin staking example here.
(2) The view.ts
example illustrates how to view the information and status of user's deposits.
view.ts
Click to toggle the code
import { createRelayerClient } from '@bithive/relayer-api';
// Create a relayer client for Bitcoin testnet4
const relayer = createRelayerClient({ url: "https://relayer-testnet4.bithive.fi" });
// Bitcoin public key of the address, e.g. "0277...288a"
const publicKey = await window.unisat.getPublicKey();
// Get all deposits of the user
const { deposits } = await relayer.user.getDeposits({
publicKey,
});
console.log(deposits);
// Example deposits output
// [
// {
// depositTxHash: '404a2034f89e8fd0abd82b0958b67175aa2abed99e5700715f3624a4b730d87a',
// depositVout: 0,
// depositTxBlockHash: '00000000fe7b192488536de91bedbcd94c59364da5db1cfbfcfa8939d1bbfe68',
// depositTxBlockHeight: 59914,
// depositTxBlockTimestamp: 1735096660,
// depositTxBroadcastTimestamp: 1735088721032,
// withdrawTxHash: '69f9f85ef1383daebe7ce2ba2c48e8e370b9ec9ab875462738711eada0d07452',
// withdrawVin: 0,
// withdrawTxBlockHash: '00000000a453f48e1c6dc1bff3ffe676a72b9b2d3a5bb9ba34a4f9d2788f1ceb',
// withdrawTxBlockHeight: 59920,
// withdrawTxBlockTimestamp: 1735099063,
// withdrawTxBroadcastTimestamp: 1735091044159,
// status: 'WithdrawConfirmed',
// amount: 5000,
// pointsMultiplier: 16
// },
// {
// depositTxHash: 'e43abd395f867ac3a20956bc567e23b6c1f48c53a50bb7a619872c642bb66079',
// depositVout: 0,
// depositTxBlockHash: '00000000000d0321dbbda68f8f4b6afdb9b64670bf92b72b0b4c0ab0f2e1561b',
// depositTxBlockHeight: 54719,
// depositTxBlockTimestamp: 1731923491,
// depositTxBroadcastTimestamp: 1731916214552,
// status: 'DepositConfirmed',
// amount: 500000,
// pointsMultiplier: 16
// }
// ]
You can find a working example of listing deposits here.
Unstake Bitcoin
To unstake Bitcoin, you have two ways of specifying the parameters:
- Specify the deposit that you'd like to unstake and withdraw with its transaction hash. In this way, you must unstake the entire deposit (UTXO).
- Specify the amount that you'd like to unstake. The amount is valid as long as it's smaller than the total BTC amount you have staked, which means you can do partial unstaking and withdrawal. The unstaked deposits will be automatically selected to maximize the rewards of the staker. A new deposit will be staked if the total amount of the selected deposits is larger than the given amount to unstake and withdraw.
We'll show how to do both ways in the below examples.
(1) The unstake.ts
example illustrates how to unstake BTC with @bithive/relayer-api
.
unstake.ts
Click to toggle the code
import { createRelayerClient } from '@bithive/relayer-api';
// Create a relayer client for Bitcoin testnet4
const relayer = createRelayerClient({ url: "https://relayer-testnet4.bithive.fi" });
// Bitcoin provider interface that is supported by most
// Bitcoin wallets like UniSat Wallet, OKX Wallet, etc.
interface BitcoinProvider {
signPsbt: (psbt: string, options?: any) => Promise<string>;
signMessage: (message: string) => Promise<string>;
};
// Unstake a specific deposit by tx hash, or unstake a certain amount of BTC
interface UnstakeInput {
depositTxHash?: string,
amount?: number,
}
/**
* Unstake BTC from BitHive
* @param provider BTC provider with `signMessage` interface
* @param publicKey User public key (compressed)
* @param depositTxHash The deposit tx hash to unstake. Cannot be used together with amount.
* @param amount The amount to unstake. Cannot be used together with depositTxHash.
*/
async function unstake(
provider: BitcoinProvider,
publicKey: string,
{
depositTxHash,
amount
}: UnstakeInput,
) {
let deposits;
// If deposit tx hash is provided, verify that the deposit is in expected status.
if (depositTxHash) {
const { deposit } = await relayer.user.getDeposit({
publicKey,
txHash: depositTxHash,
});
if (!deposit) {
throw Error(`The specified deposit (${depositTxHash}) is not found`);
}
if (
!['DepositConfirmed', 'DepositConfirmedInvalid'].includes(deposit.status)
) {
throw Error(
`The specified deposit (${depositTxHash}) with status (${deposit.status}) is not ready to unstake`,
);
}
deposits = [
{
txHash: deposit.depositTxHash,
vout: deposit.depositVout,
},
];
}
// 1. Build the unstaking message that is ready for signing
const { message } = await relayer.unstake.buildUnsignedMessage({
deposits,
amount,
publicKey,
});
// 2. Sign the unstaking message with wallet
if (!provider.signMessage) {
throw Error('signMessage is not supported');
}
const signature = await provider.signMessage(message);
// 3. Submit the unstaking signature and relay to BitHive contract on NEAR
await relayer.unstake.submitSignature({
deposits,
amount,
publicKey,
signature: Buffer.from(signature, 'base64').toString('hex'),
});
}
async function main(partial: boolean = false) {
if (partial) {
// Unstake 0.00003 BTC out of the 0.00005 staked BTC
await unstake(window.unisat, await window.unisat.getPublicKey(), {
amount: 3000
});
} else {
// Unstake the deposit with txHash "e43abd395f867ac3a20956bc567e23b6c1f48c53a50bb7a619872c642bb66079"
await unstake(window.unisat, await window.unisat.getPublicKey(), {
depositTxHash: "e43abd395f867ac3a20956bc567e23b6c1f48c53a50bb7a619872c642bb66079"
});
}
}
main().catch(console.error);
The example unstake()
function contains 3 steps to submit the Bitcoin unstaking transaction:
- Call the
relayer.unstake.buildUnsignedMessage
function to construct the unstake message; - Sign the message with BTC wallets that support signing messages, e.g. UniSat Wallet, OKX Wallet, etc.;
- Send the signed message to the relayer with
relayer.unstake.submitSignature
function. The relayer will relay the unstaking request to BitHive contract on NEAR.
You can find a working Bitcoin unstaking example here by providing deposit transaction hash, or check out the partial unstaking example here if you'd like to specify amount only.
(2) After waiting for some time (2 days in mainnet and 5 minutes in test environments), the unstaked BTC are ready to withdraw. The withdraw.ts
example illustrates how to withdraw BTC with @bithive/relayer-api
.
withdraw.ts
Click to toggle the code
import { createRelayerClient } from '@bithive/relayer-api';
// Create a relayer client for Bitcoin testnet4
const relayer = createRelayerClient({ url: "https://relayer-testnet4.bithive.fi" });
// Bitcoin provider interface that is supported by most
// Bitcoin wallets like UniSat Wallet, OKX Wallet, etc.
interface BitcoinProvider {
signPsbt: (psbt: string, options?: any) => Promise<string>;
signMessage: (message: string) => Promise<string>;
};
// Withdraw a specific deposit by tx hash, or withdraw a certain amount of BTC
interface WithdrawInput {
depositTxHash?: string,
amount?: number,
}
/**
* Withdraw BTC from BitHive
* @param provider BTC provider with `signPsbt` interface
* @param publicKey User public key (compressed)
* @param address Recipient address (can be different with user address)
* @param depositTxHash The deposit tx hash to withdraw. Cannot be used together with amount.
* @param amount The amount to withdraw. Cannot be used together with depositTxHash.
* @param options Optional: specify the fee (in sats) or fee rate (in sat/vB) for the withdrawal transaction. If not specified, the fee will be calculated automatically.
* @returns Withdrawal tx hash
*/
async function withdraw(
provider: BitcoinProvider,
publicKey: string,
address: string,
{
depositTxHash,
amount
}: WithdrawInput,
options?: {
fee?: number;
feeRate?: number;
},
) {
// Get the account info by public key
const { account } = await relayer.user.getAccount({
publicKey,
});
let pendingSignPsbtIndex: number | undefined = undefined;
let partiallySignedPsbt: string | undefined = undefined;
if (account.pendingSignPsbts.length > 0) {
// We recommend processing all the pending PSBTs before withdrawing other deposits.
// Here we illustrate processing the 1st PSBT in the pending list.
pendingSignPsbtIndex = 0;
partiallySignedPsbt = account.pendingSignPsbts[pendingSignPsbtIndex].psbt;
} else {
let deposits;
if (depositTxHash) {
// Get the deposit by public key and deposit tx hash
const { deposit } = await relayer.user.getDeposit({
publicKey,
txHash: depositTxHash,
});
if (!deposit) {
throw Error(`The specified deposit (${depositTxHash}) is not found`);
}
if (!['UnstakeConfirmed'].includes(deposit.status)) {
throw Error(
`The specified deposit (${depositTxHash}) with status (${deposit.status}) is not ready to withdraw`,
);
}
deposits = [{
txHash: deposit.depositTxHash,
vout: deposit.depositVout,
}];
}
// 1. Build the PSBT that is ready for signing
const { psbt: unsignedPsbt, deposits: depositsToSign } = await relayer.withdraw.buildUnsignedPsbt({
deposits,
amount,
recipientAddress: address,
...options,
});
// 2. Sign the PSBT with wallet. Don't finalize it.
if (!provider.signPsbt) {
throw Error('signPsbt is not supported');
}
partiallySignedPsbt = await provider.signPsbt(unsignedPsbt, {
autoFinalized: false,
toSignInputs: depositsToSign.map((_, index) => ({
index,
publicKey,
})),
});
}
// 3. Sign the PSBT with NEAR Chain Signatures
const { psbt: fullySignedPsbt } = await relayer.withdraw.chainSignPsbt({
psbt: partiallySignedPsbt,
psbtIndex: pendingSignPsbtIndex,
});
// 4. Submit the finalized PSBT for broadcasting and relaying
const { txHash } = await relayer.withdraw.submitFinalizedPsbt({
psbt: fullySignedPsbt,
});
return txHash;
}
async function main(partial: boolean = false) {
if (partial) {
// Withdraw 0.00003 BTC out of the 0.00005 staked BTC
await withdraw(window.unisat, await window.unisat.getPublicKey(), await window.unisat.getAccounts()[0], {
amount: 3000
});
} else {
// Withdraw the deposit with txHash "e43abd395f867ac3a20956bc567e23b6c1f48c53a50bb7a619872c642bb66079"
const txHash = await withdraw(window.unisat, await window.unisat.getPublicKey(), await window.unisat.getAccounts()[0], {
depositTxHash: "e43abd395f867ac3a20956bc567e23b6c1f48c53a50bb7a619872c642bb66079"
});
console.log({ txHash });
}
}
main().catch(console.error);
The example withdraw()
function contains 4 steps to submit the Bitcoin withdrawal transaction:
- Filter out the deposits that are ready to withdraw, and call the
relayer.withdraw.buildUnsignedPsbt
function to construct the PSBT; - Sign the PSBT with BTC wallets that support signing PSBT, e.g. UniSat Wallet, OKX Wallet, etc.;
- Request withdrawal PSBT signature of BitHive contract with Chain Signatures by calling
relayer.withdraw.chainSignPsbt
; - Send the withdrawal PSBT signed by both the user and BitHive contract to the relayer with
relayer.withdraw.submitFinalizedPsbt
function. The relayer will broadcast the transaction to Bitcoin network, and submit the confirmed Bitcoin transaction hash to BitHive contract on NEAR.
You can find a working Bitcoin withdrawal example here by providing deposit transaction hash, or check out the partial withdrawal example here if you'd like to specify amount only.