UNPKG

@basestamp/basestamp

Version:

TypeScript client library for Basestamp API with trustless Merkle proof verification

295 lines (290 loc) 10.1 kB
// 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 };