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