@bit-gpt/h402
Version:
BitGPT's 402 open protocol for blockchain-native payments
202 lines • 8.4 kB
JavaScript
import { address, createSolanaRpc, } from "@solana/kit";
import { solana } from "../../../shared/index.js";
import { getFacilitator } from "../../../shared/next.js";
/**
* Verify a Solana payment for the exact scheme
* Checks that the transaction is confirmed and contains a transfer to the correct address
* with the correct amount and a memo matching the resource
*/
export async function verify(payload, paymentRequirements) {
console.log("VERIFY");
if (paymentRequirements.namespace !== "solana") {
return {
isValid: false,
errorMessage: 'Payment details must use the "solana" namespace',
invalidReason: "invalid_payment_requirements",
};
}
try {
console.log("[DEBUG-SOLANA-VERIFY] Starting Solana payment verification", {
payloadType: payload.payload.type,
networkId: paymentRequirements.networkId,
tokenAddress: paymentRequirements.tokenAddress,
amountRequired: paymentRequirements.amountRequired,
});
switch (payload.payload.type) {
case "signAndSendTransaction":
// For signAndSendTransaction, verify both signature and transaction match
if (!payload.payload.signature) {
return {
isValid: false,
errorMessage: "Missing required signature in signAndSendTransaction payload",
invalidReason: "invalid_exact_solana_payload_signature",
};
}
// Fetch the transaction
const txResponse = await solana.fetchTransaction(payload.payload.signature, { waitForConfirmation: true });
if (!txResponse) {
return {
isValid: false,
errorMessage: "Transaction not found",
};
}
// Verify transaction is confirmed
if (!txResponse.meta || txResponse.meta.err) {
return {
isValid: false,
errorMessage: "Transaction failed or not confirmed",
};
}
// Verify payment amount and recipient
const isValidPayment = await verifyPaymentAmount(txResponse, paymentRequirements);
if (!isValidPayment.isValid) {
return isValidPayment;
}
return {
isValid: true,
txHash: payload.payload.signature,
type: "transaction",
};
case "signTransaction":
return {
isValid: true,
txHash: payload.payload.signature,
type: "payload", // type payload will trigger settle function
};
case "signMessage":
return {
isValid: false,
errorMessage: "SignMessage payloads cannot be verified as transactions",
};
default:
return {
isValid: false,
errorMessage: `Unsupported payload type: ${payload.payload.type}`,
};
}
}
catch (error) {
return {
isValid: false,
errorMessage: `Error verifying transaction: ${error instanceof Error ? error.message : String(error)}`,
};
}
}
/**
* Verify the payment amount and recipient in a transaction
*/
async function verifyPaymentAmount(txResponse, paymentRequirements) {
if (!txResponse?.meta) {
return {
isValid: false,
errorMessage: "Transaction metadata not available",
};
}
const payToAddress = address(paymentRequirements.payToAddress);
const requiredAmount = BigInt(paymentRequirements.amountRequired.toString());
// Check if this is a native SOL transfer or SPL token transfer
if (paymentRequirements.tokenAddress === "11111111111111111111111111111111") {
// Native SOL transfer
let totalTransferred = BigInt(0);
// Check pre- / post-balances to find transfers to the recipient
if (txResponse.meta.preBalances && txResponse.meta.postBalances) {
const accountKeys = txResponse.transaction.message.accountKeys;
for (let i = 0; i < accountKeys.length; i++) {
const accountKey = typeof accountKeys[i] === "string"
? address(accountKeys[i])
: accountKeys[i];
if (accountKey.toString() === payToAddress.toString()) {
const preBalance = txResponse.meta.preBalances[i];
const postBalance = txResponse.meta.postBalances[i];
const transferred = BigInt(postBalance) - BigInt(preBalance);
if (transferred > 0) {
totalTransferred += transferred;
}
}
}
}
if (totalTransferred < requiredAmount) {
return {
isValid: false,
errorMessage: `Insufficient payment: required ${requiredAmount}, got ${totalTransferred}`,
};
}
}
else {
// SPL token transfer
let foundValidTransfer = false;
let totalTransferred = BigInt(0);
// Check pre/post balances to find transfers to the recipient
if (txResponse.meta.preTokenBalances && txResponse.meta.postTokenBalances) {
// Match token balances by accountIndex
for (const preTokenBalance of txResponse.meta.preTokenBalances) {
// Find matching post balance
const postTokenBalance = txResponse.meta.postTokenBalances.find((post) => post.accountIndex === preTokenBalance.accountIndex);
// Skip if we don't have both pre and post balances
if (!postTokenBalance)
continue;
// Check if this is the recipient's token account
if (preTokenBalance.owner === payToAddress.toString()) {
const transferred = BigInt(postTokenBalance.uiTokenAmount.amount) -
BigInt(preTokenBalance.uiTokenAmount.amount);
if (transferred > 0) {
totalTransferred += transferred;
foundValidTransfer = true;
}
}
}
}
if (totalTransferred < requiredAmount) {
return {
isValid: false,
errorMessage: `Insufficient payment: required ${requiredAmount}, got ${totalTransferred}`,
};
}
if (!foundValidTransfer) {
return {
isValid: false,
errorMessage: "No valid token transfer found in transaction",
};
}
}
return { isValid: true };
}
/**
* Settle a Solana payment
* For broadcast transactions, this just verifies the transaction is confirmed
*/
export async function settle(payload, paymentRequirements) {
console.log("SETTLE");
// re-verify to ensure the payment is still valid
const valid = await verify(payload, paymentRequirements);
if (!valid.isValid) {
return {
success: false,
transaction: "",
errorReason: valid.invalidReason ?? "invalid_scheme", //`Payment is no longer valid: ${valid.invalidReason}`,
error: valid.errorMessage,
};
}
if (payload.payload.type === "signAndSendTransaction") {
return {
success: false,
transaction: "",
error: "This payload type is not supported",
};
}
const rpc = createSolanaRpc(`${getFacilitator()}/solana-rpc`);
const response = await rpc
.sendTransaction(payload.payload
.transaction, { encoding: "base64" })
.send();
console.log("[DEBUG-SOLANA-SETTLE] txHash", response);
if (response !== payload.payload.signature) {
throw new Error("Something went wrong");
}
return {
success: true,
transaction: payload.payload.signature,
namespace: paymentRequirements.namespace,
};
}
//# sourceMappingURL=facilitator.js.map