bwallet

package
v0.10.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 6, 2026 License: Apache-2.0 Imports: 20 Imported by: 0

Documentation

Overview

Package bwallet is an embedded BRC-100 wallet backend for an application that publishes on chain, and the Signer through which such an application signs with it or with any other wallet.Interface.

It exists because the SDK's CompletedProtoWallet is a trap: it satisfies wallet.Interface and answers nil, nil to CreateAction, SignAction, ListOutputs and the rest, so a caller nil-derefs where it should have seen an error. Every method here that this backend does not implement returns ErrNotSupported with the method's name in it, and a test walks the interface by reflection to hold that line.

Two files under one directory (mode 0700):

identity.json  {"wif": "..."}   the root key, mode 0600
wallet.json    the spendable outputs, mode 0600

Both are written atomically (a temp file in the same directory, then rename) so a crash mid-write leaves the previous file rather than half of a new one.

Create, Open, OpenIdentity and OpenPool take the application's Profile, which has no default. The funding key is derived INSIDE the wallet under the profile's fund protocol and key id with counterparty self, so only the holder of the root key can derive it and no *ec.PrivateKey ever leaves the wallet: spends are signed through CreateSignature with HashToDirectlySign, the same path go-sdk's own pushdrop.Unlocker uses. That is the whole point of the abstraction; a backend that hands its private key to a caller is not one.

The coin this wallet holds is its own. Nothing else spends from it, because two tools drawing on one set of outputs double-spend each other.

Index

Examples

Constants

View Source
const CoinbaseMaturity = 100

CoinbaseMaturity is how many blocks must bury a coinbase before it may be spent. A pool output is spendable when Height+CoinbaseMaturity <= tip.

View Source
const DefaultFundBatch = 30

DefaultFundBatch is how many blocks one generatetoaddress call asks for. It is kept small because a large call can outlast the RPC timeout, most of all while another miner contends for the chain.

Variables

View Source
var (
	// ErrNotSupported is returned, wrapped with the method name, by every
	// wallet.Interface method this backend does not implement.
	ErrNotSupported = errors.New("bwallet: not supported by the embedded backend")
	// ErrNoChain is returned by GetHeight and GetHeaderForHeight when no
	// Chain is configured. The height source is a security choice, never a
	// default, so there is no fallback.
	ErrNoChain = errors.New("bwallet: no chain source configured")
	// ErrIdentityExists is returned by Create when the directory already
	// holds an identity. Overwriting one silently would orphan every token
	// and output it ever signed.
	ErrIdentityExists = errors.New("bwallet: identity already exists")
)
View Source
var ErrNoSpendable = errors.New("bwallet: no spendable output in the wallet")

ErrNoSpendable is returned by Take when nothing in the pool is spendable at the given tip. Immature coinbase is the usual reason; Immature says so.

View Source
var ErrProfile = errors.New("bwallet: wallet profile incomplete")

ErrProfile refuses a Profile with a field missing or unusable. The wrapped error names the field; a fund derivation the SDK would refuse also wraps pushdrop.ErrDerivation.

View Source
var PaymentProtocol = wallet.Protocol{SecurityLevel: wallet.SecurityLevelEveryAppAndCounterparty, Protocol: "3241645161d8"}

PaymentProtocol is BRC-29's protocol: security level 2, the fixed name. The key id is "<derivationPrefix> <derivationSuffix>", so the invoice number is "2-3241645161d8-<prefix> <suffix>".

It is a variable only because Go has no constant of a struct type. Callers read it and must not assign to it: PaymentDestination derives every payment destination in the process under it, so an assignment would send every payment to a key the recipient, deriving under BRC-29's protocol, would not find. DerivedKey, DerivedScript and DerivedUnlocker take the protocol from the Derivation instead, so they would not follow the change.

Functions

func Counterparty

func Counterparty(idHex string) (wallet.Counterparty, error)

