@basestamp/basestamp
Version:
TypeScript client library for Basestamp API with trustless Merkle proof verification
295 lines (290 loc) • 10.1 kB
JavaScript
// src/client.ts
import fetch from "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
import { SHA256, enc } from "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 = enc.Hex.parse(input);
this._hash = SHA256(wordArray);
} else {
this._hash = SHA256(data);
}
return this;
},
digest(encoding) {
if (!this._hash) {
throw new Error("Hash not computed yet");
}
if (encoding === "hex") {
return this._hash.toString(enc.Hex);
} else {
throw new Error(`Unsupported encoding: ${encoding}`);
}
},
_hash: null
};
}
// src/merkle.ts
import { Buffer as Buffer2 } from "buffer";
function hashPair(left, right) {
const leftBytes = Buffer2.from(left, "hex");
const rightBytes = Buffer2.from(right, "hex");
let combined;
if (leftBytes.length === rightBytes.length) {
if (left < right) {
combined = Buffer2.concat([leftBytes, rightBytes]);
} else {
combined = Buffer2.concat([rightBytes, leftBytes]);
}
} else {
combined = Buffer2.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" ? Buffer2.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 fetch(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}`);
}
}
};
export {
BasestampClient,
BasestampError,
MerkleProof,
Stamp,
calculateSHA256,
verifyMerkleProof
};