package api import ( "encoding/binary" "fmt" ) // The request and result payloads, one pair per method. Each parser refuses a payload with // bytes left over: a frame that carries more than its method understands is a version // disagreement, and guessing is how a wallet signs the wrong amount. // errTrailing is a payload with bytes after the fields the method knows. func errTrailing(method Method, rest []byte) error { if len(rest) == 0 { return nil } return fmt.Errorf("%w: %s carries %d unexplained byte(s)", ErrBadFrame, method, len(rest)) } // ChainStatus is one chain's state, as a page would draw it. type ChainStatus struct { // Chain is the wallet's chain name. Chain string // Profile is the chain profile name. Profile string // HRP is the bech32 prefix the chain's addresses use. HRP string // Account is the BIP32 account. Account uint32 // HasWallet reports whether a sealed seed exists. HasWallet bool // Unlocked reports whether the account is in memory. Unlocked bool // Tip and Filters are the last known chain position. Tip uint32 Filters uint32 // Running reports whether the chain said a sync was in progress. Running bool // Receive and Change are the address cursors. Receive uint32 Change uint32 // Matches is how many heights the store recorded. Matches uint32 } // Encode renders a status. func (s ChainStatus) Encode() []byte { var out []byte out = PutText(out, s.Chain) out = PutText(out, s.Profile) out = PutText(out, s.HRP) out = PutUint32(out, s.Account) out = PutBool(out, s.HasWallet) out = PutBool(out, s.Unlocked) out = PutUint32(out, s.Tip) out = PutUint32(out, s.Filters) out = PutBool(out, s.Running) out = PutUint32(out, s.Receive) out = PutUint32(out, s.Change) return PutUint32(out, s.Matches) } // ParseChainStatus reads a status. func ParseChainStatus(payload []byte) (ChainStatus, error) { var ( status ChainStatus err error ) if status.Chain, payload, err = ReadText(payload); err != nil { return ChainStatus{}, err } if status.Profile, payload, err = ReadText(payload); err != nil { return ChainStatus{}, err } if status.HRP, payload, err = ReadText(payload); err != nil { return ChainStatus{}, err } if status.Account, payload, err = ReadUint32(payload); err != nil { return ChainStatus{}, err } if status.HasWallet, payload, err = ReadBool(payload); err != nil { return ChainStatus{}, err } if status.Unlocked, payload, err = ReadBool(payload); err != nil { return ChainStatus{}, err } if status.Tip, payload, err = ReadUint32(payload); err != nil { return ChainStatus{}, err } if status.Filters, payload, err = ReadUint32(payload); err != nil { return ChainStatus{}, err } if status.Running, payload, err = ReadBool(payload); err != nil { return ChainStatus{}, err } if status.Receive, payload, err = ReadUint32(payload); err != nil { return ChainStatus{}, err } if status.Change, payload, err = ReadUint32(payload); err != nil { return ChainStatus{}, err } if status.Matches, payload, err = ReadUint32(payload); err != nil { return ChainStatus{}, err } return status, errTrailing(MethodStatus, payload) } // ChainRequest names one chain. type ChainRequest struct { // Chain is the wallet's chain name. Chain string } // Encode renders the request. func (r ChainRequest) Encode() []byte { return PutText(nil, r.Chain) } // ParseChainRequest reads the request. func ParseChainRequest(payload []byte) (ChainRequest, error) { chain, rest, err := ReadText(payload) if err != nil { return ChainRequest{}, err } return ChainRequest{Chain: chain}, errTrailing(MethodStatus, rest) } // SetupRequest is a create or a restore: a chain, words, and the passphrase that seals them. type SetupRequest struct { // Chain is the wallet's chain name. Chain string // Words is the word count for a create, and ignored for a restore. Words uint16 // Phrase is the words for a restore, and empty for a create. Phrase string // Passphrase seals the seed at rest. Passphrase string } // Encode renders the request. func (r SetupRequest) Encode() []byte { var out []byte out = PutText(out, r.Chain) out = PutUint32(out, uint32(r.Words)) out = PutText(out, r.Phrase) return PutText(out, r.Passphrase) } // ParseSetupRequest reads the request. func ParseSetupRequest(method Method, payload []byte) (SetupRequest, error) { var ( request SetupRequest words uint32 err error ) if request.Chain, payload, err = ReadText(payload); err != nil { return SetupRequest{}, err } if words, payload, err = ReadUint32(payload); err != nil { return SetupRequest{}, err } if request.Phrase, payload, err = ReadText(payload); err != nil { return SetupRequest{}, err } if request.Passphrase, payload, err = ReadText(payload); err != nil { return SetupRequest{}, err } request.Words = uint16(words) return request, errTrailing(method, payload) } // SetupResult is the first address, and on a create the words to write down. type SetupResult struct { // Chain is the wallet's chain name. Chain string // Mnemonic is the phrase, empty on a restore. Mnemonic string // Address is the first receive address. Address string // Path is the address's derivation path, for a person who wants to see it. Path string } // Encode renders the result. func (r SetupResult) Encode() []byte { var out []byte out = PutText(out, r.Chain) out = PutText(out, r.Mnemonic) out = PutText(out, r.Address) return PutText(out, r.Path) } // ParseSetupResult reads the result. func ParseSetupResult(payload []byte) (SetupResult, error) { var ( result SetupResult err error ) if result.Chain, payload, err = ReadText(payload); err != nil { return SetupResult{}, err } if result.Mnemonic, payload, err = ReadText(payload); err != nil { return SetupResult{}, err } if result.Address, payload, err = ReadText(payload); err != nil { return SetupResult{}, err } if result.Path, payload, err = ReadText(payload); err != nil { return SetupResult{}, err } return result, errTrailing(MethodCreate, payload) } // UnlockRequest is a chain and the passphrase that opens it. type UnlockRequest struct { // Chain is the wallet's chain name. Chain string // Passphrase opens the sealed seed. Passphrase string } // Encode renders the request. func (r UnlockRequest) Encode() []byte { return PutText(PutText(nil, r.Chain), r.Passphrase) } // ParseUnlockRequest reads the request. func ParseUnlockRequest(payload []byte) (UnlockRequest, error) { var ( request UnlockRequest err error ) if request.Chain, payload, err = ReadText(payload); err != nil { return UnlockRequest{}, err } if request.Passphrase, payload, err = ReadText(payload); err != nil { return UnlockRequest{}, err } return request, errTrailing(MethodUnlock, payload) } // AddressesRequest asks for a run of addresses. type AddressesRequest struct { // Chain is the wallet's chain name. Chain string // Change selects the change branch. Change bool // From is the first index. From uint32 // Count is how many to derive. Count uint16 } // Encode renders the request. func (r AddressesRequest) Encode() []byte { var out []byte out = PutText(out, r.Chain) out = PutBool(out, r.Change) out = PutUint32(out, r.From) return PutUint32(out, uint32(r.Count)) } // ParseAddressesRequest reads the request. func ParseAddressesRequest(payload []byte) (AddressesRequest, error) { var ( request AddressesRequest count uint32 err error ) if request.Chain, payload, err = ReadText(payload); err != nil { return AddressesRequest{}, err } if request.Change, payload, err = ReadBool(payload); err != nil { return AddressesRequest{}, err } if request.From, payload, err = ReadUint32(payload); err != nil { return AddressesRequest{}, err } if count, payload, err = ReadUint32(payload); err != nil { return AddressesRequest{}, err } request.Count = uint16(count) return request, errTrailing(MethodAddresses, payload) } // AddressesResult is a run of addresses with their indexes and paths. type AddressesResult struct { // Addresses are the bech32 texts. Addresses []string // Indexes are their indexes on the branch. Indexes []uint32 // Paths are their derivation paths. Paths []string } // Encode renders the result. func (r AddressesResult) Encode() []byte { var out []byte out = PutTexts(out, r.Addresses) out = PutUint32s(out, r.Indexes) return PutTexts(out, r.Paths) } // ParseAddressesResult reads the result. func ParseAddressesResult(payload []byte) (AddressesResult, error) { var ( result AddressesResult err error ) if result.Addresses, payload, err = ReadTexts(payload); err != nil { return AddressesResult{}, err } if result.Indexes, payload, err = ReadUint32s(payload); err != nil { return AddressesResult{}, err } if result.Paths, payload, err = ReadTexts(payload); err != nil { return AddressesResult{}, err } return result, errTrailing(MethodAddresses, payload) } // SyncRequest asks a chain to advance. type SyncRequest struct { // Chain is the wallet's chain name. Chain string // Lookahead is how far past the cursor to match, or zero for the default. Lookahead uint32 } // Encode renders the request. func (r SyncRequest) Encode() []byte { return PutUint32(PutText(nil, r.Chain), r.Lookahead) } // ParseSyncRequest reads the request. func ParseSyncRequest(payload []byte) (SyncRequest, error) { var ( request SyncRequest err error ) if request.Chain, payload, err = ReadText(payload); err != nil { return SyncRequest{}, err } if request.Lookahead, payload, err = ReadUint32(payload); err != nil { return SyncRequest{}, err } return request, errTrailing(MethodSync, payload) } // SyncResult is what a sync did. type SyncResult struct { // From is the height the sync started at. From uint32 // Tip and Filters are where the chain ended up. Tip uint32 Filters uint32 // Scripts is how many scripts were matched. Scripts uint32 // Matched is every height the chain matched. Matched []uint32 // New is the part of Matched the store had not seen. New []uint32 } // Encode renders the result. func (r SyncResult) Encode() []byte { var out []byte out = PutUint32(out, r.From) out = PutUint32(out, r.Tip) out = PutUint32(out, r.Filters) out = PutUint32(out, r.Scripts) out = PutHeights(out, r.Matched) return PutHeights(out, r.New) } // ParseSyncResult reads the result. func ParseSyncResult(payload []byte) (SyncResult, error) { var ( result SyncResult err error ) if result.From, payload, err = ReadUint32(payload); err != nil { return SyncResult{}, err } if result.Tip, payload, err = ReadUint32(payload); err != nil { return SyncResult{}, err } if result.Filters, payload, err = ReadUint32(payload); err != nil { return SyncResult{}, err } if result.Scripts, payload, err = ReadUint32(payload); err != nil { return SyncResult{}, err } if result.Matched, payload, err = ReadHeights(payload); err != nil { return SyncResult{}, err } if result.New, payload, err = ReadHeights(payload); err != nil { return SyncResult{}, err } return result, errTrailing(MethodSync, payload) } // Heights is a height list, which is what a match answer is. type Heights struct { // Heights are ascending. Heights []uint32 } // Encode renders the result. func (h Heights) Encode() []byte { return PutHeights(nil, h.Heights) } // ParseHeights reads the result. func ParseHeights(method Method, payload []byte) (Heights, error) { heights, rest, err := ReadHeights(payload) if err != nil { return Heights{}, err } return Heights{Heights: heights}, errTrailing(method, rest) } // Chains is the list of chains a wallet drives. type Chains struct { // Names are the chain names, in a stable order. Names []string } // Encode renders the list. func (c Chains) Encode() []byte { return PutTexts(nil, c.Names) } // ParseChains reads the list. func ParseChains(payload []byte) (Chains, error) { names, rest, err := ReadTexts(payload) if err != nil { return Chains{}, err } return Chains{Names: names}, errTrailing(MethodChains, rest) } // PutUint32s appends a number list: a count, then each number. func PutUint32s(buffer []byte, values []uint32) []byte { buffer = binary.LittleEndian.AppendUint16(buffer, uint16(len(values))) for _, value := range values { buffer = PutUint32(buffer, value) } return buffer } // ReadUint32s reads a number list. func ReadUint32s(payload []byte) ([]uint32, []byte, error) { if len(payload) < 2 { return nil, nil, fmt.Errorf("%w: a number count", ErrBadFrame) } count := int(binary.LittleEndian.Uint16(payload[0:2])) payload = payload[2:] if len(payload) < count*4 { return nil, nil, fmt.Errorf("%w: %d numbers in %d bytes", ErrBadFrame, count, len(payload)) } values := make([]uint32, 0, count) for index := 0; index < count; index++ { values = append(values, binary.LittleEndian.Uint32(payload[index*4:])) } return values, payload[count*4:], nil } // Coin is one spendable output the wallet holds. type Coin struct { // TxID is the creating transaction, in wire order. TxID [32]byte // Vout is the output's position. Vout uint32 // Value is the amount in satoshis. Value uint64 // Height is the confirming block, or zero when unconfirmed. Height uint32 } // CoinList is the wallet's coins. type CoinList struct { // Coins are newest height first. Coins []Coin } // Encode renders the list: a count, then each coin's four fields. func (c CoinList) Encode() []byte { buffer := binary.LittleEndian.AppendUint16(nil, uint16(len(c.Coins))) for _, coin := range c.Coins { buffer = append(buffer, coin.TxID[:]...) buffer = binary.LittleEndian.AppendUint32(buffer, coin.Vout) buffer = binary.LittleEndian.AppendUint64(buffer, coin.Value) buffer = binary.LittleEndian.AppendUint32(buffer, coin.Height) } return buffer } // ParseCoinList reads the list. func ParseCoinList(payload []byte) (CoinList, error) { const size = 32 + 4 + 8 + 4 if len(payload) < 2 { return CoinList{}, fmt.Errorf("%w: a coin count", ErrBadFrame) } count := int(binary.LittleEndian.Uint16(payload[0:2])) payload = payload[2:] if len(payload) != count*size { return CoinList{}, fmt.Errorf("%w: %d coins in %d bytes", ErrBadFrame, count, len(payload)) } coins := make([]Coin, 0, count) for index := 0; index < count; index++ { raw := payload[index*size : (index+1)*size] var coin Coin copy(coin.TxID[:], raw[0:32]) coin.Vout = binary.LittleEndian.Uint32(raw[32:36]) coin.Value = binary.LittleEndian.Uint64(raw[36:44]) coin.Height = binary.LittleEndian.Uint32(raw[44:48]) coins = append(coins, coin) } return CoinList{Coins: coins}, nil }