Counterparty parses a compressed identity key into a counterparty. The key is often a sender's, taken from a payment, so only its one canonical encoding is accepted (guard.ParsePubKeyHex).

func FundFromCoinbase

func FundFromCoinbase(ctx context.Context, e *Signer, pool *Pool, rpc *nodeapi.RPC, asset *nodeapi.Asset, blocks, batch int) (added int, hashes []string, err error)

FundFromCoinbase mines blocks paying the fund address and adds each block's coinbase outputs that pay FundScript to the pool, marked Coinbase with their height so Take applies maturity. An output of zero satoshis is not added. It returns how many outputs were added and every block hash mined, including blocks whose coinbase paid nothing to us.

This is the funding rule: the pool is funded from fresh coinbase paid to the application's own fund address, never by sharing outputs another wallet also spends from, so nothing else can spend what the pool counts on. Mining is a deliberate step, never an unattended one.

func NewIdentityFile

func NewIdentityFile(path string) (*ec.PublicKey, error)

NewIdentityFile writes a fresh root key to path at 0600 and returns its public key. It refuses an existing file. It is how a rotation's successor comes to exist: a second identity file beside the primary, promoted to identity.json once the successor has published under it.

func PaymentKeyID

func PaymentKeyID(prefix, suffix string) string

PaymentKeyID is BRC-29's key id for one output.

func Rescan

func Rescan(ctx context.Context, e *Signer, pool *Pool, asset *nodeapi.Asset, fromHeight, toHeight uint32) (int, error)

Rescan walks heights fromHeight..toHeight inclusive and adds any coinbase output paying FundScript that the pool does not already hold. It is the recovery for a funding run that stopped between mining and adding.

Types

type Chain

type Chain interface {
	CurrentHeight(ctx context.Context) (uint32, error)
}

Chain answers the tip height. The headers package's Client satisfies it.

type Derivation

type Derivation struct {
	SecurityLevel   int    `json:"level"`
	Protocol        string `json:"protocol"`
	KeyID           string `json:"keyID"`
	CounterpartyHex string `json:"counterparty"`
	// OwnerHex is the identity that can derive the private key.
	OwnerHex string `json:"owner"`
}

Derivation is a BRC-42/43 derivation an output was locked with, enough to re-derive the spending key: the protocol triple, the key id, and the counterparty whose shared secret the derivation used.

type Embedded

type Embedded struct {
	*wallet.ProtoWallet

	// Pool is the funding pool.
	Pool *Pool
	// Chain is optional; without it GetHeight answers ErrNoChain.
	Chain Chain
	// Mainnet selects the address prefix and the GetNetwork answer.
	Mainnet bool
	// Originator is the BRC-100 originator presented on every key call this
	// wallet makes for itself. Against a remote wallet an empty originator
	// defeats per-application permissioning for exactly the calls that spend.
	Originator string
	// contains filtered or unexported fields
}

Embedded is the wallet. The seven crypto methods are the SDK's ProtoWallet; everything else is here.

func Create

func Create(dir string, p Profile) (*Embedded, error)

Create makes a new identity under dir and returns the opened wallet. It refuses to touch a directory that already holds one, and refuses an incomplete profile before touching the disk at all.

func Open

func Open(dir string, p Profile) (*Embedded, error)

Open reads an existing identity and its pool from dir. A missing pool is an empty pool, not an error: an identity that has never been funded is a valid state. A missing identity is an error, never a silent Create.

func OpenIdentity

func OpenIdentity(path string, pool *Pool, p Profile) (*Embedded, error)

OpenIdentity opens the identity file at path as a wallet that shares pool with the primary. Two keys share one pool across a rotation: the predecessor still spends the fee outputs locked to its fund key and unlocks the last token it locked, while the successor signs everything new. Which wallet may spend a given pool output is decided by its locking script, never by which file was opened first. The profile names the opened wallet's fund derivation; the primary's is the one to pass, so that both sides of a rotation derive their fund keys the same way.

func (*Embedded) DerivedKey

