bigblocks
Version:
Complete Bitcoin UI component library - authentication, social, wallet, market, inscriptions, and blockchain interactions for React
197 lines (196 loc) • 8.65 kB
JavaScript
import { BSM, BigNumber, Hash, Signature, Utils } from "@bsv/sdk";
// useBapSigning Hook - Sign/verify files with BAP identity using React Query pattern
import { useMutation } from "@tanstack/react-query";
import { useBitcoinAuth } from "../../../hooks/useBitcoinAuth.js";
const { toArray, toHex } = Utils;
export function useBapSigning(_options = {}) {
const { user } = useBitcoinAuth();
// Get BAP instance from context
const bapInstance = user?.bapInstance;
// Convert file to array buffer
const fileToArrayBuffer = async (file) => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => {
if (e.target?.result instanceof ArrayBuffer) {
resolve(e.target.result);
}
else {
reject(new Error("Failed to read file"));
}
};
reader.onerror = () => reject(new Error("Failed to read file"));
reader.readAsArrayBuffer(file);
});
};
// Sign file mutation
const signMutation = useMutation({
mutationFn: async ({ file, options: signOptions = {} }) => {
if (!bapInstance) {
throw new Error("BAP instance not available");
}
try {
const ids = bapInstance.listIds();
if (!ids || ids.length === 0) {
throw new Error("No BAP identity available");
}
const currentId = ids[0];
if (!currentId) {
throw new Error("No current identity found");
}
const identity = bapInstance.getId(currentId.idKey);
if (!identity) {
throw new Error("BAP identity not found");
}
// Read file data
const arrayBuffer = await fileToArrayBuffer(file);
const fileData = new Uint8Array(arrayBuffer);
// Hash the file content
const fileHash = Hash.sha256(toArray(fileData));
// Create message to sign based on protocol
let messageToSign;
if (signOptions.protocol === "raw") {
// Sign the raw file hash
messageToSign = toArray(fileHash);
}
else {
// Default to BAP attestation format
const attestationUrn = `urn:bap:attest:file:${toHex(fileHash)}:${currentId.idKey}`;
const attestationHash = Hash.sha256(attestationUrn, "utf8");
messageToSign = toArray(attestationHash);
}
// Sign the message using the identity's current signing key
const { address, signature } = identity.signMessage(messageToSign);
return {
success: true,
signature,
address,
fileHash: toHex(fileHash),
metadata: {
fileName: file.name,
fileSize: file.size,
fileType: file.type,
signedAt: new Date().toISOString(),
identityKey: currentId.idKey,
protocol: signOptions.protocol || "bap",
},
};
}
catch (error) {
return {
success: false,
error: error,
};
}
},
});
// Verify file mutation
const verifyMutation = useMutation({
mutationFn: async ({ file, signature, signatureMetadata }) => {
try {
// Read file data
const arrayBuffer = await fileToArrayBuffer(file);
const fileData = new Uint8Array(arrayBuffer);
// Hash the file content
const fileHash = Hash.sha256(toArray(fileData));
// Determine protocol and create message to verify
const protocol = signatureMetadata?.protocol || "bap";
let messageToVerify;
if (protocol === "raw") {
// Verify against raw file hash
messageToVerify = toArray(fileHash);
}
else {
// Verify against BAP attestation format
const identityKey = signatureMetadata?.identityKey;
if (!identityKey) {
throw new Error("Identity key required for BAP verification");
}
const attestationUrn = `urn:bap:attest:file:${toHex(fileHash)}:${identityKey}`;
const attestationHash = Hash.sha256(attestationUrn, "utf8");
messageToVerify = toArray(attestationHash);
}
// Verify the signature
const address = signatureMetadata?.address;
if (!address || typeof address !== "string") {
throw new Error("Signing address required for verification");
}
// We need to recreate the signature verification similar to BAP's verifySignature method
// BSM.verify expects (message: number[], sig: Signature, pubKey: PublicKey)
// But we have signature as base64 string and address, not pubKey
// So we'll use the BAP approach of trying recovery factors
let isValid = false;
try {
const sig = Signature.fromCompact(signature, "base64");
const msgHash = new BigNumber(BSM.magicHash(messageToVerify));
// Try different recovery factors to find the right public key
for (let recovery = 0; recovery < 4; recovery++) {
try {
const pubKey = sig.RecoverPublicKey(recovery, msgHash);
const sigFitsPubkey = BSM.verify(messageToVerify, sig, pubKey);
if (sigFitsPubkey && pubKey.toAddress() === address) {
isValid = true;
break;
}
}
catch {
// Try next recovery factor
}
}
}
catch (_error) {
isValid = false;
}
return {
valid: isValid,
address: isValid ? address : undefined,
identityKey: typeof signatureMetadata?.identityKey === "string"
? signatureMetadata.identityKey
: undefined,
fileHash: toHex(fileHash),
};
}
catch (error) {
return {
valid: false,
error: error.message,
};
}
},
});
// Broadcast signature mutation
const broadcastMutation = useMutation({
mutationFn: async ({ signature: _signature, metadata: _metadata }) => {
// This would broadcast the signature to the blockchain
// For now, return a mock transaction ID
return `mock-signature-tx-${Date.now()}`;
},
});
return {
// React Query mutation interface
signFile: signMutation.mutate,
signFileAsync: signMutation.mutateAsync,
verifyFile: verifyMutation.mutate,
verifyFileAsync: verifyMutation.mutateAsync,
broadcastSignature: broadcastMutation.mutate,
broadcastSignatureAsync: broadcastMutation.mutateAsync,
// State
isLoading: signMutation.isPending ||
verifyMutation.isPending ||
broadcastMutation.isPending,
isError: signMutation.isError ||
verifyMutation.isError ||
broadcastMutation.isError,
isSuccess: signMutation.isSuccess ||
verifyMutation.isSuccess ||
broadcastMutation.isSuccess,
error: signMutation.error || verifyMutation.error || broadcastMutation.error,
data: signMutation.data || verifyMutation.data || broadcastMutation.data,
// Reset
reset: () => {
signMutation.reset();
verifyMutation.reset();
broadcastMutation.reset();
},
};
}