// Package api is the vocabulary between the page and the wallet worker. // // It is the same discipline as the chain-data channel on the other side of the worker: binary // frames, an id on every request, a cancel, and a close that means the far end died. The // payloads are binary because an address is a string, a height is four bytes, and a phrase is // a secret that has no business being base64'd into a log. // // The page encodes and decodes these frames from JavaScript, so the format is fixed and // boring: an 18-byte header and a payload, little-endian, every string length-prefixed. package api import ( "encoding/binary" "errors" "fmt" ) // The frame layout. The header is fixed so a decoder can reject anything it does not // understand before it allocates. const ( // Magic is "TWA1" read little-endian. Magic uint32 = 0x31415754 // Version is the frame format. Version uint8 = 1 // HeaderLen is magic, version, kind, method, status, id and length. HeaderLen = 18 // MaxPayload bounds one frame. A phrase is 24 words; an address list is a few hundred // bytes. Anything larger is a bug or an attack. MaxPayload = 1 << 20 ) // ErrBadFrame is a frame this version cannot read. var ErrBadFrame = errors.New("api: malformed frame") // Kind is what a frame is. type Kind uint8 // The kinds. A request expects one result; a cancel withdraws one; a close ends everything. const ( // KindRequest asks for one method to run. KindRequest Kind = 1 // KindResult answers one request, with the request's id. KindResult Kind = 2 // KindCancel withdraws a request. KindCancel Kind = 3 // KindClose ends the conversation. KindClose Kind = 4 ) // String renders a kind. func (k Kind) String() string { switch k { case KindRequest: return "request" case KindResult: return "result" case KindCancel: return "cancel" case KindClose: return "close" } return "kind(" + itoa(uint32(k)) + ")" } // Method is what a request asks for. type Method uint16 // The methods. const ( // MethodStatus reports what the wallet knows about a chain. MethodStatus Method = 1 // MethodCreate makes a new wallet and returns the words once. MethodCreate Method = 2 // MethodRestore makes a wallet from words. MethodRestore Method = 3 // MethodUnlock opens the wallet with the passphrase. MethodUnlock Method = 4 // MethodLock forgets the unlocked wallet. MethodLock Method = 5 // MethodAddresses derives addresses. MethodAddresses Method = 6 // MethodSync walks the chain and records matches. MethodSync Method = 7 // MethodMatches lists the recorded heights. MethodMatches Method = 8 // MethodChains lists the chains the wallet drives. MethodChains Method = 9 // MethodUnspent lists the coins the wallet can spend. MethodUnspent Method = 10 ) // String renders a method. func (m Method) String() string { switch m { case MethodStatus: return "status" case MethodCreate: return "create" case MethodRestore: return "restore" case MethodUnlock: return "unlock" case MethodLock: return "lock" case MethodAddresses: return "addresses" case MethodSync: return "sync" case MethodMatches: return "matches" case MethodChains: return "chains" case MethodUnspent: return "unspent" } return "method(" + itoa(uint32(m)) + ")" } // Status is how an answer went. type Status uint16 // The statuses. const ( // StatusOK is a completed request. StatusOK Status = 0 // StatusFailed is a refusal with a message. StatusFailed Status = 1 // StatusCanceled is a request the caller withdrew. StatusCanceled Status = 2 // StatusUnknownMethod is a method this build does not have. StatusUnknownMethod Status = 3 // StatusMalformed is a payload that did not decode, or a lock that is wrong. StatusMalformed Status = 4 // StatusClosed is an answer to a request whose channel ended. StatusClosed Status = 5 ) // String renders a status. func (s Status) String() string { switch s { case StatusOK: return "ok" case StatusFailed: return "failed" case StatusCanceled: return "canceled" case StatusUnknownMethod: return "unknown method" case StatusMalformed: return "malformed" case StatusClosed: return "closed" } return "status(" + itoa(uint32(s)) + ")" } // Frame is one message. type Frame struct { // Kind is what the frame is. Kind Kind // Method is what it asks for, on a request. Method Method // Status is how it went, on a result. Status Status // ID echoes the request on every answer. ID uint32 // Payload is the method's bytes. Payload []byte } // Encode renders a frame. func Encode(frame Frame) ([]byte, error) { if len(frame.Payload) > MaxPayload { return nil, fmt.Errorf("%w: a %d-byte payload", ErrBadFrame, len(frame.Payload)) } out := make([]byte, HeaderLen, HeaderLen+len(frame.Payload)) binary.LittleEndian.PutUint32(out[0:4], Magic) out[4] = Version out[5] = byte(frame.Kind) binary.LittleEndian.PutUint16(out[6:8], uint16(frame.Method)) binary.LittleEndian.PutUint16(out[8:10], uint16(frame.Status)) binary.LittleEndian.PutUint32(out[10:14], frame.ID) binary.LittleEndian.PutUint32(out[14:18], uint32(len(frame.Payload))) out = append(out, frame.Payload...) return out, nil } // Decode reads a frame. func Decode(raw []byte) (Frame, error) { if len(raw) < HeaderLen { return Frame{}, fmt.Errorf("%w: %d bytes is shorter than a header", ErrBadFrame, len(raw)) } if magic := binary.LittleEndian.Uint32(raw[0:4]); magic != Magic { return Frame{}, fmt.Errorf("%w: magic %#x", ErrBadFrame, magic) } if raw[4] != Version { return Frame{}, fmt.Errorf("%w: version %d", ErrBadFrame, raw[4]) } length := binary.LittleEndian.Uint32(raw[14:18]) if length > MaxPayload { return Frame{}, fmt.Errorf("%w: a %d-byte payload", ErrBadFrame, length) } if int(length) != len(raw)-HeaderLen { return Frame{}, fmt.Errorf("%w: the header says %d bytes, the frame carries %d", ErrBadFrame, length, len(raw)-HeaderLen) } return Frame{ Kind: Kind(raw[5]), Method: Method(binary.LittleEndian.Uint16(raw[6:8])), Status: Status(binary.LittleEndian.Uint16(raw[8:10])), ID: binary.LittleEndian.Uint32(raw[10:14]), Payload: raw[HeaderLen:], }, nil } // Message is a result the worker can hand back without an error type: a status and a text. func Message(status Status, id uint32, text string) Frame { return Frame{Kind: KindResult, Status: status, ID: id, Payload: PutText(nil, text)} } // Payload codecs. Each appends to a buffer so a caller can build one payload from several // fields without a wrapper type. func PutText(buffer []byte, text string) []byte { buffer = binary.LittleEndian.AppendUint16(buffer, uint16(len(text))) return append(buffer, text...) } // ReadText reads a text. func ReadText(payload []byte) (string, []byte, error) { if len(payload) < 2 { return "", nil, fmt.Errorf("%w: a text length", ErrBadFrame) } length := int(binary.LittleEndian.Uint16(payload[0:2])) if len(payload) < 2+length { return "", nil, fmt.Errorf("%w: a %d-byte text in %d bytes", ErrBadFrame, length, len(payload)) } return string(payload[2 : 2+length]), payload[2+length:], nil } // PutUint32 appends a number. func PutUint32(buffer []byte, value uint32) []byte { return binary.LittleEndian.AppendUint32(buffer, value) } // ReadUint32 reads a number. func ReadUint32(payload []byte) (uint32, []byte, error) { if len(payload) < 4 { return 0, nil, fmt.Errorf("%w: a number", ErrBadFrame) } return binary.LittleEndian.Uint32(payload[0:4]), payload[4:], nil } // PutBool appends a flag. func PutBool(buffer []byte, value bool) []byte { if value { return append(buffer, 1) } return append(buffer, 0) } // ReadBool reads a flag. func ReadBool(payload []byte) (bool, []byte, error) { if len(payload) < 1 { return false, nil, fmt.Errorf("%w: a flag", ErrBadFrame) } return payload[0] == 1, payload[1:], nil } // PutHeights appends a height list: a count, then each height. func PutHeights(buffer []byte, heights []uint32) []byte { buffer = binary.LittleEndian.AppendUint16(buffer, uint16(len(heights))) for _, height := range heights { buffer = binary.LittleEndian.AppendUint32(buffer, height) } return buffer } // ReadHeights reads a height list. func ReadHeights(payload []byte) ([]uint32, []byte, error) { if len(payload) < 2 { return nil, nil, fmt.Errorf("%w: a height 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 heights in %d bytes", ErrBadFrame, count, len(payload)) } heights := make([]uint32, 0, count) for index := 0; index < count; index++ { heights = append(heights, binary.LittleEndian.Uint32(payload[index*4:])) } return heights, payload[count*4:], nil } // PutTexts appends a text list, which is how addresses travel. func PutTexts(buffer []byte, texts []string) []byte { buffer = binary.LittleEndian.AppendUint16(buffer, uint16(len(texts))) for _, text := range texts { buffer = PutText(buffer, text) } return buffer } // ReadTexts reads a text list. func ReadTexts(payload []byte) ([]string, []byte, error) { if len(payload) < 2 { return nil, nil, fmt.Errorf("%w: a text count", ErrBadFrame) } count := int(binary.LittleEndian.Uint16(payload[0:2])) payload = payload[2:] texts := make([]string, 0, count) for index := 0; index < count; index++ { text, rest, err := ReadText(payload) if err != nil { return nil, nil, err } texts = append(texts, text) payload = rest } return texts, payload, nil } // itoa renders a small number without pulling in formatting. func itoa(value uint32) string { if value == 0 { return "0" } var digits []byte for value > 0 { digits = append([]byte{byte('0' + value%10)}, digits...) value /= 10 } return string(digits) }