Skip to content

Commit f09ebc5

Browse files
committed
updating flow
1 parent 43b8e21 commit f09ebc5

8 files changed

Lines changed: 672 additions & 38 deletions

File tree

documentation/vision-delivery-blueprint.md

Lines changed: 491 additions & 0 deletions
Large diffs are not rendered by default.

src/lib/api/client.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,12 +63,14 @@ function mockScore(address: string): ScoreResponse {
6363
}
6464

6565
function mockClaimArtifact(address: string, campaign: string): ClaimArtifact {
66+
// Ensure artifact score matches the mock score used by getScore(address)
67+
const { score1e6 } = mockScore(address);
6668
return {
6769
circuitId: "mock-circuit",
6870
modelDigest: "0x" + "a".repeat(64),
6971
inputDigest: "0x" + "b".repeat(64),
7072
addr: address,
71-
score: 750000,
73+
score: score1e6,
7274
deadline: Math.floor(Date.now() / 1000) + 3600, // 1 hour from now
7375
campaign: campaign,
7476
sig: {
@@ -79,9 +81,11 @@ function mockClaimArtifact(address: string, campaign: string): ClaimArtifact {
7981
};
8082
}
8183

82-
function mockProofMeta(_address: string): ProofMeta {
84+
function mockProofMeta(address: string): ProofMeta {
85+
// Ensure proof meta score matches the mock score used by getScore(address)
86+
const { score1e6 } = mockScore(address);
8387
return {
84-
score1e6: 750000,
88+
score1e6,
8589
calldata: "0x" + "f".repeat(128),
8690
};
8791
}

src/lib/chain/client.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,10 @@ export function getPublicClient(chainOverride?: number) {
4545
const chainId = chainOverride ?? getCurrentChainId();
4646
const chain = getViemChain(chainId);
4747
const info = getChainInfo(chainId);
48-
const transports = info.rpcUrls.map((url) => http(url, { timeout: 30000 }));
48+
// Add lightweight retries and a reasonable timeout per RPC to mitigate transient provider slowness.
49+
const transports = info.rpcUrls.map((url) =>
50+
http(url, { timeout: 20000, retryCount: 2, retryDelay: 500 })
51+
);
4952
const transport =
5053
transports.length === 1
5154
? transports[0]

src/lib/components/ClaimCard.svelte

Lines changed: 46 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<script lang="ts">
22
import { page } from "$app/stores";
33
import { wallet } from "$lib/stores/wallet";
4-
import { score } from "$lib/stores/score";
4+
import { score, scoreActions } from "$lib/stores/score";
55
import { airdrop } from "$lib/stores/airdrop";
66
import { toasts } from "$lib/stores/ui";
77
import { getClaimArtifact, getProofMeta } from "$lib/api/client";
@@ -21,6 +21,8 @@
2121
let errorMessage = "";
2222
let txHash: string | null = null;
2323
let quote: PayoutQuote | null = null;
24+
let lastQuoteKey: string | null = null;
25+
let pendingQuoteKey: string | null = null;
2426
2527
const config = $page.data.config;
2628
// Prefer ZK path if available
@@ -29,22 +31,45 @@
2931
useZkPath ? config.AIRDROP_ZK_ADDR : config.AIRDROP_ECDSA_ADDR
3032
) as Hex;
3133
const claimAbi = useZkPath ? reputationAirdropZKScaled : reputationAirdropScaled;
34+
const SCORE_TOLERANCE = 5; // equals 0.000005 when scaled by 1e6
3235
33-
async function fetchQuote() {
34-
if (typeof $score.value !== "number" || !$airdrop.decimals) return;
36+
async function fetchQuote(scoreValue: number, decimals: number) {
37+
if (!Number.isFinite(scoreValue) || scoreValue <= 0) return;
38+
const minPayout = $airdrop.minPayout;
39+
const maxPayout = $airdrop.maxPayout;
40+
if (minPayout === undefined || maxPayout === undefined) return;
41+
42+
const key = `${scoreValue}:${decimals}:${claimContractAddress}`;
43+
if (key === lastQuoteKey || key === pendingQuoteKey) return;
44+
45+
pendingQuoteKey = key;
3546
try {
47+
quote = null;
3648
const payout = await readContract<bigint>(claimContractAddress, claimAbi, "quotePayout", [
37-
BigInt($score.value),
49+
BigInt(scoreValue),
3850
]);
3951
quote = {
4052
payout,
41-
min: $airdrop.minPayout!,
42-
max: $airdrop.maxPayout!,
43-
decimals: $airdrop.decimals!,
53+
min: minPayout,
54+
max: maxPayout,
55+
decimals,
4456
};
57+
lastQuoteKey = key;
4558
} catch (e) {
4659
console.error("Failed to fetch quote", e);
4760
toasts.error("Could not preview payout.");
61+
} finally {
62+
if (pendingQuoteKey === key) {
63+
pendingQuoteKey = null;
64+
}
65+
}
66+
}
67+
68+
$: {
69+
const scoreValue = $score.value;
70+
const decimals = $airdrop.decimals;
71+
if (typeof scoreValue === "number" && decimals !== undefined) {
72+
void fetchQuote(scoreValue, decimals);
4873
}
4974
}
5075
@@ -57,8 +82,20 @@
5782
let hash: Hex;
5883
if (useZkPath) {
5984
const proofMeta = await getProofMeta($wallet.address);
60-
if (proofMeta.score1e6 !== $score.value) {
61-
throw new Error("Score mismatch between frontend and backend proof generation.");
85+
const currentScore = $score.value;
86+
if (typeof currentScore === "number") {
87+
const delta = Math.abs(proofMeta.score1e6 - currentScore);
88+
if (delta > SCORE_TOLERANCE) {
89+
console.warn("Score mismatch detected; aligning with backend value", {
90+
frontend: currentScore,
91+
backend: proofMeta.score1e6,
92+
delta,
93+
});
94+
scoreActions.setValue(proofMeta.score1e6);
95+
toasts.warning("Score updated from backend proof. Refreshing payout…");
96+
}
97+
} else {
98+
scoreActions.setValue(proofMeta.score1e6);
6299
}
63100
state = "awaiting_wallet";
64101
hash = await writeContract(
@@ -116,8 +153,6 @@
116153
state = "error";
117154
}
118155
}
119-
120-
fetchQuote();
121156
</script>
122157

123158
<div class="bg-white p-8 rounded-xl shadow-lg border border-gray-200">

src/lib/stores/score.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { writable, derived, get } from "svelte/store";
22
import { walletMock } from "./walletMock";
33
import { wallet } from "./wallet";
4+
import { parseConfig } from "$lib/config";
45

56
export type ScoreState = {
67
loading: boolean;
@@ -17,6 +18,12 @@ export const score = writable<ScoreState>({
1718
const CACHE_KEY = "reputation_score_cache";
1819
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
1920

21+
const parsedConfig = parseConfig();
22+
const isMockMode = ((): boolean => {
23+
if ("error" in parsedConfig) return true;
24+
return !parsedConfig.API_BASE;
25+
})();
26+
2027
// Load from cache
2128
function loadFromCache(address: string): ScoreState | null {
2229
if (typeof window === "undefined") {
@@ -93,14 +100,26 @@ export const mockScore = derived([walletMock, wallet], ([$walletMock, $wallet])
93100
};
94101
if (address) saveToCache(address, state);
95102
return state;
96-
} else if (
103+
}
104+
105+
if (!isMockMode) {
106+
return {
107+
loading: Boolean(address),
108+
value: undefined,
109+
lastUpdated: undefined,
110+
error: undefined,
111+
cachedAt: undefined,
112+
};
113+
}
114+
115+
if (
97116
$wallet.connected &&
98117
!$walletMock.enabled &&
99118
address &&
100119
typeof address === "string" &&
101120
address.startsWith("0x")
102121
) {
103-
// For real wallet connections without mock, generate deterministic score
122+
// For real wallet connections in mock mode, generate deterministic score
104123
const state = {
105124
loading: false,
106125
value: generateDeterministicScore(address as `0x${string}`),

src/lib/web3/onboard.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ function syncWalletStore(ws: OnboardWalletState[] = []) {
1919
const chainId = chainIdHex ? parseInt(chainIdHex, 16) : undefined;
2020
const address = primary.accounts?.[0]?.address as `0x${string}` | undefined;
2121

22+
// Guard against transient states where wallet emits without address yet; avoid flicker.
23+
if (!address) {
24+
return; // wait for a stable state with an address before marking connected
25+
}
26+
2227
wallet.update((current) => ({
2328
...current,
2429
connected: true,
@@ -49,13 +54,8 @@ export async function initOnboard() {
4954
const existing = get(onboard);
5055
if (existing) return existing;
5156

57+
// Allow all injected wallets; still promote popular ones in UI via displayUnavailable.
5258
const injected = injectedModule({
53-
filter: {
54-
// Include popular wallets
55-
[Symbol.for("web3-onboard-injected-metamask")]: true,
56-
[Symbol.for("web3-onboard-injected-trust")]: true,
57-
[Symbol.for("web3-onboard-injected-coinbase")]: true,
58-
},
5959
displayUnavailable: ["MetaMask", "Trust Wallet", "Coinbase Wallet"],
6060
});
6161

@@ -151,8 +151,9 @@ export async function initOnboard() {
151151
],
152152
},
153153
connect: {
154-
autoConnectLastWallet: true,
155-
autoConnectAllPreviousWallet: true,
154+
// Disable autoconnect to prevent modal flicker/auto-dismiss on init.
155+
autoConnectLastWallet: false,
156+
autoConnectAllPreviousWallet: false,
156157
},
157158
accountCenter: {
158159
desktop: {

src/routes/claim/+page.svelte

Lines changed: 76 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,55 @@
11
<script lang="ts">
22
import { wallet } from "$lib/stores/wallet";
33
import { walletMock } from "$lib/stores/walletMock";
4-
import { score } from "$lib/stores/score";
4+
import { score, scoreActions } from "$lib/stores/score";
55
import { airdrop } from "$lib/stores/airdrop";
66
import type { AirdropState } from "$lib/stores/airdrop";
77
import ClaimCard from "$lib/components/ClaimCard.svelte";
88
import ChecklistItem from "$lib/components/ChecklistItem.svelte";
9-
import { onMount } from "svelte";
9+
import { onDestroy, onMount } from "svelte";
10+
import { getScore } from "$lib/api/client";
1011
import type { PageData } from "./$types";
12+
import { browser } from "$app/environment";
13+
import { formatUnits } from "viem";
1114
1215
export let data: PageData;
1316
17+
let lastLoadedAddress: `0x${string}` | null = null;
18+
let currentLoadAddress: `0x${string}` | null = null;
19+
let unsubscribeWallet: () => void;
20+
21+
async function loadScore(address: `0x${string}`) {
22+
if (!browser) return;
23+
if (currentLoadAddress === address || lastLoadedAddress === address) return;
24+
25+
currentLoadAddress = address;
26+
scoreActions.setLoading(true);
27+
28+
try {
29+
const scoreData = await getScore(address);
30+
if (!scoreData) {
31+
throw new Error("No score data returned");
32+
}
33+
34+
scoreActions.setValue(scoreData.score1e6 ?? 0);
35+
score.update((s) => ({
36+
...s,
37+
loading: false,
38+
lastUpdated: scoreData.updatedAt,
39+
error: undefined,
40+
}));
41+
42+
lastLoadedAddress = address;
43+
} catch (error) {
44+
console.error("Failed to load score for claim page:", error);
45+
scoreActions.setError("Failed to load reputation score.");
46+
} finally {
47+
if (currentLoadAddress === address) {
48+
currentLoadAddress = null;
49+
}
50+
}
51+
}
52+
1453
onMount(() => {
1554
if (data.scoreData) {
1655
score.set({
@@ -21,6 +60,21 @@
2160
} else if (data.error) {
2261
score.set({ loading: false, error: data.error });
2362
}
63+
64+
unsubscribeWallet = wallet.subscribe(($wallet) => {
65+
if (!browser) return;
66+
67+
if (!$walletMock.enabled && $wallet.connected && $wallet.address) {
68+
void loadScore($wallet.address);
69+
} else if (!$walletMock.enabled && (!$wallet.connected || !$wallet.address)) {
70+
lastLoadedAddress = null;
71+
scoreActions.reset();
72+
}
73+
});
74+
});
75+
76+
onDestroy(() => {
77+
if (unsubscribeWallet) unsubscribeWallet();
2478
});
2579
2680
// Determine connection status considering both real and mock states
@@ -39,23 +93,32 @@
3993
$: expectedPayout = calculatePayout($score.value || 0, $airdrop);
4094
4195
function calculatePayout(scoreValue: number, airdropConfig: Partial<AirdropState>): number {
42-
if (!scoreValue || !airdropConfig.floor || !airdropConfig.cap) return 0;
96+
const { floor, cap, minPayout, maxPayout, decimals } = airdropConfig;
97+
if (
98+
!scoreValue ||
99+
floor === undefined ||
100+
cap === undefined ||
101+
minPayout === undefined ||
102+
maxPayout === undefined ||
103+
decimals === undefined
104+
) {
105+
return 0;
106+
}
43107
44-
if (scoreValue < airdropConfig.floor) return 0;
108+
if (scoreValue < floor) return 0;
45109
46-
const clampedScore = Math.min(scoreValue, airdropConfig.cap);
47-
const range = airdropConfig.cap - airdropConfig.floor;
110+
const clampedScore = Math.min(scoreValue, cap);
111+
const range = cap - floor;
48112
if (range <= 0) {
49-
return Number(airdropConfig.maxPayout ?? 0n);
113+
return Number(formatUnits(maxPayout, decimals));
50114
}
51115
52-
const normalizedScore = (clampedScore - airdropConfig.floor) / range;
53-
54-
const minPayout = Number(airdropConfig.minPayout ?? 0n);
55-
const maxPayout = Number(airdropConfig.maxPayout ?? minPayout);
56-
const payoutRange = Math.max(0, maxPayout - minPayout);
116+
const normalizedScore = (clampedScore - floor) / range;
117+
const minTokens = Number(formatUnits(minPayout, decimals));
118+
const maxTokens = Number(formatUnits(maxPayout, decimals));
119+
const payoutRange = Math.max(0, maxTokens - minTokens);
57120
58-
return minPayout + normalizedScore * payoutRange;
121+
return minTokens + normalizedScore * payoutRange;
59122
}
60123
</script>
61124

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { getScore, getClaimArtifact, getProofMeta } from '../../src/lib/api/client';
3+
4+
// In mock mode (no VITE_API_BASE), all three should return the same score for a given address
5+
6+
describe('mock api consistency', () => {
7+
const addr = '0x1234567890abcdef1234567890abcdef12345678';
8+
9+
it('getScore/getClaimArtifact/getProofMeta share the same score1e6', async () => {
10+
const score = await getScore(addr);
11+
const artifact = await getClaimArtifact(addr, '0x' + '1'.repeat(64));
12+
const proofMeta = await getProofMeta(addr);
13+
14+
expect(score.score1e6).toBeGreaterThan(0);
15+
expect(artifact.score).toEqual(score.score1e6);
16+
expect(proofMeta.score1e6).toEqual(score.score1e6);
17+
});
18+
});

0 commit comments

Comments
 (0)