-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathclaim.ts
More file actions
144 lines (134 loc) · 5.63 KB
/
Copy pathclaim.ts
File metadata and controls
144 lines (134 loc) · 5.63 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
/**
* Claim distributions — the off-chain half of PerkosClaimVault (pull model).
*
* Each distribution rolls up every wallet's CUMULATIVE owed amounts and builds a
* Merkle tree whose root the platform posts on-chain. Participants then PULL
* from their dashboard: the vault releases the delta vs. what they've already
* claimed. The tree uses @openzeppelin/merkle-tree's StandardMerkleTree with
* leaf types ["address","uint256","uint256"] — byte-identical to the contract's
* leaf = keccak256(bytes.concat(keccak256(abi.encode(account, cumUsdc, cumReward))))
* so the off-chain proofs verify on-chain.
*
* Amounts are TOKEN BASE UNITS (USDC 6-dec, $PERKOS 18-dec):
* - cumUsdc = the wallet's cumulative provider earnings (agent_accounts.total_earned).
* - cumReward = the wallet's cumulative $PERKOS reward (token_rewards, filled by
* the buyback — 0 until that's wired).
*/
import { StandardMerkleTree } from "@openzeppelin/merkle-tree";
import { parseUnits } from "viem";
import type { Client } from "pg";
export type ClaimEntry = { wallet: string; cumUsdc: bigint; cumReward: bigint };
const LEAF_TYPES = ["address", "uint256", "uint256"];
/** Decimal USDC (e.g. 0.075) → base units (75000 at 6 decimals). */
export function usdcBaseUnits(amount: number): bigint {
const dec = Number(process.env.KNOWLEDGE_USDC_DECIMALS || 6);
if (!(amount > 0)) return 0n;
return parseUnits(amount.toFixed(dec), dec);
}
export function buildTree(entries: ClaimEntry[]): StandardMerkleTree<string[]> {
const values = entries.map((e) => [e.wallet, e.cumUsdc.toString(), e.cumReward.toString()]);
return StandardMerkleTree.of(values, LEAF_TYPES);
}
export type BuiltDistribution = {
root: string;
dump: ReturnType<StandardMerkleTree<string[]>["dump"]>;
totalUsdc: bigint;
totalReward: bigint;
entryCount: number;
};
/** Build a tree from entries with anything owed; null if nobody is owed. */
export function buildDistribution(entries: ClaimEntry[]): BuiltDistribution | null {
const usable = entries.filter((e) => e.cumUsdc > 0n || e.cumReward > 0n);
if (usable.length === 0) return null;
const tree = buildTree(usable);
return {
root: tree.root,
dump: tree.dump(),
totalUsdc: usable.reduce((a, e) => a + e.cumUsdc, 0n),
totalReward: usable.reduce((a, e) => a + e.cumReward, 0n),
entryCount: usable.length,
};
}
/**
* Per-wallet cumulative claimable USDC earnings ON A CHAIN. Provider earnings are
* segregated by the chain the consumer paid on (`knowledge_attributions.chain`),
* so each chain's distribution is independent — a provider claims their Base
* earnings on Base and their Celo earnings on Celo, never the same amount twice.
* cumReward is the wallet's cumulative $PERKOS drop on this chain (token_rewards,
* filled by the monthly usage-drop). A wallet may have USDC (earned), $PERKOS
* (drop), or both — a consumer who only spent has $PERKOS but no USDC — so we
* union both per-chain sources.
*/
export async function rollupEntries(client: Client, chain: string): Promise<ClaimEntry[]> {
const [usdc, reward] = await Promise.all([
client.query(
`SELECT lower(provider_wallet) w, coalesce(sum(amount),0)::float8 e
FROM knowledge_attributions
WHERE lower(chain) = lower($1) AND provider_wallet IS NOT NULL
GROUP BY 1 HAVING sum(amount) > 0`,
[chain],
),
client.query(
`SELECT lower(wallet) w, cumulative_perkos::text p
FROM token_rewards WHERE chain = $1 AND cumulative_perkos > 0`,
[chain],
),
]);
const map = new Map<string, { cumUsdc: bigint; cumReward: bigint }>();
for (const row of usdc.rows as { w: string; e: number }[]) {
map.set(row.w, { cumUsdc: usdcBaseUnits(row.e), cumReward: 0n });
}
for (const row of reward.rows as { w: string; p: string }[]) {
const cur = map.get(row.w) ?? { cumUsdc: 0n, cumReward: 0n };
// cumulative_perkos is stored in 18-dec base units; floor to integer.
cur.cumReward = BigInt(String(row.p).split(".")[0] || "0");
map.set(row.w, cur);
}
return [...map.entries()].map(([wallet, x]) => ({ wallet, cumUsdc: x.cumUsdc, cumReward: x.cumReward }));
}
export async function persistDistribution(
client: Client,
d: BuiltDistribution,
chain: string,
createdBy: string | null,
): Promise<number> {
const r = await client.query(
`INSERT INTO claim_distributions (chain, root, tree_dump, total_usdc, total_reward, entry_count, created_by)
VALUES ($1, $2, $3::jsonb, $4, $5, $6, $7) RETURNING id`,
[chain, d.root, JSON.stringify(d.dump), d.totalUsdc.toString(), d.totalReward.toString(), d.entryCount, createdBy],
);
return r.rows[0].id as number;
}
export type WalletClaim = {
wallet: string;
cumUsdc: string;
cumReward: string;
proof: string[];
root: string;
distributionId: number;
posted: boolean;
} | null;
/** A wallet's entry + Merkle proof from the latest distribution ON A CHAIN. */
export async function getWalletClaim(client: Client, wallet: string, chain: string): Promise<WalletClaim> {
const w = wallet.toLowerCase();
const r = await client.query(
`SELECT id, root, tree_dump, posted FROM claim_distributions WHERE lower(chain) = lower($1) ORDER BY created_at DESC LIMIT 1`,
[chain],
);
if (!r.rows[0]) return null;
const tree = StandardMerkleTree.load(r.rows[0].tree_dump);
for (const [i, v] of tree.entries()) {
if (String(v[0]).toLowerCase() === w) {
return {
wallet: w,
cumUsdc: String(v[1]),
cumReward: String(v[2]),
proof: tree.getProof(i),
root: r.rows[0].root,
distributionId: r.rows[0].id,
posted: Boolean(r.rows[0].posted),
};
}
}
return null;
}