Skip to content

Commit 3458c95

Browse files
feat(iapp)!: add bulk processing support (#234)
BREAKING CHANGE: result file renamed to result.json; single protectedData result file now contains {"success": <boolean>, "protectedData"?: <address>, "isEmailValid"?: <boolean>, "error"?: <string> }; upon error, the iapp will now exit 0 and output "success": false and "error": <string> rather than falling the task. Co-authored-by: Pierre Jeanjacquot <[email protected]>
1 parent 12c0ca4 commit 3458c95

File tree

11 files changed

+6249
-10152
lines changed

11 files changed

+6249
-10152
lines changed

dapp/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
.env
22
node_modules
33
coverage
4+
tests/_test_outputs_

dapp/package-lock.json

Lines changed: 5072 additions & 9582 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dapp/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
"eslint-config-prettier": "^9.0.0",
3737
"eslint-plugin-import": "^2.28.1",
3838
"eslint-plugin-sonarjs": "^0.21.0",
39-
"iexec": "^8.2.1",
39+
"iexec": "^8.22.0",
4040
"jest": "^29.7.0",
4141
"prettier": "^2.8.8"
4242
}

dapp/src/app.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
const start = require('./sendEmail');
1+
const start = require('./executeTask');
22

33
start().catch((error) => {
44
console.error(`Error: ${error.message}`);

dapp/src/checkEmailPreviousValidation.js

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
const { request, gql } = require('graphql-request');
22

3+
/**
4+
* returns true if valid, false if invalid, undefined if no prior validation or unable to check
5+
*/
36
async function checkEmailPreviousValidation({
47
datasetAddress,
58
dappAddresses,
69
pocoSubgraphUrl,
710
}) {
11+
// TODO: add check in bulk task
812
const query = gql`
913
query checkSuccessfulTaskQuery($apps: [String!], $dataset: String!) {
1014
tasks(
@@ -27,23 +31,38 @@ async function checkEmailPreviousValidation({
2731
try {
2832
const data = await request(pocoSubgraphUrl, query, variables);
2933
const tasks = data?.tasks || [];
30-
31-
return tasks.some((task) => {
32-
const callback = task.resultsCallback?.toLowerCase();
33-
return (
34-
callback &&
35-
callback.startsWith('0x') &&
36-
callback.endsWith(
37-
'0000000000000000000000000000000000000000000000000000000000000001'
38-
)
39-
);
40-
});
34+
if (
35+
tasks.some((task) => {
36+
const callback = task.resultsCallback?.toLowerCase();
37+
return (
38+
callback ===
39+
'0x0000000000000000000000000000000000000000000000000000000000000001' || // 0b01 legacy format valid
40+
callback ===
41+
'0x0000000000000000000000000000000000000000000000000000000000000003' // 0b11 checked valid
42+
);
43+
})
44+
) {
45+
return true;
46+
}
47+
if (
48+
tasks.some((task) => {
49+
const callback = task.resultsCallback?.toLowerCase();
50+
return (
51+
callback ===
52+
'0x0000000000000000000000000000000000000000000000000000000000000002' // 0b10 checked invalid
53+
);
54+
})
55+
) {
56+
return false;
57+
}
58+
// no prior validation found
59+
return undefined;
4160
} catch (error) {
4261
console.error(
4362
'GraphQL error:',
4463
error.response?.errors || error.message || error
4564
);
46-
return false;
65+
return undefined;
4766
}
4867
}
4968

dapp/src/emailService.js

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,5 @@ async function sendEmail({
4040
.catch(() => {
4141
throw new Error('Failed to send email');
4242
});
43-
return {
44-
message: 'Your email has been sent successfully.',
45-
status: 200,
46-
};
4743
}
4844
module.exports = sendEmail;

dapp/src/executeTask.js

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
const { promises: fs } = require('fs');
2+
const {
3+
IExecDataProtectorDeserializer,
4+
} = require('@iexec/dataprotector-deserializer');
5+
const sendEmail = require('./emailService');
6+
const {
7+
validateWorkerEnv,
8+
validateAppSecret,
9+
validateRequesterSecret,
10+
validateProtectedData,
11+
} = require('./validation');
12+
const {
13+
downloadEncryptedContent,
14+
decryptContent,
15+
} = require('./decryptEmailContent');
16+
const { validateEmailAddress } = require('./validateEmailAddress');
17+
const {
18+
checkEmailPreviousValidation,
19+
} = require('./checkEmailPreviousValidation');
20+
21+
async function processProtectedData({
22+
index,
23+
IEXEC_IN,
24+
appDeveloperSecret,
25+
requesterSecret,
26+
}) {
27+
const datasetFilename =
28+
index > 0
29+
? process.env[`IEXEC_DATASET_${index}_FILENAME`]
30+
: process.env.IEXEC_DATASET_FILENAME;
31+
const result = {
32+
index,
33+
protectedData: datasetFilename,
34+
isEmailValid: undefined,
35+
};
36+
try {
37+
let protectedData;
38+
try {
39+
const deserializerConfig = datasetFilename
40+
? { protectedDataPath: `${IEXEC_IN}/${datasetFilename}` }
41+
: {};
42+
const deserializer = new IExecDataProtectorDeserializer(
43+
deserializerConfig
44+
);
45+
protectedData = {
46+
email: await deserializer.getValue('email', 'string'),
47+
};
48+
} catch (e) {
49+
throw Error(`Failed to parse ProtectedData ${index}: ${e.message}`);
50+
}
51+
52+
// Validate the protected data
53+
validateProtectedData(protectedData);
54+
55+
// Step 1: Check if email was already validated
56+
result.isEmailValid = await checkEmailPreviousValidation({
57+
datasetAddress: protectedData.email,
58+
dappAddresses: appDeveloperSecret.WEB3MAIL_WHITELISTED_APPS,
59+
pocoSubgraphUrl: appDeveloperSecret.POCO_SUBGRAPH_URL,
60+
});
61+
62+
// Step 2: If not, try Mailgun
63+
if (result.isEmailValid === undefined) {
64+
console.log('No prior verification found. Trying Mailgun...');
65+
result.isEmailValid = await validateEmailAddress({
66+
emailAddress: protectedData.email,
67+
mailgunApiKey: appDeveloperSecret.MAILGUN_APIKEY,
68+
});
69+
} else {
70+
console.log('Email already verified, skipping Mailgun check.');
71+
}
72+
73+
if (result.isEmailValid === false) {
74+
throw Error('The protected email address seems to be invalid.');
75+
}
76+
77+
// Step 3: Decrypt email content
78+
const encryptedEmailContent = await downloadEncryptedContent(
79+
requesterSecret.emailContentMultiAddr
80+
);
81+
const requesterEmailContent = decryptContent(
82+
encryptedEmailContent,
83+
requesterSecret.emailContentEncryptionKey
84+
);
85+
86+
// Step 4: Send email
87+
await sendEmail({
88+
email: protectedData.email,
89+
mailJetApiKeyPublic: appDeveloperSecret.MJ_APIKEY_PUBLIC,
90+
mailJetApiKeyPrivate: appDeveloperSecret.MJ_APIKEY_PRIVATE,
91+
mailJetSender: appDeveloperSecret.MJ_SENDER,
92+
mailgunApiKey: appDeveloperSecret.MAILGUN_APIKEY,
93+
emailContent: requesterEmailContent,
94+
emailSubject: requesterSecret.emailSubject,
95+
contentType: requesterSecret.contentType,
96+
senderName: requesterSecret.senderName,
97+
});
98+
result.success = true;
99+
} catch (e) {
100+
result.success = false;
101+
result.error = e.message;
102+
}
103+
console.log(`Protected data ${index} processed:`, result);
104+
return result;
105+
}
106+
107+
async function start() {
108+
const {
109+
IEXEC_IN,
110+
IEXEC_OUT,
111+
IEXEC_APP_DEVELOPER_SECRET,
112+
IEXEC_REQUESTER_SECRET_1,
113+
IEXEC_BULK_SLICE_SIZE,
114+
} = process.env;
115+
116+
// Check worker env
117+
const workerEnv = validateWorkerEnv({ IEXEC_OUT });
118+
119+
let result; // { success: boolean, error?: string, protectedData?: string, results?: { index: number, protectedData: string, success: boolean, error?: string }[] }
120+
let callbackData;
121+
try {
122+
// Parse the app developer secret environment variable
123+
let appDeveloperSecret;
124+
try {
125+
appDeveloperSecret = JSON.parse(IEXEC_APP_DEVELOPER_SECRET);
126+
appDeveloperSecret.WEB3MAIL_WHITELISTED_APPS =
127+
appDeveloperSecret.WEB3MAIL_WHITELISTED_APPS
128+
? JSON.parse(appDeveloperSecret.WEB3MAIL_WHITELISTED_APPS)
129+
: undefined;
130+
} catch {
131+
throw Error('Failed to parse the developer secret');
132+
}
133+
appDeveloperSecret = validateAppSecret(appDeveloperSecret);
134+
135+
let requesterSecret;
136+
try {
137+
requesterSecret = JSON.parse(IEXEC_REQUESTER_SECRET_1);
138+
} catch {
139+
throw Error('Failed to parse requester secret');
140+
}
141+
requesterSecret = validateRequesterSecret(requesterSecret);
142+
143+
const bulkSize = parseInt(IEXEC_BULK_SLICE_SIZE, 10) || 0;
144+
145+
// Process multiple protected data
146+
if (bulkSize > 0) {
147+
console.log(`Processing ${bulkSize} protected data...`);
148+
const results = [];
149+
// Process each protected data one by one to avoid rate limiting issues
150+
for (let index = 1; index <= bulkSize; index += 1) {
151+
// eslint-disable-next-line no-await-in-loop
152+
const protectedDataResult = await processProtectedData({
153+
index,
154+
IEXEC_IN,
155+
appDeveloperSecret,
156+
requesterSecret,
157+
});
158+
results.push(protectedDataResult);
159+
// eslint-disable-next-line no-await-in-loop
160+
await new Promise((res) => {
161+
setTimeout(res, 1000);
162+
}); // Add a delay to avoid rate limiting
163+
}
164+
const successCount = results.filter((r) => r.success === true).length;
165+
const errorCount = results.filter((r) => r.success !== true).length;
166+
result = {
167+
success: errorCount === 0,
168+
error: errorCount > 0 ? 'Partial failure' : undefined,
169+
totalCount: results.length,
170+
successCount,
171+
errorCount,
172+
results: results.map((r) => ({
173+
index: r.index,
174+
protectedData: r.protectedData,
175+
success: r.success,
176+
isEmailValid: r.isEmailValid,
177+
error: r.error,
178+
})),
179+
};
180+
} else {
181+
console.log('Processing single protected data...');
182+
const { protectedData, success, error, isEmailValid } =
183+
await processProtectedData({
184+
index: 0,
185+
IEXEC_IN,
186+
appDeveloperSecret,
187+
requesterSecret,
188+
});
189+
// set result json
190+
result = { protectedData, success, isEmailValid, error };
191+
// Add callback data for single processing if useCallback is enabled
192+
if (requesterSecret.useCallback) {
193+
const bool32Bytes = Buffer.alloc(32);
194+
// Encode 2 bits:
195+
// - Bit 1: Email validation was performed (1 = yes, 0 = no)
196+
// - Bit 0: Email is valid (1 = yes, 0 = no)
197+
if (isEmailValid === true) {
198+
// eslint-disable-next-line no-bitwise
199+
bool32Bytes[31] |= 0b11;
200+
} else if (isEmailValid === false) {
201+
// eslint-disable-next-line no-bitwise
202+
bool32Bytes[31] |= 0b10;
203+
}
204+
callbackData = `0x${bool32Bytes.toString('hex')}`;
205+
}
206+
}
207+
} catch (e) {
208+
console.error('Something went wrong:', e.message);
209+
result = { success: false, error: e.message };
210+
}
211+
212+
console.log('Writing results:', JSON.stringify(result));
213+
await fs.writeFile(
214+
`${workerEnv.IEXEC_OUT}/result.json`,
215+
JSON.stringify(result, null, 2)
216+
);
217+
218+
const computedData = {
219+
'deterministic-output-path': `${workerEnv.IEXEC_OUT}/result.json`,
220+
'callback-data': callbackData,
221+
};
222+
223+
await fs.writeFile(
224+
`${workerEnv.IEXEC_OUT}/computed.json`,
225+
JSON.stringify(computedData, null, 2)
226+
);
227+
}
228+
229+
module.exports = start;

0 commit comments

Comments
 (0)