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 storedstatic 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)walletCreationDateis 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 addressesData 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
TransactionAnalyzerfor 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 tafrom 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 & signingData 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 walletshdWallets = DatasAddDatasCopy(hdWallets, TTAllActiveDerivations(hdWallet(hdWalletData, "1"))) // all change walletstrans = TransactionSort(trans)Datas signatures = DatasNew()trans = TransactionSign(trans, anyKeysFromHdWallets(hdWallets), &signatures)- if
signatures.countis equal totrans.inputs.countthen 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)
NodeManagerBlockchainSyncChangeNodeConnectionStatusChangedTransactionTrackerTransactionAddedDatabaseNewTxNotificationDatabaseNewBlockNotificationDatabaseNodeListChangedNotification