@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 +159 -0
- package/dist/{InvitationBuilder.d.ts → Invitations.d.ts} +20 -10
- package/dist/Invitations.d.ts.map +1 -0
- package/dist/Invitations.js +455 -0
- package/dist/InviteFarm.d.ts +59 -0
- package/dist/InviteFarm.d.ts.map +1 -0
- package/dist/InviteFarm.js +123 -0
- package/dist/Referrals.d.ts +44 -0
- package/dist/Referrals.d.ts.map +1 -0
- package/dist/Referrals.js +129 -0
- package/dist/errors.js +69 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/types.d.ts +83 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/package.json +1 -1
- package/dist/InvitationBuilder.d.ts.map +0 -1
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
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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<
|
|
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=
|
|
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
|
+
}
|