1 // Package api is the vocabulary between the page and the wallet worker.
2 //
3 // It is the same discipline as the chain-data channel on the other side of the worker: binary
4 // frames, an id on every request, a cancel, and a close that means the far end died. The
5 // payloads are binary because an address is a string, a height is four bytes, and a phrase is
6 // a secret that has no business being base64'd into a log.
7 //
8 // The page encodes and decodes these frames from JavaScript, so the format is fixed and
9 // boring: an 18-byte header and a payload, little-endian, every string length-prefixed.
10 package api
11 12 import (
13 "encoding/binary"
14 "errors"
15 "fmt"
16 )
17 18 // The frame layout. The header is fixed so a decoder can reject anything it does not
19 // understand before it allocates.
20 const (
21 // Magic is "TWA1" read little-endian.
22 Magic uint32 = 0x31415754
23 // Version is the frame format.
24 Version uint8 = 1
25 // HeaderLen is magic, version, kind, method, status, id and length.
26 HeaderLen = 18
27 // MaxPayload bounds one frame. A phrase is 24 words; an address list is a few hundred
28 // bytes. Anything larger is a bug or an attack.
29 MaxPayload = 1 << 20
30 )
31 32 // ErrBadFrame is a frame this version cannot read.
33 var ErrBadFrame = errors.New("api: malformed frame")
34 35 // Kind is what a frame is.
36 type Kind uint8
37 38 // The kinds. A request expects one result; a cancel withdraws one; a close ends everything.
39 const (
40 // KindRequest asks for one method to run.
41 KindRequest Kind = 1
42 // KindResult answers one request, with the request's id.
43 KindResult Kind = 2
44 // KindCancel withdraws a request.
45 KindCancel Kind = 3
46 // KindClose ends the conversation.
47 KindClose Kind = 4
48 )
49 50 // String renders a kind.
51 func (k Kind) String() string {
52 switch k {
53 case KindRequest:
54 return "request"
55 case KindResult:
56 return "result"
57 case KindCancel:
58 return "cancel"
59 case KindClose:
60 return "close"
61 }
62 63 return "kind(" + itoa(uint32(k)) + ")"
64 }
65 66 // Method is what a request asks for.
67 type Method uint16
68 69 // The methods.
70 const (
71 // MethodStatus reports what the wallet knows about a chain.
72 MethodStatus Method = 1
73 // MethodCreate makes a new wallet and returns the words once.
74 MethodCreate Method = 2
75 // MethodRestore makes a wallet from words.
76 MethodRestore Method = 3
77 // MethodUnlock opens the wallet with the passphrase.
78 MethodUnlock Method = 4
79 // MethodLock forgets the unlocked wallet.
80 MethodLock Method = 5
81 // MethodAddresses derives addresses.
82 MethodAddresses Method = 6
83 // MethodSync walks the chain and records matches.
84 MethodSync Method = 7
85 // MethodMatches lists the recorded heights.
86 MethodMatches Method = 8
87 // MethodChains lists the chains the wallet drives.
88 MethodChains Method = 9
89 // MethodUnspent lists the coins the wallet can spend.
90 MethodUnspent Method = 10
91 )
92 93 // String renders a method.
94 func (m Method) String() string {
95 switch m {
96 case MethodStatus:
97 return "status"
98 case MethodCreate:
99 return "create"
100 case MethodRestore:
101 return "restore"
102 case MethodUnlock:
103 return "unlock"
104 case MethodLock:
105 return "lock"
106 case MethodAddresses:
107 return "addresses"
108 case MethodSync:
109 return "sync"
110 case MethodMatches:
111 return "matches"
112 case MethodChains:
113 return "chains"
114 case MethodUnspent:
115 return "unspent"
116 }
117 118 return "method(" + itoa(uint32(m)) + ")"
119 }
120 121 // Status is how an answer went.
122 type Status uint16
123 124 // The statuses.
125 const (
126 // StatusOK is a completed request.
127 StatusOK Status = 0
128 // StatusFailed is a refusal with a message.
129 StatusFailed Status = 1
130 // StatusCanceled is a request the caller withdrew.
131 StatusCanceled Status = 2
132 // StatusUnknownMethod is a method this build does not have.
133 StatusUnknownMethod Status = 3
134 // StatusMalformed is a payload that did not decode, or a lock that is wrong.
135 StatusMalformed Status = 4
136 // StatusClosed is an answer to a request whose channel ended.
137 StatusClosed Status = 5
138 )
139 140 // String renders a status.
141 func (s Status) String() string {
142 switch s {
143 case StatusOK:
144 return "ok"
145 case StatusFailed:
146 return "failed"
147 case StatusCanceled:
148 return "canceled"
149 case StatusUnknownMethod:
150 return "unknown method"
151 case StatusMalformed:
152 return "malformed"
153 case StatusClosed:
154 return "closed"
155 }
156 157 return "status(" + itoa(uint32(s)) + ")"
158 }
159 160 // Frame is one message.
161 type Frame struct {
162 // Kind is what the frame is.
163 Kind Kind
164 // Method is what it asks for, on a request.
165 Method Method
166 // Status is how it went, on a result.
167 Status Status
168 // ID echoes the request on every answer.
169 ID uint32
170 // Payload is the method's bytes.
171 Payload []byte
172 }
173 174 // Encode renders a frame.
175 func Encode(frame Frame) ([]byte, error) {
176 if len(frame.Payload) > MaxPayload {
177 return nil, fmt.Errorf("%w: a %d-byte payload", ErrBadFrame, len(frame.Payload))
178 }
179 180 out := make([]byte, HeaderLen, HeaderLen+len(frame.Payload))
181 182 binary.LittleEndian.PutUint32(out[0:4], Magic)
183 out[4] = Version
184 out[5] = byte(frame.Kind)
185 binary.LittleEndian.PutUint16(out[6:8], uint16(frame.Method))
186 binary.LittleEndian.PutUint16(out[8:10], uint16(frame.Status))
187 binary.LittleEndian.PutUint32(out[10:14], frame.ID)
188 binary.LittleEndian.PutUint32(out[14:18], uint32(len(frame.Payload)))
189 out = append(out, frame.Payload...)
190 191 return out, nil
192 }
193 194 // Decode reads a frame.
195 func Decode(raw []byte) (Frame, error) {
196 if len(raw) < HeaderLen {
197 return Frame{}, fmt.Errorf("%w: %d bytes is shorter than a header", ErrBadFrame, len(raw))
198 }
199 200 if magic := binary.LittleEndian.Uint32(raw[0:4]); magic != Magic {
201 return Frame{}, fmt.Errorf("%w: magic %#x", ErrBadFrame, magic)
202 }
203 204 if raw[4] != Version {
205 return Frame{}, fmt.Errorf("%w: version %d", ErrBadFrame, raw[4])
206 }
207 208 length := binary.LittleEndian.Uint32(raw[14:18])
209 210 if length > MaxPayload {
211 return Frame{}, fmt.Errorf("%w: a %d-byte payload", ErrBadFrame, length)
212 }
213 214 if int(length) != len(raw)-HeaderLen {
215 return Frame{}, fmt.Errorf("%w: the header says %d bytes, the frame carries %d",
216 ErrBadFrame, length, len(raw)-HeaderLen)
217 }
218 219 return Frame{
220 Kind: Kind(raw[5]),
221 Method: Method(binary.LittleEndian.Uint16(raw[6:8])),
222 Status: Status(binary.LittleEndian.Uint16(raw[8:10])),
223 ID: binary.LittleEndian.Uint32(raw[10:14]),
224 Payload: raw[HeaderLen:],
225 }, nil
226 }
227 228 // Message is a result the worker can hand back without an error type: a status and a text.
229 func Message(status Status, id uint32, text string) Frame {
230 return Frame{Kind: KindResult, Status: status, ID: id, Payload: PutText(nil, text)}
231 }
232 233 // Payload codecs. Each appends to a buffer so a caller can build one payload from several
234 // fields without a wrapper type.
235 func PutText(buffer []byte, text string) []byte {
236 buffer = binary.LittleEndian.AppendUint16(buffer, uint16(len(text)))
237 238 return append(buffer, text...)
239 }
240 241 // ReadText reads a text.
242 func ReadText(payload []byte) (string, []byte, error) {
243 if len(payload) < 2 {
244 return "", nil, fmt.Errorf("%w: a text length", ErrBadFrame)
245 }
246 247 length := int(binary.LittleEndian.Uint16(payload[0:2]))
248 249 if len(payload) < 2+length {
250 return "", nil, fmt.Errorf("%w: a %d-byte text in %d bytes", ErrBadFrame, length, len(payload))
251 }
252 253 return string(payload[2 : 2+length]), payload[2+length:], nil
254 }
255 256 // PutUint32 appends a number.
257 func PutUint32(buffer []byte, value uint32) []byte {
258 return binary.LittleEndian.AppendUint32(buffer, value)
259 }
260 261 // ReadUint32 reads a number.
262 func ReadUint32(payload []byte) (uint32, []byte, error) {
263 if len(payload) < 4 {
264 return 0, nil, fmt.Errorf("%w: a number", ErrBadFrame)
265 }
266 267 return binary.LittleEndian.Uint32(payload[0:4]), payload[4:], nil
268 }
269 270 // PutBool appends a flag.
271 func PutBool(buffer []byte, value bool) []byte {
272 if value {
273 return append(buffer, 1)
274 }
275 276 return append(buffer, 0)
277 }
278 279 // ReadBool reads a flag.
280 func ReadBool(payload []byte) (bool, []byte, error) {
281 if len(payload) < 1 {
282 return false, nil, fmt.Errorf("%w: a flag", ErrBadFrame)
283 }
284 285 return payload[0] == 1, payload[1:], nil
286 }
287 288 // PutHeights appends a height list: a count, then each height.
289 func PutHeights(buffer []byte, heights []uint32) []byte {
290 buffer = binary.LittleEndian.AppendUint16(buffer, uint16(len(heights)))
291 292 for _, height := range heights {
293 buffer = binary.LittleEndian.AppendUint32(buffer, height)
294 }
295 296 return buffer
297 }
298 299 // ReadHeights reads a height list.
300 func ReadHeights(payload []byte) ([]uint32, []byte, error) {
301 if len(payload) < 2 {
302 return nil, nil, fmt.Errorf("%w: a height count", ErrBadFrame)
303 }
304 305 count := int(binary.LittleEndian.Uint16(payload[0:2]))
306 payload = payload[2:]
307 308 if len(payload) < count*4 {
309 return nil, nil, fmt.Errorf("%w: %d heights in %d bytes", ErrBadFrame, count, len(payload))
310 }
311 312 heights := make([]uint32, 0, count)
313 314 for index := 0; index < count; index++ {
315 heights = append(heights, binary.LittleEndian.Uint32(payload[index*4:]))
316 }
317 318 return heights, payload[count*4:], nil
319 }
320 321 // PutTexts appends a text list, which is how addresses travel.
322 func PutTexts(buffer []byte, texts []string) []byte {
323 buffer = binary.LittleEndian.AppendUint16(buffer, uint16(len(texts)))
324 325 for _, text := range texts {
326 buffer = PutText(buffer, text)
327 }
328 329 return buffer
330 }
331 332 // ReadTexts reads a text list.
333 func ReadTexts(payload []byte) ([]string, []byte, error) {
334 if len(payload) < 2 {
335 return nil, nil, fmt.Errorf("%w: a text count", ErrBadFrame)
336 }
337 338 count := int(binary.LittleEndian.Uint16(payload[0:2]))
339 payload = payload[2:]
340 341 texts := make([]string, 0, count)
342 343 for index := 0; index < count; index++ {
344 text, rest, err := ReadText(payload)
345 346 if err != nil {
347 return nil, nil, err
348 }
349 350 texts = append(texts, text)
351 payload = rest
352 }
353 354 return texts, payload, nil
355 }
356 357 // itoa renders a small number without pulling in formatting.
358 func itoa(value uint32) string {
359 if value == 0 {
360 return "0"
361 }
362 363 var digits []byte
364 365 for value > 0 {
366 digits = append([]byte{byte('0' + value%10)}, digits...)
367 value /= 10
368 }
369 370 return string(digits)
371 }
372