func (e *Embedded) DerivedKey(ctx context.Context, d *Derivation) (*ec.PublicKey, error)

func (*Embedded) DerivedScript

func (e *Embedded) DerivedScript(ctx context.Context, d *Derivation) (*script.Script, error)

func (*Embedded) DerivedUnlocker

func (e *Embedded) DerivedUnlocker(d *Derivation) transaction.UnlockingScriptTemplate

func (*Embedded) Dir

func (e *Embedded) Dir() string

Dir is the directory the wallet was opened from.

func (*Embedded) FundAddress

func (e *Embedded) FundAddress(mainnet bool) (string, error)

FundAddress is FundKey as an address. See Signer.

func (*Embedded) FundKey

func (e *Embedded) FundKey() (*ec.PublicKey, error)

FundKey is the public half of the funding key. See Signer.

func (*Embedded) FundScript

func (e *Embedded) FundScript() (*script.Script, error)

FundScript is the P2PKH script for FundKey. See Signer.

func (*Embedded) FundUnlocker

func (e *Embedded) FundUnlocker() transaction.UnlockingScriptTemplate

FundUnlocker spends an output locked to FundScript. See Signer.

func (*Embedded) GetHeaderForHeight

func (e *Embedded) GetHeaderForHeight(ctx context.Context, args wallet.GetHeaderArgs, _ string) (*wallet.GetHeaderResult, error)

GetHeaderForHeight answers from Chain when it also serves headers, or ErrNoChain, or ErrNotSupported when the chain source serves heights only.

func (*Embedded) GetHeight

func (e *Embedded) GetHeight(ctx context.Context, _ any, _ string) (*wallet.GetHeightResult, error)

GetHeight answers from Chain, or ErrNoChain.

func (*Embedded) GetNetwork

GetNetwork is testnet unless configured mainnet, so a private chain answers testnet too.

func (*Embedded) GetVersion

GetVersion names this backend: the profile's Version.

func (*Embedded) IdentityKey

func (e *Embedded) IdentityKey() *ec.PublicKey

IdentityKey is the root public key: the subject of every record.

func (*Embedded) IsAuthenticated

func (e *Embedded) IsAuthenticated(context.Context, any, string) (*wallet.AuthenticatedResult, error)

IsAuthenticated is always true: the key is on disk, there is no session.

func (*Embedded) ListOutputs

ListOutputs answers from the pool. Only the profile's FundBasket (or an empty basket, meaning all) holds anything; any other basket is legitimately empty.

Spendable is decided by coinbase maturity against Chain's tip when a Chain is configured. Without one a coinbase's maturity is unknowable, and it is reported NOT spendable rather than guessed: a wrong "spendable" is a rejected transaction, a wrong "not yet" is a wait.

func (*Embedded) PaymentDestination

func (e *Embedded) PaymentDestination(ctx context.Context, recipientHex, prefix, suffix string) (*script.Script, error)

PaymentDestination, DerivedKey, DerivedScript and DerivedUnlocker on the embedded wallet are the Signer's; see signer.go.

func (*Embedded) Signer

func (e *Embedded) Signer() *Signer

Signer views the embedded wallet as a Signer, under the profile it was opened with.

func (*Embedded) WaitForAuthentication

func (e *Embedded) WaitForAuthentication(context.Context, any, string) (*wallet.AuthenticatedResult, error)

WaitForAuthentication has nothing to wait for and says so rather than answering true: a caller that waits expects a session to appear.

type HeaderSource

type HeaderSource interface {
	HeaderForHeight(ctx context.Context, height uint32) ([]byte, error)
}

HeaderSource is optionally implemented by a Chain that can also serve the 80 raw header bytes at a height, which is what GetHeaderForHeight returns.

type Output

