@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.
- package/dist/Invitations.d.ts +19 -1
- package/dist/Invitations.d.ts.map +1 -1
- package/dist/Invitations.js +467 -0
- package/dist/InviteFarm.d.ts +49 -0
- package/dist/InviteFarm.d.ts.map +1 -0
- package/dist/InviteFarm.js +109 -0
- package/dist/Referrals.js +129 -0
- package/dist/errors.js +69 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/types.d.ts +33 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -0
- package/package.json +1 -1
package/dist/Invitations.d.ts
CHANGED
|
@@ -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
|
-
|
|
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;
|
|
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
|
+
}
|