156 lines
4.7 KiB
TypeScript
156 lines
4.7 KiB
TypeScript
import BN from 'bn.js';
|
|
|
|
import { Connection, ParsedInstruction, ParsedTransactionWithMeta, PartiallyDecodedInstruction } from '@solana/web3.js';
|
|
import { TOKEN_PROGRAM_ID } from '@solana/spl-token';
|
|
|
|
import { getRecipientAddress } from '@/services/solana';
|
|
import { PaymentMethod } from '../types';
|
|
|
|
// Extract transaction info for native GOR transfers
|
|
const extractTxInfo = async (
|
|
connection: Connection,
|
|
parsedTx: ParsedTransactionWithMeta,
|
|
paymentMethod: PaymentMethod
|
|
): Promise<{ authority: string; amount: string; destination: string }> => {
|
|
|
|
if (!parsedTx) {
|
|
throw new Error('Transaction not found');
|
|
}
|
|
|
|
let transferInstruction: ParsedInstruction | PartiallyDecodedInstruction | undefined;
|
|
|
|
switch (paymentMethod) {
|
|
case PaymentMethod.NAT_GOR:
|
|
// Look for system program transfer instruction
|
|
transferInstruction = parsedTx.transaction.message.instructions.find(
|
|
(instr) => 'parsed' in instr && instr.parsed.type === 'transfer'
|
|
);
|
|
|
|
if (!transferInstruction || !('parsed' in transferInstruction)) {
|
|
throw new Error('Native GOR transfer instruction not found');
|
|
}
|
|
|
|
const { info: { lamports, source, destination } } = transferInstruction.parsed;
|
|
return { authority: source, amount: lamports.toString(), destination };
|
|
|
|
case PaymentMethod.SPL_TOKEN:
|
|
// Look for token transfer instruction using TOKEN_PROGRAM_ID
|
|
transferInstruction = parsedTx.transaction.message.instructions.find(
|
|
(instr) => 'parsed' in instr && instr.programId.equals(TOKEN_PROGRAM_ID)
|
|
);
|
|
|
|
if (!transferInstruction || !('parsed' in transferInstruction)) {
|
|
throw new Error('SPL token transfer instruction not found');
|
|
}
|
|
|
|
const parsed = transferInstruction.parsed;
|
|
|
|
// Handle both transferChecked and transfer types
|
|
if (parsed.type === 'transferChecked') {
|
|
const { info: { tokenAmount, authority, destination } } = parsed;
|
|
return {
|
|
authority,
|
|
amount: tokenAmount.amount,
|
|
destination
|
|
};
|
|
} else if (parsed.type === 'transfer') {
|
|
const { info: { amount, authority, destination } } = parsed;
|
|
return { authority, amount, destination };
|
|
}
|
|
|
|
throw new Error('Unsupported token transfer type');
|
|
|
|
default:
|
|
throw new Error('Invalid payment method');
|
|
}
|
|
};
|
|
|
|
export const verifyUnusedSolanaPayment = async (
|
|
connection: Connection,
|
|
parsedTx: ParsedTransactionWithMeta,
|
|
expectedAmount: BN,
|
|
paymentMethod: PaymentMethod,
|
|
): Promise<{
|
|
valid: boolean,
|
|
reason?: string,
|
|
amount?: string,
|
|
sender?: string
|
|
}> => {
|
|
try {
|
|
// TODO: Check if provided signature is already used
|
|
|
|
// Fetch transaction details
|
|
if (!parsedTx) {
|
|
return {
|
|
valid: false,
|
|
reason: 'Transaction not found on Solana blockchain'
|
|
};
|
|
}
|
|
|
|
// Check if transaction was successful
|
|
if (parsedTx.meta?.err) {
|
|
return {
|
|
valid: false,
|
|
reason: `Transaction failed: ${JSON.stringify(parsedTx.meta.err)}`
|
|
};
|
|
}
|
|
|
|
// Check transaction timestamp (5-minute window)
|
|
const txTimestamp = parsedTx.blockTime ? new Date(parsedTx.blockTime * 1000) : null;
|
|
if (!txTimestamp) {
|
|
return {
|
|
valid: false,
|
|
reason: 'Transaction timestamp not available'
|
|
};
|
|
}
|
|
|
|
const now = new Date();
|
|
const timeDiffMs = now.getTime() - txTimestamp.getTime();
|
|
const timeWindowMs = 5 * 60 * 1000; // 5 minutes
|
|
|
|
if (timeDiffMs > timeWindowMs) {
|
|
return {
|
|
valid: false,
|
|
reason: `Transaction is older than 5 minutes (${Math.round(timeDiffMs / 60000)} minutes old)`
|
|
};
|
|
}
|
|
|
|
// Extract transaction info based on payment method
|
|
const transferInfo = await extractTxInfo(connection, parsedTx, paymentMethod);
|
|
const amount = transferInfo.amount;
|
|
const authority = transferInfo.authority;
|
|
const destination = transferInfo.destination;
|
|
|
|
// Verify amount using BN comparison
|
|
const transactionAmount = new BN(amount);
|
|
if (transactionAmount.lt(expectedAmount)) {
|
|
return {
|
|
valid: false,
|
|
reason: `Payment amount (${amount}) is less than required (${expectedAmount.toString()})`
|
|
};
|
|
}
|
|
|
|
// Verify recipient address
|
|
const expectedRecipientAddress = getRecipientAddress(paymentMethod);
|
|
|
|
if (destination !== expectedRecipientAddress) {
|
|
return {
|
|
valid: false,
|
|
reason: `Invalid recipient address. Expected: ${expectedRecipientAddress}, Got: ${destination}`
|
|
};
|
|
}
|
|
|
|
return {
|
|
valid: true,
|
|
amount,
|
|
sender: authority
|
|
};
|
|
} catch (error) {
|
|
console.error('Error verifying Solana payment:', error);
|
|
return {
|
|
valid: false,
|
|
reason: `Failed to verify transaction: ${error instanceof Error ? error.message : 'Unknown error'}`
|
|
};
|
|
}
|
|
};
|