Bitcoin Spoon is a Bitcoin implementation in C that has minimal dependencies.
Why Bitcoin Spoon? Because a spoon is not a fork. Bitcoin Spoon was written to run on more limited hardware.
- Compile with ./compile.sh
- Test by running ./test/test
- Run the example with ./test/example
- Check out the code in test/example.c to learn how to use the library.
#include "Database.h"
databaseRootPath = "/tmp" // Folder where live and testnet dbs are stored (cannot end in a slash)
#include "KeyManager.h"
keyManagerKeyDirectory = "/tmp" // Folder where live & testnet key files are stored
static void myKeyManagerCustomEntropy(char *buf, int length) { ... }
KeyManagerCustomEntropy = myKeyManagerCustomEntropy;
Data myKeyManagerUniqueData() { ... }
KeyManagerUniqueData = myKeyManagerUniqueData
#include "BTCUtil.h"
BTCUtilStartup()
#include "BasicStorage.h"
basicStorageSetup(StringRef("/tmp/bs.basicStorage"))
bsSave("testnet", DataInt((int)testnet)) // 1 for testnet, 0 for mainnet
#include "KeyManager.h"
KMInit()
KMSetTestnet(&km, (int)testnet) // 1 for testnet, 0 for mainnet
#include "TransactionTracker.h"
tracker = TTNew((int)testnet) // 1 for testnet, 0 for mainnet
#include "Database.h"
database = DatabaseNew()
#include "NodeManager.h"
NodeManager = NodeManagerNew(walletCreationDate)
walletCreationDate
is a unix timestamp of when this wallet was first created. This limits how far back in the blockchain we will scan.NodeManager.testnet = (int)testnet // 1 for testnet, 0 for mainnet
Your program must have a main loop. That main loop needs to call out to a few things repeatedly on some form of "main thread". Those things are:
- Each loop must start with
DataTrackPush()
and end withDataTrackPop()
- This enables semi-automatic memory tracking during the loop, freeing objects during
DataTrackPop
. #include "Notifications.h"
NotificationsProcess()
#include "NodeManager.h"
NodeManagerProcessNodes(&NodeManager)
NodeManagerConnectNodes(&NodeManager)
- This will connect to 8 nodes and peridocially replace node connections with new ones.
- If
KMMasterPrivKey(&km).bytes == NULL
, then generate the master private key KMGenerateMasterPrivKey(&km)
KMNamedKeyCount(&km)
KMKeyName(&km, (uint32_t)index)
KMSetKeyName(&km, "Account Name", KMNamedKeyCount(&km))
KMVaultNames(&km)
- Indicies are consistent amount vault retrieval methods
- TBD
Data hdWalletData = KMHdWalletIndex(&km, (uint32_t)index)
hdWalletData = hdWallet(hdWalletData, "0'/0")
hdWalletData = TTUnusedWallet(tracker, hdWalletData, (unsigned int)0) // Increment 0 to get lookahead addresses
Data pubKey = pubKeyFromHdWallet(hdWalletData)
String address = p2pkhAddress(pubKey)
- Or, for testnet:
String address = p2pkhAddressTestNet(pubKey)
TransactionAnalyzer ta = TTAnalyzerFor(tracker, KMHdWalletIndex(&km, (uint32_t)walletIndex))
TATotalBalance(&ta)
Datas/*TAEvent*/ events = TAEvents(&ta, (TAEventType)typeMask)
- Some options for typeMask:
typedef enum {
TAEventTypeDeposit = 1,
TAEventTypeWithdrawl = 2,
TAEventTypeChange = 4,
TAEventTypeUnspent = 8,
TAEventTypeTransfer = 16,
TAEventTypeFee = 32,
TAEventBalanceMask = TAEventTypeDeposit | TAEventTypeWithdrawl,
TAEventChangeMask = TAEventTypeChange,
TAEventAllMask = TAEventBalanceMask | TAEventChangeMask | TAEventTypeUnspent,
} TAEventType;
TAAnalyzerForTransactionsMatching(&ta, custom_filter)
TATotalAmount((Datas/*TAEvent*/)events)
TAPaymentCandidate paymentCandidate = TAPaymentCandidateSearch(&ta, (Datas/*TAEvent*/)events)
TAPaymentCandidateRemainder(&paymentCandidate)
- If you want to keep a
TransactionAnalyzer
for longer than one loop, you must: DataUntrack(ta)
to capture it, and later,DataTrack(ta)
when you are done with it.
Transaction trans = TransactionEmpty()
Data outputScript = addressToPubScript((String)destinationAddress)
- You should always verify the outputScript matches the destinationAddress to prevent funds being permanently lost
if(!DataEqual(pubScriptToAddress(outputScript), destinationAddress)) abort()
TTAddOutput(outputScript, (uint64_t)amount) // amount is in satoshies
- You should already have a
TransactionAnalyzer ta
from above Datas/*TAEvent*/ unspents = TAEvents(&ta, TAEventTypeUnspent)
TAPaymentCandidate payment = TAPaymentCandidateSearch((uint64_t)amount + (uint64_t)transactionFee, unspents)
if(payment.amount < (uint64_t)amount + (uint64_t)transactionFee) abort() // Insufficent funds
FORIN(TAEvent, event, payment.events) {
Data prevHash = TransactionTxid(event->transaction);
Data pubScript = TransactionOutputOrNilAt(&event->transaction, event->outputIndex)->script;
uint64_t value = TransactionOutputOrNilAt(&event->transaction, event->outputIndex)->value;
TransactionAddInput(&trans, prevHash, event->outputIndex, pubScript, value)->sequence = 0;
}
Data hdWalletData = KMHdWalletIndex(&km, (uint32_t)index) // This will be used for change & signing
Data hdWalletData = hdWallet(hdWalletData, "0'")
- If we have change in
payment.remainder
(which we almost always will), then we must make an output back to ourselves as change Data changeWallet = TTUnusedWallet(hdWallet(hdWalletData, "1"), (unsigned int)0)
Data changePubScript = p2wpkhPubScriptFromPubKey(pubKeyFromHdWallet(changeWallet))
trans = TransactionAddOutput(trans, changePubScript, payment.remainder)
- Now we sign the transaction
Datas hdWallets = TTAllActiveDerivations(hdWallet(hdWalletData, "0")) // all primary wallets
hdWallets = DatasAddDatasCopy(hdWallets, TTAllActiveDerivations(hdWallet(hdWalletData, "1"))) // all change wallets
trans = TransactionSort(trans)
Datas signatures = DatasNew()
trans = TransactionSign(trans, anyKeysFromHdWallets(hdWallets), &signatures)
- if
signatures.count
is equal totrans.inputs.count
then our transaction is valid & can be published!
static void mySendTxResult(NodeManagerErrorType result, void *ptr) { ... }
NodeManagerSendTx(&NodeManager, trans, mySendTxResult, NULL)
void myListener(Dict dict) { ... }
NotificationsAddListener(EventName, myListener)
NodeManagerBlockchainSyncChange
NodeConnectionStatusChanged
TransactionTrackerTransactionAdded
DatabaseNewTxNotification
DatabaseNewBlockNotification
DatabaseNodeListChangedNotification