Documentation
¶
Overview ¶
Package payee is the payee side of BRC-105 payments for priced questions: the payee key a host is configured with, the ledger in which a host's listener records each payment it accepted, and settle, which takes those payments into the payee's coin pool.
A host that prices a question is paid to its payee key: each payment is a BRC-29 output to a key derived from it, recorded by the host in its ledger (LedgerFile in its state directory) before the question is answered, and not broadcast by the host. Until it is settled the payer can still spend the coins elsewhere: a payment is money only once settled.
The payee key and the home ¶
The payee is a home of the embedded wallet (package bwallet): its root key in IdentityFile is the payee key, and its coin pool is where settled payments go. HomeKey reads that key, KeyLine is the line a host's environment takes it in, and CreateKeyFile writes that line to a new file at mode 0600, never over an existing one.
The ledger ¶
The ledger is JSON Lines, one accepted payment a line, appended and flushed before the question is answered. A line cut short by a crash is passed over: its question was never answered. The format is versioned (LedgerV1, LedgerV2); every version is read, a line written by a later version is reported and left for a reader that knows it, and Payment.Line writes the current version byte for byte as a host writes it. Claims is the host's own rule over the ledger: a txid is accepted once (a replay is refused) and a coin pays once (a payment that spends a coin an accepted payment spent is a conflict); a host answers both 409.
Settle ¶
Settler takes every payment in one or more ledgers that the payee has not settled into its pool. Each is checked to pay the key the payee derives for its remittance and payer, verified against the headers, broadcast through the settlement leg, waited for until it mines and added to the pool, and its txid is recorded as settled. Every payment is broadcast before any is waited for, so a run takes about one block however many there are; InFlight bounds how many are broadcast and not yet mined at once. A payment the network refuses for good (its payer spent an input elsewhere, which the purse reads from the leg's answer and from the node's view of the inputs through chainview) is reported once, recorded, and passed over by later runs. A run is idempotent, so it is safe on a timer: the sooner a payment is settled, the shorter the window in which the payer can take it back.
The record of what is settled is the application's own state, through Record; Book is its JSON shape, which an application embeds in its state file. The settle step itself is the purse's (package purse), through Payer.
Every piece of text someone else wrote (a class name, a network's reason) is held to one line and filtered for the terminal (Field) before it is printed.
Index ¶
- Constants
- Variables
- func CreateKeyFile(path, env string, k *ec.PrivateKey) (string, error)
- func Field(s string) string
- func HomeKey(dir, app string) (*ec.PrivateKey, error)
- func KeyEnv(app string) string
- func KeyLine(env string, k *ec.PrivateKey) string
- func Words(err error) error
- type Book
- type Claim
- type Claims
- type Payer
- type Payment
- type Pool
- type Record
- type Report
- type Settler
- type Unsettleable
Examples ¶
Constants ¶
const ( // LedgerV1 is txid, beef, outputIndex, satoshis, derivationPrefix, // derivationSuffix, senderIdentityKey and class. LedgerV1 = 1 // LedgerV2 adds inputs (every outpoint the payment spends) and at // (when it was accepted, Unix seconds). LedgerV2 = 2 // LedgerVersion is the latest version this package reads and the one // Payment.Line writes. LedgerVersion = LedgerV2 )
The ledger's versions. A line carries no version of its own up to LedgerV2, and is told apart by its shape; a later version writes its number in the "v" key.
const ( // DefaultInFlight is how many payments a run has broadcast and not yet // mined at once, unless told otherwise. DefaultInFlight = 16 // MaxInFlight is the most it may. MaxInFlight = 64 )
Settling.
const IdentityFile = "identity.json"
IdentityFile is the file in a home that holds its root key, {"wif": ...}, as package bwallet writes it. The payee key is that key: the home is then the payee's wallet, and settled payments go to its pool.
const LedgerFile = "payments.jsonl"
LedgerFile is the ledger's name in a host's state directory.
const MaxLine = 16 << 20
MaxLine is the longest ledger line read: a payment rides in one line, its Atomic BEEF in base64.
const PrintedKeyNote = "the line above is a private key: keep it out of logs and shells' history"
PrintedKeyNote is what an application says, on its warning stream, after it prints KeyLine to its output.
Variables ¶
var ErrKeyFileExists = errors.New("payee: the key file exists")
ErrKeyFileExists is a key file that is already there: a key is never written over a file.
var ErrNewerLedger = errors.New("payee: the line is of a later ledger version than this reader")
ErrNewerLedger is a ledger line written by a later version than this package reads: it is not settled, and stays in the ledger for a reader that knows it.
var ErrNotPayment = errors.New("payee: not a payment")
ErrNotPayment is a ledger line that is not a payment.
Functions ¶
func CreateKeyFile ¶
func CreateKeyFile(path, env string, k *ec.PrivateKey) (string, error)
CreateKeyFile writes KeyLine to a new file at path, mode 0600. A file already at path is left as it is, and the error is ErrKeyFileExists (and os.ErrExist). It returns what an application says on its output once it has: "wrote <env> to <path> for payee <compressed public key>".
func Field ¶
Field is someone else's text that fills one field of a line (a class name, a network's reason): held to that line, every line break a space, and filtered for the terminal. A line break in it would otherwise write lines of its own in a report, in the command's voice.
func HomeKey ¶
func HomeKey(dir, app string) (*ec.PrivateKey, error)
HomeKey is the payee key of the home at dir, read from its IdentityFile. app names the command that makes a home, in the error for a home that has none.
func KeyEnv ¶
KeyEnv is the environment variable a host of app takes its payee key in: for "app", APP_PAYEE_KEY.
Types ¶
type Book ¶
type Book struct {
// Settled are the txids of payments to this identity, as a host's
// payee, that settle internalized.
Settled []string `json:"settled,omitempty"`
// Unsettleable are payments to this identity, as a payee, that the
// network refused for good (the payer spent the inputs elsewhere):
// settle reports each once and then passes it over.
Unsettleable []Unsettleable `json:"unsettleable,omitempty"`
}
Book is a payee's record in its state file's JSON: an application embeds it in its state struct, where the two keys keep their place and their bytes.
func (*Book) AddSettled ¶
AddSettled records txid as settled, once.
func (*Book) AddUnsettleable ¶
AddUnsettleable records txid as refused, once.
func (*Book) IsUnsettleable ¶
IsUnsettleable reports whether txid is a payment recorded as refused.
type Claim ¶
type Claim int
Claim is what a host says of a payment offered to it.
const ( // Accepted is a payment taken. Accepted Claim = iota // Replayed is a txid accepted before: one payment buys one question. Replayed // Conflict is a payment that spends a coin an accepted payment spent, // or names no coin at all: at most one of two such payments can ever // settle, whatever their txids. Conflict )
type Claims ¶
type Claims struct {
// contains filtered or unexported fields
}
Claims are the payments a host took, by txid and by the coins they spend. A payment is unbroadcast when it is offered, so two transactions spending one coin both verify. The zero value holds none.
Example ¶
A host accepts a txid once, and a coin once: it answers a replay and a conflict 409.
package main
import (
"fmt"
"github.com/lightwebinc/bcommon/payee"
)
func main() {
var c payee.Claims
fmt.Println(c.Claim("aa", []string{"cc.0"}))
fmt.Println(c.Claim("aa", []string{"cc.0"}))
fmt.Println(c.Claim("bb", []string{"cc.0", "dd.1"}))
fmt.Println(c.Claim("bb", []string{"dd.1"}))
}
Output: accepted replayed conflict accepted
func ClaimsOf ¶
ClaimsOf are the claims a ledger already holds: each line's txid, and the coins it spends (Spends); a line whose coins cannot be read claims its txid alone.
type Payer ¶
type Payer interface {
Check(ctx context.Context, args wallet.InternalizeActionArgs) (*purse.Incoming, error)
Broadcast(ctx context.Context, in *purse.Incoming) error
Await(ctx context.Context, in *purse.Incoming) error
Take(in *purse.Incoming) error
}
Payer is the payee leg of the purse (*purse.Purse): Check, Broadcast, Await and Take, the halves of InternalizeAction, run apart so that every payment is broadcast before any is waited for. Broadcast and Await answer a *purse.RefusedError for a payment the network will never mine: the leg refused it for good, or the node shows an input spent by another transaction (package chainview).
type Payment ¶
type Payment struct {
// Txid is the payment's txid, in display order.
Txid string `json:"txid"`
// Beef is the payment as Atomic BEEF, standard base64.
Beef string `json:"beef"`
// OutputIndex is the output that pays the payee.
OutputIndex uint32 `json:"outputIndex"`
// Satoshis is what that output holds.
Satoshis uint64 `json:"satoshis"`
// DerivationPrefix and DerivationSuffix are the BRC-29 remittance,
// base64, and SenderIdentityKey the payer, compressed hex.
DerivationPrefix string `json:"derivationPrefix"`
DerivationSuffix string `json:"derivationSuffix"`
SenderIdentityKey string `json:"senderIdentityKey"`
// Class is the class of the question it paid for.
Class string `json:"class"`
// Inputs are every outpoint the payment spends, "<txid>.<index>" with
// the txid in display order (LedgerV2).
Inputs []string `json:"inputs,omitempty"`
// At is when the host accepted it, Unix seconds (LedgerV2).
At int64 `json:"at,omitempty"`
// V is the version a line names, from the first version that names
// one; zero on every LedgerV1 and LedgerV2 line.
V int `json:"v,omitempty"`
}
Payment is one line of a host's ledger: a BRC-105 payment the host accepted. Its JSON is the line, in the order a host writes it.
func ParseLine ¶
ParseLine reads one ledger line. A line that is not JSON, or names no txid, is ErrNotPayment.
func ReadLedger ¶
ReadLedger reads a ledger. A line that does not parse (one cut short by a crash, whose question was never answered) is passed over and named on warn, as "<name> line <n>: not a payment; skipped"; warn may be nil.
Example ¶
A host's ledger is read whatever version wrote each line, and a line cut short by a crash is passed over: its question was never answered.
package main
import (
"bytes"
"fmt"
"strings"
"github.com/lightwebinc/bcommon/payee"
)
func main() {
ledger := `{"txid":"aa","beef":"","outputIndex":0,"satoshis":5,"derivationPrefix":"cA==","derivationSuffix":"cw==","senderIdentityKey":"02ab","class":"history"}
{"txid":"bb","beef":"","outputIndex":0,"satoshis":7,"derivationPrefix":"cA==","derivationSuffix":"cw==","senderIdentityKey":"02ab","class":"history","inputs":["cc.0"],"at":1}
{"txid":"dd","be`
var warn bytes.Buffer
ps, err := payee.ReadLedger(strings.NewReader(ledger), "payments.jsonl", &warn)
for _, p := range ps {
fmt.Println(p.Txid, p.Satoshis, "version", p.Version())
}
fmt.Print(warn.String(), err, "\n")
line, _ := ps[1].Line()
fmt.Print(string(line))
}
Output: aa 5 version 1 bb 7 version 2 payments.jsonl line 3: not a payment; skipped <nil> {"txid":"bb","beef":"","outputIndex":0,"satoshis":7,"derivationPrefix":"cA==","derivationSuffix":"cw==","senderIdentityKey":"02ab","class":"history","inputs":["cc.0"],"at":1}
func ReadLedgers ¶
ReadLedgers reads every ledger at paths in turn. A payment in two (a copy, or two hosts sharing one) is returned once, as first read.
func (Payment) Line ¶
Line is the payment as a ledger line, newline included, byte for byte as a host writes it: JSON with no HTML escaping, keys in the order above. An empty Inputs, which a host never accepts, is left out.
type Record ¶
type Record interface {
IsSettled(txid string) bool
IsUnsettleable(txid string) bool
RecordSettled(txid string) error
RecordUnsettleable(txid, why string) error
}
Record is where a payee keeps what settle did, across runs: the payments it settled and those the network refused for good. Settle asks before it touches a payment and records each outcome as it comes, so a run cut short loses nothing it finished. Each Record method that records persists before it returns.
type Report ¶
type Report struct {
// Settled payments this run, and the satoshis they paid.
Settled int
Sats uint64
// Before were settled by an earlier run.
Before int
// NotSettled failed a check, or did not mine in time: a later run
// tries them again, and until one settles, its payer can spend the
// coins elsewhere.
NotSettled int
// Refused were refused by the network this run, and RefusedBefore by
// an earlier one: they will never settle.
Refused int
RefusedBefore int
// PoolOutputs and PoolSats are the pool after the run.
PoolOutputs int
PoolSats uint64
}
Report counts a run.
type Settler ¶
type Settler struct {
// App is the application's name: a payment is internalized with the
// description "<App> priced question <class>" and the labels App and
// "payee".
App string
// Payer is the payee's purse, and Record its record of what is
// settled. Pool, when set, is reported.
Payer Payer
Record Record
Pool Pool
// InFlight bounds the payments broadcast and not yet mined at once,
// 1 to MaxInFlight; zero is DefaultInFlight.
InFlight int
// Words puts a refusal of the purse in the application's words (which
// setting names the node or the leg); nil is Words.
Words func(error) error
// Out takes a line for each payment settled and the report; Warn a
// line for each payment not settled or refused. Either may be nil.
Out, Warn io.Writer
}
Settler settles a host's payments into a payee's pool.
Example ¶
An application settles from its command: it reads the ledgers before it opens its home, hands the Settler its purse and its record, and ends a run that did not settle everything with its refusal status. Here the record already holds every payment, so the run touches no purse.
package main
import (
"bytes"
"context"
"fmt"
"github.com/lightwebinc/bcommon/payee"
)
func main() {
ps := []payee.Payment{{Txid: "aa", Satoshis: 5, Class: "history"}, {Txid: "bb", Satoshis: 7, Class: "audit"}}
var book payee.Book // embedded in the application's state
book.AddSettled("aa")
book.AddUnsettleable("bb", "input 0 (cc.0) is spent by dd")
var out bytes.Buffer
s := &payee.Settler{
App: "sample",
Payer: noPurse{}, // the payee's *purse.Purse
Record: payee.Saved(&book, func() error { return nil }),
Out: &out,
}
rep, err := s.Settle(context.Background(), ps)
fmt.Print(out.String())
fmt.Printf("%q %v\n", rep.Problem(), err)
}
// noPurse stands in for the purse in the example; it is never asked.
type noPurse struct{ payee.Payer }
Output: 0 payment(s) settled, 0 sat; 1 settled before; 0 not settled; 0 refused (1 before); pool 0 output(s), 0 sat "" <nil>
func (*Settler) Settle ¶
Settle takes into the pool every payment in ps the record holds neither as settled nor as refused, and reports the run. It returns an error only when the run cannot go on (a Settler not set up, a record that does not persist); a payment that is not settled, a context that ended among them, is counted, named on Warn, and left for the next run.
type Unsettleable ¶
Unsettleable is a payment the network refused, and why.