jadets
Version:
jade impl in typescript
656 lines (648 loc) • 21 kB
JavaScript
var __defProp = Object.defineProperty;
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __propIsEnum = Object.prototype.propertyIsEnumerable;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __spreadValues = (a, b) => {
for (var prop in b || (b = {}))
if (__hasOwnProp.call(b, prop))
__defNormalProp(a, prop, b[prop]);
if (__getOwnPropSymbols)
for (var prop of __getOwnPropSymbols(b)) {
if (__propIsEnum.call(b, prop))
__defNormalProp(a, prop, b[prop]);
}
return a;
};
var __async = (__this, __arguments, generator) => {
return new Promise((resolve, reject) => {
var fulfilled = (value) => {
try {
step(generator.next(value));
} catch (e) {
reject(e);
}
};
var rejected = (value) => {
try {
step(generator.throw(value));
} catch (e) {
reject(e);
}
};
var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
step((generator = generator.apply(__this, __arguments)).next());
});
};
// src/transport/SerialTransport.ts
import { EventEmitter } from "events";
import { encode, decode } from "cbor2";
var SerialTransport = class extends EventEmitter {
constructor(options) {
super();
this.port = null;
this.reader = null;
this.receivedBuffer = new Uint8Array(0);
this.options = options;
}
drain() {
this.receivedBuffer = new Uint8Array(0);
}
connect() {
return __async(this, null, function* () {
var _a;
try {
const serial = navigator.serial;
if (!serial) {
throw new Error("Web Serial API is not supported in this browser.");
}
const ports = yield serial.getPorts();
if (ports.length === 0) {
this.port = yield serial.requestPort();
} else {
this.port = ports[0];
}
if (!this.port) {
throw new Error("No serial port selected.");
}
yield this.port.open({
baudRate: this.options.baudRate || 115200,
bufferSize: this.options.bufferSize || 4 * 1024
});
this.reader = ((_a = this.port.readable) == null ? void 0 : _a.getReader()) || null;
if (this.reader) {
this.readLoop();
}
} catch (error) {
console.error("[WebSerialPort] Failed to connect:", error);
throw error;
}
});
}
readLoop() {
return __async(this, null, function* () {
if (!this.reader) return;
try {
while (true) {
const { value, done } = yield this.reader.read();
if (done) {
break;
}
if (value) {
this.receivedBuffer = this.concatBuffers(this.receivedBuffer, value);
this.processReceivedData();
}
}
} catch (error) {
console.error("[WebSerialPort] Read error:", error);
} finally {
if (this.reader) {
this.reader.releaseLock();
this.reader = null;
}
}
});
}
processReceivedData() {
let index = 1;
while (index <= this.receivedBuffer.length) {
try {
const sliceToTry = this.receivedBuffer.slice(0, index);
const decoded = decode(sliceToTry);
if (decoded && typeof decoded === "object" && ("error" in decoded || "result" in decoded || "log" in decoded || "method" in decoded)) {
this.emit("message", decoded);
}
this.receivedBuffer = this.receivedBuffer.slice(index);
index = 1;
} catch (error) {
if (error.message && (error.message.includes("Offset is outside") || error.message.includes("Insufficient data") || error.message.includes("Unexpected end of stream"))) {
index++;
if (index > this.receivedBuffer.length) {
break;
}
} else {
console.error("[WebSerialPort] CBOR decode error:", error);
this.receivedBuffer = new Uint8Array(0);
break;
}
}
}
}
concatBuffers(a, b) {
const result = new Uint8Array(a.length + b.length);
result.set(a);
result.set(b, a.length);
return result;
}
disconnect() {
return __async(this, null, function* () {
try {
if (this.reader) {
yield this.reader.cancel();
}
if (this.port) {
yield this.port.close();
}
this.port = null;
this.reader = null;
} catch (error) {
console.error("[WebSerialPort] Error during disconnect:", error);
}
});
}
sendMessage(message) {
return __async(this, null, function* () {
try {
if (!this.port || !this.port.writable) {
throw new Error("Port not available");
}
const encoded = encode(message);
const writer = this.port.writable.getWriter();
yield writer.write(encoded);
writer.releaseLock();
} catch (error) {
console.error("[WebSerialPort] Failed to send message:", error);
throw error;
}
});
}
onMessage(callback) {
this.on("message", callback);
}
};
// src/transport/TCPTransport.ts
import { EventEmitter as EventEmitter2 } from "events";
import net from "net";
import { encode as encode2, decode as decode2 } from "cbor2";
var TCPTransport = class extends EventEmitter2 {
constructor(host, port) {
super();
this.host = host;
this.port = port;
this.socket = null;
this.recvBuffer = Buffer.alloc(0);
}
connect() {
return __async(this, null, function* () {
return new Promise((resolve, reject) => {
this.socket = new net.Socket();
this.socket.once("error", reject);
this.socket.connect(this.port, this.host, () => {
this.socket.on("data", this.onData.bind(this)).on("error", (err) => this.emit("error", err)).on("close", () => this.emit("disconnect"));
resolve();
});
});
});
}
disconnect() {
return __async(this, null, function* () {
if (!this.socket) return;
return new Promise((resolve) => {
this.socket.end(() => {
this.socket = null;
resolve();
});
});
});
}
sendMessage(msg) {
return __async(this, null, function* () {
if (!this.socket) throw new Error("Not connected");
const chunk = encode2(msg);
this.socket.write(chunk);
});
}
onMessage(callback) {
this.on("message", callback);
}
onData(data) {
this.recvBuffer = Buffer.concat([this.recvBuffer, data]);
while (this.recvBuffer.length > 0) {
try {
const obj = decode2(this.recvBuffer);
this.emit("message", obj);
const consumed = encode2(obj).length;
this.recvBuffer = this.recvBuffer.slice(consumed);
} catch (e) {
if (e.message.includes("Insufficient data") || e.message.includes("Unexpected end of stream")) {
break;
}
this.emit("error", e);
this.recvBuffer = Buffer.alloc(0);
break;
}
}
}
};
// src/interfaces/JadeInterface.ts
var JadeInterface = class {
constructor(transport) {
this.transport = transport;
}
connect() {
return __async(this, null, function* () {
yield this.transport.connect();
});
}
disconnect() {
return __async(this, null, function* () {
yield this.transport.disconnect();
});
}
buildRequest(id, method, params) {
return { id, method, params };
}
/**
* Makes an RPC call and handles extended data responses automatically
*/
makeRPCCall(request, long_timeout = false) {
return __async(this, null, function* () {
if (!request.id || request.id.length > 16) {
throw new Error("Request id must be non-empty and less than 16 characters");
}
if (!request.method || request.method.length > 32) {
throw new Error("Request method must be non-empty and less than 32 characters");
}
yield this.transport.sendMessage(request);
const initialResponse = yield this.waitForResponse(request.id, long_timeout);
if (this.isExtendedDataResponse(initialResponse)) {
return yield this.handleExtendedDataResponse(initialResponse, request.id, long_timeout);
}
return initialResponse;
});
}
/**
* Waits for a single response message
*/
waitForResponse(requestId, long_timeout) {
return __async(this, null, function* () {
return new Promise((resolve, reject) => {
const onResponse = (msg) => {
if (msg && msg.id === requestId) {
this.transport.removeListener("message", onResponse);
if (timeoutId) clearTimeout(timeoutId);
resolve(msg);
}
};
this.transport.onMessage(onResponse);
let timeoutId;
if (!long_timeout) {
timeoutId = setTimeout(() => {
this.transport.removeListener("message", onResponse);
reject(new Error("RPC call timed out"));
}, 5e3);
}
});
});
}
/**
* Checks if a response indicates extended data
*/
isExtendedDataResponse(response) {
return response.seqnum !== void 0 && response.seqlen !== void 0 && response.seqnum < response.seqlen;
}
/**
* Handles extended data responses by collecting all chunks
*/
handleExtendedDataResponse(initialResponse, requestId, long_timeout) {
return __async(this, null, function* () {
const chunks = [initialResponse];
const totalChunks = initialResponse.seqlen;
console.log(`Receiving extended data: chunk ${initialResponse.seqnum + 1}/${totalChunks}`);
for (let expectedSeqnum = initialResponse.seqnum + 1; expectedSeqnum < totalChunks; expectedSeqnum++) {
const extendedRequest = {
id: requestId,
method: "get_extended_data",
params: { seqnum: expectedSeqnum }
};
yield this.transport.sendMessage(extendedRequest);
const chunkResponse = yield this.waitForResponse(requestId, long_timeout);
if (chunkResponse.seqnum !== expectedSeqnum) {
throw new Error(`Expected chunk ${expectedSeqnum}, got ${chunkResponse.seqnum}`);
}
if (chunkResponse.seqlen !== totalChunks) {
throw new Error(`Inconsistent seqlen: expected ${totalChunks}, got ${chunkResponse.seqlen}`);
}
chunks.push(chunkResponse);
console.log(`Received extended data chunk: ${expectedSeqnum + 1}/${totalChunks}`);
}
return this.reassembleExtendedData(chunks);
});
}
/**
* Reassembles chunks into a complete response
*/
reassembleExtendedData(chunks) {
chunks.sort((a, b) => (a.seqnum || 0) - (b.seqnum || 0));
const completeResponse = {
id: chunks[0].id,
error: chunks[0].error
};
if (completeResponse.error) {
return completeResponse;
}
completeResponse.result = this.mergeChunkData(chunks);
console.log(`Extended data reassembly complete: ${chunks.length} chunks processed`);
return completeResponse;
}
mergeChunkData(chunks) {
const firstResult = chunks[0].result;
if (firstResult instanceof Uint8Array) {
const totalLength = chunks.reduce((sum, chunk) => {
const chunkData = chunk.result;
return sum + chunkData.length;
}, 0);
const merged = new Uint8Array(totalLength);
let offset = 0;
for (const chunk of chunks) {
const chunkData = chunk.result;
merged.set(chunkData, offset);
offset += chunkData.length;
}
return merged;
}
if (typeof Buffer !== "undefined" && Buffer.isBuffer(firstResult)) {
return Buffer.concat(chunks.map((chunk) => chunk.result));
}
if (Array.isArray(firstResult)) {
return chunks.reduce((merged, chunk) => {
return merged.concat(chunk.result);
}, []);
}
if (typeof firstResult === "string") {
return chunks.map((chunk) => chunk.result).join("");
}
if (typeof firstResult === "object" && firstResult !== null) {
return chunks.reduce((merged, chunk) => {
return __spreadValues(__spreadValues({}, merged), chunk.result);
}, {});
}
console.warn("Unknown data type for extended data merge, returning first chunk");
return firstResult;
}
};
// src/utils/getFingerPrint.ts
import * as ecc from "tiny-secp256k1";
import BIP32Factory from "bip32";
import { Buffer as Buffer2 } from "buffer";
var bip32 = BIP32Factory(ecc);
var MAINNET_BIP32 = {
wif: 128,
bip32: {
public: 76067358,
// “xpub”
private: 76066276
// “xprv”
},
messagePrefix: "Bitcoin Signed Message:\n",
bech32: "bc",
pubKeyHash: 0,
scriptHash: 5
};
var TESTNET_BIP32 = {
wif: 239,
bip32: {
public: 70617039,
// “tpub”
private: 70615956
// “tprv”
},
messagePrefix: "Bitcoin Signed Message:\n",
bech32: "tb",
pubKeyHash: 111,
scriptHash: 196
};
function getFingerprintFromXpub(xpub, networkType) {
try {
const network = networkType === "testnet" ? TESTNET_BIP32 : MAINNET_BIP32;
const node = bip32.fromBase58(xpub, network);
return Buffer2.from(node.fingerprint).toString("hex");
} catch (err) {
console.error("Error getting fingerprint:", err);
return null;
}
}
// src/utils/hexToBytes.ts
function hexToBytes(hex) {
if (hex.length % 2 !== 0) {
throw new Error("hex string must have even length");
}
const bytes = new Uint8Array(hex.length / 2);
for (let i = 0; i < hex.length; i += 2) {
bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);
}
return bytes;
}
// src/utils/base64ToBytes.ts
function base64ToBytes(b64) {
return Uint8Array.from(Buffer.from(b64, "base64"));
}
// src/utils/bytesToBase64.ts
function bytesToBase64(bytes) {
return Buffer.from(bytes).toString("base64");
}
// src/Jade.ts
import { randomBytes } from "crypto";
var Jade = class {
constructor(iface) {
this.iface = iface;
}
connect() {
return __async(this, null, function* () {
return this.iface.connect();
});
}
disconnect() {
return __async(this, null, function* () {
return this.iface.disconnect();
});
}
_jadeRpc(method, params, id, long_timeout = false, http_request_fn) {
return __async(this, null, function* () {
const requestId = id || Math.floor(Math.random() * 1e6).toString();
const request = this.iface.buildRequest(requestId, method, params);
const reply = yield this.iface.makeRPCCall(request, long_timeout);
if (reply.error) {
throw new Error(`RPC Error ${reply.error.code}: ${reply.error.message}`);
}
if (reply.result && typeof reply.result === "object" && "http_request" in reply.result) {
if (!http_request_fn) {
throw new Error("HTTP request function not provided");
}
const httpRequest = reply.result["http_request"];
const httpResponse = yield http_request_fn(httpRequest["params"]);
return this._jadeRpc(
httpRequest["on-reply"],
httpResponse["body"],
void 0,
long_timeout,
http_request_fn
);
}
return reply.result;
});
}
cleanReset() {
return __async(this, null, function* () {
return this._jadeRpc("debug_clean_reset");
});
}
ping() {
return __async(this, null, function* () {
return this._jadeRpc("ping");
});
}
getVersionInfo(nonblocking = false) {
return __async(this, null, function* () {
const params = nonblocking ? { nonblocking: true } : void 0;
return this._jadeRpc("get_version_info", params);
});
}
setMnemonic(mnemonic, passphrase, temporaryWallet = false) {
return __async(this, null, function* () {
const params = { mnemonic, temporary_wallet: temporaryWallet };
if (passphrase !== void 0) {
params.passphrase = passphrase;
}
return this._jadeRpc("debug_set_mnemonic", params);
});
}
authUser(network, http_request_fn, epoch) {
return __async(this, null, function* () {
if (typeof network !== "string" || network.length === 0) {
throw new Error('authUser: "network" must be a non-empty string');
}
const computedEpoch = epoch !== void 0 ? epoch : Math.floor(Date.now() / 1e3);
const params = {
network,
epoch: computedEpoch
};
return this._jadeRpc("auth_user", params, void 0, true, http_request_fn);
});
}
addEntropy(entropy) {
return __async(this, null, function* () {
const params = { entropy };
return this._jadeRpc("add_entropy", params);
});
}
logout() {
return __async(this, null, function* () {
return this._jadeRpc("logout");
});
}
getXpub(network, path) {
return __async(this, null, function* () {
const params = { network, path };
return this._jadeRpc("get_xpub", params);
});
}
setEpoch(epoch) {
return __async(this, null, function* () {
const now = Math.floor(Date.now() / 1e3);
const params = { epoch: epoch !== void 0 ? epoch : now };
return this._jadeRpc("set_epoch", params);
});
}
registerMultisig(network, multisigName, descriptor) {
return __async(this, null, function* () {
let mname = multisigName;
if (mname === void 0) {
mname = "jade" + randomBytes(4).toString("hex");
}
const params = {
network,
multisig_name: mname,
descriptor
};
return this._jadeRpc("register_multisig", params, void 0, true);
});
}
getRegisteredMultisigs() {
return __async(this, null, function* () {
return this._jadeRpc("get_registered_multisigs");
});
}
getMultiSigName(network, target) {
return __async(this, null, function* () {
const summaries = yield this.getRegisteredMultisigs();
for (const [name, sum] of Object.entries(summaries)) {
if (sum.variant !== target.variant || sum.sorted !== target.sorted || sum.threshold !== target.threshold || sum.num_signers !== target.signers.length) {
continue;
}
const full = yield this.getRegisteredMultisig(name, false);
const desc = full.descriptor;
const normalize = (o) => new Uint8Array(Object.values(o.fingerprint));
const match = desc.signers.length === target.signers.length && desc.signers.every((s, i) => {
const t = target.signers[i];
const sf = normalize(s);
const tf = t.fingerprint;
if (sf.length !== tf.length || sf.some((b, idx) => b !== tf[idx])) return false;
if (s.xpub !== t.xpub) return false;
if (s.derivation.length !== t.derivation.length || s.derivation.some((v, idx) => v !== t.derivation[idx])) return false;
return true;
});
if (match) {
return name;
}
}
return void 0;
});
}
getRegisteredMultisig(name, asFile = false) {
return __async(this, null, function* () {
const params = {
"multisig_name": name,
"as_file": asFile
};
return this._jadeRpc("get_registered_multisig", params);
});
}
getReceiveAddress(network, opts) {
return __async(this, null, function* () {
const params = { network };
if (opts.path) params.path = opts.path;
if (opts.paths) params.paths = opts.paths;
if (opts.multisigName) params.multisig_name = opts.multisigName;
if (opts.descriptorName) params.descriptor_name = opts.descriptorName;
if (opts.variant) params.variant = opts.variant;
if (opts.recoveryXpub) params.recovery_xpub = opts.recoveryXpub;
if (opts.csvBlocks) params.csv_blocks = opts.csvBlocks;
if (opts.confidential) params.confidential = opts.confidential;
return this._jadeRpc("get_receive_address", params);
});
}
signMessage(path, message, useAeSignatures, aeHostCommitment, aeHostEntropy) {
return __async(this, null, function* () {
if (useAeSignatures) {
throw new Error("ae sig not implemented");
} else {
const params = { "path": path, "message": message };
return this._jadeRpc("sign_message", params, void 0, true);
}
});
}
signPSBT(network, psbt) {
return __async(this, null, function* () {
const params = { "network": network, "psbt": psbt };
return this._jadeRpc("sign_psbt", params, void 0, true);
});
}
getMasterFingerPrint(network) {
return __async(this, null, function* () {
const xpub = yield this.getXpub(network, []);
return getFingerprintFromXpub(xpub, network);
});
}
};
export {
Jade,
JadeInterface,
SerialTransport,
TCPTransport,
base64ToBytes,
bytesToBase64,
getFingerprintFromXpub,
hexToBytes
};
//# sourceMappingURL=index.mjs.map