type Output struct {
	TxID          string `json:"txid"`
	Vout          uint32 `json:"vout"`
	Satoshis      uint64 `json:"satoshis"`
	LockingScript string `json:"lockingScript"`
	Height        uint32 `json:"height,omitempty"`
	Coinbase      bool   `json:"coinbase,omitempty"`
	Raw           string `json:"raw,omitempty"`
	Bump          string `json:"bump,omitempty"`
	// Unproven marks change from a transaction that was published before it
	// mined. Such a coin is not taken until its parent's proof arrives: a
	// transaction spending it would otherwise carry an unproven parent in
	// its BEEF, and that parent's own inputs after it, so one quick update
	// after another would publish an ever-deeper chain of unmined ancestry.
	// Holding it back keeps every fee input one hop from a proven parent.
	Unproven bool `json:"unproven,omitempty"`
	// Derivation names the key that spends this output when it is not the
	// fund key: a payment received under BRC-29 is locked to a key derived
	// from the sender's identity, and only the wallet that owns Owner can
	// re-derive it.
	Derivation *Derivation `json:"derivation,omitempty"`
}

Output is one spendable output the wallet holds. Raw and Bump are optional: a coinbase output straight from generatetoaddress has neither until its block's proof is fetched, and a spend that needs to be a BEEF ancestor asks for them then.

Height and Coinbase are fields of their own because maturity is what decides whether an output is spendable.

func (Output) Outpoint

func (o Output) Outpoint() string

Outpoint renders txid.vout, the BRC-100 form.

func (Output) Spendable

func (o Output) Spendable(tip uint32) bool

Spendable reports whether the output may be spent at tip. Only coinbase has a maturity rule; everything else is spendable on arrival.

type Pool

type Pool struct {
	// contains filtered or unexported fields
}

Pool is the on-disk funding pool. Every mutation that changes what may be spent (Add, Take, Return, Remove) saves before it returns: a reservation that lives only in memory is a double spend after a crash.

func LoadPool

func LoadPool(path string) (*Pool, error)

LoadPool opens the pool at path, or an empty pool when the file does not exist yet.

func OpenPool

func OpenPool(dir string, prof Profile) (*Pool, error)

OpenPool opens the spendable outputs under dir without an identity file, creating the directory and an empty file when there is none. It is what a directory backed by a wire wallet uses: the key lives in that wallet, the coin lives here. The profile names the legacy file it adopts.

func (*Pool) Add

func (p *Pool) Add(outs ...Output) (int, error)

Add appends outputs the pool does not already hold and saves. It returns how many were new. Duplicates are keyed by outpoint, so a rescan over blocks already funded from is a no-op rather than a double entry.

func (*Pool) Balance

func (p *Pool) Balance() uint64

Balance is the satoshi total of everything held, spendable or not.

func (*Pool) Count

func (p *Pool) Count() int

Count is how many outputs are held, spendable or not.

func (*Pool) Immature

func (p *Pool) Immature(tip uint32) []Output

Immature lists the outputs that are held but not spendable at tip.

func (*Pool) Outputs

func (p *Pool) Outputs() []Output

Outputs is a snapshot of everything held, oldest first.

func (*Pool) Path

func (p *Pool) Path() string

Path is the file the pool persists to.

func (*Pool) Prove

func (p *Pool) Prove(txid, bump string, height uint32) (int, error)

Prove records that txid mined: every output of it the pool holds gets its proof and height and becomes spendable. It reports how many changed.

func (*Pool) Remove added in v0.6.2

func (p *Pool) Remove(o Output) (bool, error)

Remove drops the held output with o's outpoint and saves the pool without it, reporting whether one was held. It is for a coin the chain shows spent while the pool still holds it: Take reserves a coin for a spend the caller is about to make, and Remove forgets one a transaction already spent. An output the pool does not hold changes nothing and is not an error. When the save fails the output stays held, so memory and disk agree, and the error is returned.

func (*Pool) Return

func (p *Pool) Return(o Output) error

Return puts a taken output back (a build or submit that did not happen).

func (*Pool) Save

func (p *Pool) Save() error

Save writes the pool to disk.

func (*Pool) Take

