Skip to content

Commit 6143884

Browse files
committed
Add Swarm Payroll for multi-agent card distribution
1 parent 5982b02 commit 6143884

File tree

3 files changed

+415
-0
lines changed

3 files changed

+415
-0
lines changed

packages/kamiyo-blindfold/src/index.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,23 @@ export type {
6060
GatedPaymentParams,
6161
GatedPaymentResult,
6262
} from './reputation-gate';
63+
64+
// Swarm Payroll
65+
export {
66+
SwarmPayroll,
67+
createEqualWeightSwarm,
68+
createPerformanceWeights,
69+
} from './swarm-payroll';
70+
71+
export type {
72+
SwarmPayrollConfig,
73+
} from './swarm-payroll';
74+
75+
export type {
76+
SwarmMember,
77+
SwarmConfig,
78+
SwarmDistribution,
79+
SwarmPayoutResult,
80+
BatchPaymentRequest,
81+
BatchPaymentResponse,
82+
} from './types';
Lines changed: 333 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,333 @@
1+
import {
2+
Connection,
3+
PublicKey,
4+
Transaction,
5+
SystemProgram,
6+
Keypair,
7+
sendAndConfirmTransaction,
8+
LAMPORTS_PER_SOL,
9+
} from '@solana/web3.js';
10+
import { BN } from '@coral-xyz/anchor';
11+
import { BlindfoldClient } from './client';
12+
import {
13+
SwarmConfig,
14+
SwarmMember,
15+
SwarmDistribution,
16+
SwarmPayoutResult,
17+
CardTier,
18+
CARD_TIERS,
19+
NATIVE_SOL_MINT,
20+
} from './types';
21+
22+
export interface SwarmPayrollConfig {
23+
connection: Connection;
24+
blindfoldBaseUrl?: string;
25+
}
26+
27+
// Manages swarm registration and payroll distribution
28+
export class SwarmPayroll {
29+
private connection: Connection;
30+
private blindfold: BlindfoldClient;
31+
private swarms: Map<string, SwarmConfig> = new Map();
32+
33+
constructor(config: SwarmPayrollConfig) {
34+
this.connection = config.connection;
35+
this.blindfold = new BlindfoldClient({ baseUrl: config.blindfoldBaseUrl });
36+
}
37+
38+
// Register a new swarm with members and weights
39+
registerSwarm(
40+
swarmId: string,
41+
name: string,
42+
members: Array<{ agentPk: PublicKey | string; email: string; weight: number; tier?: CardTier }>
43+
): SwarmConfig {
44+
const totalWeight = members.reduce((sum, m) => sum + m.weight, 0);
45+
if (Math.abs(totalWeight - 100) > 0.01) {
46+
throw new Error(`Weights must sum to 100, got ${totalWeight}`);
47+
}
48+
49+
const swarmMembers: SwarmMember[] = members.map((m) => ({
50+
agentPk: typeof m.agentPk === 'string' ? new PublicKey(m.agentPk) : m.agentPk,
51+
email: m.email,
52+
weight: m.weight,
53+
tier: m.tier,
54+
}));
55+
56+
const config: SwarmConfig = {
57+
swarmId,
58+
name,
59+
members: swarmMembers,
60+
createdAt: Date.now(),
61+
updatedAt: Date.now(),
62+
};
63+
64+
this.swarms.set(swarmId, config);
65+
return config;
66+
}
67+
68+
// Get swarm config
69+
getSwarm(swarmId: string): SwarmConfig | undefined {
70+
return this.swarms.get(swarmId);
71+
}
72+
73+
// Update member weights
74+
updateWeights(
75+
swarmId: string,
76+
newWeights: Array<{ agentPk: string; weight: number }>
77+
): SwarmConfig {
78+
const swarm = this.swarms.get(swarmId);
79+
if (!swarm) {
80+
throw new Error(`Swarm ${swarmId} not found`);
81+
}
82+
83+
const totalWeight = newWeights.reduce((sum, w) => sum + w.weight, 0);
84+
if (Math.abs(totalWeight - 100) > 0.01) {
85+
throw new Error(`Weights must sum to 100, got ${totalWeight}`);
86+
}
87+
88+
for (const update of newWeights) {
89+
const member = swarm.members.find((m) => m.agentPk.toBase58() === update.agentPk);
90+
if (member) {
91+
member.weight = update.weight;
92+
}
93+
}
94+
95+
swarm.updatedAt = Date.now();
96+
return swarm;
97+
}
98+
99+
// Calculate distribution for a given amount
100+
calculateDistribution(swarmId: string, totalAmount: bigint): SwarmDistribution[] {
101+
const swarm = this.swarms.get(swarmId);
102+
if (!swarm) {
103+
throw new Error(`Swarm ${swarmId} not found`);
104+
}
105+
106+
return swarm.members.map((member) => ({
107+
member,
108+
amount: (totalAmount * BigInt(Math.floor(member.weight * 100))) / 10000n,
109+
percentage: member.weight,
110+
}));
111+
}
112+
113+
// Distribute funds to all swarm members' Blindfold cards
114+
async distribute(
115+
swarmId: string,
116+
totalAmount: BN,
117+
payer: Keypair,
118+
tokenMint: PublicKey = NATIVE_SOL_MINT
119+
): Promise<SwarmPayoutResult> {
120+
const swarm = this.swarms.get(swarmId);
121+
if (!swarm) {
122+
throw new Error(`Swarm ${swarmId} not found`);
123+
}
124+
125+
const distributions = this.calculateDistribution(swarmId, BigInt(totalAmount.toString()));
126+
const results: SwarmPayoutResult['distributions'] = [];
127+
128+
// Process each member's payment
129+
for (const dist of distributions) {
130+
if (dist.amount === 0n) continue;
131+
132+
const amountSol = Number(dist.amount) / LAMPORTS_PER_SOL;
133+
const tier = dist.member.tier || this.getTierForAmount(amountSol);
134+
135+
// Create payment on Blindfold
136+
const payment = await this.blindfold.createPayment({
137+
amount: amountSol,
138+
currency: 'SOL',
139+
recipientEmail: dist.member.email,
140+
useZkProof: true,
141+
agentPk: dist.member.agentPk.toBase58(),
142+
requestedTier: tier,
143+
});
144+
145+
// Create holding wallet
146+
const holding = await this.blindfold.createHoldingWallet(
147+
payment.paymentId,
148+
dist.amount.toString(),
149+
tokenMint.toBase58()
150+
);
151+
152+
// Transfer to holding wallet
153+
const holdingWalletPk = new PublicKey(holding.holdingWalletAddress);
154+
const transferSig = await this.transferSOL(
155+
payer,
156+
holdingWalletPk,
157+
new BN(dist.amount.toString())
158+
);
159+
160+
results.push({
161+
agentPk: dist.member.agentPk.toBase58(),
162+
email: dist.member.email,
163+
amount: dist.amount,
164+
paymentId: payment.paymentId,
165+
holdingWallet: holding.holdingWalletAddress,
166+
transferSignature: transferSig,
167+
tier,
168+
});
169+
}
170+
171+
return {
172+
swarmId,
173+
totalAmount: BigInt(totalAmount.toString()),
174+
distributions: results,
175+
timestamp: Date.now(),
176+
};
177+
}
178+
179+
// Batch distribute - when Blindfold supports batch endpoint
180+
async distributeBatch(
181+
swarmId: string,
182+
totalAmount: BN,
183+
payer: Keypair,
184+
_tokenMint: PublicKey = NATIVE_SOL_MINT
185+
): Promise<SwarmPayoutResult> {
186+
const swarm = this.swarms.get(swarmId);
187+
if (!swarm) {
188+
throw new Error(`Swarm ${swarmId} not found`);
189+
}
190+
191+
const distributions = this.calculateDistribution(swarmId, BigInt(totalAmount.toString()));
192+
193+
// Prepare batch request (for when Blindfold implements batch endpoint)
194+
const _batchPayments = distributions
195+
.filter((d) => d.amount > 0n)
196+
.map((dist) => ({
197+
amount: Number(dist.amount) / LAMPORTS_PER_SOL,
198+
currency: 'SOL' as const,
199+
recipientEmail: dist.member.email,
200+
agentPk: dist.member.agentPk.toBase58(),
201+
requestedTier: dist.member.tier || this.getTierForAmount(Number(dist.amount) / LAMPORTS_PER_SOL),
202+
}));
203+
204+
// For now, fall back to sequential distribution
205+
// TODO: Replace with batch API call when available
206+
// const batchResponse = await this.blindfold.createBatchPayment({ payments: batchPayments, swarmId });
207+
return this.distribute(swarmId, totalAmount, payer);
208+
}
209+
210+
// Get appropriate tier based on amount
211+
private getTierForAmount(amountUsd: number): CardTier {
212+
for (let i = CARD_TIERS.length - 1; i >= 0; i--) {
213+
if (amountUsd <= CARD_TIERS[i].limit) {
214+
return CARD_TIERS[i].tier;
215+
}
216+
}
217+
return 'elite';
218+
}
219+
220+
// Transfer SOL to holding wallet
221+
private async transferSOL(
222+
payer: Keypair,
223+
destination: PublicKey,
224+
amount: BN
225+
): Promise<string> {
226+
const tx = new Transaction().add(
227+
SystemProgram.transfer({
228+
fromPubkey: payer.publicKey,
229+
toPubkey: destination,
230+
lamports: amount.toNumber(),
231+
})
232+
);
233+
return sendAndConfirmTransaction(this.connection, tx, [payer]);
234+
}
235+
236+
// Add a member to existing swarm
237+
addMember(
238+
swarmId: string,
239+
member: { agentPk: PublicKey | string; email: string; weight: number; tier?: CardTier }
240+
): SwarmConfig {
241+
const swarm = this.swarms.get(swarmId);
242+
if (!swarm) {
243+
throw new Error(`Swarm ${swarmId} not found`);
244+
}
245+
246+
swarm.members.push({
247+
agentPk: typeof member.agentPk === 'string' ? new PublicKey(member.agentPk) : member.agentPk,
248+
email: member.email,
249+
weight: member.weight,
250+
tier: member.tier,
251+
});
252+
253+
swarm.updatedAt = Date.now();
254+
return swarm;
255+
}
256+
257+
// Remove a member from swarm
258+
removeMember(swarmId: string, agentPk: string): SwarmConfig {
259+
const swarm = this.swarms.get(swarmId);
260+
if (!swarm) {
261+
throw new Error(`Swarm ${swarmId} not found`);
262+
}
263+
264+
swarm.members = swarm.members.filter((m) => m.agentPk.toBase58() !== agentPk);
265+
swarm.updatedAt = Date.now();
266+
return swarm;
267+
}
268+
269+
// Validate swarm weights sum to 100
270+
validateWeights(swarmId: string): boolean {
271+
const swarm = this.swarms.get(swarmId);
272+
if (!swarm) return false;
273+
274+
const total = swarm.members.reduce((sum, m) => sum + m.weight, 0);
275+
return Math.abs(total - 100) < 0.01;
276+
}
277+
278+
// Export swarm config for persistence
279+
exportSwarm(swarmId: string): string {
280+
const swarm = this.swarms.get(swarmId);
281+
if (!swarm) {
282+
throw new Error(`Swarm ${swarmId} not found`);
283+
}
284+
285+
return JSON.stringify({
286+
...swarm,
287+
members: swarm.members.map((m) => ({
288+
...m,
289+
agentPk: m.agentPk.toBase58(),
290+
})),
291+
});
292+
}
293+
294+
// Import swarm config
295+
importSwarm(configJson: string): SwarmConfig {
296+
const parsed = JSON.parse(configJson);
297+
const config: SwarmConfig = {
298+
...parsed,
299+
members: parsed.members.map((m: { agentPk: string; email: string; weight: number; tier?: CardTier }) => ({
300+
...m,
301+
agentPk: new PublicKey(m.agentPk),
302+
})),
303+
};
304+
305+
this.swarms.set(config.swarmId, config);
306+
return config;
307+
}
308+
}
309+
310+
// Helper to create equal-weight swarm
311+
export function createEqualWeightSwarm(
312+
swarmId: string,
313+
name: string,
314+
members: Array<{ agentPk: PublicKey | string; email: string; tier?: CardTier }>
315+
): Array<{ agentPk: PublicKey | string; email: string; weight: number; tier?: CardTier }> {
316+
const weight = 100 / members.length;
317+
return members.map((m) => ({ ...m, weight }));
318+
}
319+
320+
// Helper to create performance-weighted distribution
321+
export function createPerformanceWeights(
322+
contributions: Array<{ agentPk: string; score: number }>
323+
): Array<{ agentPk: string; weight: number }> {
324+
const totalScore = contributions.reduce((sum, c) => sum + c.score, 0);
325+
if (totalScore === 0) {
326+
return contributions.map((c) => ({ agentPk: c.agentPk, weight: 100 / contributions.length }));
327+
}
328+
329+
return contributions.map((c) => ({
330+
agentPk: c.agentPk,
331+
weight: (c.score / totalScore) * 100,
332+
}));
333+
}

0 commit comments

Comments
 (0)