Skip to content
126 changes: 87 additions & 39 deletions src/repositories/donationRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { QfRound } from '../entities/qfRound';
import { ChainType } from '../types/network';
import { ORGANIZATION_LABELS } from '../entities/organization';
import { AppDataSource } from '../orm';
import { getPowerRound } from './powerRoundRepository';

export const exportClusterMatchingDonationsFormat = async (
qfRoundId: number,
Expand Down Expand Up @@ -584,45 +583,94 @@ export const getRecentDonations = async (take: number): Promise<Donation[]> => {
.getMany();
};

export const getSumOfGivbackEligibleDonationsForSpecificRound = async (params: {
powerRound?: number;
}): Promise<number> => {
// This function calculates the total USD value of all donations that are eligible for Givback in a specific PowerRound

try {
// If no powerRound is passed, get the latest one from the PowerRound table
const powerRound = params.powerRound || (await getPowerRound())?.round;
if (!powerRound) {
throw new Error('No powerRound found in the database.');
export const getSumOfGivbackEligibleDonationsForSpecificRound =
async (_params: { powerRound?: number }): Promise<number> => {
// This function calculates the total USD value of all donations eligible for GIVback
// Uses the operational window (16:00 UTC offset) to match givbacks service
// Groups recurring donations and applies minimum thresholds

try {
// Calculate date range for current month with 16:00 UTC offset (matches givbacks service)
const now = new Date();
const year = now.getUTCFullYear();
const month = now.getUTCMonth();

// Start: 1st of current month at 16:00 UTC
const startDate = new Date(Date.UTC(year, month, 1, 16, 0, 0));
// End: Last day of current month at 15:59:59 UTC (or 1st of next month at 15:59:59)
const endDate = new Date(Date.UTC(year, month + 1, 0, 15, 59, 59));

const startDateStr = startDate
.toISOString()
.replace('T', ' ')
.replace('Z', '');
const endDateStr = endDate
.toISOString()
.replace('T', ' ')
.replace('Z', '');

// Minimum threshold: $4 for most projects, $0.05 for community project
const minEligibleValueUsd = 4;
const communityMinValueUsd = 0.05;
const communityProjectSlug = 'the-giveth-community-of-makers';

// Execute query with:
// 1. Date range (operational window)
// 2. Group recurring donations by parent ID
// 3. Apply minimum threshold on grouped totals
// 4. Purple list exclusion
const result = await AppDataSource.getDataSource().query(
`
WITH grouped_donations AS (
SELECT
COALESCE("recurringDonationId"::text, "id"::text) AS group_id,
SUM("valueUsd") AS total_usd,
SUM("valueUsd" * "givbackFactor") AS total_usd_with_factor,
MAX("fromWalletAddress") AS from_address,
MAX("projectId") AS project_id
FROM "donation"
WHERE "status" = 'verified'
AND "isProjectGivbackEligible" = true
AND "createdAt" > $1
AND "createdAt" < $2
GROUP BY COALESCE("recurringDonationId"::text, "id"::text)
)
SELECT COALESCE(SUM(gd.total_usd_with_factor), 0) AS "totalUsdWithGivbackFactor"
FROM grouped_donations gd
JOIN "project" p ON p.id = gd.project_id
WHERE
(
(p.slug = $3 AND gd.total_usd > $4)
OR (p.slug != $3 AND gd.total_usd >= $5)
)
AND NOT EXISTS (
SELECT 1
FROM "project_address" pa
INNER JOIN "project" proj ON proj.id = pa."projectId"
WHERE LOWER(pa.address) = LOWER(gd.from_address)
AND pa."isRecipient" = true
AND proj.verified = true
AND proj."statusId" = 5
);
`,
[
startDateStr,
endDateStr,
communityProjectSlug,
communityMinValueUsd,
minEligibleValueUsd,
],
);

return result?.[0]?.totalUsdWithGivbackFactor ?? 0;
} catch (e) {
logger.error(
'getSumOfGivbackEligibleDonationsForSpecificRound() error',
e,
);
return 0;
}

// Execute the main raw SQL query with the powerRound
const result = await AppDataSource.getDataSource().query(
`
SELECT
SUM("donation"."valueUsd" * "donation"."givbackFactor") AS "totalUsdWithGivbackFactor"
FROM "donation"
WHERE "donation"."status" = 'verified'
AND "donation"."isProjectGivbackEligible" = true
AND "donation"."powerRound" = $1
AND NOT EXISTS (
SELECT 1
FROM "project_address" "pa"
INNER JOIN "project" "p" ON "p"."id" = "pa"."projectId"
WHERE "pa"."address" = "donation"."fromWalletAddress"
AND "pa"."isRecipient" = true
AND "p"."verified" = true
);
`,
[powerRound],
);

return result?.[0]?.totalUsdWithGivbackFactor ?? 0;
} catch (e) {
logger.error('getSumOfGivbackEligibleDonationsForSpecificRound() error', e);
return 0;
}
};
};

export const getPendingDonationsIds = (): Promise<{ id: number }[]> => {
const date = moment()
Expand Down
2 changes: 1 addition & 1 deletion src/server/cors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { logger } from '../utils/logger';

// Hostnames that are always allowed, regardless of env.
// NOTE: CORS check below also allows subdomains of any entry here.
const staticWhitelistHostnames: string[] = ['base.giveth.io'];
const staticWhitelistHostnames: string[] = ['base.giveth.io', 'qf.giveth.io'];

export const whitelistHostnames: string[] = Array.from(
new Set([
Expand Down
139 changes: 136 additions & 3 deletions src/services/chains/evm/transactionService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,130 @@ const closeTo = (a: number, b: number, delta = 0.001) => {
return Math.abs(1 - a / b) < delta;
};

function parseTraceValueToWei(value: unknown) {
try {
if (
value === undefined ||
value === null ||
(typeof value === 'string' && value.trim() === '')
) {
return ethers.BigNumber.from(0);
}
return ethers.BigNumber.from(value);
} catch {
return ethers.BigNumber.from(0);
}
}

function collectValueTransfersFromCallTrace(
node: any,
transfers: Array<{ from: string; to: string; value: string }>,
) {
if (!node || typeof node !== 'object') return;

const from = typeof node.from === 'string' ? node.from.toLowerCase() : '';
const to = typeof node.to === 'string' ? node.to.toLowerCase() : '';
const value = parseTraceValueToWei(node.value);

if (from && to && value.gt(0)) {
transfers.push({
from,
to,
value: value.toString(),
});
}

if (Array.isArray(node.calls)) {
for (const call of node.calls) {
collectValueTransfersFromCallTrace(call, transfers);
}
}
}

async function getInternalTransactionsByTxHashFromRpcTrace(params: {
networkId: number;
txHash: string;
}) {
const { networkId, txHash } = params;
const provider = getProvider(networkId);

try {
logger.debug(
'NODE RPC request count - getInternalTransactionsByTxHashFromRpcTrace provider.send debug_traceTransaction txHash:',
txHash,
);
const debugTraceResult = await provider.send('debug_traceTransaction', [
txHash,
{ tracer: 'callTracer' },
]);
const transfers: Array<{ from: string; to: string; value: string }> = [];
collectValueTransfersFromCallTrace(debugTraceResult, transfers);
txTrace(
params,
'getInternalTransactionsByTxHashFromRpcTrace:debug_success',
{
transfersCount: transfers.length,
},
);
return transfers;
} catch (debugError) {
txTrace(
params,
'getInternalTransactionsByTxHashFromRpcTrace:debug_failed',
{
error: debugError?.message,
},
);
}

try {
logger.debug(
'NODE RPC request count - getInternalTransactionsByTxHashFromRpcTrace provider.send trace_transaction txHash:',
txHash,
);
const traceResult = await provider.send('trace_transaction', [txHash]);
const transfers: Array<{ from: string; to: string; value: string }> = [];
const calls = Array.isArray(traceResult) ? traceResult : [];

for (const call of calls) {
const from =
typeof call?.action?.from === 'string'
? call.action.from.toLowerCase()
: '';
const to =
typeof call?.action?.to === 'string'
? call.action.to.toLowerCase()
: '';
const value = parseTraceValueToWei(call?.action?.value);
if (from && to && value.gt(0)) {
transfers.push({
from,
to,
value: value.toString(),
});
}
}

txTrace(
params,
'getInternalTransactionsByTxHashFromRpcTrace:trace_success',
{
transfersCount: transfers.length,
},
);
return transfers;
} catch (traceError) {
txTrace(
params,
'getInternalTransactionsByTxHashFromRpcTrace:trace_failed',
{
error: traceError?.message,
},
);
return [];
}
}

async function getInternalTransactionsByTxHash(params: {
networkId: number;
txHash: string;
Expand Down Expand Up @@ -413,10 +537,19 @@ async function getInternalTransactionsByTxHash(params: {
networkId: params.networkId,
txHash: params.txHash,
});
txTrace(params, 'getInternalTransactionsByTxHash:catch_return_empty', {
error: e?.message,
txTrace(
params,
'getInternalTransactionsByTxHash:explorer_failed_fallback',
{
error: e?.message,
},
);
const internalTxsFromTrace =
await getInternalTransactionsByTxHashFromRpcTrace(params);
txTrace(params, 'getInternalTransactionsByTxHash:fallback_done', {
internalTxsCount: internalTxsFromTrace.length,
});
return [];
return internalTxsFromTrace;
}
}

Expand Down
Loading