@aboutcircles/sdk-invitations 0.1.10 → 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,5 @@
1
1
  import type { Address, TransactionRequest, CirclesConfig } from '@aboutcircles/sdk-types';
2
+ import type { ReferralPreviewList } from './types';
2
3
  export interface ProxyInviter {
3
4
  address: Address;
4
5
  possibleInvites: number;
@@ -24,7 +25,16 @@ export declare class Invitations {
24
25
  * @description
25
26
  * Sends a POST request to the referrals service to store referral data.
26
27
  */
27
- private saveReferralData;
28
+ saveReferralData(inviter: Address, privateKey: `0x${string}`): Promise<void>;
29
+ /**
30
+ * List referrals for a given inviter with key previews
31
+ *
32
+ * @param inviter - Address of the inviter
33
+ * @param limit - Maximum number of referrals to return (default 10)
34
+ * @param offset - Number of referrals to skip for pagination (default 0)
35
+ * @returns Paginated list of referral previews with masked keys
36
+ */
37
+ listReferrals(inviter: Address, limit?: number, offset?: number): Promise<ReferralPreviewList>;
28
38
  /**
29
39
  * Order real inviters by preference (best to worst)
30
40
  *
@@ -1 +1 @@
1
- {"version":3,"file":"Invitations.d.ts","sourceRoot":"","sources":["../src/Invitations.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAmB1F,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,OAAO,CAAC;IACjB,eAAe,EAAE,MAAM,CAAC;CACzB;AAED;;;GAGG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,MAAM,CAAgB;IAC9B,OAAO,CAAC,SAAS,CAAY;IAC7B,OAAO,CAAC,UAAU,CAAoB;IACtC,OAAO,CAAC,KAAK,CAAe;IAC5B,OAAO,CAAC,KAAK,CAAuB;IACpC,OAAO,CAAC,eAAe,CAAiC;gBAE5C,MAAM,EAAE,aAAa;IAsBjC;;;;;;;;OAQG;YACW,gBAAgB;IAmC9B;;;;;;;;;;OAUG;IACH,OAAO,CAAC,iBAAiB;IAezB;;;;;;;;;;;;;;;;;OAiBG;IACG,cAAc,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,kBAAkB,EAAE,CAAC;IA+CvF;;;;;;;;;;;OAWG;IACG,cAAc,CAAC,OAAO,EAAE,OAAO,EAAE,mBAAmB,CAAC,EAAE,OAAO;IA+CpE;;;;;;;;;;;;;;;;;OAiBG;IACG,eAAe,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC;IAwFhE;;;;;;;;;;;;;;OAcG;IACG,gBAAgB,CACpB,OAAO,EAAE,OAAO,GACf,OAAO,CAAC;QAAE,YAAY,EAAE,kBAAkB,EAAE,CAAC;QAAC,UAAU,EAAE,KAAK,MAAM,EAAE,CAAA;KAAE,CAAC;IAiD7E;;;;;;;;;;;;;OAaG;IACG,kBAAkB,CAAC,SAAS,EAAE,OAAO,EAAE,EAAE,eAAe,GAAE,OAAc,GAAG,OAAO,CAAC,KAAK,MAAM,EAAE,CAAC;IA8CvG;;;;;;;;;;;;;;;;;;;;;OAqBG;IACH,cAAc,CAAC,MAAM,EAAE,OAAO,GAAG,OAAO;CA8BzC"}
1
+ {"version":3,"file":"Invitations.d.ts","sourceRoot":"","sources":["../src/Invitations.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAI1F,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAgBnD,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,OAAO,CAAC;IACjB,eAAe,EAAE,MAAM,CAAC;CACzB;AAED;;;GAGG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,MAAM,CAAgB;IAC9B,OAAO,CAAC,SAAS,CAAY;IAC7B,OAAO,CAAC,UAAU,CAAoB;IACtC,OAAO,CAAC,KAAK,CAAe;IAC5B,OAAO,CAAC,KAAK,CAAuB;IACpC,OAAO,CAAC,eAAe,CAAiC;gBAE5C,MAAM,EAAE,aAAa;IAsBjC;;;;;;;;OAQG;IACG,gBAAgB,CACpB,OAAO,EAAE,OAAO,EAChB,UAAU,EAAE,KAAK,MAAM,EAAE,GACxB,OAAO,CAAC,IAAI,CAAC;IAgChB;;;;;;;OAOG;IACG,aAAa,CACjB,OAAO,EAAE,OAAO,EAChB,KAAK,GAAE,MAAW,EAClB,MAAM,GAAE,MAAU,GACjB,OAAO,CAAC,mBAAmB,CAAC;IAiC/B;;;;;;;;;;OAUG;IACH,OAAO,CAAC,iBAAiB;IAezB;;;;;;;;;;;;;;;;;OAiBG;IACG,cAAc,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,kBAAkB,EAAE,CAAC;IA+CvF;;;;;;;;;;;OAWG;IACG,cAAc,CAAC,OAAO,EAAE,OAAO,EAAE,mBAAmB,CAAC,EAAE,OAAO;IA+CpE;;;;;;;;;;;;;;;;;OAiBG;IACG,eAAe,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC;IAwFhE;;;;;;;;;;;;;;OAcG;IACG,gBAAgB,CACpB,OAAO,EAAE,OAAO,GACf,OAAO,CAAC;QAAE,YAAY,EAAE,kBAAkB,EAAE,CAAC;QAAC,UAAU,EAAE,KAAK,MAAM,EAAE,CAAA;KAAE,CAAC;IAiD7E;;;;;;;;;;;;;OAaG;IACG,kBAAkB,CAAC,SAAS,EAAE,OAAO,EAAE,EAAE,eAAe,GAAE,OAAc,GAAG,OAAO,CAAC,KAAK,MAAM,EAAE,CAAC;IA8CvG;;;;;;;;;;;;;;;;;;;;;OAqBG;IACH,cAAc,CAAC,MAAM,EAAE,OAAO,GAAG,OAAO;CA8BzC"}
@@ -0,0 +1,455 @@
1
+ import { RpcClient, PathfinderMethods, TrustMethods } from '@aboutcircles/sdk-rpc';
2
+ import { HubV2ContractMinimal, ReferralsModuleContractMinimal } from '@aboutcircles/sdk-core/minimal';
3
+ import { InvitationError } from './errors';
4
+ import { TransferBuilder } from '@aboutcircles/sdk-transfers';
5
+ import { hexToBytes, INVITATION_FEE, MAX_FLOW, generatePrivateKey, privateKeyToAddress, encodeAbiParameters, keccak256, SAFE_PROXY_FACTORY, ACCOUNT_INITIALIZER_HASH, ACCOUNT_CREATION_CODE_HASH, checksumAddress } from '@aboutcircles/sdk-utils';
6
+ /**
7
+ * Invitations handles invitation operations for Circles
8
+ * Supports both referral invitations (new users) and direct invitations (existing Safe wallets)
9
+ */
10
+ export class Invitations {
11
+ config;
12
+ rpcClient;
13
+ pathfinder;
14
+ trust;
15
+ hubV2;
16
+ referralsModule;
17
+ constructor(config) {
18
+ if (!config.referralsServiceUrl) {
19
+ throw new InvitationError('referralsServiceUrl is required in config', {
20
+ code: 'INVITATION_MISSING_CONFIG',
21
+ source: 'INVITATIONS',
22
+ context: { missingField: 'referralsServiceUrl' }
23
+ });
24
+ }
25
+ this.config = config;
26
+ this.rpcClient = new RpcClient(config.circlesRpcUrl);
27
+ this.pathfinder = new PathfinderMethods(this.rpcClient);
28
+ this.trust = new TrustMethods(this.rpcClient);
29
+ this.hubV2 = new HubV2ContractMinimal({
30
+ address: config.v2HubAddress,
31
+ rpcUrl: config.circlesRpcUrl,
32
+ });
33
+ this.referralsModule = new ReferralsModuleContractMinimal({
34
+ address: config.referralsModuleAddress,
35
+ rpcUrl: config.circlesRpcUrl,
36
+ });
37
+ }
38
+ /**
39
+ * Save referral data to the referrals service
40
+ *
41
+ * @param inviter - Address of the inviter
42
+ * @param privateKey - Private key generated for the new user
43
+ *
44
+ * @description
45
+ * Sends a POST request to the referrals service to store referral data.
46
+ */
47
+ async saveReferralData(inviter, privateKey) {
48
+ try {
49
+ const response = await fetch(`${this.config.referralsServiceUrl}/store`, {
50
+ method: 'POST',
51
+ headers: {
52
+ 'accept': 'application/json',
53
+ 'Content-Type': 'application/json'
54
+ },
55
+ body: JSON.stringify({
56
+ privateKey,
57
+ inviter
58
+ })
59
+ });
60
+ if (!response.ok) {
61
+ throw new InvitationError(`HTTP error! status: ${response.status}`, {
62
+ code: 'INVITATION_HTTP_ERROR',
63
+ source: 'INVITATIONS',
64
+ context: { status: response.status, url: `${this.config.referralsServiceUrl}/store` }
65
+ });
66
+ }
67
+ }
68
+ catch (error) {
69
+ console.error('Failed to save referral data:', error);
70
+ throw new InvitationError(`Failed to save referral data: ${error instanceof Error ? error.message : 'Unknown error'}`, {
71
+ code: 'INVITATION_SAVE_REFERRAL_FAILED',
72
+ source: 'INVITATIONS',
73
+ cause: error
74
+ });
75
+ }
76
+ }
77
+ /**
78
+ * List referrals for a given inviter with key previews
79
+ *
80
+ * @param inviter - Address of the inviter
81
+ * @param limit - Maximum number of referrals to return (default 10)
82
+ * @param offset - Number of referrals to skip for pagination (default 0)
83
+ * @returns Paginated list of referral previews with masked keys
84
+ */
85
+ async listReferrals(inviter, limit = 10, offset = 0) {
86
+ try {
87
+ const url = new URL(`${this.config.referralsServiceUrl}/referrals/list/${inviter}`);
88
+ url.searchParams.set('limit', String(limit));
89
+ url.searchParams.set('offset', String(offset));
90
+ const response = await fetch(url.toString(), {
91
+ method: 'GET',
92
+ headers: { 'accept': 'application/json' },
93
+ });
94
+ if (!response.ok) {
95
+ throw new InvitationError(`HTTP error! status: ${response.status}`, {
96
+ code: 'INVITATION_HTTP_ERROR',
97
+ source: 'INVITATIONS',
98
+ context: { status: response.status, url: url.toString() },
99
+ });
100
+ }
101
+ return await response.json();
102
+ }
103
+ catch (error) {
104
+ if (error instanceof InvitationError)
105
+ throw error;
106
+ throw new InvitationError(`Failed to list referrals: ${error instanceof Error ? error.message : 'Unknown error'}`, {
107
+ code: 'INVITATION_LIST_REFERRALS_FAILED',
108
+ source: 'INVITATIONS',
109
+ cause: error,
110
+ });
111
+ }
112
+ }
113
+ /**
114
+ * Order real inviters by preference (best to worst)
115
+ *
116
+ * @param realInviters - Array of valid real inviters with their addresses and possible invites
117
+ * @param inviter - Address of the current inviter (prioritized first)
118
+ * @returns Ordered array of real inviters (best candidates first)
119
+ *
120
+ * @description
121
+ * This function determines the optimal order for selecting real inviters.
122
+ * Prioritizes the inviter's own tokens first, then others.
123
+ */
124
+ orderRealInviters(realInviters, inviter) {
125
+ const inviterLower = inviter.toLowerCase();
126
+ return realInviters.sort((a, b) => {
127
+ const aIsInviter = a.address.toLowerCase() === inviterLower;
128
+ const bIsInviter = b.address.toLowerCase() === inviterLower;
129
+ // Prioritize the inviter's own tokens first
130
+ if (aIsInviter && !bIsInviter)
131
+ return -1;
132
+ if (!aIsInviter && bIsInviter)
133
+ return 1;
134
+ return 0;
135
+ });
136
+ }
137
+ /**
138
+ * Generate invitation transaction for a user who already has a Safe wallet but is not yet registered in Circles Hub
139
+ *
140
+ * @param inviter - Address of the inviter
141
+ * @param invitee - Address of the invitee (must have an existing Safe wallet but NOT be registered in Circles Hub)
142
+ * @returns Array of transactions to execute in order
143
+ *
144
+ * @description
145
+ * This function:
146
+ * 1. Verifies the invitee is NOT already registered as a human in Circles Hub
147
+ * 2. Finds a path from inviter to invitation module using available proxy inviters
148
+ * 3. Generates invitation data for the existing Safe wallet address
149
+ * 4. Builds transaction batch with proper wrapped token handling
150
+ * 5. Returns transactions ready to execute
151
+ *
152
+ * Note: The invitee MUST have a Safe wallet but MUST NOT be registered in Circles Hub yet.
153
+ * If they are already registered, an error will be thrown.
154
+ */
155
+ async generateInvite(inviter, invitee) {
156
+ const inviterLower = inviter.toLowerCase();
157
+ const inviteeLower = invitee.toLowerCase();
158
+ // Step 1: Verify invitee is NOT already registered as a human in Circles Hub
159
+ const isHuman = await this.hubV2.isHuman(inviteeLower);
160
+ if (isHuman) {
161
+ throw InvitationError.inviteeAlreadyRegistered(inviterLower, inviteeLower);
162
+ }
163
+ // Step 2: Find path to invitation module using proxy inviters
164
+ const path = await this.findInvitePath(inviterLower);
165
+ // Step 3: Generate invitation data for existing Safe wallet
166
+ // For non-registered addresses (existing Safe wallets), we pass their address directly
167
+ // useSafeCreation = false because the invitee already has a Safe wallet
168
+ const transferData = await this.generateInviteData([inviteeLower], false);
169
+ // Step 4: Build transactions using TransferBuilder to properly handle wrapped tokens
170
+ const transferBuilder = new TransferBuilder(this.config);
171
+ // Get the real inviter address from the path
172
+ const realInviters = await this.getRealInviters(inviterLower);
173
+ if (realInviters.length === 0) {
174
+ throw InvitationError.noPathFound(inviterLower, this.config.invitationModuleAddress);
175
+ }
176
+ const realInviterAddress = realInviters[0].address;
177
+ // Use the buildFlowMatrixTx method to construct transactions from the path
178
+ const transferTransactions = await transferBuilder.buildFlowMatrixTx(inviterLower, this.config.invitationModuleAddress, path, {
179
+ toTokens: [realInviterAddress],
180
+ useWrappedBalances: true,
181
+ txData: hexToBytes(transferData)
182
+ }, true);
183
+ return transferTransactions;
184
+ }
185
+ /**
186
+ * Find a path from inviter to the invitation module for a specific proxy inviter
187
+ *
188
+ * @param inviter - Address of the inviter
189
+ * @param proxyInviterAddress - Optional specific proxy inviter address to use for the path
190
+ * @returns PathfindingResult containing the transfer path
191
+ *
192
+ * @description
193
+ * This function finds a path from the inviter to the invitation module.
194
+ * If proxyInviterAddress is provided, it will find a path using that specific token.
195
+ * Otherwise, it will use the first available proxy inviter.
196
+ */
197
+ async findInvitePath(inviter, proxyInviterAddress) {
198
+ const inviterLower = inviter.toLowerCase();
199
+ let tokenToUse;
200
+ if (proxyInviterAddress) {
201
+ tokenToUse = proxyInviterAddress.toLowerCase();
202
+ }
203
+ else {
204
+ // Get real inviters and use the first one
205
+ const realInviters = await this.getRealInviters(inviterLower);
206
+ if (realInviters.length === 0) {
207
+ throw InvitationError.noPathFound(inviterLower, this.config.invitationModuleAddress);
208
+ }
209
+ tokenToUse = realInviters[0].address;
210
+ }
211
+ // Find path using the selected token
212
+ const path = await this.pathfinder.findPath({
213
+ from: inviterLower,
214
+ to: this.config.invitationModuleAddress,
215
+ targetFlow: INVITATION_FEE,
216
+ toTokens: [tokenToUse],
217
+ useWrappedBalances: true
218
+ });
219
+ if (!path.transfers || path.transfers.length === 0) {
220
+ throw InvitationError.noPathFound(inviterLower, this.config.invitationModuleAddress);
221
+ }
222
+ if (path.maxFlow < INVITATION_FEE) {
223
+ const requestedInvites = 1;
224
+ const availableInvites = Number(path.maxFlow / INVITATION_FEE);
225
+ throw InvitationError.insufficientBalance(requestedInvites, availableInvites, INVITATION_FEE, path.maxFlow, inviterLower, this.config.invitationModuleAddress);
226
+ }
227
+ return path;
228
+ }
229
+ /**
230
+ * Get real inviters who have enough balance to cover invitation fees
231
+ *
232
+ * @param inviter - Address of the inviter
233
+ * @returns Array of real inviters with their addresses and possible number of invitations
234
+ *
235
+ * @description
236
+ * This function:
237
+ * 1. Gets all addresses that trust the inviter (set1) - includes both one-way trusts and mutual trusts
238
+ * 2. Gets all addresses trusted by the invitation module (set2) - includes both one-way trusts and mutual trusts
239
+ * 3. Finds the intersection of set1 and set2
240
+ * 4. Adds the inviter's own address to the list of possible tokens
241
+ * 5. Builds a path from inviter to invitation module using intersection addresses as toTokens
242
+ * 6. Sums up transferred token amounts by tokenOwner
243
+ * 7. Calculates possible invites (1 invite = 96 CRC)
244
+ * 8. Orders real inviters by preference (best candidates first)
245
+ * 9. Returns only those token owners whose total amounts exceed the invitation fee (96 CRC)
246
+ */
247
+ async getRealInviters(inviter) {
248
+ const inviterLower = inviter.toLowerCase();
249
+ // Step 1: Get addresses that trust the inviter (set1)
250
+ // This includes both one-way incoming trusts and mutual trusts
251
+ const trustedByRelations = await this.trust.getTrustedBy(inviterLower);
252
+ const mutualTrustRelations = await this.trust.getMutualTrusts(inviterLower);
253
+ // Extract the addresses of avatars who trust the inviter
254
+ // Combine both trustedBy (one-way) and mutualTrusts
255
+ const trustedByInviter = new Set([
256
+ ...trustedByRelations.map(relation => relation.objectAvatar.toLowerCase()),
257
+ ...mutualTrustRelations.map(relation => relation.objectAvatar.toLowerCase())
258
+ ]);
259
+ // Step 2: Get addresses trusted by the invitation module (set2)
260
+ // This includes both one-way outgoing trusts and mutual trusts
261
+ const trustsRelations = await this.trust.getTrusts(this.config.invitationModuleAddress);
262
+ const mutualTrustRelationsModule = await this.trust.getMutualTrusts(this.config.invitationModuleAddress);
263
+ const trustedByModule = new Set([
264
+ ...trustsRelations.map(relation => relation.objectAvatar.toLowerCase()),
265
+ ...mutualTrustRelationsModule.map(relation => relation.objectAvatar.toLowerCase())
266
+ ]);
267
+ // Step 3: Find intersection - addresses that trust inviter AND are trusted by invitation module
268
+ const intersection = [];
269
+ for (const address of trustedByInviter) {
270
+ if (trustedByModule.has(address)) {
271
+ intersection.push(address);
272
+ }
273
+ }
274
+ // Step 4: Add the inviter's own address to the list of possible tokens
275
+ // This allows the inviter to use their own personal tokens for invitations
276
+ const tokensToUse = [...intersection, inviterLower];
277
+ // If no tokens available at all, return empty
278
+ if (tokensToUse.length === 0) {
279
+ return [];
280
+ }
281
+ // Step 5: Build path from inviter to invitation module
282
+ const path = await this.pathfinder.findPath({
283
+ from: inviterLower,
284
+ to: this.config.invitationModuleAddress,
285
+ useWrappedBalances: true,
286
+ targetFlow: MAX_FLOW,
287
+ toTokens: tokensToUse,
288
+ });
289
+ if (!path.transfers || path.transfers.length === 0) {
290
+ return [];
291
+ }
292
+ // Step 6: Sum up transferred token amounts by tokenOwner (only terminal transfers to invitation module)
293
+ const tokenOwnerAmounts = new Map();
294
+ const invitationModuleLower = this.config.invitationModuleAddress.toLowerCase();
295
+ for (const transfer of path.transfers) {
296
+ // Only count transfers that go to the invitation module (terminal transfers)
297
+ if (transfer.to.toLowerCase() === invitationModuleLower) {
298
+ const tokenOwnerLower = transfer.tokenOwner.toLowerCase();
299
+ const currentAmount = tokenOwnerAmounts.get(tokenOwnerLower) || BigInt(0);
300
+ tokenOwnerAmounts.set(tokenOwnerLower, currentAmount + transfer.value);
301
+ }
302
+ }
303
+ // Step 7: Calculate possible invites and filter token owners
304
+ const realInviters = [];
305
+ for (const [tokenOwner, amount] of tokenOwnerAmounts.entries()) {
306
+ const possibleInvites = Number(amount / INVITATION_FEE);
307
+ if (possibleInvites >= 1) {
308
+ realInviters.push({
309
+ address: tokenOwner,
310
+ possibleInvites
311
+ });
312
+ }
313
+ }
314
+ // Step 8: Order real inviters by preference (best candidates first)
315
+ // Prioritizes the inviter's own tokens first
316
+ const orderedRealInviters = this.orderRealInviters(realInviters, inviterLower);
317
+ return orderedRealInviters;
318
+ }
319
+ /**
320
+ * Generate a referral for inviting a new user
321
+ *
322
+ * @param inviter - Address of the inviter
323
+ * @returns Object containing transactions and the generated private key
324
+ *
325
+ * @description
326
+ * This function:
327
+ * 1. Generates a new private key and signer address for the invitee
328
+ * 2. Finds a proxy inviter (someone who has balance and is trusted by both inviter and invitation module)
329
+ * 3. Builds transaction batch including transfers and invitation
330
+ * 4. Uses generateInviteData to properly encode the Safe account creation data
331
+ * 5. Saves the referral data (private key, signer, inviter) to database
332
+ * 6. Returns transactions and the generated private key
333
+ */
334
+ async generateReferral(inviter) {
335
+ const inviterLower = inviter.toLowerCase();
336
+ // Step 1: Generate private key and derive signer address
337
+ const privateKey = generatePrivateKey();
338
+ const signerAddress = privateKeyToAddress(privateKey);
339
+ // Step 2: Get real inviters
340
+ const realInviters = await this.getRealInviters(inviterLower);
341
+ if (realInviters.length === 0) {
342
+ throw InvitationError.noProxyInviters(inviterLower);
343
+ }
344
+ // Step 3: Pick the first real inviter
345
+ const firstRealInviter = realInviters[0];
346
+ const realInviterAddress = firstRealInviter.address;
347
+ // Step 4: Find path to invitation module
348
+ const path = await this.findInvitePath(inviterLower, realInviterAddress);
349
+ // Step 5: Build transactions using TransferBuilder to properly handle wrapped tokens
350
+ const transferBuilder = new TransferBuilder(this.config);
351
+ // useSafeCreation = true because we're creating a new Safe wallet via ReferralsModule
352
+ const transferData = await this.generateInviteData([signerAddress], true);
353
+ // Use the new buildFlowMatrixTx method to construct transactions from the path
354
+ const transferTransactions = await transferBuilder.buildFlowMatrixTx(inviterLower, this.config.invitationModuleAddress, path, {
355
+ toTokens: [realInviterAddress],
356
+ useWrappedBalances: true,
357
+ txData: hexToBytes(transferData)
358
+ }, true);
359
+ // Step 6: Save referral data to database
360
+ await this.saveReferralData(inviterLower, privateKey);
361
+ // Step 7: Build final transaction batch
362
+ const transactions = [];
363
+ transactions.push(...transferTransactions);
364
+ return { transactions, privateKey };
365
+ }
366
+ /**
367
+ * Generate invitation data based on whether addresses need Safe account creation or already have Safe wallets
368
+ *
369
+ * @param addresses - Array of addresses to check and encode
370
+ * @param useSafeCreation - If true, uses ReferralsModule to create Safe accounts (for new users without wallets)
371
+ * @returns Encoded data for the invitation transfer
372
+ *
373
+ * @description
374
+ * Two modes:
375
+ * 1. Direct invitation (useSafeCreation = false): Encodes addresses directly for existing Safe wallets
376
+ * 2. Safe creation (useSafeCreation = true): Uses ReferralsModule to create Safe accounts for new users
377
+ *
378
+ * Note: Addresses passed here should NEVER be registered humans in the hub (that's validated before calling this)
379
+ */
380
+ async generateInviteData(addresses, useSafeCreation = true) {
381
+ if (addresses.length === 0) {
382
+ throw InvitationError.noAddressesProvided();
383
+ }
384
+ // If NOT using Safe creation, encode addresses directly (for existing Safe wallets)
385
+ if (!useSafeCreation) {
386
+ if (addresses.length === 1) {
387
+ // Single address - encode as single address (not array)
388
+ return encodeAbiParameters(['address'], [addresses[0]]);
389
+ }
390
+ else {
391
+ // Multiple addresses - encode as address array
392
+ return encodeAbiParameters(['address[]'], [addresses]);
393
+ }
394
+ }
395
+ // Use ReferralsModule to create Safe accounts for new users (signers without Safe wallets)
396
+ if (addresses.length === 1) {
397
+ // Single address - use createAccount(address signer)
398
+ const createAccountTx = this.referralsModule.createAccount(addresses[0]);
399
+ const createAccountData = createAccountTx.data;
400
+ // Encode (address target, bytes callData) for the invitation module
401
+ return encodeAbiParameters(['address', 'bytes'], [this.config.referralsModuleAddress, createAccountData]);
402
+ }
403
+ else {
404
+ // Multiple addresses - use createAccounts(address[] signers)
405
+ const createAccountsTx = this.referralsModule.createAccounts(addresses);
406
+ const createAccountsData = createAccountsTx.data;
407
+ // Encode (address target, bytes callData) for the invitation module
408
+ return encodeAbiParameters(['address', 'bytes'], [this.config.referralsModuleAddress, createAccountsData]);
409
+ }
410
+ }
411
+ /**
412
+ * Predicts the pre-made Safe address for a given signer without deploying it
413
+ * Uses CREATE2 with ACCOUNT_INITIALIZER_HASH and ACCOUNT_CREATION_CODE_HASH via SAFE_PROXY_FACTORY
414
+ *
415
+ * @param signer - The offchain public address chosen by the origin inviter as the pre-deployment key
416
+ * @returns The deterministic Safe address that would be deployed for the signer
417
+ *
418
+ * @description
419
+ * This implements the same logic as the ReferralsModule.computeAddress() contract function:
420
+ * ```solidity
421
+ * bytes32 salt = keccak256(abi.encodePacked(ACCOUNT_INITIALIZER_HASH, uint256(uint160(signer))));
422
+ * predictedAddress = address(
423
+ * uint160(
424
+ * uint256(
425
+ * keccak256(
426
+ * abi.encodePacked(bytes1(0xff), address(SAFE_PROXY_FACTORY), salt, ACCOUNT_CREATION_CODE_HASH)
427
+ * )
428
+ * )
429
+ * )
430
+ * );
431
+ * ```
432
+ */
433
+ computeAddress(signer) {
434
+ // Step 1: Calculate salt = keccak256(abi.encodePacked(ACCOUNT_INITIALIZER_HASH, uint256(uint160(signer))))
435
+ // abi.encodePacked means concatenate without padding
436
+ // uint256(uint160(signer)) converts address to uint256 (32 bytes, left-padded with zeros)
437
+ const signerLower = signer.toLowerCase().replace('0x', '');
438
+ const signerUint256 = signerLower.padStart(64, '0'); // 32 bytes as hex string
439
+ // Concatenate: ACCOUNT_INITIALIZER_HASH (32 bytes) + signerUint256 (32 bytes)
440
+ const saltPreimage = ACCOUNT_INITIALIZER_HASH.replace('0x', '') + signerUint256;
441
+ const salt = keccak256(('0x' + saltPreimage));
442
+ // Step 2: Calculate CREATE2 address
443
+ // address = keccak256(0xff ++ factory ++ salt ++ initCodeHash)[12:]
444
+ const ff = 'ff';
445
+ const factory = SAFE_PROXY_FACTORY.toLowerCase().replace('0x', '');
446
+ const saltClean = salt.replace('0x', '');
447
+ const initCodeHash = ACCOUNT_CREATION_CODE_HASH.replace('0x', '');
448
+ // Concatenate all parts
449
+ const create2Preimage = ff + factory + saltClean + initCodeHash;
450
+ const hash = keccak256(('0x' + create2Preimage));
451
+ // Take last 20 bytes (40 hex chars) as the address
452
+ const addressHex = '0x' + hash.slice(-40);
453
+ return checksumAddress(addressHex);
454
+ }
455
+ }
@@ -0,0 +1,59 @@
1
+ import type { Address, CirclesConfig, TransactionRequest, Hex } from '@aboutcircles/sdk-types';
2
+ import type { ReferralPreviewList } from './types';
3
+ export interface GeneratedInvite {
4
+ secret: Hex;
5
+ signer: Address;
6
+ }
7
+ export interface GenerateInvitesResult {
8
+ invites: GeneratedInvite[];
9
+ transactions: TransactionRequest[];
10
+ }
11
+ /**
12
+ * InviteFarm handles batch invitation generation via the InvitationFarm contract
13
+ *
14
+ * This class provides methods to generate multiple invitations at once using
15
+ * the InvitationFarm contract, which manages a farm of InvitationBot instances.
16
+ */
17
+ export declare class InviteFarm {
18
+ private config;
19
+ private invitations;
20
+ private invitationFarm;
21
+ private referralsModule;
22
+ private hubV2;
23
+ constructor(config: CirclesConfig);
24
+ /**
25
+ * Get the remaining invite quota for a specific inviter
26
+ */
27
+ getQuota(inviter: Address): Promise<bigint>;
28
+ /**
29
+ * Get the invitation fee (96 CRC)
30
+ */
31
+ getInvitationFee(): Promise<bigint>;
32
+ /**
33
+ * Get the invitation module address from the farm
34
+ */
35
+ getInvitationModule(): Promise<Address>;
36
+ /**
37
+ * Generate batch invitations using the InvitationFarm
38
+ *
39
+ * This method:
40
+ * 1. Simulates claimInvites to get token IDs that will be claimed
41
+ * 2. Generates random secrets and derives signer addresses
42
+ * 3. Builds transaction batch: claimInvites + safeBatchTransferFrom
43
+ *
44
+ * @param inviter - Address of the inviter (must have quota)
45
+ * @param count - Number of invitations to generate
46
+ * @returns Generated invites with secrets/signers and transactions to execute
47
+ */
48
+ generateInvites(inviter: Address, count: number): Promise<GenerateInvitesResult>;
49
+ /**
50
+ * List referrals for a given inviter with key previews
51
+ *
52
+ * @param inviter - Address of the inviter
53
+ * @param limit - Maximum number of referrals to return (default 10)
54
+ * @param offset - Number of referrals to skip for pagination (default 0)
55
+ * @returns Paginated list of referral previews with masked keys
56
+ */
57
+ listReferrals(inviter: Address, limit?: number, offset?: number): Promise<ReferralPreviewList>;
58
+ }
59
+ //# sourceMappingURL=InviteFarm.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"InviteFarm.d.ts","sourceRoot":"","sources":["../src/InviteFarm.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,aAAa,EAAE,kBAAkB,EAAE,GAAG,EAAE,MAAM,yBAAyB,CAAC;AAO/F,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AASnD,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,GAAG,CAAC;IACZ,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,eAAe,EAAE,CAAC;IAC3B,YAAY,EAAE,kBAAkB,EAAE,CAAC;CACpC;AAED;;;;;GAKG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,MAAM,CAAgB;IAC9B,OAAO,CAAC,WAAW,CAAc;IACjC,OAAO,CAAC,cAAc,CAAgC;IACtD,OAAO,CAAC,eAAe,CAAiC;IACxD,OAAO,CAAC,KAAK,CAAuB;gBAExB,MAAM,EAAE,aAAa;IAiBjC;;OAEG;IACG,QAAQ,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC;IAIjD;;OAEG;IACG,gBAAgB,IAAI,OAAO,CAAC,MAAM,CAAC;IAIzC;;OAEG;IACG,mBAAmB,IAAI,OAAO,CAAC,OAAO,CAAC;IAI7C;;;;;;;;;;;OAWG;IACG,eAAe,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,CAAC;IA0EtF;;;;;;;OAOG;IACG,aAAa,CACjB,OAAO,EAAE,OAAO,EAChB,KAAK,GAAE,MAAW,EAClB,MAAM,GAAE,MAAU,GACjB,OAAO,CAAC,mBAAmB,CAAC;CAGhC"}
@@ -0,0 +1,123 @@
1
+ import { InvitationFarmContractMinimal, ReferralsModuleContractMinimal, HubV2ContractMinimal } from '@aboutcircles/sdk-core/minimal';
2
+ import { InvitationError } from './errors';
3
+ import { Invitations } from './Invitations';
4
+ import { generatePrivateKey, privateKeyToAddress, encodeAbiParameters, INVITATION_FEE } from '@aboutcircles/sdk-utils';
5
+ /**
6
+ * InviteFarm handles batch invitation generation via the InvitationFarm contract
7
+ *
8
+ * This class provides methods to generate multiple invitations at once using
9
+ * the InvitationFarm contract, which manages a farm of InvitationBot instances.
10
+ */
11
+ export class InviteFarm {
12
+ config;
13
+ invitations;
14
+ invitationFarm;
15
+ referralsModule;
16
+ hubV2;
17
+ constructor(config) {
18
+ this.config = config;
19
+ this.invitations = new Invitations(config);
20
+ this.invitationFarm = new InvitationFarmContractMinimal({
21
+ address: config.invitationFarmAddress,
22
+ rpcUrl: config.circlesRpcUrl,
23
+ });
24
+ this.referralsModule = new ReferralsModuleContractMinimal({
25
+ address: config.referralsModuleAddress,
26
+ rpcUrl: config.circlesRpcUrl,
27
+ });
28
+ this.hubV2 = new HubV2ContractMinimal({
29
+ address: config.v2HubAddress,
30
+ rpcUrl: config.circlesRpcUrl,
31
+ });
32
+ }
33
+ /**
34
+ * Get the remaining invite quota for a specific inviter
35
+ */
36
+ async getQuota(inviter) {
37
+ return this.invitationFarm.inviterQuota(inviter);
38
+ }
39
+ /**
40
+ * Get the invitation fee (96 CRC)
41
+ */
42
+ async getInvitationFee() {
43
+ return this.invitationFarm.invitationFee();
44
+ }
45
+ /**
46
+ * Get the invitation module address from the farm
47
+ */
48
+ async getInvitationModule() {
49
+ return this.invitationFarm.invitationModule();
50
+ }
51
+ /**
52
+ * Generate batch invitations using the InvitationFarm
53
+ *
54
+ * This method:
55
+ * 1. Simulates claimInvites to get token IDs that will be claimed
56
+ * 2. Generates random secrets and derives signer addresses
57
+ * 3. Builds transaction batch: claimInvites + safeBatchTransferFrom
58
+ *
59
+ * @param inviter - Address of the inviter (must have quota)
60
+ * @param count - Number of invitations to generate
61
+ * @returns Generated invites with secrets/signers and transactions to execute
62
+ */
63
+ async generateInvites(inviter, count) {
64
+ if (count <= 0) {
65
+ throw new InvitationError('Count must be greater than 0', {
66
+ code: 'INVITATION_INVALID_COUNT',
67
+ source: 'VALIDATION',
68
+ context: { count },
69
+ });
70
+ }
71
+ const inviterLower = inviter.toLowerCase();
72
+ const numberOfInvites = BigInt(count);
73
+ // Step 1: Simulate claimInvites to get token IDs
74
+ const ids = await this.invitationFarm.read('claimInvites', [numberOfInvites], {
75
+ from: inviterLower
76
+ });
77
+ if (!ids || ids.length === 0) {
78
+ throw new InvitationError('No invitation IDs returned from claimInvites', {
79
+ code: 'INVITATION_NO_IDS',
80
+ source: 'INVITATIONS',
81
+ context: { inviter: inviterLower, count },
82
+ });
83
+ }
84
+ // Step 2: Generate secrets and signers
85
+ const invites = [];
86
+ const signers = [];
87
+ for (let i = 0; i < count; i++) {
88
+ const secret = generatePrivateKey();
89
+ const signer = privateKeyToAddress(secret).toLowerCase();
90
+ invites.push({ secret, signer });
91
+ signers.push(signer);
92
+ }
93
+ // Step 3: Get addresses
94
+ const invitationModuleAddress = await this.invitationFarm.invitationModule();
95
+ // Step 4: Build transactions
96
+ const claimTx = this.invitationFarm.claimInvites(numberOfInvites);
97
+ // Encode createAccounts call
98
+ const createAccountsTx = this.referralsModule.createAccounts(signers);
99
+ const createAccountsData = createAccountsTx.data;
100
+ // Encode data for safeBatchTransferFrom
101
+ const encodedData = encodeAbiParameters(['address', 'bytes'], [this.config.referralsModuleAddress, createAccountsData]);
102
+ // Build amounts array (96 CRC per invite)
103
+ const amounts = ids.map(() => INVITATION_FEE);
104
+ const batchTransferTx = this.hubV2.safeBatchTransferFrom(inviterLower, invitationModuleAddress, ids, amounts, encodedData);
105
+ // Save all referrals to database
106
+ await Promise.all(invites.map((inv) => this.invitations.saveReferralData(inviterLower, inv.secret)));
107
+ return {
108
+ invites,
109
+ transactions: [claimTx, batchTransferTx],
110
+ };
111
+ }
112
+ /**
113
+ * List referrals for a given inviter with key previews
114
+ *
115
+ * @param inviter - Address of the inviter
116
+ * @param limit - Maximum number of referrals to return (default 10)
117
+ * @param offset - Number of referrals to skip for pagination (default 0)
118
+ * @returns Paginated list of referral previews with masked keys
119
+ */
120
+ async listReferrals(inviter, limit = 10, offset = 0) {
121
+ return this.invitations.listReferrals(inviter, limit, offset);
122
+ }
123
+ }