@basestamp/basestamp
Version:
TypeScript client library for Basestamp API with trustless Merkle proof verification
337 lines (330 loc) • 12.1 kB
JavaScript
;
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var src_exports = {};
__export(src_exports, {
BasestampClient: () => BasestampClient,
BasestampError: () => BasestampError,
MerkleProof: () => MerkleProof,
Stamp: () => Stamp,
calculateSHA256: () => calculateSHA256,
verifyMerkleProof: () => verifyMerkleProof
});
module.exports = __toCommonJS(src_exports);
// src/client.ts
var import_cross_fetch = __toESM(require("cross-fetch"));
// src/types.ts
var MerkleProof = class {
constructor(data, verifier, hasher) {
this.leaf_hash = data.leaf_hash;
this.leaf_index = data.leaf_index;
this.siblings = data.siblings;
this.directions = data.directions;
this.root_hash = data.root_hash;
this.nonce = data.nonce;
this.original_hash = data.original_hash;
this._verifier = verifier;
this._hasher = hasher;
}
verify(hash_value) {
if (hash_value !== this.original_hash) {
throw new BasestampError(`Hash mismatch: provided hash '${hash_value}' does not match proof's original hash '${this.original_hash}'`);
}
if (this.nonce) {
const expectedLeafHash = this._hasher(this.nonce + this.original_hash);
if (expectedLeafHash !== this.leaf_hash) {
throw new BasestampError(`Leaf hash verification failed: expected '${expectedLeafHash}' but merkle proof contains '${this.leaf_hash}'. This indicates the nonce '${this.nonce}' and original hash '${this.original_hash}' do not match the merkle proof.`);
}
} else {
if (this.original_hash !== this.leaf_hash) {
throw new BasestampError(`Legacy verification failed: original hash '${this.original_hash}' does not match merkle proof leaf hash '${this.leaf_hash}'`);
}
}
const isValidProof = this._verifier(this);
if (!isValidProof) {
throw new BasestampError(`Merkle proof verification failed: the proof structure (leaf_index: ${this.leaf_index}, siblings: [${this.siblings.join(", ")}], directions: [${this.directions.join(", ")}]) does not produce the expected root hash '${this.root_hash}'`);
}
return true;
}
};
var Stamp = class {
constructor(data, verifier, hasher) {
this.stamp_id = data.stamp_id;
this.hash = data.hash;
this.original_hash = data.original_hash || data.hash;
this.nonce = data.nonce || "";
this.timestamp = data.timestamp;
this.status = data.status;
this.message = data.message;
this.tx_id = data.tx_id;
this.block_hash = data.block_hash;
this.network = data.network;
this.chain_id = data.chain_id;
this.merkle_proof = data.merkle_proof;
this._verifier = verifier;
this._hasher = hasher;
}
verify(original_hash) {
if (!this.merkle_proof) {
throw new BasestampError("Merkle proof not available for verification");
}
if (original_hash !== this.original_hash) {
throw new BasestampError(`Hash mismatch: provided hash '${original_hash}' does not match stamp's original hash '${this.original_hash}'`);
}
if (this.nonce) {
const expectedLeafHash = this._hasher(this.nonce + this.original_hash);
if (expectedLeafHash !== this.merkle_proof.leaf_hash) {
throw new BasestampError(`Leaf hash verification failed: expected '${expectedLeafHash}' but merkle proof contains '${this.merkle_proof.leaf_hash}'. This indicates the nonce '${this.nonce}' and original hash '${this.original_hash}' do not match the merkle proof.`);
}
} else {
if (this.original_hash !== this.merkle_proof.leaf_hash) {
throw new BasestampError(`Legacy verification failed: original hash '${this.original_hash}' does not match merkle proof leaf hash '${this.merkle_proof.leaf_hash}'`);
}
}
const isValidProof = this._verifier(this.merkle_proof);
if (!isValidProof) {
throw new BasestampError(`Merkle proof verification failed: the proof structure (leaf_index: ${this.merkle_proof.leaf_index}, siblings: [${this.merkle_proof.siblings.join(", ")}], directions: [${this.merkle_proof.directions.join(", ")}]) does not produce the expected root hash '${this.merkle_proof.root_hash}'`);
}
return true;
}
getMerkleProof() {
if (!this.merkle_proof) {
throw new BasestampError("Merkle proof not available");
}
return new MerkleProof({
...this.merkle_proof,
nonce: this.nonce,
original_hash: this.original_hash
}, this._verifier, this._hasher);
}
};
var BasestampError = class extends Error {
constructor(message) {
super(message);
this.name = "BasestampError";
}
};
// src/crypto-utils.ts
var import_crypto_js = require("crypto-js");
function createHash(algorithm) {
if (algorithm !== "sha256") {
throw new Error(`Unsupported hash algorithm: ${algorithm}`);
}
return {
update(data) {
let input;
if (Buffer.isBuffer(data)) {
input = data.toString("hex");
const wordArray = import_crypto_js.enc.Hex.parse(input);
this._hash = (0, import_crypto_js.SHA256)(wordArray);
} else {
this._hash = (0, import_crypto_js.SHA256)(data);
}
return this;
},
digest(encoding) {
if (!this._hash) {
throw new Error("Hash not computed yet");
}
if (encoding === "hex") {
return this._hash.toString(import_crypto_js.enc.Hex);
} else {
throw new Error(`Unsupported encoding: ${encoding}`);
}
},
_hash: null
};
}
// src/merkle.ts
var import_buffer = require("buffer");
function hashPair(left, right) {
const leftBytes = import_buffer.Buffer.from(left, "hex");
const rightBytes = import_buffer.Buffer.from(right, "hex");
let combined;
if (leftBytes.length === rightBytes.length) {
if (left < right) {
combined = import_buffer.Buffer.concat([leftBytes, rightBytes]);
} else {
combined = import_buffer.Buffer.concat([rightBytes, leftBytes]);
}
} else {
combined = import_buffer.Buffer.concat([leftBytes, rightBytes]);
}
return createHash("sha256").update(combined).digest("hex");
}
function verifyMerkleProof(proof) {
if (!proof) {
return false;
}
if (!proof.leaf_hash || !proof.root_hash) {
return false;
}
if (proof.siblings.length !== proof.directions.length) {
return false;
}
let currentHash = proof.leaf_hash;
for (let i = 0; i < proof.siblings.length; i++) {
const sibling = proof.siblings[i];
const direction = proof.directions[i];
if (direction) {
currentHash = hashPair(currentHash, sibling);
} else {
currentHash = hashPair(sibling, currentHash);
}
}
return currentHash === proof.root_hash;
}
function calculateSHA256(data) {
const buffer = typeof data === "string" ? import_buffer.Buffer.from(data, "utf8") : data;
return createHash("sha256").update(buffer).digest("hex");
}
// src/client.ts
var BasestampClient = class {
constructor(options = {}) {
this.baseURL = options.baseURL || "https://api.basestamp.io";
this.timeout = options.timeout || 3e4;
}
async submitSHA256(hash) {
const request = { hash };
const response = await this.makeRequest("POST", "/stamp", request);
if ("stamp_id" in response) {
return response.stamp_id;
}
return response.hash;
}
async getStamp(stampId, options = {}) {
const { wait = false, timeout = 30 } = options;
let attempts = 0;
const maxAttempts = wait ? Math.ceil(timeout) : 1;
const delayMs = 1e3;
while (attempts < maxAttempts) {
try {
const stampData = await this.makeRequest("GET", `/stamp/${stampId}`);
if (!stampData.merkle_proof) {
if (!wait || attempts === maxAttempts - 1) {
throw new BasestampError("Merkle proof not yet available");
}
await new Promise((resolve) => setTimeout(resolve, delayMs));
attempts++;
continue;
}
return new Stamp(stampData, verifyMerkleProof, calculateSHA256);
} catch (error) {
if (error instanceof BasestampError) {
throw error;
}
throw new BasestampError(`Failed to get stamp: ${error}`);
}
}
throw new BasestampError(`Timeout waiting for stamp after ${timeout} seconds`);
}
/**
* @deprecated Use getStamp() which now returns a Stamp object with verify method
*/
async getStampLegacy(stampId) {
const response = await this.makeRequest("GET", `/stamp/${stampId}`);
return response;
}
/**
* @deprecated Use getStamp() and call stamp.getMerkleProof() instead
*/
async get_merkle_proof(stampId, wait = false, timeout = 30) {
const stamp = await this.getStamp(stampId, { wait, timeout });
return stamp.getMerkleProof();
}
/**
* @deprecated Use getStamp() and call stamp.verify() instead
*/
async verifyStamp(stampId, hashValue) {
try {
const stamp = await this.getStamp(stampId);
const hashToVerify = hashValue || stamp.original_hash;
return stamp.verify(hashToVerify);
} catch (error) {
if (error instanceof BasestampError) {
throw error;
}
throw new BasestampError(`Failed to verify stamp: ${error}`);
}
}
async info() {
const response = await this.makeRequest("GET", "/info");
return response;
}
async health() {
const response = await this.makeRequest("GET", "/health");
return response;
}
async batchStats() {
const response = await this.makeRequest("GET", "/batch/stats");
return response;
}
async makeRequest(method, path, body) {
const url = `${this.baseURL}${path}`;
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
try {
const init = {
method,
headers: {
"Content-Type": "application/json"
},
signal: controller.signal
};
if (body && method === "POST") {
init.body = JSON.stringify(body);
}
const response = await (0, import_cross_fetch.default)(url, init);
clearTimeout(timeoutId);
if (!response.ok) {
throw new BasestampError(`Server returned error: ${response.status} ${response.statusText}`);
}
const data = await response.json();
return data;
} catch (error) {
clearTimeout(timeoutId);
if (error instanceof BasestampError) {
throw error;
}
if (error.name === "AbortError") {
throw new BasestampError("Request timeout");
}
throw new BasestampError(`Request failed: ${error}`);
}
}
};
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
BasestampClient,
BasestampError,
MerkleProof,
Stamp,
calculateSHA256,
verifyMerkleProof
});