@iexec/web3mail 1.6.0 → 1.7.1

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.
Files changed (35) hide show
  1. package/dist/utils/subgraphQuery.js +5 -2
  2. package/dist/utils/subgraphQuery.js.map +1 -1
  3. package/dist/utils/validators.d.ts +35 -0
  4. package/dist/utils/validators.js +45 -1
  5. package/dist/utils/validators.js.map +1 -1
  6. package/dist/web3mail/IExecWeb3mail.d.ts +4 -1
  7. package/dist/web3mail/IExecWeb3mail.js +38 -0
  8. package/dist/web3mail/IExecWeb3mail.js.map +1 -1
  9. package/dist/web3mail/fetchMyContacts.d.ts +1 -1
  10. package/dist/web3mail/fetchMyContacts.js +3 -1
  11. package/dist/web3mail/fetchMyContacts.js.map +1 -1
  12. package/dist/web3mail/fetchUserContacts.d.ts +1 -1
  13. package/dist/web3mail/fetchUserContacts.js +20 -3
  14. package/dist/web3mail/fetchUserContacts.js.map +1 -1
  15. package/dist/web3mail/internalTypes.d.ts +4 -0
  16. package/dist/web3mail/prepareEmailCampaign.d.ts +4 -0
  17. package/dist/web3mail/prepareEmailCampaign.js +106 -0
  18. package/dist/web3mail/prepareEmailCampaign.js.map +1 -0
  19. package/dist/web3mail/sendEmail.js +9 -5
  20. package/dist/web3mail/sendEmail.js.map +1 -1
  21. package/dist/web3mail/sendEmailCampaign.d.ts +4 -0
  22. package/dist/web3mail/sendEmailCampaign.js +43 -0
  23. package/dist/web3mail/sendEmailCampaign.js.map +1 -0
  24. package/dist/web3mail/types.d.ts +96 -1
  25. package/package.json +5 -6
  26. package/src/utils/subgraphQuery.ts +17 -14
  27. package/src/utils/validators.ts +68 -1
  28. package/src/web3mail/IExecWeb3mail.ts +53 -0
  29. package/src/web3mail/fetchMyContacts.ts +3 -0
  30. package/src/web3mail/fetchUserContacts.ts +28 -11
  31. package/src/web3mail/internalTypes.ts +5 -0
  32. package/src/web3mail/prepareEmailCampaign.ts +170 -0
  33. package/src/web3mail/sendEmail.ts +31 -5
  34. package/src/web3mail/sendEmailCampaign.ts +69 -0
  35. package/src/web3mail/types.ts +102 -1
