Skip to main content
With Privy, you can create Bitcoin segwit and taproot wallets and sign over transactions and any other arbitrary input. See the Tier 2 section for more details.

Signing transaction inputs

Bitcoin uses the UTXO model, where each transaction consumes one or more inputs and produces one or more outputs. To sign a transaction using Privy, use Privy’s raw sign functionality to sign each input hash, and then add the signature(s) to the transaction.

Segwit

Segwit (SegWit v0) wallets use the ECDSA signing algorithm with the secp256k1 curve. Use Privy’s raw sign functionality to sign each input UTXO for your Bitcoin segwit transaction.

Example

The following is an example using the scure/btc-signer library to sign a segwit transaction input.
import {p2pkh, OutScript} from '@scure/btc-signer/payment';
import {getInputType, getPrevOut, Transaction} from '@scure/btc-signer/transaction';
import {concatBytes} from '@scure/btc-signer/utils';
import secp256k1 from 'secp256k1';

const publicKey = "<the wallet's public key>";

const publicKeyBuffer = Buffer.from(publicKey, 'hex');
const tx = new Transaction({version: 1, allowLegacyWitnessUtxo: true});
// add as many outputs as needed, in this example there is only one
tx.addOutputAddress(outputAddress, outputAmount);

tx.addInput({
  txid, // buffer of utxo txid
  index: 0, // index of the output in the tx
  witnessUtxo: {
    amount: inputAmount,
    script: p2pkh(publicKeyBuffer).script
  }
});

// Loop through each input and sign it
for (let i = 0; i < tx.inputsLength; i++) {
  const input = tx.getInput(i);
  const inputType = getInputType(input, tx.opts.allowLegacyWitnessUtxo);
  const prevOut = getPrevOut(input);
  let script = inputType.lastScript;
  if (inputType.last.type === 'wpkh') {
    script = OutScript.encode({type: 'pkh', hash: inputType.last.hash});
  }
  const hash = tx.preimageWitnessV0(i, script, inputType.sighash, prevOut.amount);

  const signature = ''; // call Privy's raw sign function with bytesToHex(hash), returns '0x...'

  // convert to DER format
  const derSig = secp256k1.signatureImport(signature);

  // update the input with the signature
  tx.updateInput(
    i,
    {
      partialSig: [[publicKeyBuffer, concatBytes(derSig, new Uint8Array([inputType.sighash]))]]
    },
    true
  );
}

// finalize the transaction. It is now ready to be sent!
tx.finalize();

Taproot

Taproot (SegWit v1) wallets use the Schnorr signing algorithm (BIP-340) with the secp256k1 curve. Privy automatically applies the BIP-341 key tweak when signing with a taproot wallet, so the resulting signature is valid for key-path spends against the wallet’s P2TR output.
Privy’s taproot support uses key-path spending only. The applied tweak assumes no custom Merkle roots or scripts, so script-path spends (e.g., custom Tapscript trees) are not supported at this time.

Example

The following is an example using the scure/btc-signer library to sign a taproot transaction input. Note that taproot uses preimageWitnessV1 for the BIP-341 sighash and attaches the 64-byte Schnorr signature as tapKeySig (rather than partialSig as with segwit).
import {p2tr} from '@scure/btc-signer/payment';
import {Transaction, getInputType, getPrevOut} from '@scure/btc-signer/transaction';
import {bytesToHex} from 'viem';

// The wallet's `public_key` from the Privy Wallet object (33-byte compressed secp256k1 key, hex)
const publicKey = wallet.public_key;

// Extract the 32-byte x-only public key by stripping the first byte (compression prefix)
const pubKeyBytes = Buffer.from(publicKey, 'hex');
const xOnlyPubKey = pubKeyBytes.subarray(1);

// Build the P2TR output for this wallet
const taprootOutput = p2tr(xOnlyPubKey);

const tx = new Transaction({allowLegacyWitnessUtxo: true});

// Add outputs (as many as needed)
tx.addOutputAddress(outputAddress, outputAmount);

// Add a taproot input
tx.addInput({
  txid, // buffer of UTXO txid
  index: 0, // index of the output in the funding tx
  witnessUtxo: {
    amount: inputAmount, // must match the UTXO amount exactly
    script: taprootOutput.script
  },
  tapInternalKey: xOnlyPubKey
});

// Loop through each input and sign it
for (let i = 0; i < tx.inputsLength; i++) {
  const input = tx.getInput(i);
  const inputType = getInputType(input, true);
  const prevOut = getPrevOut(input);

  // Compute the BIP-341 sighash
  const sighash = tx.preimageWitnessV1(i, [prevOut.script], inputType.sighash, [prevOut.amount]);

  const signature = ''; // call Privy's raw sign function with bytesToHex(sighash), returns '0x...'

  // Decode the 64-byte Schnorr signature and attach it as tapKeySig
  const signatureBytes = Buffer.from(signature.slice(2), 'hex');
  tx.updateInput(i, {tapKeySig: signatureBytes}, true);
}

// Finalize the transaction. finalize() validates the Schnorr signature
// against the tweaked output key and throws if the signature is invalid.
tx.finalize();