func (p *Pool) Take(tip uint32) (Output, error)

Take removes and returns the oldest output spendable at tip, saving the pool without it. The output is reserved from that moment; Return undoes it. A coin of zero satoshis is never taken: it can pay for nothing. Take is TakeAtLeast(tip, 1, nil).

func (*Pool) TakeAllowing

func (p *Pool) TakeAllowing(tip uint32, allow []string) (Output, error)

TakeAllowing is Take, falling back to an unproven coin whose parent is one of allow when no proven coin is spendable.

A caller allows a parent its transaction already carries in full: a state token that the next transition spends as its previous-token input, or a funding tree its carrier spends. Taking that parent's change adds nothing to the spender's BEEF, so it does not deepen the unmined chain the hold on unproven change exists to bound, and it is what lets a wallet with a single coin publish twice inside one block. TakeAllowing is TakeAtLeast(tip, 1, allow).

func (*Pool) TakeAtLeast added in v0.5.0

func (p *Pool) TakeAtLeast(tip uint32, minSats uint64, allow []string) (Output, error)

TakeAtLeast is TakeAllowing restricted to coins of at least minSats satoshis, for a caller that knows what the coin must pay: a coin below it is left in the pool rather than handed out to fail the build. A minSats of zero counts as one, since a coin of nothing pays for nothing. The oldest proven coin that is large enough is taken first, then the oldest allowed unproven one. When coins are held but none is large enough the error wraps ErrNoSpendable and says so.

func (*Pool) UnprovenTxids

func (p *Pool) UnprovenTxids() []string

UnprovenTxids is every distinct parent the pool is waiting on a proof for.

type Profile

type Profile struct {
	// FundProtocol and FundKeyID derive the funding key, always with the
	// counterparty self. Changing either after the first coin is paid to
	// the key strands that coin. The security level is taken as given:
	// BRC-43's level 0 is valid, and Validate cannot tell it from a level
	// left unset, so a profile states its level and 0 is derived under as
	// 0, never replaced by a default. Validate does refuse a level outside
	// 0..2, and a protocol name or key id the SDK's key deriver would
	// refuse, so such a profile fails when a wallet is opened rather than
	// at its first fund derivation.
	FundProtocol wallet.Protocol
	FundKeyID    string
	// FundBasket is the one basket ListOutputs answers for. BRC-100 basket
	// names allow lowercase letters, digits and spaces only.
	FundBasket string
	// Version is what GetVersion answers.
	Version string
	// LegacyPoolFile is the name the coin file had before it was
	// wallet.json. Open and OpenPool rename a file left under it, in place,
	// when the directory holds no wallet.json.
	//
	// It is for an application whose existing wallet directories keep coin
	// under an earlier name: without the rename such a directory opens as an
	// empty wallet, which reads exactly like spent coin. It is required,
	// rather than empty meaning none, so that such an application cannot leave
	// it out by accident. An application with no earlier name puts a file name
	// of its own there that no program writes into the wallet's directory,
	// such as "<application>-pool.json"; the file never exists, so nothing is
	// ever adopted. Any file found under the name is adopted as the coin file.
	LegacyPoolFile string
}

Profile is what makes a wallet one application's: the derivation of its funding key, the basket its coin is listed under, the version it answers, and the name its coin file had before wallet.json.

It is required and has no default. Each field is fixed by what already exists outside the program: coin is paid to the fund key, other wallets and tools look that coin up by the basket, and a directory whose coin still sits under the legacy name opens empty if the name is wrong, which reads exactly like spent coin. A default would let a caller that forgot the profile derive some other application's key without an error.

func (Profile) Validate

func (p Profile) Validate() error

Validate reports the first field of p that is missing or unusable.

The legacy name must be a bare file name and neither of the wallet's own files: adoption renames it over wallet.json, so naming identity.json there would move the root key out from under the wallet.

Example

A Profile has no default. Validate names the first field that is missing or unusable, before any wallet is opened or any key is derived.

