@aboutcircles/sdk-invitations 0.1.10 → 0.1.12

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
  *
@@ -145,5 +155,13 @@ export declare class Invitations {
145
155
  * ```
146
156
  */
147
157
  computeAddress(signer: Address): Address;
158
+ /**
159
+ * Generate secrets and derive signer addresses for multiple invitations
160
+ * @param count Number of secrets to generate
161
+ */
162
+ generateSecrets(count: number): Array<{
163
+ secret: `0x${string}`;
164
+ signer: Address;
165
+ }>;
148
166
  }
149
167
  //# sourceMappingURL=Invitations.d.ts.map
@@ -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;IA8BxC;;;OAGG;IACH,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,CAAC;QAAE,MAAM,EAAE,KAAK,MAAM,EAAE,CAAC;QAAC,MAAM,EAAE,OAAO,CAAA;KAAE,CAAC;CAOlF"}
@@ -0,0 +1,467 @@
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}/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
+ // @todo use `generateSecrets` here
337
+ // Step 1: Generate private key and derive signer address
338
+ const privateKey = generatePrivateKey();
339
+ const signerAddress = privateKeyToAddress(privateKey);
340
+ // Step 2: Get real inviters
341
+ const realInviters = await this.getRealInviters(inviterLower);
342
+ if (realInviters.length === 0) {
343
+ throw InvitationError.noProxyInviters(inviterLower);
344
+ }
345
+ // Step 3: Pick the first real inviter
346
+ const firstRealInviter = realInviters[0];
347
+ const realInviterAddress = firstRealInviter.address;
348
+ // Step 4: Find path to invitation module
349
+ const path = await this.findInvitePath(inviterLower, realInviterAddress);
350
+ // Step 5: Build transactions using TransferBuilder to properly handle wrapped tokens
351
+ const transferBuilder = new TransferBuilder(this.config);
352
+ // useSafeCreation = true because we're creating a new Safe wallet via ReferralsModule
353
+ const transferData = await this.generateInviteData([signerAddress], true);
354
+ // Use the new buildFlowMatrixTx method to construct transactions from the path
355
+ const transferTransactions = await transferBuilder.buildFlowMatrixTx(inviterLower, this.config.invitationModuleAddress, path, {
356
+ toTokens: [realInviterAddress],
357
+ useWrappedBalances: true,
358
+ txData: hexToBytes(transferData)
359
+ }, true);
360
+ // Step 6: Save referral data to database
361
+ await this.saveReferralData(inviterLower, privateKey);
362
+ // Step 7: Build final transaction batch
363
+ const transactions = [];
364
+ transactions.push(...transferTransactions);
365
+ return { transactions, privateKey };
366
+ }
367
+ /**
368
+ * Generate invitation data based on whether addresses need Safe account creation or already have Safe wallets
369
+ *
370
+ * @param addresses - Array of addresses to check and encode
371
+ * @param useSafeCreation - If true, uses ReferralsModule to create Safe accounts (for new users without wallets)
372
+ * @returns Encoded data for the invitation transfer
373
+ *
374
+ * @description
375
+ * Two modes:
376
+ * 1. Direct invitation (useSafeCreation = false): Encodes addresses directly for existing Safe wallets
377
+ * 2. Safe creation (useSafeCreation = true): Uses ReferralsModule to create Safe accounts for new users
378
+ *
379
+ * Note: Addresses passed here should NEVER be registered humans in the hub (that's validated before calling this)
380
+ */
381
+ async generateInviteData(addresses, useSafeCreation = true) {
382
+ if (addresses.length === 0) {
383
+ throw InvitationError.noAddressesProvided();
384
+ }
385
+ // If NOT using Safe creation, encode addresses directly (for existing Safe wallets)
386
+ if (!useSafeCreation) {
387
+ if (addresses.length === 1) {
388
+ // Single address - encode as single address (not array)
389
+ return encodeAbiParameters(['address'], [addresses[0]]);
390
+ }
391
+ else {
392
+ // Multiple addresses - encode as address array
393
+ return encodeAbiParameters(['address[]'], [addresses]);
394
+ }
395
+ }
396
+ // Use ReferralsModule to create Safe accounts for new users (signers without Safe wallets)
397
+ if (addresses.length === 1) {
398
+ // Single address - use createAccount(address signer)
399
+ const createAccountTx = this.referralsModule.createAccount(addresses[0]);
400
+ const createAccountData = createAccountTx.data;
401
+ // Encode (address target, bytes callData) for the invitation module
402
+ return encodeAbiParameters(['address', 'bytes'], [this.config.referralsModuleAddress, createAccountData]);
403
+ }
404
+ else {
405
+ // Multiple addresses - use createAccounts(address[] signers)
406
+ const createAccountsTx = this.referralsModule.createAccounts(addresses);
407
+ const createAccountsData = createAccountsTx.data;
408
+ // Encode (address target, bytes callData) for the invitation module
409
+ return encodeAbiParameters(['address', 'bytes'], [this.config.referralsModuleAddress, createAccountsData]);
410
+ }
411
+ }
412
+ /**
413
+ * Predicts the pre-made Safe address for a given signer without deploying it
414
+ * Uses CREATE2 with ACCOUNT_INITIALIZER_HASH and ACCOUNT_CREATION_CODE_HASH via SAFE_PROXY_FACTORY
415
+ *
416
+ * @param signer - The offchain public address chosen by the origin inviter as the pre-deployment key
417
+ * @returns The deterministic Safe address that would be deployed for the signer
418
+ *
419
+ * @description
420
+ * This implements the same logic as the ReferralsModule.computeAddress() contract function:
421
+ * ```solidity
422
+ * bytes32 salt = keccak256(abi.encodePacked(ACCOUNT_INITIALIZER_HASH, uint256(uint160(signer))));
423
+ * predictedAddress = address(
424
+ * uint160(
425
+ * uint256(
426
+ * keccak256(
427
+ * abi.encodePacked(bytes1(0xff), address(SAFE_PROXY_FACTORY), salt, ACCOUNT_CREATION_CODE_HASH)
428
+ * )
429
+ * )
430
+ * )
431
+ * );
432
+ * ```
433
+ */
434
+ computeAddress(signer) {
435
+ // Step 1: Calculate salt = keccak256(abi.encodePacked(ACCOUNT_INITIALIZER_HASH, uint256(uint160(signer))))
436
+ // abi.encodePacked means concatenate without padding
437
+ // uint256(uint160(signer)) converts address to uint256 (32 bytes, left-padded with zeros)
438
+ const signerLower = signer.toLowerCase().replace('0x', '');
439
+ const signerUint256 = signerLower.padStart(64, '0'); // 32 bytes as hex string
440
+ // Concatenate: ACCOUNT_INITIALIZER_HASH (32 bytes) + signerUint256 (32 bytes)
441
+ const saltPreimage = ACCOUNT_INITIALIZER_HASH.replace('0x', '') + signerUint256;
442
+ const salt = keccak256(('0x' + saltPreimage));
443
+ // Step 2: Calculate CREATE2 address
444
+ // address = keccak256(0xff ++ factory ++ salt ++ initCodeHash)[12:]
445
+ const ff = 'ff';
446
+ const factory = SAFE_PROXY_FACTORY.toLowerCase().replace('0x', '');
447
+ const saltClean = salt.replace('0x', '');
448
+ const initCodeHash = ACCOUNT_CREATION_CODE_HASH.replace('0x', '');
449
+ // Concatenate all parts
450
+ const create2Preimage = ff + factory + saltClean + initCodeHash;
451
+ const hash = keccak256(('0x' + create2Preimage));
452
+ // Take last 20 bytes (40 hex chars) as the address
453
+ const addressHex = '0x' + hash.slice(-40);
454
+ return checksumAddress(addressHex);
455
+ }
456
+ /**
457
+ * Generate secrets and derive signer addresses for multiple invitations
458
+ * @param count Number of secrets to generate
459
+ */
460
+ generateSecrets(count) {
461
+ return Array.from({ length: count }, () => {
462
+ const secret = generatePrivateKey();
463
+ const signer = privateKeyToAddress(secret).toLowerCase();
464
+ return { secret, signer };
465
+ });
466
+ }
467
+ }
@@ -0,0 +1,49 @@
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
+ * Manages a farm of InvitationBot instances for generating multiple invitations at once.
14
+ */
15
+ export declare class InviteFarm {
16
+ private readonly referralsModuleAddress;
17
+ private readonly invitations;
18
+ private readonly invitationFarm;
19
+ private readonly referralsModule;
20
+ private readonly hubV2;
21
+ constructor(config: CirclesConfig);
22
+ /** Get the remaining invite quota for a specific inviter */
23
+ getQuota(inviter: Address): Promise<bigint>;
24
+ /** Get the invitation fee (96 CRC) */
25
+ getInvitationFee(): Promise<bigint>;
26
+ /** Get the invitation module address from the farm */
27
+ getInvitationModule(): Promise<Address>;
28
+ /**
29
+ * Generate batch invitations using the InvitationFarm.
30
+ * Simulates claimInvites to get token IDs, generates secrets/signers, and builds transactions.
31
+ * @param inviter Address of the inviter (must have quota)
32
+ * @param count Number of invitations to generate
33
+ */
34
+ generateInvites(inviter: Address, count: number): Promise<GenerateInvitesResult>;
35
+ /**
36
+ * List referrals for a given inviter with key previews
37
+ * @param inviter Address of the inviter
38
+ * @param limit Max referrals to return (default 10)
39
+ * @param offset Pagination offset (default 0)
40
+ */
41
+ listReferrals(inviter: Address, limit?: number, offset?: number): Promise<ReferralPreviewList>;
42
+ /** Simulate claim to get token IDs that will be claimed */
43
+ private simulateClaim;
44
+ /** Build single safeTransferFrom with createAccount calldata */
45
+ private buildSingleTransfer;
46
+ /** Build batch safeBatchTransferFrom with createAccounts calldata */
47
+ private buildBatchTransfer;
48
+ }
49
+ //# 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;AAInD,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;;;GAGG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,QAAQ,CAAC,sBAAsB,CAAU;IACjD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAc;IAC1C,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAgC;IAC/D,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAiC;IACjE,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAuB;gBAEjC,MAAM,EAAE,aAAa;IAiBjC,4DAA4D;IACtD,QAAQ,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC;IAIjD,sCAAsC;IAChC,gBAAgB,IAAI,OAAO,CAAC,MAAM,CAAC;IAIzC,sDAAsD;IAChD,mBAAmB,IAAI,OAAO,CAAC,OAAO,CAAC;IAI7C;;;;;OAKG;IACG,eAAe,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,CAAC;IAwCtF;;;;;OAKG;IACG,aAAa,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,SAAK,EAAE,MAAM,SAAI,GAAG,OAAO,CAAC,mBAAmB,CAAC;IAI3F,2DAA2D;YAC7C,aAAa;IAQ3B,gEAAgE;IAChE,OAAO,CAAC,mBAAmB;IAW3B,qEAAqE;IACrE,OAAO,CAAC,kBAAkB;CAW3B"}
@@ -0,0 +1,109 @@
1
+ import { InvitationFarmContractMinimal, ReferralsModuleContractMinimal, HubV2ContractMinimal } from '@aboutcircles/sdk-core/minimal';
2
+ import { InvitationError } from './errors';
3
+ import { Invitations } from './Invitations';
4
+ import { encodeAbiParameters, INVITATION_FEE } from '@aboutcircles/sdk-utils';
5
+ /**
6
+ * InviteFarm handles batch invitation generation via the InvitationFarm contract.
7
+ * Manages a farm of InvitationBot instances for generating multiple invitations at once.
8
+ */
9
+ export class InviteFarm {
10
+ referralsModuleAddress;
11
+ invitations;
12
+ invitationFarm;
13
+ referralsModule;
14
+ hubV2;
15
+ constructor(config) {
16
+ this.referralsModuleAddress = config.referralsModuleAddress;
17
+ this.invitations = new Invitations(config);
18
+ this.invitationFarm = new InvitationFarmContractMinimal({
19
+ address: config.invitationFarmAddress,
20
+ rpcUrl: config.circlesRpcUrl,
21
+ });
22
+ this.referralsModule = new ReferralsModuleContractMinimal({
23
+ address: config.referralsModuleAddress,
24
+ rpcUrl: config.circlesRpcUrl,
25
+ });
26
+ this.hubV2 = new HubV2ContractMinimal({
27
+ address: config.v2HubAddress,
28
+ rpcUrl: config.circlesRpcUrl,
29
+ });
30
+ }
31
+ /** Get the remaining invite quota for a specific inviter */
32
+ async getQuota(inviter) {
33
+ return this.invitationFarm.inviterQuota(inviter);
34
+ }
35
+ /** Get the invitation fee (96 CRC) */
36
+ async getInvitationFee() {
37
+ return this.invitationFarm.invitationFee();
38
+ }
39
+ /** Get the invitation module address from the farm */
40
+ async getInvitationModule() {
41
+ return this.invitationFarm.invitationModule();
42
+ }
43
+ /**
44
+ * Generate batch invitations using the InvitationFarm.
45
+ * Simulates claimInvites to get token IDs, generates secrets/signers, and builds transactions.
46
+ * @param inviter Address of the inviter (must have quota)
47
+ * @param count Number of invitations to generate
48
+ */
49
+ async generateInvites(inviter, count) {
50
+ if (count <= 0) {
51
+ throw new InvitationError('Count must be greater than 0', {
52
+ code: 'INVITATION_INVALID_COUNT',
53
+ source: 'VALIDATION',
54
+ context: { count },
55
+ });
56
+ }
57
+ const inviterLower = inviter.toLowerCase();
58
+ const isSingle = count === 1;
59
+ const ids = await this.simulateClaim(inviterLower, count);
60
+ if (!ids.length) {
61
+ throw new InvitationError('No invitation IDs returned from claim', {
62
+ code: 'INVITATION_NO_IDS',
63
+ source: 'INVITATIONS',
64
+ context: { inviter: inviterLower, count },
65
+ });
66
+ }
67
+ const invites = this.invitations.generateSecrets(count);
68
+ const signers = invites.map(inv => inv.signer);
69
+ const invitationModule = await this.invitationFarm.invitationModule();
70
+ const claimTx = isSingle
71
+ ? this.invitationFarm.claimInvite()
72
+ : this.invitationFarm.claimInvites(BigInt(count));
73
+ const transferTx = isSingle
74
+ ? this.buildSingleTransfer(inviterLower, invitationModule, ids[0], signers[0])
75
+ : this.buildBatchTransfer(inviterLower, invitationModule, ids, signers);
76
+ await Promise.all(invites.map(inv => this.invitations.saveReferralData(inviterLower, inv.secret)));
77
+ return { invites, transactions: [claimTx, transferTx] };
78
+ }
79
+ /**
80
+ * List referrals for a given inviter with key previews
81
+ * @param inviter Address of the inviter
82
+ * @param limit Max referrals to return (default 10)
83
+ * @param offset Pagination offset (default 0)
84
+ */
85
+ async listReferrals(inviter, limit = 10, offset = 0) {
86
+ return this.invitations.listReferrals(inviter, limit, offset);
87
+ }
88
+ /** Simulate claim to get token IDs that will be claimed */
89
+ async simulateClaim(inviter, count) {
90
+ if (count === 1) {
91
+ const id = await this.invitationFarm.read('claimInvite', [], { from: inviter });
92
+ return [id];
93
+ }
94
+ return this.invitationFarm.read('claimInvites', [BigInt(count)], { from: inviter });
95
+ }
96
+ /** Build single safeTransferFrom with createAccount calldata */
97
+ buildSingleTransfer(from, to, id, signer) {
98
+ const calldata = this.referralsModule.createAccount(signer).data;
99
+ const data = encodeAbiParameters(['address', 'bytes'], [this.referralsModuleAddress, calldata]);
100
+ return this.hubV2.safeTransferFrom(from, to, id, INVITATION_FEE, data);
101
+ }
102
+ /** Build batch safeBatchTransferFrom with createAccounts calldata */
103
+ buildBatchTransfer(from, to, ids, signers) {
104
+ const calldata = this.referralsModule.createAccounts(signers).data;
105
+ const data = encodeAbiParameters(['address', 'bytes'], [this.referralsModuleAddress, calldata]);
106
+ const amounts = ids.map(() => INVITATION_FEE);
107
+ return this.hubV2.safeBatchTransferFrom(from, to, ids, amounts, data);
108
+ }
109
+ }