Ir al contenido

vBTC Raw Integration Guide

Esta página aún no está disponible en tu idioma.

This guide is for integrations whose VFX keys never reach the node: web wallets, exchange signers, HSMs. A node builds each vBTC transaction unsigned, the client checks it and signs its hash, and the node verifies and broadcasts the signed transaction. The node needs no account, key or balance of yours.

It describes vBTC V2 on Core 9.0. Read vBTC first for the concepts (vaults, owners and holders, watched and legacy vaults, networks and heights, transaction types): this page covers what differs when you sign outside the node. All routes are under /vbtcapi/VBTC on the node’s API (http://localhost:7292, testnet 17292); send the apitoken header if the node requires one.

Every action that sends a VFX transaction follows the same four steps.

Call the action’s GetRaw… route. It checks the request against the same rules block validation applies and answers either with a refusal ("Success": false and a Message) or with the unsigned transaction:

{
"Success": true,
"Hash": "39ff6b9b…a22b",
"Timestamp": 1792774390,
"FromAddress": "<signer>",
"ToAddress": "<signer or recipient>",
"Amount": 0.0,
"Fee": 0.00001375,
"Nonce": 2,
"TransactionType": "VBTC_V2_WITHDRAWAL_REQUEST",
"Data": "{\"Function\":\"VBTCWithdrawalRequestMultiV2()\",…}",
"HashPreimage": "1792774390<signer><signer>0.00.000013752VBTC_V2_WITHDRAWAL_REQUEST{\"Function\":…}",
"Message": "Sign the Hash field and submit via SendRawRequestWithdrawalTx. …"
}

The build is kept on that node for about 65 minutes. Submit it to the same node.

Never sign a hash you have not checked. The hash is:

Hash = SHA-256( lowercase hex of SHA-256( HashPreimage as UTF-8 ) ), as lowercase hex
HashPreimage = Timestamp + FromAddress + ToAddress + Amount + Fee + Nonce + TransactionType + Data

with no separators, TransactionType as its name (for example VBTC_V2_WITHDRAWAL_REQUEST), and Data exactly as returned. A reserve (xRBX) transfer appends its UnlockTime.

  1. Recompute the hash. Where the route returns HashPreimage (vault creation, multi-vault transfer, multi-vault withdrawal), hash it and compare with Hash, and check that the preimage starts with Timestamp and FromAddress and contains Data verbatim. The single transfer, fee-raise and release builders return every field of the preimage. The single withdrawal-request and cancel builders leave out ToAddress and Amount, which are always the sender’s address and 0.0. Amount and Fee enter the preimage as the node’s decimal text (0.0 for zero), which a JSON parser may not reproduce, so compare field by field if you can’t rebuild the text exactly.
  2. Read Data and check that it says what you asked for: the action’s Function, the vault (ContractUID), the amount or allocation, the Bitcoin address and the fee rate. The sections below list what to check for each action.
  3. Check FromAddress is the address whose key you are about to use, and Fee (in VFX) is what you expect.

Sign Hash (the hex string, as UTF-8 text) with the sender’s key, ECDSA over secp256k1, exactly as for any VFX transaction (see Transaction Creation). The signature is the DER signature in Base64, a ., and the public key (64 bytes, without the 04 prefix) in Base58.

Post to the matching SendRaw… route:

{
"Hash": "<the Hash the build returned>",
"Signature": "<Base64 DER signature>.<Base58 public key>",
"PublicKey": "<the signer's public key>"
}

The node runs its full transaction check on the signed build, then broadcasts it:

  • {"Success": true, "Hash": "…", "Message": "Withdrawal request transaction broadcast successfully."}
  • A bad signature: Transaction verification failed: Signature Failed to verify. The build is kept, so sign again and resubmit the same hash.
  • An unknown or already used hash: No pending transaction found for hash …. Call GetRawRequestWithdrawalTxData first., or Transaction … was already submitted.
  • With the sender’s other transactions still waiting to be mined it would overspend: Not broadcast: together with this address's transactions still waiting to be mined it would overspend (…). Submit it again once they are mined.
  • Hash, Signature, and PublicKey are required.

The withdrawal-request, cancel, completion, fee-raise and release SendRaw… routes accept only their own build type, and a fee raise can’t be sent as a release or the reverse. The transfer and vault-creation routes don’t check the type, so always submit a build through its matching route. Submit vBTC builds through these routes, not the general txapi/TXV1 raw routes.

The builders take the sender’s next nonce from chain state when they build. Build and submit one transaction at a time per address, and build the next once the previous one is in a block, unless the route lets you pass Nonce.

The read routes answer from chain state, so any node answers for any address or vault:

  • Balances: GET GetAllVBTCBalances/{address}, GET GetVBTCBalance/{address}/{scUID}.
  • A vault: GET GetContractDetails/{scUID}, GET GetVaultBitcoin/{scUID}, GET GetMPCDepositAddress/{scUID}.
  • A withdrawal: GET GetWithdrawalPin/{scUID}/{withdrawalRequestHash} and, for every share of a request, GET GetWithdrawalRequestShares/{withdrawalRequestHash}.
  • The network: GET GetBitcoinAttestationStatus.

The answers are described on the vBTC page.

POST PrepareCompleteWithdrawalRaw also tells a raw client where a withdrawal stands. On a watched vault it prepares nothing and returns the completion answer (Attested: true, Status, Paid, NothingToDo, NotMinedYet, PinnedBTCTransactionHash, BTCTransactionHash and the Withdrawal object); on a legacy vault it prepares a FROST signing session (see Legacy vaults). Read Attested to tell the two apart.

{ "OwnerAddress": "<requester>", "SmartContractUID": "<contract UID>", "WithdrawalRequestHash": "<request hash>" }

A vault is created by its owner in two parts: a key ceremony among the validators, authorised by the owner’s signatures, and the creation transaction (type 25).

  1. Prepare the ceremony. POST PrepareMPCCeremonyRaw with {"OwnerAddress": "<owner>"}. The node checks that enough validators are reachable and returns the messages to sign:

    {
    "Success": true,
    "CeremonyId": "<32 hex>:<unix time>",
    "SessionId": "<guid>",
    "OwnerAddress": "<owner>",
    "ValidatorCount": 13,
    "Threshold": 51,
    "StartMessage": "<sessionId>.<owner>.<timestamp>",
    "StartTimestamp": 1792760000,
    "ShareDistributionMessage": "<sessionId>.<owner>.<timestamp + 1>",
    "ShareDistributionTimestamp": 1792760001,
    "RetrySessions": [ { "SessionId": "…", "StartMessage": "…", "StartTimestamp": 0, "ShareDistributionMessage": "…", "ShareDistributionTimestamp": 0 } ]
    }
  2. Sign StartMessage and ShareDistributionMessage with the owner’s key (the same signature format). Check that each message names your owner address. Sign the RetrySessions messages too: they let the ceremony run again without a validator that fails it. Without them it runs once.

  3. Run it. POST ExecuteMPCCeremonyRaw:

    {
    "OwnerAddress": "<owner>",
    "CeremonyId": "<ceremony id>",
    "SessionId": "<session id>",
    "StartTimestamp": 1792760000,
    "StartSignature": "<signature>",
    "ShareDistributionTimestamp": 1792760001,
    "ShareDistributionSignature": "<signature>",
    "RetrySessions": [ { "SessionId": "…", "StartTimestamp": 0, "StartSignature": "…", "ShareDistributionTimestamp": 0, "ShareDistributionSignature": "…" } ]
    }

    Errors: Invalid start signature, Invalid share distribution signature, Invalid retry session signature (…), Active ceremony already in progress.

  4. Wait. Poll GET GetCeremonyStatus/{ceremonyId} until Status is Completed (or Failed, with ErrorMessage). Use ContractUID from that answer.

  5. Build the creation. POST GetRawCreateContractTxData on the same node (the ceremony’s result is held in that node’s memory):

    { "OwnerAddress": "<owner>", "CeremonyId": "<ceremony id>", "Name": "Treasury vault", "Description": "", "Ticker": "vBTC" }

    The answer has Hash, HashPreimage, SmartContractUID, DepositAddress, DepositAddressConfirmed: false and the transaction’s fields. Check: TransactionType is VBTC_V2_CONTRACT_CREATE, FromAddress and ToAddress are the owner, Amount is 0, and Data is [{"Function":"Mint()","ContractUID":"<the ceremony's contract UID>",…}].

  6. Sign and submit with POST SendRawCreateContractTx: vBTC contract creation transaction broadcast successfully.

  7. Get the deposit address with GET GetMPCDepositAddress/{scUID} once the creation is in a block. Do not deposit before this route returns the address.

CreateVBTCContractRaw is not an external-signer route: it verifies a signed request but then signs the creation with the owner’s account on the node.

  1. POST GetRawTransferVBTCData:

    { "SmartContractUID": "<contract UID>", "FromAddress": "<sender>", "ToAddress": "<recipient>", "Amount": 0.00003 }
  2. Check: TransactionType is VBTC_V2_TRANSFER, Amount (the VFX amount) is 0, and Data is {"Function":"TransferVBTCV2()","ContractUID":…,"FromAddress":<sender>,"ToAddress":<recipient>,"Amount":<vBTC amount>}. For a reserve (xRBX) sender, IsReserveSend is true and UnlockTime is part of the hash: the transfer is held for 24 hours and can be called back until then.

  3. Sign and submit with POST SendRawTransferVBTCTx.

Refusals at build: Insufficient balance. Available: …, Requested: …, Reserve accounts cannot send vBTC to another Reserve Account.

  1. POST GetRawTransferVBTCMultiData:

    {
    "FromAddress": "<sender>",
    "ToAddress": "<recipient>",
    "TotalAmount": 0.00012,
    "Inputs": [ { "SCUID": "<vault A UID>", "Amount": 0.00008 }, { "SCUID": "<vault B UID>", "Amount": 0.00004 } ],
    "Nonce": null
    }

    Pass Inputs yourself where you can: the node checks them against chain state. Without Inputs the node plans the split from the vaults it knows. Nonce is optional; without it the node uses the next one.

  2. Check: HashPreimage hashes to Hash; Function is TransferVBTCMultiV2() (or TransferVBTCV2() when one vault covers the total, IsMultiContract: false); Allocations sum to TotalAmount; Data names your recipient.

  3. Sign and submit with POST SendRawTransferVBTCTx.

At most 25 vaults per transfer, amounts with at most 8 decimals, no reserve senders and no shielded recipient.

Ownership moves with a smart-contract transfer (type 18, TKNZ_TX) built through the general raw transaction routes.

  1. GET /vbtcapi/VBTC/GetVBTCOwnershipTransferData/{scUID}/{toAddress}/NA returns the transaction’s Data: [{"Function":"Transfer()","ContractUID":…,"ToAddress":<new owner>,"Data":…,"Locators":"NA","MD5List":"NA"}]. While the network uses the default vBTC image no beacon upload is needed and the locator is NA.
  2. Build a transaction with TransactionType 18, FromAddress the current owner, ToAddress the new owner, Amount 0 and that Data, then use txapi/TXV1/GetRawTxFee, GetTxHash, sign, VerifyRawTransaction and SendRawTransaction as in Transaction Creation.

The new owner takes over the vault’s vBTC that no holder holds.

  1. POST GetRawRequestWithdrawalTxData:

    {
    "SmartContractUID": "<contract UID>",
    "RequestorAddress": "<holder>",
    "BTCAddress": "bc1q…",
    "Amount": 0.00002,
    "FeeRate": 3,
    "UniqueId": null
    }
  2. Check before signing: TransactionType is VBTC_V2_WITHDRAWAL_REQUEST; FromAddress is the holder; Data is {"Function":"VBTCWithdrawalRequest()","ContractUID":<vault>,"RequestorAddress":<holder>,"BTCAddress":<your destination, exactly>,"Amount":<amount>,"FeeRate":<rate>,"UniqueId":…}. Validate the Bitcoin address yourself for the right network. Work out what the destination will receive: Amount less FeeRate × 154 sats (more if the vault needs several coins), which must leave at least 546 sats.

  3. Sign and submit with POST SendRawRequestWithdrawalTx. The transaction’s hash is the withdrawal’s RequestHash from now on.

Refusals at build: Fee rate must be greater than zero, the fee-floor message (… cannot pay a Bitcoin withdrawal at … sat/vB. …), Insufficient balance. Available: …, Requested: …, and at submit the consensus refusals listed on the vBTC page (address not on this network, fee rate above 5,000, less than 546 sats left, a frozen vault).

RequestWithdrawalRaw is not part of this flow: it only records a request on that node and puts nothing on chain.

On a watched vault nothing more needs to be sent. In the block that mines the request the chain pins the Bitcoin transaction that pays it; the vault’s signers sign it and broadcast it; it completes when Bitcoin confirms it. Follow it with GET GetWithdrawalPin/{scUID}/{requestHash} (or PrepareCompleteWithdrawalRaw): State goes from Waiting or Pinned to Settled, or to Cancelled, Expired, Void or Released with the vBTC refunded. Until the request is mined the answer is Success: false, Attested: true, Withdrawal: null; ask again after the next block.

The node reports a withdrawal’s pin in the same block as the request. The explorer API shows it only after a later Bitcoin report, so follow withdrawals through the node.

While State is Waiting (CanCancel: true) the requester can cancel at once, with no vote:

  1. POST GetRawCancelWithdrawalTxData with {"SmartContractUID": "<vault>", "RequestorAddress": "<requester>", "WithdrawalRequestHash": "<request hash>"}.
  2. Check: TransactionType is VBTC_V2_WITHDRAWAL_CANCEL, Data is {"Function":"VBTCWithdrawalCancel()","ContractUID":<vault>,"WithdrawalRequestHash":<request hash>}, and Attested: true (the message says no vote is held).
  3. Sign and submit with POST SendRawCancelWithdrawalTx. When it is mined the withdrawal closes as Cancelled and the vBTC is back.

The builder refuses before you sign when the cancel can’t be accepted: Only the original requestor can cancel., Cannot cancel an already completed withdrawal., and once the withdrawal is pinned This withdrawal's Bitcoin transaction is fixed and may already be signed, so it cannot be cancelled. It completes when Bitcoin confirms it. (with the Withdrawal object). CancelWithdrawalRaw does not cancel anything: it answers with the message to use these routes.

When a pinned withdrawal is not confirming (CanRaiseFee: true):

  1. POST GetRawBumpWithdrawalFeeTxData:

    { "SmartContractUID": "<vault>", "RequestorAddress": "<requester>", "WithdrawalRequestHash": "<request hash>", "FeeRate": 8 }
  2. Check: TransactionType is VBTC_V2_COIN_PIN (TransactionTypeId 44), FromAddress and ToAddress are the requester, Amount is 0, and Data is {"Function":"VBTCWithdrawalFeeBump()","ContractUID":<vault>,"WithdrawalRequestHash":<request hash>,"FeeRate":<new rate>}. The new rate must be above the FeeRate the withdrawal pays now. The higher fee comes out of the withdrawal: the destination receives less.

  3. Sign and submit with POST SendRawBumpWithdrawalFeeTx. When it is mined, the chain pins a replacement that spends the same coins; follow it with GetWithdrawalPin.

When a pinned withdrawal is not confirming and the requester would rather have the vBTC back (CanRelease: true):

  1. POST GetRawReleaseWithdrawalTxData with the same body as a fee raise.
  2. Check: as for a fee raise, with Function VBTCWithdrawalRelease(). The release’s Bitcoin fee comes out of the refund.
  3. Sign and submit with POST SendRawReleaseWithdrawalTx. When it is mined, the chain pins a Bitcoin transaction returning the coins to the vault; when Bitcoin shows it, the withdrawal closes as Released and the vBTC is refunded less that fee. If one of the withdrawal’s payments confirms first, it is paid instead.

Both builders check the request against the rule block validation applies and refuse with the reason and the Withdrawal object, for example The new fee rate must be above the 3 sat/vB the withdrawal's newest Bitcoin transaction pays. or This withdrawal has no Bitcoin transaction yet. Cancel it and request again with the fee rate you want. A fee raise sent to the release route, or the reverse, is refused: This transaction is a fee bump; submit it with SendRawBumpWithdrawalFeeTx.

This is how an exchange or any holder with vBTC on several vaults withdraws a total amount to one Bitcoin address. One VFX transaction (type 27, VBTCWithdrawalRequestMultiV2()) opens the withdrawal; each vault in it is a share, paid by its own Bitcoin transaction from that vault. The requester pays one miner fee per share, out of that share, and shares settle independently, so a withdrawal can be partly paid. A total that one vault covers is built as an ordinary single withdrawal. At most 10 vaults per request.

POST GetRawRequestWithdrawalMultiTxData:

{
"RequestorAddress": "<holder>",
"BTCAddress": "bc1q…",
"TotalAmount": 0.00006,
"FeeRate": 2,
"Inputs": [
{ "SCUID": "<vault A UID>", "Amount": 0.00003 },
{ "SCUID": "<vault B UID>", "Amount": 0.00003 }
],
"UniqueId": null
}

Pass Inputs (each vault and the amount to draw from it) whenever you can: the node checks them against chain state, so any node can build for any holder. Without Inputs the node allocates the total itself, largest available balance first, from the vaults it can see.

{
"Success": true,
"Hash": "39ff6b9b…a22b",
"Timestamp": 1792774390,
"FromAddress": "<holder>",
"Fee": 0.00001375,
"Nonce": 2,
"TransactionType": "VBTC_V2_WITHDRAWAL_REQUEST",
"Data": "{\"Function\":\"VBTCWithdrawalRequestMultiV2()\",\"RequestorAddress\":\"<holder>\",\"BTCAddress\":\"bc1q…\",\"TotalAmount\":0.00006,\"FeeRate\":2,\"Inputs\":[{\"SCUID\":\"<vault A UID>\",\"Amount\":0.00003},{\"SCUID\":\"<vault B UID>\",\"Amount\":0.00003}],\"UniqueId\":null}",
"HashPreimage": "…",
"Function": "VBTCWithdrawalRequestMultiV2()",
"IsMultiContract": true,
"Allocations": [ { "SCUID": "<vault A UID>", "Amount": 0.00003 }, { "SCUID": "<vault B UID>", "Amount": 0.00003 } ],
"BitcoinTransactions": 2,
"TotalAmount": 0.00006,
"Destination": "bc1q…",
"Message": "Sign the Hash field and submit via SendRawRequestWithdrawalTx. …"
}

On a watched vault, ignore the part of Message that asks you to complete each contract: the vault’s signers pay every share.

  1. Hash. SHA-256 of the lowercase hex of SHA-256(HashPreimage) equals Hash. The preimage starts with Timestamp and the holder’s address (as FromAddress and ToAddress) and ends with Data exactly as returned.
  2. Allocation. Data.Inputs (and Allocations) name the vaults and amounts you intended, each vault once, and add up to TotalAmount.
  3. Destination. Data.BTCAddress is your destination exactly, valid for the network. Check it yourself before you build.
  4. Fees. Data.FeeRate is the rate you meant. Each share pays FeeRate × 154 sats or more out of its own amount, so the destination receives TotalAmount less BitcoinTransactions fees, and every share must leave at least 546 sats after its fee. Fee is the VFX fee of the request.
  5. Signer. FromAddress is the holder whose key you are using.

Sign Hash with the holder’s key and submit with POST SendRawRequestWithdrawalTx, as for a single withdrawal. On testnet a two-share request at 2 sat/vB was mined in the next block, each share pinned in that block to its own Bitcoin transaction (2,692 sats to the destination, 308 sats fee each), and both closed paid when Bitcoin confirmed them.

  • GET GetWithdrawalRequestShares/{requestHash} lists the shares once the request is mined: each vault, amount, Status, IsCompleted, Paid, Refunded and BTCTxHash, and the totals PaidCount, RefundedCount, AllPaid.
  • GET GetWithdrawalPin/{shareVaultUID}/{requestHash} gives one share’s state, its pinned Bitcoin transaction and what can be done with it. A share goes through the same states as a single withdrawal.

To raise the fee of, release or cancel one share, use the routes above with that share’s vault UID as SmartContractUID and the request hash as WithdrawalRequestHash. Each is one transaction for one share; the other shares are not touched.

You don’t need CompleteWithdrawalMulti or PrepareCompleteWithdrawalRaw/ExecuteCompleteWithdrawalRaw for shares on watched vaults: they send nothing for them.

Situation Message
Inputs don’t add up TotalAmount must equal the sum of input amounts for multi-contract vBTC withdrawal.
A vault listed twice Multi-contract vBTC withdrawal inputs must reference distinct contracts.
An input above the holder’s balance on that vault Insufficient vBTC balance for withdrawal input …. Available: …, Requested: …
No Inputs and not enough across the vaults Insufficient combined vBTC balance. Available: …, Requested: …
The holder has nothing No spendable vBTC balance found for this address.
More than 8 decimals TotalAmount cannot have more than 8 decimal places
More than 10 vaults Multi-contract vBTC withdrawal exceeds the maximum of 10 inputs.
A share too small for its fee Multi-contract vBTC withdrawal input …: … BTC cannot pay a Bitcoin withdrawal at … sat/vB. …
Fee rate 0 FeeRate must be greater than zero
Not a Bitcoin address on this network The withdrawal's Bitcoin address is not an address on this network: …
A reserve holder Reserve accounts cannot request BTC withdrawals. Move the vBTC to a normal VFX address first.

On a vault created before the attestation recording height, until the enforcement height, a withdrawal is completed by its requester after the request is mined:

  1. POST PrepareCompleteWithdrawalRaw (OwnerAddress = the requester, SmartContractUID, WithdrawalRequestHash) returns SessionId, StartMessage, ShareDistributionMessage, their timestamps, InputCount and StartMessages (one per Bitcoin input), plus the withdrawal’s Amount, BTCDestination and FeeRate to confirm. Unpayable: true means the request can never be paid at its fee rate: cancel it.
  2. Sign every message in StartMessages and ShareDistributionMessage, and POST ExecuteCompleteWithdrawalRaw with StartSignature, ShareDistributionSignature, StartSignatures and the confirmed fields. The validators sign the Bitcoin transaction and the node returns SignedBTCTxHex.
  3. Broadcast SignedBTCTxHex to Bitcoin. If CompleteAfterConfirmation is true, wait for its first confirmation.
  4. Record the completion: POST GetRawCompleteWithdrawalTxData (SmartContractUID, FromAddress, WithdrawalRequestHash, BTCTransactionHash, Amount, BTCDestination), sign, and POST SendRawCompleteWithdrawalTx (type 28, no fee).

A cancel on a legacy vault is the same type 29 transaction, after which the vault’s validators vote (type 30). From the enforcement height a legacy vault is frozen: transfers, withdrawals and cancels on it are refused, and types 28 and 30 are refused for every vault.

The raw V1 flow (btcapi/BTCV2/GetTokenizationDetails, TokenizeBitcoin, WithdrawalCoinRawTX, transaction types 17 to 21) was retired at block 7,400,500 on mainnet. The network no longer accepts V1 tokens, transfers or withdrawals, and V1 balances are frozen as they stand.