@aboutcircles/sdk-invitations 0.1.9 → 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.
package/README.md ADDED
@@ -0,0 +1,159 @@
1
+ # @aboutcircles/sdk-invitations
2
+
3
+ Invitation package for Circles protocol. Create referrals for new users or invite existing Safe wallet users.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @aboutcircles/sdk-invitations
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```typescript
14
+ import { Invitations, Referrals } from '@aboutcircles/sdk-invitations';
15
+ import { circlesConfig } from '@aboutcircles/sdk-utils';
16
+
17
+ const invitations = new Invitations(circlesConfig[100]);
18
+ const referrals = new Referrals('https://referrals.circles.example');
19
+ ```
20
+
21
+ ---
22
+
23
+ ## API Reference
24
+
25
+ ### Invitations
26
+
27
+ #### `constructor(config: CirclesConfig)`
28
+
29
+ Initialize the Invitations client.
30
+
31
+ ---
32
+
33
+ #### `generateReferral(inviter: Address)`
34
+
35
+ Generate a new referral for a user without a Safe wallet.
36
+
37
+ ```typescript
38
+ Promise<{
39
+ transactions: TransactionRequest[];
40
+ privateKey: `0x${string}`;
41
+ }>
42
+ ```
43
+
44
+ Creates a new private key, generates a Safe wallet via ReferralsModule, and saves to referrals service.
45
+
46
+ ---
47
+
48
+ #### `generateInvite(inviter: Address, invitee: Address)`
49
+
50
+ Invite a user who has a Safe wallet but isn't registered in Circles Hub.
51
+
52
+ ```typescript
53
+ Promise<TransactionRequest[]>
54
+ ```
55
+
56
+ ---
57
+
58
+ #### `getRealInviters(inviter: Address)`
59
+
60
+ Get addresses whose tokens can pay for invitations.
61
+
62
+ ```typescript
63
+ Promise<ProxyInviter[]>
64
+
65
+ interface ProxyInviter {
66
+ address: Address;
67
+ possibleInvites: number;
68
+ }
69
+ ```
70
+
71
+ ---
72
+
73
+ #### `findInvitePath(inviter: Address, proxyInviterAddress?: Address)`
74
+
75
+ Find path from inviter to invitation module.
76
+
77
+ ```typescript
78
+ Promise<PathfindingResult>
79
+ ```
80
+
81
+ ---
82
+
83
+ #### `generateInviteData(addresses: Address[], useSafeCreation: boolean)`
84
+
85
+ Generate encoded invitation data for transactions.
86
+
87
+ ```typescript
88
+ Promise<`0x${string}`>
89
+ ```
90
+
91
+ - `useSafeCreation = true`: Creates Safe via ReferralsModule
92
+ - `useSafeCreation = false`: Uses existing Safe addresses
93
+
94
+ ---
95
+
96
+ #### `computeAddress(signer: Address)`
97
+
98
+ Predict Safe address for a signer using CREATE2.
99
+
100
+ ```typescript
101
+ Address
102
+ ```
103
+
104
+ ---
105
+
106
+ ### Referrals
107
+
108
+ #### `constructor(baseUrl: string, getToken?: () => Promise<string>)`
109
+
110
+ Initialize the Referrals service client.
111
+
112
+ ---
113
+
114
+ #### `retrieve(privateKey: string)`
115
+
116
+ Get referral information by private key (public endpoint).
117
+
118
+ ```typescript
119
+ Promise<ReferralInfo>
120
+
121
+ interface ReferralInfo {
122
+ inviter: string;
123
+ status: "pending" | "confirmed" | "claimed" | "expired";
124
+ accountAddress?: string;
125
+ }
126
+ ```
127
+
128
+ ---
129
+
130
+ #### `listMine()`
131
+
132
+ List all referrals created by authenticated user.
133
+
134
+ ```typescript
135
+ Promise<ReferralList>
136
+
137
+ interface ReferralList {
138
+ referrals: Referral[];
139
+ count: number;
140
+ }
141
+
142
+ interface Referral {
143
+ id: string;
144
+ privateKey: string;
145
+ status: "pending" | "confirmed" | "claimed" | "expired";
146
+ accountAddress?: string;
147
+ createdAt: string;
148
+ confirmedAt: string | null;
149
+ claimedAt: string | null;
150
+ }
151
+ ```
152
+
153
+ Requires authentication via `getToken` function.
154
+
155
+ ---
156
+
157
+ ## License
158
+
159
+ MIT
@@ -1,13 +1,14 @@
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;
5
6
  }
6
7
  /**
7
- * InvitationBuilder handles invitation operations for Circles
8
+ * Invitations handles invitation operations for Circles
8
9
  * Supports both referral invitations (new users) and direct invitations (existing Safe wallets)
9
10
  */
10
- export declare class InvitationBuilder {
11
+ export declare class Invitations {
11
12
  private config;
12
13
  private rpcClient;
13
14
  private pathfinder;
@@ -24,7 +25,16 @@ export declare class InvitationBuilder {
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
  *
@@ -92,7 +102,7 @@ export declare class InvitationBuilder {
92
102
  * Generate a referral for inviting a new user
93
103
  *
94
104
  * @param inviter - Address of the inviter
95
- * @returns Array of transactions to execute in order
105
+ * @returns Object containing transactions and the generated private key
96
106
  *
97
107
  * @description
98
108
  * This function:
@@ -101,12 +111,12 @@ export declare class InvitationBuilder {
101
111
  * 3. Builds transaction batch including transfers and invitation
102
112
  * 4. Uses generateInviteData to properly encode the Safe account creation data
103
113
  * 5. Saves the referral data (private key, signer, inviter) to database
104
- * 6. Returns transactions ready to execute
105
- *
106
- * Note: The private key is saved via saveReferralData and not returned.
107
- * Retrieve it from the database using the inviter and signer addresses.
114
+ * 6. Returns transactions and the generated private key
108
115
  */
109
- generateReferral(inviter: Address): Promise<TransactionRequest[]>;
116
+ generateReferral(inviter: Address): Promise<{
117
+ transactions: TransactionRequest[];
118
+ privateKey: `0x${string}`;
119
+ }>;
110
120
  /**
111
121
  * Generate invitation data based on whether addresses need Safe account creation or already have Safe wallets
112
122
  *
@@ -146,4 +156,4 @@ export declare class InvitationBuilder {
146
156
  */
147
157
  computeAddress(signer: Address): Address;
148
158
  }
149
- //# sourceMappingURL=InvitationBuilder.d.ts.map
159
+ //# sourceMappingURL=Invitations.d.ts.map
@@ -0,0 +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;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
+ }