UNPKG

@bit-gpt/h402

Version:

BitGPT's 402 open protocol for blockchain-native payments

445 lines 16.4 kB
import { evm } from "../../../shared/index.js"; import { SCHEME } from "../index.js"; import { encodeFunctionData, parseTransaction, } from "viem"; import { h402Version } from "../../../index.js"; import { validateTransferData, validateTransferEventLog, } from "./validation.js"; const BLOCK_TIME = 2; // Average block time in seconds const SAFETY_BLOCKS = 3; // Number of blocks for safety margin const GAS_LIMIT_SAFETY_FACTOR = 1.25; // Allow 25% more than estimated gas async function getMaxGasForChain(client) { try { const block = await client.getBlock(); return BigInt(Math.floor(Number(block.gasLimit) * 0.3 * GAS_LIMIT_SAFETY_FACTOR)); } catch (error) { const baseFee = await client.getGasPrice(); return baseFee * BigInt(Math.floor(100000 * GAS_LIMIT_SAFETY_FACTOR)); } } async function verify(client, payload, paymentRequirements) { try { if (!validateBasePayload(payload, paymentRequirements)) { return { isValid: false, errorMessage: "Invalid payload structure or version mismatch", }; } if (!validateChain(payload, paymentRequirements)) { return { isValid: false, errorMessage: "Invalid chain configuration", }; } switch (payload.payload.type) { case "authorization": return await verifyAuthorizationPayload(client, payload.payload, paymentRequirements); case "signAndSendTransaction": return await verifySignAndSendTransactionPayload(client, payload.payload, paymentRequirements); case "signedTransaction": return await verifySignedTransactionPayload(payload.payload, paymentRequirements); default: return { isValid: false, errorMessage: "Unsupported payload type", }; } } catch (error) { return { isValid: false, errorMessage: error instanceof Error ? error.message : String(error), }; } } function validateBasePayload(payload, paymentRequirements) { return (payload.h402Version === h402Version && payload.scheme === SCHEME && payload.namespace === paymentRequirements.namespace && payload.networkId === paymentRequirements.networkId && payload.resource === paymentRequirements.resource); } function validateChain(payload, paymentRequirements) { return (payload.networkId === paymentRequirements.networkId && payload.namespace === "evm"); } async function verifyAuthorizationPayload(client, payload, paymentRequirements) { const currentTime = Math.floor(Date.now() / 1000); const safetyMargin = BLOCK_TIME * SAFETY_BLOCKS; if (payload.authorization.validBefore < BigInt(currentTime + safetyMargin)) { return { isValid: false, errorMessage: "Authorization deadline too close to current time", }; } if (payload.authorization.validAfter > BigInt(currentTime)) { return { isValid: false, errorMessage: "Authorization not yet valid", }; } if (payload.authorization.value < paymentRequirements.amountRequired) { return { isValid: false, errorMessage: "Insufficient authorized amount", }; } if (payload.authorization.to !== paymentRequirements.payToAddress) { return { isValid: false, errorMessage: "Invalid recipient address", }; } const balance = await client.readContract({ address: paymentRequirements.tokenAddress, abi: [ { name: "balanceOf", type: "function", inputs: [{ name: "account", type: "address" }], outputs: [{ name: "balance", type: "uint256" }], stateMutability: "view", }, ], functionName: "balanceOf", args: [payload.authorization.from], }); if (balance < payload.authorization.value) { return { isValid: false, errorMessage: "Insufficient token balance", }; } try { const data = encodeFunctionData({ abi: [ { name: "transferWithAuthorization", type: "function", inputs: [ { name: "from", type: "address" }, { name: "to", type: "address" }, { name: "value", type: "uint256" }, { name: "validAfter", type: "uint256" }, { name: "validBefore", type: "uint256" }, { name: "nonce", type: "bytes32" }, { name: "signature", type: "bytes" }, ], outputs: [], stateMutability: "nonpayable", }, ], functionName: "transferWithAuthorization", args: [ payload.authorization.from, payload.authorization.to, payload.authorization.value, payload.authorization.validAfter, payload.authorization.validBefore, payload.authorization.nonce, payload.signature, ], }); const gasEstimate = await client.estimateGas({ account: payload.authorization.from, to: paymentRequirements.tokenAddress, data, }); const maxGasAmount = await getMaxGasForChain(client); if (gasEstimate > maxGasAmount) { return { isValid: false, errorMessage: `Transaction would require too much gas (${gasEstimate} > ${maxGasAmount})`, }; } } catch (error) { return { isValid: false, errorMessage: "Failed to estimate gas: " + (error instanceof Error ? error.message : String(error)), }; } const isValidSignature = await client.readContract({ address: paymentRequirements.tokenAddress, abi: [ { name: "verifyTransferAuthorization", type: "function", inputs: [ { name: "from", type: "address" }, { name: "to", type: "address" }, { name: "value", type: "uint256" }, { name: "validAfter", type: "uint256" }, { name: "validBefore", type: "uint256" }, { name: "nonce", type: "bytes32" }, { name: "signature", type: "bytes" }, ], outputs: [{ name: "valid", type: "bool" }], stateMutability: "view", }, ], functionName: "verifyTransferAuthorization", args: [ payload.authorization.from, payload.authorization.to, payload.authorization.value, payload.authorization.validAfter, payload.authorization.validBefore, payload.authorization.nonce, payload.signature, ], }); if (!isValidSignature) { return { isValid: false, errorMessage: "Invalid signature", }; } return { isValid: true, type: "payload" }; } async function verifySignAndSendTransactionPayload(client, payload, paymentRequirements) { try { const txData = await client.getTransaction({ hash: payload.transactionHash, }); if (!txData) { return { isValid: false, errorMessage: "Transaction not found", }; } const txReceipt = await client.getTransactionReceipt({ hash: payload.transactionHash, }); if (!txReceipt) { return { isValid: false, errorMessage: "Transaction not yet confirmed", }; } if (txReceipt.status !== "success") { return { isValid: false, errorMessage: "Transaction failed", }; } // Use consolidated validation for transaction data const transferValidation = await validateTransferData(txData, paymentRequirements); if (!transferValidation.isValid) { return { isValid: false, errorMessage: transferValidation.errorMessage, }; } // For ERC20 tokens, also validate the event log (double-check that transfer actually happened) if (paymentRequirements.tokenAddress !== evm.ZERO_ADDRESS) { const eventValidation = validateTransferEventLog(txReceipt.logs, paymentRequirements); if (!eventValidation.isValid) { return { isValid: false, errorMessage: eventValidation.errorMessage, }; } } // Verify signed message const messageVerified = await client.verifyMessage({ address: txData.from, message: paymentRequirements.resource ?? `402 signature ${Date.now()}`, signature: payload.signedMessage, }); if (!messageVerified) { return { isValid: false, errorMessage: "Invalid signature" }; } return { isValid: true, type: "transaction", txHash: payload.transactionHash, }; } catch (error) { return { isValid: false, errorMessage: `Failed to verify sign and send transaction: ${error instanceof Error ? error.message : String(error)}`, }; } } async function verifySignedTransactionPayload(payload, paymentRequirements) { try { // Parse the signed transaction to validate its structure and content const parsedTx = parseTransaction(payload.signedTransaction); // Use consolidated validation const transferValidation = await validateTransferData(parsedTx, paymentRequirements); if (!transferValidation.isValid) { return { isValid: false, errorMessage: transferValidation.errorMessage, }; } return { isValid: true, type: "payload", }; } catch (error) { return { isValid: false, errorMessage: `Failed to verify signed transaction: ${error instanceof Error ? error.message : String(error)}`, }; } } async function settle(client, payload, paymentRequirements) { try { if (!validateBasePayload(payload, paymentRequirements)) { return { success: false, transaction: "", namespace: payload.namespace, errorReason: "invalid_payload", error: "Invalid payload structure or version mismatch", }; } if (!validateChain(payload, paymentRequirements)) { return { success: false, transaction: "", namespace: payload.namespace, errorReason: "invalid_network", error: "Invalid chain configuration", }; } switch (payload.payload.type) { case "authorization": return await settleAuthorizationPayload(client, payload.payload, paymentRequirements, payload.namespace); case "signedTransaction": return await settleSignedTransactionPayload(client, payload.payload, payload.namespace); case "signAndSendTransaction": return { success: false, transaction: "", namespace: payload.namespace, errorReason: "invalid_scheme", error: "SignAndSendTransaction payloads cannot be settled (already executed)", }; default: return { success: false, transaction: "", namespace: payload.namespace, errorReason: "invalid_payload", error: "Unsupported payload type", }; } } catch (error) { return { success: false, transaction: "", namespace: payload.namespace, errorReason: "unexpected_settle_error", error: `Failed to settle payment: ${error instanceof Error ? error.message : String(error)}`, }; } } async function settleAuthorizationPayload(client, payload, paymentRequirements, namespace) { try { const data = encodeFunctionData({ abi: [ { name: "transferWithAuthorization", type: "function", inputs: [ { name: "from", type: "address" }, { name: "to", type: "address" }, { name: "value", type: "uint256" }, { name: "validAfter", type: "uint256" }, { name: "validBefore", type: "uint256" }, { name: "nonce", type: "bytes32" }, { name: "signature", type: "bytes" }, ], outputs: [], stateMutability: "nonpayable", }, ], functionName: "transferWithAuthorization", args: [ payload.authorization.from, payload.authorization.to, payload.authorization.value, payload.authorization.validAfter, payload.authorization.validBefore, payload.authorization.nonce, payload.signature, ], }); const txHash = await client.sendTransaction({ account: client.account, to: paymentRequirements.tokenAddress, data, chain: evm.getChain(paymentRequirements.networkId), }); const receipt = await client.waitForTransactionReceipt({ hash: txHash, }); if (receipt.status === "success") { return { success: true, transaction: txHash, namespace: namespace, payer: payload.authorization.from, }; } return { success: false, transaction: txHash, namespace: namespace, errorReason: "invalid_transaction_state", error: "Authorization transaction failed", }; } catch (error) { return { success: false, transaction: "", namespace: namespace, errorReason: "unexpected_settle_error", error: `Failed to settle authorization: ${error instanceof Error ? error.message : String(error)}`, }; } } async function settleSignedTransactionPayload(client, payload, namespace) { try { // Broadcast the signed transaction const txHash = await client.sendRawTransaction({ serializedTransaction: payload.signedTransaction, }); const receipt = await client.waitForTransactionReceipt({ hash: txHash, }); if (receipt.status === "success") { return { success: true, transaction: txHash, namespace: namespace, payer: receipt.from, }; } return { success: false, transaction: txHash, namespace: namespace, errorReason: "invalid_transaction_state", error: "Signed transaction failed", }; } catch (error) { return { success: false, transaction: "", namespace: namespace, errorReason: "unexpected_settle_error", error: `Failed to settle signed transaction: ${error instanceof Error ? error.message : String(error)}`, }; } } export { verify, settle }; //# sourceMappingURL=facilitator.js.map