This guide is for exchanges that are currently using siad to track and manage Siacoin deposits. Exchanges should upgrade to walletd before the V2 hardfork to continue supporting the Sia network.
The hardfork activates on June 6th, 2025 at a block height of 526,000 at 06:00 UTC.
walletd is the new reference wallet for exchanges. It is designed to be more secure, reliable, and scalable than siad. walletd also has a more robust API, supports multiple wallets simultaneously, and provides easier support for secure key management setups.
Differences from siad
The most important difference between siad and walletd is that walletd is a watch-only server. That means that it does not store any private keys. This makes walletd more secure, but also means that transactions must be signed by an external service with access to the private keys. Either a hardware security device, like a YubiKey, Hashicorp Vault, or an offline signing node. This can be more complex than with siad, but it provides significantly more flexibility and security.
Another difference is that walletd does not need to rescan the blockchain when adding new addresses to a wallet. This makes managing deposit addresses significantly easier than with siad.
walletd supports significantly more addresses than siad and can handle a much larger number of transactions. This makes it a better choice for exchanges that need to manage a large number of deposit addresses.
walletd does not require addresses to be added to wallets before they can be used for addresses. However, there are a few benefits to using addresses that have been added to a wallet.
The balance of a wallet is the sum of all addresses that have been added to a wallet
The wallet event list contains transactions for all of its addresses
walletd can construct transactions using UTXOs controlled by all of the addresses in a wallet
How to run walletd
walletd can be installed as a standalone binary or as a Docker container. The Docker container is the recommended way to run walletd in a production environment. For exchanges, we recommend running walletd in "full" index mode. This mode is designed for exchanges and wallet integrators that need to track all addresses and transactions on the Sia network.
It is also possible to disable HTTP authentication on endpoints that are safe to expose to the public using the --http.public flag.
walletd supports multiple wallets. Each wallet has its own set of addresses and transactions. Wallets can be created and updated using the wallets API. When running in "full" index mode, it is not required to group addresses into wallets. However, it is recommended to do so for easier indexing.
Creating a wallet
curl -u :"sia is cool" "http://localhost:9980/api/wallets" -X POST -d '{"name":"exchange"}'
To add an address to a wallet, you can use the [PUT] /api/wallets/:id/addresses endpoint. When an address is added in "full" index mode, the wallet is immediately updated without needing to rescan the chain. The spend policy is used to track the address' unlock conditions. It can be ignored for addresses where the unlock conditions are not known. If the address is already in the wallet, its metadata will be updated.
curl -u :"sia is cool" "http://localhost:9980/api/wallets/1/addresses" -X PUT -d '{"address":"3dbc6e05dfe9db593dd4796a9522b5df435e9fc45ec9b90b6f2ae5d2d18550b6bd8db06bc824","spendPolicy":{"type":"uc","policy":{"timelock":0,"publicKeys":["ed25519:0d49fba38e80a888f847cb9661fd91f97cfba1e355ddeb15de2b8dd9a4b58614"],"signaturesRequired":1}}}'
If successful, the body will be empty and a 204 response code will be returned.
Getting the balance of a wallet
To get the balance of a wallet, you can use the [GET] /api/wallets/:id/balance endpoint. This endpoint will return the balance of the specified wallet.
curl -u :"sia is cool" "http://localhost:9980/api/wallets/1/balance"
To get the events of a wallet, you can use the [GET] /api/wallets/:id/events endpoint. This endpoint will return a paginated list of events for the specified wallet. You can use this event list to track deposits and other changes from the wallet. The pagination can be controlled with the offset and limit query parameters. It defaults to 0 and 50, respectively.
curl -u :"sia is cool" "http://localhost:9980/api/wallets/1/events
When using wallets, you can construct transactions using the construct API. This API will return a transaction that must be signed before it can be broadcast to the network. You can sign transactions using a hardware security device, like a YubiKey, Hashicorp Vault, or another offline signing node. This can be more complex than with siad, but it provides significantly more flexibility and security.
The example is provided in Go, but the same process can be done in any language that supports ed25519 signing.
package main
import (
"go.sia.tech/core/types"
"go.sia.tech/walletd/api"
"go.sia.tech/walletd/wallet"
)
const (
walletdAPIAddress = "http://localhost:9980/api"
walletdAPIPassword = "change me"
)
func main() {
privateKey := types.GeneratePrivateKey() // private key is a standard ed25519 private key
unlockConditions := types.StandardUnlockConditions(privateKey.PublicKey())
depositAddress := unlockConditions.UnlockHash()
recipientAddress := types.VoidAddress
sendAmount := types.Siacoins(10)
client := api.NewClient(walletdAPIAddress, walletdAPIPassword)
w, err := client.AddWallet(api.WalletUpdateRequest{
Name: "test wallet",
})
if err != nil {
panic(err)
}
// create a wallet and add an address
wc := client.Wallet(w.ID)
err = wc.AddAddress(wallet.Address{
Address: depositAddress,
SpendPolicy: &types.SpendPolicy{
Type: types.PolicyTypeUnlockConditions(unlockConditions),
},
})
if err != nil {
panic(err)
}
cs, err := client.ConsensusTipState()
if err != nil {
panic(err)
}
// construct a transaction sending 10 SC to the void address
resp, err := wc.Construct([]types.SiacoinOutput{
{Address: recipientAddress, Value: sendAmount},
}, nil, depositAddress)
if err != nil {
panic(err)
}
basis := resp.Basis
txn := resp.Transaction
for i, sig := range txn.Signatures {
// calculate the sig hash
sigHash := cs.WholeSigHash(txn, sig.ParentID, 0, 0, nil)
// sign the hash
sig := privateKey.SignHash(sigHash)
// add the signature to the transaction
txn.Signatures[i].Signature = sig[:]
}
// broadcast the transaction
if err := client.TxpoolBroadcast(basis, []types.Transaction{txn}, nil); err != nil {
panic(err)
}
}
Addresses
In "full" index mode, walletd will automatically index all addresses on the Sia network. This means that you can query any address on the Sia network without needing to add it to a wallet. However, it is recommended to add addresses to a wallet for easier management.
Getting the balance of an address
To get the balance of an address, you can use the [GET] /api/addresses/:address/balance endpoint. This endpoint will return the balance of the specified address. When in full index mode, you can use this endpoint to check the balance of any address on the Sia network.
To get the UTXOs of an address, you can use the [GET] /api/addresses/:address/outputs/siacoin endpoint. This endpoint will return a paginated list of UTXOs controlled by the address. By default, 50 UTXOs will be returned. When in full index mode, you can use this endpoint to get the UTXOs of any address on the Sia network.
The returned basis field should be used when broadcasting a transaction using [POST] /api/txpool/broadcast.
walletd has an endpoint designed to make scanning the blockchain for deposits simpler. [GET] /api/consensus/updates/:index will return 10 blocks after the specified index. This endpoint is designed to be polled by exchanges that do not use wallets to track new deposits. index is the chain index of the last block that has been processed. The updates will also handle reverted blocks in the path. When a changeset is processed, index should be updated to the last block processed to process the next batch of blocks.
The block data, the spent UTXOs, and the height are all included in the response. This data can be used to update the exchange's internal database with new deposits from relevant addresses.
If you do not want to use a wallet to construct a transaction, you can manually select UTXOs and build the transaction yourself. This is more complex than using a wallet, but it provides more control over the transaction construction process. It is required to use this method if you are using multi-sig addresses.
package main
import (
"go.sia.tech/core/types"
"go.sia.tech/walletd/api"
)
const (
walletdAPIAddress = "http://localhost:9980/api"
walletdAPIPassword = "change me"
)
func main() {
const txnSizeBytes = 1200
privateKey := types.GeneratePrivateKey() // private key is a standard ed25519 private key
unlockConditions := types.StandardUnlockConditions(privateKey.PublicKey())
depositAddress := unlockConditions.UnlockHash()
recipientAddress := types.VoidAddress
sendAmount := types.Siacoins(10)
client := api.NewClient(walletdAPIAddress, walletdAPIPassword)
utxos, basis, err := client.AddressSiacoinOutputs(depositAddress, 0, 10)
if err != nil {
panic(err)
}
fee, err := client.TxpoolFee()
if err != nil {
panic(err)
}
minerFee := fee.Mul64(txnSizeBytes)
txn := types.Transaction{
MinerFees: []types.Currency{minerFee},
SiacoinOutputs: []types.SiacoinOutput{
{Address: recipientAddress, Value: sendAmount},
},
}
// construct a transaction sending 10 SC to the recipient
outputSum := sendAmount.Add(minerFee)
var inputSum types.Currency
for _, utxo := range utxos {
if inputSum.Cmp(outputSum) >= 0 {
break
}
inputSum = inputSum.Add(utxo.SiacoinOutput.Value)
txn.SiacoinInputs = append(txn.SiacoinInputs, types.SiacoinInput{
ParentID: utxo.ID,
UnlockConditions: unlockConditions,
})
txn.Signatures = append(txn.Signatures, types.TransactionSignature{
ParentID: types.Hash256(utxo.ID),
PublicKeyIndex: 0,
Timelock: 0,
CoveredFields: types.CoveredFields{WholeTransaction: true},
})
}
if inputSum.Cmp(outputSum) < 0 {
panic("insufficient funds")
} else if change := inputSum.Sub(outputSum); !change.IsZero() {
txn.SiacoinOutputs = append(txn.SiacoinOutputs, types.SiacoinOutput{
Address: depositAddress,
Value: change,
})
}
cs, err := client.ConsensusTipState()
if err != nil {
panic(err)
}
for i, sig := range txn.Signatures {
// calculate the sig hash
sigHash := cs.WholeSigHash(txn, sig.ParentID, 0, 0, nil)
// sign the hash
sig := privateKey.SignHash(sigHash)
// add the signature to the transaction
txn.Signatures[i].Signature = sig[:]
}
// broadcast the transaction
if err := client.TxpoolBroadcast(basis, []types.Transaction{txn}, nil); err != nil {
panic(err)
}
}
How to Support V2?
After June 6th, 2025 at block height 526,000 at 06:00 UTC, siad will no longer be able to send or receive Siacoins. To continue supporting the Sia network, exchanges must upgrade to walletd. In addition to upgrading to walletd, exchanges will need to start sending V2 transactions and using the new V2 API endpoints.
Sending V2 transactions
This example uses our Go SDK, but you can also use the walletd API directly. It is not required to create a wallet to send transactions, but it does simplify transaction creation.
Changes from V1
The miner fee is no longer an array of Currency but a single Currency value.
There is a slightly different structure to siacoin inputs
The input signature has moved to the siacoin input instead of in a separate signatures array.
The sig hash is calculated using the InputSigHash method on the consensus state instead of WholeSigHash or PartialSigHash.
With a wallet
When using a wallet, there are two primary changes to make. The first, is to use [POST] /api/wallets/:id/construct/v2/transaction instead of [POST] /api/wallets/:id/construct/transaction.
package main
import (
"go.sia.tech/core/types"
"go.sia.tech/walletd/api"
"go.sia.tech/walletd/wallet"
)
const (
walletdAPIAddress = "http://localhost:9980/api"
walletdAPIPassword = "change me"
)
func main() {
privateKey := types.GeneratePrivateKey() // private key is a standard ed25519 private key
unlockConditions := types.StandardUnlockConditions(privateKey.PublicKey())
depositAddress := unlockConditions.UnlockHash()
recipientAddress := types.VoidAddress
sendAmount := types.Siacoins(10)
client := api.NewClient(walletdAPIAddress, walletdAPIPassword)
w, err := client.AddWallet(api.WalletUpdateRequest{
Name: "test wallet",
})
if err != nil {
panic(err)
}
// create a wallet and add an address
wc := client.Wallet(w.ID)
err = wc.AddAddress(wallet.Address{
Address: depositAddress,
SpendPolicy: &types.SpendPolicy{
Type: types.PolicyTypeUnlockConditions(unlockConditions),
},
})
if err != nil {
panic(err)
}
cs, err := client.ConsensusTipState()
if err != nil {
panic(err)
}
// construct a transaction sending 10 SC to the void address
resp, err := wc.ConstructV2([]types.SiacoinOutput{
{Address: recipientAddress, Value: sendAmount},
}, nil, depositAddress)
if err != nil {
panic(err)
}
basis := resp.Basis
txn := resp.Transaction
// calculate the hash to sign
sigHash := cs.InputSigHash(txn)
// sign the transaction
for i := range txn.SiacoinInputs {
txn.SiacoinInputs[i].SatisfiedPolicy.Signatures = []types.Signature{privateKey.SignHash(sigHash)}
}
// broadcast the transaction
if err := client.TxpoolBroadcast(basis, nil, []types.V2Transaction{txn}); err != nil {
panic(err)
}
}
Without a wallet
package main
import (
"go.sia.tech/core/types"
"go.sia.tech/walletd/api"
)
const (
walletdAPIAddress = "http://localhost:9980/api"
walletdAPIPassword = "change me"
)
func main() {
const txnSizeBytes = 1200
privateKey := types.GeneratePrivateKey() // private key is a standard ed25519 private key
unlockConditions := types.SpendPolicy{Type: types.PolicyTypeUnlockConditions(types.StandardUnlockConditions(privateKey.PublicKey()))}
depositAddress := unlockConditions.Address()
recipientAddress := types.VoidAddress
sendAmount := types.Siacoins(10)
client := api.NewClient(walletdAPIAddress, walletdAPIPassword)
utxos, basis, err := client.AddressSiacoinOutputs(depositAddress, 0, 10)
if err != nil {
panic(err)
}
fee, err := client.TxpoolFee()
if err != nil {
panic(err)
}
minerFee := fee.Mul64(txnSizeBytes)
txn := types.V2Transaction{
MinerFee: minerFee,
SiacoinOutputs: []types.SiacoinOutput{
{Address: recipientAddress, Value: sendAmount},
},
}
// construct a transaction sending 10 SC to the recipient
outputSum := sendAmount.Add(minerFee)
var inputSum types.Currency
for _, utxo := range utxos {
if inputSum.Cmp(outputSum) >= 0 {
break
}
inputSum = inputSum.Add(utxo.SiacoinOutput.Value)
txn.SiacoinInputs = append(txn.SiacoinInputs, types.V2SiacoinInput{
Parent: utxo,
SatisfiedPolicy: types.SatisfiedPolicy{
Policy: unlockConditions,
},
})
}
if inputSum.Cmp(outputSum) < 0 {
panic("insufficient funds")
} else if change := inputSum.Sub(outputSum); !change.IsZero() {
txn.SiacoinOutputs = append(txn.SiacoinOutputs, types.SiacoinOutput{
Address: depositAddress,
Value: change,
})
}
cs, err := client.ConsensusTipState()
if err != nil {
panic(err)
}
// calculate the hash to sign
sigHash := cs.InputSigHash(txn)
// sign the transaction
for i := range txn.SiacoinInputs {
txn.SiacoinInputs[i].SatisfiedPolicy.Signatures = []types.Signature{privateKey.SignHash(sigHash)}
}
// broadcast the transaction
if err := client.TxpoolBroadcast(basis, nil, []types.V2Transaction{txn}); err != nil {
panic(err)
}
}
How Can I Test?
To test your integration with walletd without risking real Siacoin, you can use one of our two testnets.
To test V2 transactions, you can use the Anagami testnet. Pass the --network=anagami CLI flag to walletd to connect to the Anagami testnet.
To test V1 transactions, you can use the Zen testnet. Pass the --network=zen CLI flag to walletd to connect to the Zen testnet.