@@ -0,0 +1,170 @@
1
+ import { Buffer } from 'buffer';
2
+ import {
3
+ DEFAULT_CONTENT_TYPE,
4
+ MAX_DESIRED_APP_ORDER_PRICE,
5
+ MAX_DESIRED_WORKERPOOL_ORDER_PRICE,
6
+ } from '../config/config.js';
7
+ import { handleIfProtocolError, WorkflowError } from '../utils/errors.js';
8
+ import * as ipfs from '../utils/ipfs-service.js';
9
+ import {
10
+ addressOrEnsSchema,
11
+ contentTypeSchema,
12
+ emailContentSchema,
13
+ emailSubjectSchema,
14
+ labelSchema,
15
+ positiveNumberSchema,
16
+ senderNameSchema,
17
+ throwIfMissing,
18
+ } from '../utils/validators.js';
19
+ import {
20
+ PrepareEmailCampaignParams,
21
+ PrepareEmailCampaignResponse,
22
+ } from './types.js';
23
+ import {
24
+ DappAddressConsumer,
25
+ DataProtectorConsumer,
26
+ IExecConsumer,
27
+ IpfsGatewayConfigConsumer,
28
+ IpfsNodeConfigConsumer,
29
+ } from './internalTypes.js';
30
+
31
+ export type PrepareEmailCampaign = typeof prepareEmailCampaign;
32
+
33
+ export const prepareEmailCampaign = async ({
34
+ iexec = throwIfMissing(),
35
+ dataProtector = throwIfMissing(),
36
+ workerpoolAddressOrEns,
37
+ dappAddressOrENS,
38
+ ipfsNode,
39
+ ipfsGateway,
40
+ senderName,
41
+ emailSubject,
42
+ emailContent,
43
+ contentType = DEFAULT_CONTENT_TYPE,
44
+ label,
45
+ appMaxPrice = MAX_DESIRED_APP_ORDER_PRICE,
46
+ workerpoolMaxPrice = MAX_DESIRED_WORKERPOOL_ORDER_PRICE,
47
+ grantedAccesses,
48
+ maxProtectedDataPerTask,
49
+ }: IExecConsumer &
50
+ DappAddressConsumer &
51
+ IpfsNodeConfigConsumer &
52
+ IpfsGatewayConfigConsumer &
53
+ DataProtectorConsumer &
54
+ PrepareEmailCampaignParams): Promise<PrepareEmailCampaignResponse> => {
55
+ try {
56
+ const vWorkerpoolAddressOrEns = addressOrEnsSchema()
57
+ .label('WorkerpoolAddressOrEns')
58
+ .validateSync(workerpoolAddressOrEns);
59
+
60
+ const vSenderName = senderNameSchema()
61
+ .label('senderName')
62
+ .validateSync(senderName);
63
+
64
+ const vEmailSubject = emailSubjectSchema()
65
+ .required()
66
+ .label('emailSubject')
67
+ .validateSync(emailSubject);
68
+
69
+ const vEmailContent = emailContentSchema()
70
+ .required()
71
+ .label('emailContent')
72
+ .validateSync(emailContent);
73
+
74
+ const vContentType = contentTypeSchema()
75
+ .label('contentType')
76
+ .validateSync(contentType);
77
+
78
+ const vLabel = labelSchema().label('label').validateSync(label);
79
+
80
+ const vDappAddressOrENS = addressOrEnsSchema()
81
+ .required()
82
+ .label('dappAddressOrENS')
83
+ .validateSync(dappAddressOrENS);
84
+
85
+ const vAppMaxPrice = positiveNumberSchema()
86
+ .label('appMaxPrice')
87
+ .validateSync(appMaxPrice);
88
+
89
+ const vWorkerpoolMaxPrice = positiveNumberSchema()
90
+ .label('workerpoolMaxPrice')
91
+ .validateSync(workerpoolMaxPrice);
92
+
93
+ const vMaxProtectedDataPerTask = positiveNumberSchema()
94
+ .label('maxProtectedDataPerTask')
95
+ .validateSync(maxProtectedDataPerTask);
96
+
97
+ // TODO: factor this
98
+ // Encrypt email content
99
+ const emailContentEncryptionKey = iexec.dataset.generateEncryptionKey();
100
+ const encryptedFile = await iexec.dataset
101
+ .encrypt(Buffer.from(vEmailContent, 'utf8'), emailContentEncryptionKey)
102
+ .catch((e) => {
103
+ throw new WorkflowError({
104
+ message: 'Failed to encrypt email content',
105
+ errorCause: e,
106
+ });
107
+ });
108
+
109
+ // Push email content to IPFS
110
+ const cid = await ipfs
111
+ .add(encryptedFile, {
112
+ ipfsNode,
113
+ ipfsGateway,
114
+ })
115
+ .catch((e) => {
116
+ throw new WorkflowError({
117
+ message: 'Failed to upload encrypted email content',
118
+ errorCause: e,
119
+ });
120
+ });
121
+
122
+ const multiaddr = `/ipfs/${cid}`;
123
+
124
+ // Prepare secrets for the requester
125
+ // Use a positive integer as secret ID (required by iexec)
126
+ // Using "1" as a fixed ID for the requester secret
127
+ const requesterSecretId = 1;
128
+ const secrets = {
129
+ [requesterSecretId]: JSON.stringify({
130
+ emailSubject: vEmailSubject,
131
+ emailContentMultiAddr: multiaddr,
132
+ contentType: vContentType,
133
+ senderName: vSenderName,
134
+ emailContentEncryptionKey,
135
+ useCallback: true,
136
+ }),
137
+ };
138
+
139
+ // TODO: end factor this
140
+ const { bulkRequest: campaignRequest } =
141
+ await dataProtector.prepareBulkRequest({
142
+ app: vDappAddressOrENS,
143
+ appMaxPrice: vAppMaxPrice,
144
+ workerpoolMaxPrice: vWorkerpoolMaxPrice,
145
+ workerpool: vWorkerpoolAddressOrEns,
146
+ args: vLabel,
147
+ inputFiles: [],
148
+ secrets,
149
+ bulkAccesses: grantedAccesses,
150
+ maxProtectedDataPerTask: vMaxProtectedDataPerTask,
151
+ });
152
+
153
+ return { campaignRequest };
154
+ } catch (error) {
155
+ // Protocol error detected, re-throwing as-is
156
+ if ((error as any)?.isProtocolError === true) {
157
+ throw error;
158
+ }
159
+
160
+ // Handle protocol errors - this will throw if it's an ApiCallError
161
+ // handleIfProtocolError transforms ApiCallError into a WorkflowError with isProtocolError=true
162
+ handleIfProtocolError(error);
163
+
164
+ // For all other errors
165
+ throw new WorkflowError({
166
+ message: 'Failed to prepareEmailCampaign',
167
+ errorCause: error,
168
+ });
169
+ }
170
+ };
@@ -67,43 +67,55 @@ export const sendEmail = async ({
67
67
  .required()
68
68
  .label('protectedData')
69
69
  .validateSync(protectedData);
70
+
70
71
  const vEmailSubject = emailSubjectSchema()
71
72
  .required()
72
73
  .label('emailSubject')
73
74
  .validateSync(emailSubject);
75
+
74
76
  const vEmailContent = emailContentSchema()
75
77
  .required()
76
78
  .label('emailContent')
77
79
  .validateSync(emailContent);
80
+
78
81
  const vContentType = contentTypeSchema()
79
82
  .required()
80
83
  .label('contentType')
81
84
  .validateSync(contentType);
85
+
82
86
  const vSenderName = senderNameSchema()
83
87
  .label('senderName')
84
88
  .validateSync(senderName);
89
+
85
90
  const vLabel = labelSchema().label('label').validateSync(label);
91
+
86
92
  const vWorkerpoolAddressOrEns = addressOrEnsSchema()
87
93
  .required()
88
94
  .label('WorkerpoolAddressOrEns')
89
95
  .validateSync(workerpoolAddressOrEns);
96
+
90
97
  const vDappAddressOrENS = addressOrEnsSchema()
91
98
  .required()
92
99
  .label('dappAddressOrENS')
93
100
  .validateSync(dappAddressOrENS);
101
+
94
102
  const vDappWhitelistAddress = addressSchema()
95
103
  .required()
96
104
  .label('dappWhitelistAddress')
97
105
  .validateSync(dappWhitelistAddress);
106
+
98
107
  const vDataMaxPrice = positiveNumberSchema()
99
108
  .label('dataMaxPrice')
100
109
  .validateSync(dataMaxPrice);
110
+
101
111
  const vAppMaxPrice = positiveNumberSchema()
102
112
  .label('appMaxPrice')
103
113
  .validateSync(appMaxPrice);
114
+
104
115
  const vWorkerpoolMaxPrice = positiveNumberSchema()
105
116
  .label('workerpoolMaxPrice')
106
117
  .validateSync(workerpoolMaxPrice);
118
+
107
119
  const vUseVoucher = booleanSchema()
108
120
  .label('useVoucher')
109
121
  .validateSync(useVoucher);
@@ -113,6 +125,7 @@ export const sendEmail = async ({
113
125
  graphQLClient,
114
126
  vDatasetAddress
115
127
  );
128
+
116
129
  if (!isValidProtectedData) {
117
130
  throw new Error(
118
131
  'This protected data does not contain "email:string" in its schema.'
@@ -145,7 +158,8 @@ export const sendEmail = async ({
145
158
  ] = await Promise.all([
146
159
  // Fetch dataset order for web3mail app
147
160
  iexec.orderbook
148
- .fetchDatasetOrderbook(vDatasetAddress, {
161
+ .fetchDatasetOrderbook({
162
+ dataset: vDatasetAddress,
149
163
  app: dappAddressOrENS,
150
164
  requester: requesterAddress,
151
165
  })
@@ -155,9 +169,11 @@ export const sendEmail = async ({
155
169
  );
156
170
  return desiredPriceDataOrderbook[0]?.order; // may be undefined
157
171
  }),
172
+
158
173
  // Fetch dataset order for web3mail whitelist
159
174
  iexec.orderbook
160
- .fetchDatasetOrderbook(vDatasetAddress, {
175
+ .fetchDatasetOrderbook({
176
+ dataset: vDatasetAddress,
161
177
  app: vDappWhitelistAddress,
162
178
  requester: requesterAddress,
163
179
  })
@@ -167,9 +183,11 @@ export const sendEmail = async ({
167
183
  );
168
184
  return desiredPriceDataOrderbook[0]?.order; // may be undefined
169
185
  }),
186
+
170
187
  // Fetch app order
171
188
  iexec.orderbook
172
- .fetchAppOrderbook(dappAddressOrENS, {
189
+ .fetchAppOrderbook({
190
+ app: dappAddressOrENS,
173
191
  minTag: ['tee', 'scone'],
174
192
  maxTag: ['tee', 'scone'],
175
193
  workerpool: workerpoolAddressOrEns,
@@ -184,6 +202,7 @@ export const sendEmail = async ({
184
202
  }
185
203
  return desiredPriceAppOrder;
186
204
  }),
205
+
187
206
  // Fetch workerpool order for App or AppWhitelist
188
207
  Promise.all([
189
208
  // for app
@@ -219,9 +238,11 @@ export const sendEmail = async ({
219
238
  useVoucher: vUseVoucher,
220
239
  userVoucher,
221
240
  });
241
+
222
242
  if (!desiredPriceWorkerpoolOrder) {
223
243
  throw new Error('No Workerpool order found for the desired price');
224
244
  }
245
+
225
246
  return desiredPriceWorkerpoolOrder;
226
247
  }
227
248
  ),
@@ -247,6 +268,7 @@ export const sendEmail = async ({
247
268
  errorCause: e,
248
269
  });
249
270
  });
271
+
250
272
  const cid = await ipfs
251
273
  .add(encryptedFile, {
252
274
  ipfsNode: ipfsNode,
@@ -258,6 +280,7 @@ export const sendEmail = async ({
258
280
  errorCause: e,
259
281
  });
260
282
  });
283
+
261
284
  const multiaddr = `/ipfs/${cid}`;
262
285
 
263
286
  await iexec.secrets.pushRequesterSecret(
@@ -289,10 +312,11 @@ export const sendEmail = async ({
289
312
  iexec_args: vLabel,
290
313
  },
291
314
  });
315
+
292
316
  const requestorder = await iexec.order.signRequestorder(requestorderToSign);
293
317
 
294
318
  // Match orders and compute task ID
295
- const { dealid } = await iexec.order.matchOrders(
319
+ const { dealid: dealId } = await iexec.order.matchOrders(
296
320
  {
297
321
  apporder: apporder,
298
322
  datasetorder: datasetorder,
@@ -301,10 +325,12 @@ export const sendEmail = async ({
301
325
  },
302
326
  { useVoucher: vUseVoucher }
303
327
  );
304
- const taskId = await iexec.deal.computeTaskId(dealid, 0);
328
+
329
+ const taskId = await iexec.deal.computeTaskId(dealId, 0);
305
330
 
306
331
  return {
307
332
  taskId,
333
+ dealId,
308
334
  };
309
335
  } catch (error) {
310
336
  handleIfProtocolError(error);
@@ -0,0 +1,69 @@
1
+ import { NULL_ADDRESS } from 'iexec/utils';
2
+ import { ValidationError } from 'yup';
3
+ import { handleIfProtocolError, WorkflowError } from '../utils/errors.js';
4
+ import {
5
+ addressOrEnsSchema,
6
+ campaignRequestSchema,
7
+ throwIfMissing,
8
+ } from '../utils/validators.js';
9
+ import {
10
+ CampaignRequest,
11
+ SendEmailCampaignParams,
12
+ SendEmailCampaignResponse,
13
+ } from './types.js';
14
+ import { DataProtectorConsumer } from './internalTypes.js';
15
+
16
+ export type SendEmailCampaign = typeof sendEmailCampaign;
17
+
18
+ export const sendEmailCampaign = async ({
19
+ dataProtector = throwIfMissing(),
20
+ workerpoolAddressOrEns = throwIfMissing(),
21
+ campaignRequest,
22
+ }: DataProtectorConsumer &
23
+ SendEmailCampaignParams): Promise<SendEmailCampaignResponse> => {
24
+ const vCampaignRequest = campaignRequestSchema()
25
+ .required()
26
+ .label('campaignRequest')
27
+ .validateSync(campaignRequest) as CampaignRequest;
28
+
29
+ const vWorkerpoolAddressOrEns = addressOrEnsSchema()
30
+ .required()
31
+ .label('workerpoolAddressOrEns')
32
+ .validateSync(workerpoolAddressOrEns);
33
+
34
+ if (
35
+ vCampaignRequest.workerpool !== NULL_ADDRESS &&
36
+ vCampaignRequest.workerpool.toLowerCase() !==
37
+ vWorkerpoolAddressOrEns.toLowerCase()
38
+ ) {
39
+ throw new ValidationError(
40
+ "workerpoolAddressOrEns doesn't match campaignRequest workerpool"
41
+ );
42
+ }
43
+
44
+ try {
45
+ // Process the prepared bulk request
46
+ const processBulkRequestResponse = await dataProtector.processBulkRequest({
47
+ bulkRequest: vCampaignRequest,
48
+ workerpool: vWorkerpoolAddressOrEns,
49
+ waitForResult: false,
50
+ });
51
+
52
+ return processBulkRequestResponse;
53
+ } catch (error) {
54
+ // Protocol error detected, re-throwing as-is
55
+ if ((error as any)?.isProtocolError === true) {
56
+ throw error;
57
+ }
58
+
59
+ // Handle protocol errors - this will throw if it's an ApiCallError
60
+ // handleIfProtocolError transforms ApiCallError into a WorkflowError with isProtocolError=true
61
+ handleIfProtocolError(error);
62
+
63
+ // For all other errors
64
+ throw new WorkflowError({
65
+ message: 'Failed to sendEmailCampaign',
66
+ errorCause: error,
67
+ });
68
+ }
69
+ };
@@ -1,5 +1,6 @@
1
1
  import { EnhancedWallet } from 'iexec';
2
2
  import { IExecConfigOptions } from 'iexec/IExecConfig';
3
+ import type { BulkRequest } from '@iexec/dataprotector';
3
4
 
4
5
  export type Web3SignerProvider = EnhancedWallet;
5
6
 
@@ -11,14 +12,44 @@ export type Address = string;
11
12
 
12
13
  export type TimeStamp = string;
13
14
 
15
+ /**
16
+ * request to send email in bulk
17
+ *
18
+ * use `prepareEmailCampaign()` to create a `CampaignRequest`
19
+ *
20
+ * then use `sendEmailCampaign()` to send the campaign
21
+ */
22
+ export type CampaignRequest = BulkRequest;
23
+
24
+ /**
25
+ * authorization signed by the data owner granting access to this contact
26
+ *
27
+ * `GrantedAccess` are obtained by fetching contacts (e.g. `fetchMyContacts()` or `fetchUserContacts()`)
28
+ *
29
+ * `GrantedAccess` can be consumed for email campaigns (e.g. `prepareEmailCampaign()` then `sendEmailCampaign()`)
30
+ */
31
+ export type GrantedAccess = {
32
+ dataset: string;
33
+ datasetprice: string;
34
+ volume: string;
35
+ tag: string;
36
+ apprestrict: string;
37
+ workerpoolrestrict: string;
38
+ requesterrestrict: string;
39
+ salt: string;
40
+ sign: string;
41
+ remainingAccess: number;
42
+ };
43
+
14
44
  export type Contact = {
15
45
  address: Address;
16
46
  owner: Address;
17
47
  accessGrantTimestamp: TimeStamp;
18
48
  isUserStrict: boolean;
19
- name: string;
49
+ name?: string;
20
50
  remainingAccess: number;
21
51
  accessPrice: number;
52
+ grantedAccess: GrantedAccess;
22
53
  };
23
54
 
24
55
  export type SendEmailParams = {
@@ -40,6 +71,10 @@ export type FetchMyContactsParams = {
40
71
  * Get contacts for this specific user only
41
72
  */
42
73
  isUserStrict?: boolean;
74
+ /**
75
+ * If true, returns only contacts with bulk processing access grants
76
+ */
77
+ bulkOnly?: boolean;
43
78
  };
44
79
 
45
80
  export type FetchUserContactsParams = {
@@ -50,7 +85,14 @@ export type FetchUserContactsParams = {
50
85
  } & FetchMyContactsParams;
51
86
 
52
87
  export type SendEmailResponse = {
88
+ /**
89
+ * ID of the task
90
+ */
53
91
  taskId: string;
92
+ /**
93
+ * ID of the deal containing the task
94
+ */
95
+ dealId: string;
54
96
  };
55
97
 
56
98
  /**
@@ -100,3 +142,62 @@ export type Web3MailConfigOptions = {
100
142
  */
101
143
  allowExperimentalNetworks?: boolean;
102
144
  };
145
+
146
+ export type PrepareEmailCampaignParams = {
147
+ /**
148
+ * List of `GrantedAccess` to contacts to send emails to in bulk.
149
+ *
150
+ * use `fetchMyContacts({ bulkOnly: true })` to get granted accesses.
151
+ */
152
+ grantedAccesses: GrantedAccess[];
153
+ maxProtectedDataPerTask?: number;
154
+ senderName?: string;
155
+ emailSubject: string;
156
+ emailContent: string;
157
+ contentType?: string;
158
+ label?: string;
159
+ workerpoolAddressOrEns?: AddressOrENS;
160
+ dataMaxPrice?: number;
161
+ appMaxPrice?: number;
162
+ workerpoolMaxPrice?: number;
163
+ };
164
+
165
+ export type PrepareEmailCampaignResponse = {
166
+ /**
167
+ * The prepared campaign request
168
+ *
169
+ * Use this in `sendEmailCampaign()` to start or continue sending the campaign
170
+ */
171
+ campaignRequest: CampaignRequest;
172
+ };
173
+
174
+ export type SendEmailCampaignParams = {
175
+ /**
176
+ * The prepared campaign request from `prepareEmailCampaign()`
177
+ */
178
+ campaignRequest: CampaignRequest;
179
+ /**
180
+ * Workerpool address or ENS to use for processing
181
+ */
182
+ workerpoolAddressOrEns?: AddressOrENS;
183
+ };
184
+
185
+ export type SendEmailCampaignResponse = {
186
+ /**
187
+ * List of tasks created for the campaign
188
+ */
189
+ tasks: Array<{
190
+ /**
191
+ * ID of the task
192
+ */
193
+ taskId: string;
194
+ /**
195
+ * ID of the deal containing the task
196
+ */
197
+ dealId: string;
198
+ /**
199
+ * Index of the task in the bulk request
200
+ */
201
+ bulkIndex: number;
202
+ }>;
203
+ };