wallet.js raw
1 // The frame protocol of ../pkg/api, in the page.
2 //
3 // Same layout as the Go encoder: an 18-byte header and a payload, little-endian, every string
4 // length-prefixed. Keeping the two in step is the whole job of this file, so it stays boring.
5
6 export const KIND = { REQUEST: 1, RESULT: 2, CANCEL: 3, CLOSE: 4 };
7
8 export const METHOD = {
9 STATUS: 1, CREATE: 2, RESTORE: 3, UNLOCK: 4, LOCK: 5,
10 ADDRESSES: 6, SYNC: 7, MATCHES: 8, CHAINS: 9, UNSPENT: 10,
11 };
12
13 export const STATUS = { OK: 0, FAILED: 1, CANCELED: 2, UNKNOWN: 3, MALFORMED: 4, CLOSED: 5 };
14
15 const MAGIC = 0x31415754;
16 const HEADER = 18;
17 const encoder = new TextEncoder();
18 const decoder = new TextDecoder();
19
20 export class Writer {
21 constructor() { this.parts = []; }
22
23 u16(value) { const b = new Uint8Array(2); new DataView(b.buffer).setUint16(0, value, true); this.parts.push(b); return this; }
24 u32(value) { const b = new Uint8Array(4); new DataView(b.buffer).setUint32(0, value, true); this.parts.push(b); return this; }
25 bool(value) { this.parts.push(new Uint8Array([value ? 1 : 0])); return this; }
26
27 text(value) {
28 const bytes = encoder.encode(value || '');
29 return this.u16(bytes.length).raw(bytes);
30 }
31
32 raw(bytes) { this.parts.push(bytes); return this; }
33
34 heights(list) {
35 this.u16(list.length);
36 for (const height of list) this.u32(height);
37 return this;
38 }
39
40 u32s(list) {
41 this.u16(list.length);
42 for (const value of list) this.u32(value);
43 return this;
44 }
45
46 texts(list) {
47 this.u16(list.length);
48 for (const value of list) this.text(value);
49 return this;
50 }
51
52 bytes() {
53 const total = this.parts.reduce((sum, part) => sum + part.length, 0);
54 const out = new Uint8Array(total);
55 let at = 0;
56 for (const part of this.parts) { out.set(part, at); at += part.length; }
57 return out;
58 }
59 }
60
61 export class Reader {
62 constructor(bytes) { this.view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); this.at = 0; this.bytes = bytes; }
63 need(n) { if (this.at + n > this.view.byteLength) throw new Error('a short payload'); }
64 u16() { this.need(2); const v = this.view.getUint16(this.at, true); this.at += 2; return v; }
65 u32() { this.need(4); const v = this.view.getUint32(this.at, true); this.at += 4; return v; }
66 u64() { this.need(8); const v = this.view.getBigUint64(this.at, true); this.at += 8; return Number(v); }
67 bool() { this.need(1); return this.bytes[this.at++] === 1; }
68
69 raw(n) { this.need(n); const out = this.bytes.subarray(this.at, this.at + n); this.at += n; return out; }
70
71 text() {
72 const length = this.u16();
73 this.need(length);
74 const value = decoder.decode(this.bytes.subarray(this.at, this.at + length));
75 this.at += length;
76 return value;
77 }
78
79 heights() { const n = this.u16(); const out = []; for (let i = 0; i < n; i++) out.push(this.u32()); return out; }
80 u32s() { const n = this.u16(); const out = []; for (let i = 0; i < n; i++) out.push(this.u32()); return out; }
81 texts() { const n = this.u16(); const out = []; for (let i = 0; i < n; i++) out.push(this.text()); return out; }
82 }
83
84 export function encode(kind, method, status, id, payload) {
85 const body = payload || new Uint8Array(0);
86 const out = new Uint8Array(HEADER + body.length);
87 const view = new DataView(out.buffer);
88 view.setUint32(0, MAGIC, true);
89 view.setUint8(4, 1);
90 view.setUint8(5, kind);
91 view.setUint16(6, method, true);
92 view.setUint16(8, status, true);
93 view.setUint32(10, id, true);
94 view.setUint32(14, body.length, true);
95 out.set(body, HEADER);
96 return out.buffer;
97 }
98
99 export function decode(buffer) {
100 const bytes = new Uint8Array(buffer);
101 if (bytes.length < HEADER) throw new Error('a short frame');
102 const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
103 if (view.getUint32(0, true) !== MAGIC) throw new Error('not a wallet frame');
104 if (view.getUint8(4) !== 1) throw new Error('frame version ' + view.getUint8(4));
105 const length = view.getUint32(14, true);
106 if (length !== bytes.length - HEADER) throw new Error('the frame length does not match');
107 return { kind: view.getUint8(5), method: view.getUint16(6, true), status: view.getUint16(8, true), id: view.getUint32(10, true), payload: bytes.subarray(HEADER) };
108 }
109
110 // Wallet is the page's end of the worker.
111 export class Wallet {
112 constructor(port) {
113 this.port = port;
114 this.seq = 1;
115 this.pending = new Map();
116 this.closed = false;
117 port.onmessage = (event) => this.receive(event.data);
118 port.start?.();
119 }
120
121 receive(buffer) {
122 let frame;
123 try { frame = decode(buffer); } catch (error) { console.warn('wallet:', error.message); return; }
124 const waiter = this.pending.get(frame.id);
125 if (!waiter) return;
126 this.pending.delete(frame.id);
127 if (frame.status === STATUS.OK) waiter.resolve(frame.payload);
128 else waiter.reject(new Error(new Reader(frame.payload).text() || 'the request failed'));
129 }
130
131 call(method, payload) {
132 if (this.closed) return Promise.reject(new Error('the wallet worker is gone'));
133 const id = this.seq++;
134 return new Promise((resolve, reject) => {
135 this.pending.set(id, { resolve, reject });
136 this.port.postMessage(encode(KIND.REQUEST, method, 0, id, payload));
137 });
138 }
139
140 async status(chain) {
141 const reader = new Reader(await this.call(METHOD.STATUS, new Writer().text(chain).bytes()));
142 return {
143 chain: reader.text(), profile: reader.text(), hrp: reader.text(), account: reader.u32(),
144 hasWallet: reader.bool(), unlocked: reader.bool(), tip: reader.u32(), filters: reader.u32(),
145 running: reader.bool(), receive: reader.u32(), change: reader.u32(), matches: reader.u32(),
146 };
147 }
148
149 async chains() {
150 return new Reader(await this.call(METHOD.CHAINS, new Uint8Array(0))).texts();
151 }
152
153 async create(chain, words, passphrase) {
154 const payload = new Writer().text(chain).u32(words).text('').text(passphrase).bytes();
155 return this.setup(METHOD.CREATE, payload);
156 }
157
158 async restore(chain, phrase, passphrase) {
159 const payload = new Writer().text(chain).u32(0).text(phrase).text(passphrase).bytes();
160 return this.setup(METHOD.RESTORE, payload);
161 }
162
163 async setup(method, payload) {
164 const reader = new Reader(await this.call(method, payload));
165 return { chain: reader.text(), mnemonic: reader.text(), address: reader.text(), path: reader.text() };
166 }
167
168 unlock(chain, passphrase) { return this.call(METHOD.UNLOCK, new Writer().text(chain).text(passphrase).bytes()); }
169 lock(chain) { return this.call(METHOD.LOCK, new Writer().text(chain).bytes()); }
170
171 async addresses(chain, change, from, count) {
172 const reader = new Reader(await this.call(METHOD.ADDRESSES, new Writer().text(chain).bool(change).u32(from).u32(count).bytes()));
173 return { addresses: reader.texts(), indexes: reader.u32s(), paths: reader.texts() };
174 }
175
176 async sync(chain, lookahead) {
177 const reader = new Reader(await this.call(METHOD.SYNC, new Writer().text(chain).u32(lookahead).bytes()));
178 return { from: reader.u32(), tip: reader.u32(), filters: reader.u32(), scripts: reader.u32(), matched: reader.heights(), found: reader.heights() };
179 }
180
181 async matches(chain) {
182 return new Reader(await this.call(METHOD.MATCHES, new Writer().text(chain).bytes())).heights();
183 }
184
185 async unspent() {
186 const reader = new Reader(await this.call(METHOD.UNSPENT, new Uint8Array(0)));
187 const count = reader.u16();
188 const coins = [];
189 for (let i = 0; i < count; i++) {
190 const txid = reader.raw(32);
191 coins.push({ txid: Array.from(txid).reverse().map((b) => b.toString(16).padStart(2, '0')).join(''), vout: reader.u32(), value: reader.u64(), height: reader.u32() });
192 }
193 return coins;
194 }
195 }
196