package main

import (
	"fmt"

	"github.com/bsv-blockchain/go-sdk/wallet"

	"github.com/lightwebinc/bcommon/bwallet"
)

func main() {
	p := bwallet.Profile{
		FundProtocol:   wallet.Protocol{SecurityLevel: wallet.SecurityLevelEveryAppAndCounterparty, Protocol: "example app"},
		FundKeyID:      "coin",
		FundBasket:     "example coin",
		Version:        "example-embedded-1",
		LegacyPoolFile: "example-pool.json",
	}
	fmt.Println(p.Validate())

	p.FundBasket = ""
	fmt.Println(p.Validate())

	p.FundBasket = "example coin"
	p.LegacyPoolFile = "identity.json"
	fmt.Println(p.Validate())
}
Output:
<nil>
bwallet: wallet profile incomplete: FundBasket is empty
bwallet: wallet profile incomplete: LegacyPoolFile "identity.json" is one of the wallet's own files

type Signer

type Signer struct {
	wallet.Interface
	// Identity is the root public key: the subject of every record.
	Identity *ec.PublicKey
	// Originator is the BRC-100 originator presented on every call.
	Originator string
	// Mainnet selects the address prefix where one is rendered.
	Mainnet bool
	// Profile names the funding key. It has no default: the fund methods
	// refuse a Signer whose Profile is incomplete rather than derive some
	// other key, while the calls that never touch the fund key work
	// without one.
	Profile Profile
}

Signer is an identity that signs through a BRC-100 wallet: the embedded wallet here, or one reached over the wallet wire. Every derivation hangs off the identity key through GetPublicKey and every signature goes through CreateSignature, so nothing here depends on which program holds the root key.

func (*Signer) DerivedKey

func (s *Signer) DerivedKey(ctx context.Context, d *Derivation) (*ec.PublicKey, error)

DerivedKey is the recipient's side: our public key under d, which must equal what the sender derived for us. It is how a payment is proven to be ours before it enters the pool.

func (*Signer) DerivedScript

func (s *Signer) DerivedScript(ctx context.Context, d *Derivation) (*script.Script, error)

DerivedScript is the P2PKH script for DerivedKey.

func (*Signer) DerivedUnlocker

func (s *Signer) DerivedUnlocker(d *Derivation) transaction.UnlockingScriptTemplate

DerivedUnlocker spends a P2PKH output locked to the key d derives, the same way the fund unlocker spends the fund key: the sighash goes to CreateSignature under the derivation and the key never leaves the deriver.

func (*Signer) FundAddress

func (s *Signer) FundAddress(mainnet bool) (string, error)

FundAddress renders the funding key as a base58 address for generatetoaddress.

func (*Signer) FundKey

func (s *Signer) FundKey() (*ec.PublicKey, error)

FundKey is the public half of the funding key, derived through the wallet under the Signer's Profile.

func (*Signer) FundScript

func (s *Signer) FundScript() (*script.Script, error)

FundScript is the P2PKH locking script of the funding key: what coinbase pays to and what the pool holds.

func (*Signer) FundUnlocker

func (s *Signer) FundUnlocker() transaction.UnlockingScriptTemplate

FundUnlocker returns a template that signs a pool output through the wallet. It satisfies transaction.UnlockingScriptTemplate, so it can be handed to AddInputFrom and signed by tx.Sign.

func (*Signer) IdentityHex

func (s *Signer) IdentityHex() string

IdentityHex is the identity key as compressed hex, the form state files and the domain's resolve document carry.

func (*Signer) IdentityKey

func (s *Signer) IdentityKey() *ec.PublicKey

IdentityKey is the root public key.

func (*Signer) PaymentDestination

func (s *Signer) PaymentDestination(ctx context.Context, recipientHex, prefix, suffix string) (*script.Script, error)

PaymentDestination is the sender's side of BRC-29: the P2PKH script for recipient under the sender's root, derived through the wallet.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL