Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion README.md

This file was deleted.

13 changes: 13 additions & 0 deletions functions/api/claim/status.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { Env } from '../../types/env';
import { errorResponse } from '../../services/vmClient';

// TODO: Integrate with VM backend for claim status polling
export const onRequestGet: PagesFunction<Env> = async (context) => {
const hash = new URL(context.request.url).searchParams.get('hash');

if (!hash) {
return errorResponse('hash query parameter is required', 400);
}

return errorResponse('Claim status not yet integrated with VM backend', 501);
};
20 changes: 20 additions & 0 deletions functions/api/claim/submit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { Env } from '../../types/env';
import { errorResponse, optionsResponse } from '../../services/vmClient';

// TODO: Integrate with VM backend for claim submission
export const onRequestPost: PagesFunction<Env> = async (context) => {
try {
const body = await context.request.json() as { stakeAddress: string; assets: string[]; airdropHash: string };

if (!body.stakeAddress || !body.assets?.length || !body.airdropHash) {
return errorResponse('stakeAddress, assets, and airdropHash are required', 400);
}

return errorResponse('Claim submission not yet integrated with VM backend', 501);
} catch (error) {
console.error('Claim submit error:', error);
return errorResponse('Submit failed');
}
};

export const onRequestOptions: PagesFunction = async () => optionsResponse();
20 changes: 20 additions & 0 deletions functions/api/claim/submitTransaction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { Env } from '../../types/env';
import { errorResponse, optionsResponse } from '../../services/vmClient';

// TODO: Integrate with VM backend for transaction submission
export const onRequestPost: PagesFunction<Env> = async (context) => {
try {
const body = await context.request.json() as { signedTx: string; airdropHash: string };

if (!body.signedTx || !body.airdropHash) {
return errorResponse('signedTx and airdropHash are required', 400);
}

return errorResponse('Transaction submission not yet integrated with VM backend', 501);
} catch (error) {
console.error('Submit transaction error:', error);
return errorResponse('Transaction submission failed');
}
};

export const onRequestOptions: PagesFunction = async () => optionsResponse();
20 changes: 20 additions & 0 deletions functions/api/claim/validate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { Env } from '../../types/env';
import { errorResponse, optionsResponse } from '../../services/vmClient';

// TODO: Integrate with VM backend for actual claim validation
export const onRequestPost: PagesFunction<Env> = async (context) => {
try {
const body = await context.request.json() as { stakeAddress: string; assets: string[] };

if (!body.stakeAddress || !body.assets?.length) {
return errorResponse('stakeAddress and assets are required', 400);
}

return errorResponse('Claim validation not yet integrated with VM backend', 501);
} catch (error) {
console.error('Claim validate error:', error);
return errorResponse('Validation failed');
}
};

export const onRequestOptions: PagesFunction = async () => optionsResponse();
213 changes: 65 additions & 148 deletions functions/api/getRewards.ts
Original file line number Diff line number Diff line change
@@ -1,183 +1,100 @@
import {
isNativeToken,
getTokenValue,
type ClaimableToken,
type GetRewardsDto,
type TokenInfo,
} from '../../src/shared/rewards';
import type { Env } from '../types/env';
import { initVmSdk, jsonResponse, errorResponse } from '../services/vmClient';

function mergeAmounts(...sources: (Record<string, number> | undefined)[]): Record<string, number> {
const merged: Record<string, number> = {};
for (const source of sources) {
if (!source) continue;
for (const [id, amount] of Object.entries(source)) {
merged[id] = (merged[id] ?? 0) + amount;
}
}
return merged;
}

interface Env {
VITE_VM_API_KEY: string;
function toClaimableTokens(
amounts: Record<string, number>,
tokens: Record<string, TokenInfo>,
premium: boolean,
): ClaimableToken[] {
return Object.entries(amounts)
.filter(([assetId]) => tokens[assetId])
.map(([assetId, rawAmount]) => {
const { decimals: tokenDecimals = 0, logo = '', ticker = '' } = tokens[assetId];
const decimals = Number(tokenDecimals);
return {
assetId,
ticker,
logo,
decimals,
amount: rawAmount / Math.pow(10, decimals),
premium,
native: premium ? isNativeToken(assetId) : false,
};
});
}

async function getRewards(stakeAddress: string, env: Env): Promise<ClaimableToken[]> {
const { getRewards: getRewardsFromVM, getTokens: getTokensFromVM, setApiToken } = await import('vm-sdk');
setApiToken(env.VITE_VM_API_KEY);
const { getRewards: getRewardsFromVM, getTokens: getTokensFromVM } = await initVmSdk(env);

const [getRewardsResponse, tokensRaw] = await Promise.all([
const [rewardsResponse, tokensRaw] = await Promise.all([
getRewardsFromVM(stakeAddress) as Promise<GetRewardsDto | null>,
getTokensFromVM(),
]);

let tokens = tokensRaw as unknown as Record<string, TokenInfo> | null;
if (!rewardsResponse || !tokens) {
console.warn('getRewards: SDK returned null for', !rewardsResponse ? 'rewards' : 'tokens', { stakeAddress });
return [];
}

const claimableTokens: ClaimableToken[] = [];

if (getRewardsResponse == null) return claimableTokens;
if (tokens == null) return claimableTokens;

const consolidatedAvailableReward: { [key: string]: number } = {};
const consolidatedAvailableRewardPremium: { [key: string]: number } = {};

// Accumulate regular rewards from consolidated_promises and consolidated_rewards
const addToConsolidated = (target: { [key: string]: number }, source: Record<string, number> | undefined) => {
if (!source) return;
Object.entries(source).forEach(([assetId, amount]) => {
const numAmount = Number(amount);
if (!isNaN(numAmount)) {
target[assetId] = (target[assetId] || 0) + numAmount;
const regular = mergeAmounts(rewardsResponse.consolidated_promises, rewardsResponse.consolidated_rewards);
const premium = mergeAmounts(
rewardsResponse.project_locked_rewards?.consolidated_promises,
rewardsResponse.project_locked_rewards?.consolidated_rewards,
);

const allAssetIds = [...Object.keys(regular), ...Object.keys(premium)];
for (const assetId of allAssetIds) {
if (!tokens[assetId]) {
tokens = (await getTokensFromVM()) as unknown as Record<string, TokenInfo> | null;
if (!tokens) {
console.warn('getRewards: token re-fetch returned null', { stakeAddress });
return [];
}
});
};

addToConsolidated(consolidatedAvailableReward, getRewardsResponse.consolidated_promises);
addToConsolidated(consolidatedAvailableReward, getRewardsResponse.consolidated_rewards);

// Accumulate premium rewards from project_locked counterparts
if (getRewardsResponse.project_locked_rewards) {
addToConsolidated(consolidatedAvailableRewardPremium, getRewardsResponse.project_locked_rewards.consolidated_promises);
addToConsolidated(consolidatedAvailableRewardPremium, getRewardsResponse.project_locked_rewards.consolidated_rewards);
break;
}
}

const allAssetIds = [
...Object.keys(consolidatedAvailableReward),
...Object.keys(consolidatedAvailableRewardPremium),
return [
...toClaimableTokens(regular, tokens, false),
...toClaimableTokens(premium, tokens, true),
];

let hasMissingToken = false;
if (tokens) {
hasMissingToken = allAssetIds.some((id) => !(tokens as Record<string, TokenInfo>)[id]);
} else {
hasMissingToken = true;
}
if (hasMissingToken) {
const refreshedTokens = await getTokensFromVM();
tokens = refreshedTokens as unknown as Record<string, TokenInfo> | null;
if (tokens == null) return claimableTokens;
}

const addTokensToClaimable = (rewardsByAsset: Record<string, number>, premium: boolean) => {
Object.keys(rewardsByAsset).forEach((assetId) => {
const token = tokens[assetId];
if (!token) {
console.warn(`Token metadata missing for asset: ${assetId}`);
return;
}
const { decimals: tokenDecimals = 0, logo = "", ticker = "" } = token || {};
const decimals = Number(tokenDecimals);
const amount = rewardsByAsset[assetId] / Math.pow(10, decimals);
// TODO: Integrate real pricing when available - currently using empty prices map for minimal implementation
const { price, total } = getTokenValue(assetId, amount, {});

claimableTokens.push({
assetId,
ticker: ticker as string,
logo: logo as string,
decimals,
amount,
premium,
native: isNativeToken(assetId),
price,
total,
});
});
};

addTokensToClaimable(consolidatedAvailableReward, false);
addTokensToClaimable(consolidatedAvailableRewardPremium, true);

return claimableTokens;
}

export const onRequestGet: PagesFunction<Env> = async (context) => {
const { request, env } = context;
const url = new URL(request.url);
const stakeAddress = url.searchParams.get("walletId");

console.log("getRewards called with stakeAddress:", stakeAddress);
console.log("API Key exists:", !!env.VITE_VM_API_KEY);
console.log("API Key length:", env.VITE_VM_API_KEY?.length);
const stakeAddress = new URL(request.url).searchParams.get('walletId');

if (!stakeAddress) {
return new Response(
JSON.stringify({ error: "stakeAddress is required" }),
{
status: 400,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET",
"Access-Control-Allow-Headers": "Content-Type"
}
}
);
return errorResponse('walletId is required', 400);
}

if (!env.VITE_VM_API_KEY || env.VITE_VM_API_KEY === 'your_api_key_here' || env.VITE_VM_API_KEY.trim() === '') {
return new Response(
JSON.stringify({
error: "API key not available in environment. Please set VITE_VM_API_KEY in .dev.vars file",
details: "The API key is missing or set to a placeholder value"
}),
{
status: 500,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET",
"Access-Control-Allow-Headers": "Content-Type"
}
}
);
if (!env.VITE_VM_API_KEY || env.VITE_VM_API_KEY.trim() === '') {
return errorResponse('Server configuration error', 500);
}

try {
console.log("About to call getRewards with stakeAddress:", stakeAddress);
const claimableTokens = await getRewards(stakeAddress, env);
console.log("getRewards completed successfully, found", claimableTokens.length, "tokens");

return new Response(
JSON.stringify({ rewards: claimableTokens }),
{
status: 200,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET",
"Access-Control-Allow-Headers": "Content-Type"
}
}
);
return jsonResponse({ rewards: claimableTokens });
} catch (error) {
console.error("Full error object:", error);
console.error("Error message:", error instanceof Error ? error.message : 'Unknown error');
console.error("Error stack:", error instanceof Error ? error.stack : 'No stack trace');

return new Response(
JSON.stringify({
error: 'Failed to process request',
details: 'Internal server error',
stakeAddress: stakeAddress
}),
{
status: 500,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET",
"Access-Control-Allow-Headers": "Content-Type"
}
}
);
console.error('getRewards error:', error);
return errorResponse('Failed to process request');
}
};
};
43 changes: 43 additions & 0 deletions functions/api/getSettings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type { Env } from '../types/env';
import { jsonResponse, errorResponse, optionsResponse } from '../services/vmClient';

const VM_URL = 'https://vmprev.adaseal.eu';
const CACHE_KEY = '__internal:settings_cache';
const CACHE_TTL = 3600;

export const onRequestGet: PagesFunction<Env> = async (context) => {
const { env } = context;

if (!env.VITE_VM_API_KEY || env.VITE_VM_API_KEY.trim() === '') {
return errorResponse('Server configuration error', 500);
}

try {
const cached = await env.VM_WEB_PROFILES.get(CACHE_KEY, { type: 'json' });
if (cached !== null) {
return jsonResponse(cached);
}

const url = `${VM_URL}/api.php?action=get_settings`;
const response = await fetch(url, {
headers: { 'X-API-Token': env.VITE_VM_API_KEY },
});

if (!response.ok) {
return errorResponse('Upstream service error', 502);
}

const settings = await response.json();

await env.VM_WEB_PROFILES.put(CACHE_KEY, JSON.stringify(settings), {
expirationTtl: CACHE_TTL,
});

return jsonResponse(settings);
} catch (error) {
console.error('getSettings error:', error);
return errorResponse('Failed to fetch settings');
}
};

export const onRequestOptions: PagesFunction = async () => optionsResponse();
Loading