import type { AxiosResponse } from "axios"; import axios, { isAxiosError } from "axios"; import { ethers } from "ethers"; import type { BesuTransactionReceipt, Logger, UnsignedTransaction, } from "./interfaces.js"; export function getErrorMessage(err: unknown): string { if (!(err instanceof Error)) return "Unknown error"; if (isAxiosError(err)) { const data: unknown = err.response?.data; if (!data) return "Unknown error"; if (typeof data === "string") return data; return JSON.stringify(data); } return err.message; } export function logAxiosError(error: unknown, logger: Logger): void { if (!error || !(error instanceof Error) || !isAxiosError(error)) return; logger.error("Axios error intercepted.", error.stack); if (error.response) { // The request was made and the server responded with a status code // that falls out of the range of 2xx logger.error({ data: error.response.data as unknown, headers: error.response.headers as unknown, status: error.response.status, }); } else if (error.request) { // The request was made but no response was received // `error.request` is an instance of XMLHttpRequest in the browser and an instance of // http.ClientRequest in node.js logger.error({ request: error.request as unknown, }); } else { // Something happened in setting up the request that triggered an Error logger.error({ message: error.message, }); } logger.error(error.toJSON()); } export function logAxiosRequestError(err: unknown, logger: Logger) { if (err instanceof Error) { if (isAxiosError(err)) { logAxiosError(err, logger); } else { logger.error(err.message, err.stack); } } else { logger.error(err); } } export async function preregisterAttribute( taoDid: string, attributeIdTao: string, taoPrivateKeyHex: string, trustedIssuersRegistryApiJsonrpcUrl: string, ledgerApiUrl: string, logger: Logger, did: string, issuerType: number, revisionId: string, accessToken: string, ): Promise { // build unsigned transaction const wallet = new ethers.Wallet(taoPrivateKeyHex); let responseBuild: AxiosResponse<{ result: UnsignedTransaction; }>; try { responseBuild = await axios.post( trustedIssuersRegistryApiJsonrpcUrl, { id: 1, jsonrpc: "2.0", method: "setAttributeMetadata", params: [ { attributeIdTao, did, from: wallet.address, issuerType, revisionId, taoDid, }, ], }, { headers: { authorization: `Bearer ${accessToken}` }, }, ); } catch (error) { logAxiosRequestError(error, logger); throw new Error( "The server encountered an internal error and was unable to complete your request: Unable to build the transaction to register the attribute", ); } const unsignedTransaction = responseBuild!.data.result; const transactionResult = await signAndSendTransaction( unsignedTransaction, wallet, trustedIssuersRegistryApiJsonrpcUrl, logger, accessToken, ); if (!transactionResult.success) { throw new Error("Unable to send the transaction to register the attribute"); } const { txId } = transactionResult; const miningResult = await waitToBeMined(ledgerApiUrl, logger, txId); if (!miningResult.success) { throw new Error( `Unable to get the transaction mined to register the attribute: ${miningResult.error.message}`, ); } } /** * Helper function to sign and send a transaction. */ export async function signAndSendTransaction( unsignedTransaction: UnsignedTransaction, wallet: ethers.Wallet, jsonrpcEndpoint: string, logger: Logger, accessToken: string, ): Promise< { error: unknown; success: false } | { success: true; txId: string } > { const sgnTx = await wallet.signTransaction({ chainId: Number(unsignedTransaction.chainId), data: unsignedTransaction.data, gasLimit: unsignedTransaction.gasLimit, gasPrice: unsignedTransaction.gasPrice, nonce: Number(unsignedTransaction.nonce), to: unsignedTransaction.to, type: 0, // Legacy transaction type value: unsignedTransaction.value, }); const signature = ethers.Transaction.from(sgnTx).signature; if (!signature) { throw new Error("Unable to get the signature"); } const { r, s, v } = signature; let txId = ""; try { const responseSend = await axios.post<{ result: string; }>( jsonrpcEndpoint, { id: 1, jsonrpc: "2.0", method: "sendSignedTransaction", params: [ { protocol: "eth", r, s, signedRawTransaction: sgnTx, unsignedTransaction, v: `0x${v.toString(16)}`, }, ], }, { headers: { authorization: `Bearer ${accessToken}` }, }, ); txId = responseSend.data.result; } catch (error) { logAxiosRequestError(error, logger); return { error, success: false }; } return { success: true, txId }; } export async function waitToBeMined( ledgerApiUrl: string, logger: Logger, txId: string, ): Promise<{ error: Error; success: false } | { success: true }> { try { let mined = false; for (let i = 0; i < 40; i += 1) { await new Promise((resolve) => { setTimeout(resolve, 500); }); const { data } = await axios.post<{ result: BesuTransactionReceipt | null; }>(ledgerApiUrl, { // eslint-disable-next-line unicorn/no-null id: null, jsonrpc: "2.0", method: "eth_getTransactionReceipt", params: [txId], }); mined = !!data.result; if (mined && data.result) { if (Number(data.result.status) !== 1) { const revertReason = data.result.revertReason ? Buffer.from(data.result.revertReason.slice(2), "hex") .toString() .replaceAll(/[^a-z0-9:\-' ]/gi, "") : ""; return { error: new Error( `Transaction failed: Status ${data.result.status}. Revert reason: ${revertReason}`, ), success: false, }; } break; } } if (!mined) { return { error: new Error(`Timeout exceeded for transaction ID ${txId}`), success: false, }; } } catch (error) { logAxiosRequestError(error, logger); return { error: new Error(`Transaction not mined: ${getErrorMessage(error)}`), success: false, }; } return { success: true }; }