Skip to content

Commit 1eabf93

Browse files
feat: add bulk Telegram campaigns (#83)
Co-authored-by: Pierre Jeanjacquot <[email protected]>
1 parent 088b0cf commit 1eabf93

23 files changed

+7248
-1841
lines changed

package-lock.json

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

package.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,18 @@
2020
"scripts": {
2121
"build": "rm -rf dist && tsc --project tsconfig.build.json",
2222
"check-types": "tsc --noEmit",
23-
"test:prepare": "node tests/scripts/prepare-bellecour-fork-for-tests.js",
23+
"test:prepare": "node tests/scripts/prepare-bellecour-fork-for-tests.js && node tests/scripts/prepare-iexec.js",
2424
"test": "NODE_OPTIONS=--experimental-vm-modules jest --testMatch \"**/tests/**/*.test.ts\" --forceExit -b",
2525
"test:coverage": "NODE_OPTIONS=--experimental-vm-modules jest --testMatch \"**/tests/**/*.test.ts\" --forceExit --coverage",
2626
"test:unit": "NODE_OPTIONS=--experimental-vm-modules jest --testMatch \"**/tests/unit/**/*.test.ts\" -b",
2727
"test:unit:coverage": "NODE_OPTIONS=--experimental-vm-modules jest --testMatch \"**/tests/unit/**/*.unit.ts\" --coverage",
2828
"test:e2e": "NODE_OPTIONS=--experimental-vm-modules jest --testMatch \"**/tests/e2e/**/*.test.ts\" --forceExit -b",
2929
"test:e2e:coverage": "NODE_OPTIONS=--experimental-vm-modules jest --testMatch \"**/tests/e2e/**/*.test.ts\" --coverage",
3030
"lint": "eslint .",
31-
"format": "prettier --write \"src/**/*.ts\"",
32-
"check-format": "prettier --check \"src/**/*.ts\"",
31+
"format": "prettier --write \"src/**/*.ts\" \"tests/**/*.ts\"",
32+
"check-format": "prettier --check \"src/**/*.ts\" \"tests/**/*.ts\"",
3333
"stop-test-stack": "cd tests && docker compose down --volumes --remove-orphans",
34-
"start-test-stack": "cd tests && npm run stop-test-stack && node scripts/prepare-test-env.js && docker compose build && docker compose up -d && node scripts/prepare-bellecour-fork-for-tests.js"
34+
"start-test-stack": "cd tests && npm run stop-test-stack && node scripts/prepare-test-env.js && docker compose build && docker compose up -d && npm run test:prepare"
3535
},
3636
"repository": {
3737
"type": "git",
@@ -49,15 +49,15 @@
4949
"dependencies": {
5050
"@ethersproject/bytes": "^5.7.0",
5151
"@ethersproject/random": "^5.7.0",
52+
"@iexec/dataprotector": "^2.0.0-beta.21",
5253
"buffer": "^6.0.3",
5354
"ethers": "^6.8.1",
5455
"graphql-request": "^6.1.0",
55-
"iexec": "^8.20.0",
56+
"iexec": "^8.22.0",
5657
"kubo-rpc-client": "^4.1.3",
5758
"yup": "^1.1.1"
5859
},
5960
"devDependencies": {
60-
"@iexec/dataprotector": "^2.0.0-beta.19",
6161
"@jest/globals": "^29.7.0",
6262
"@swc/core": "^1.3.96",
6363
"@swc/jest": "^0.2.29",

src/utils/subgraphQuery.ts

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -71,20 +71,23 @@ export const getValidContact = async (
7171
);
7272

7373
// Convert protectedData[] into Contact[] using the map for constant time lookups
74-
return protectedDataList.map(({ id, name }) => {
75-
const contact = contactsMap.get(id);
76-
if (contact) {
77-
return {
78-
address: id,
79-
name: name,
80-
remainingAccess: contact.remainingAccess,
81-
accessPrice: contact.accessPrice,
82-
owner: contact.owner,
83-
accessGrantTimestamp: contact.accessGrantTimestamp,
84-
isUserStrict: contact.isUserStrict,
85-
};
86-
}
87-
});
74+
return protectedDataList
75+
.map(({ id, name }) => {
76+
const contact = contactsMap.get(id);
77+
if (contact) {
78+
return {
79+
address: id,
80+
name: name,
81+
remainingAccess: contact.remainingAccess,
82+
accessPrice: contact.accessPrice,
83+
owner: contact.owner,
84+
accessGrantTimestamp: contact.accessGrantTimestamp,
85+
isUserStrict: contact.isUserStrict,
86+
grantedAccess: contact.grantedAccess,
87+
};
88+
}
89+
})
90+
.filter((contact) => !!contact);
8891
} catch (error) {
8992
throw new WorkflowError({
9093
message: 'Failed to fetch subgraph',

src/utils/validators.ts

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { isAddress } from 'ethers';
22
import { IExec } from 'iexec';
3-
import { ValidationError, boolean, number, string } from 'yup';
3+
import { NULL_ADDRESS } from 'iexec/utils';
4+
import { ValidationError, boolean, number, object, string } from 'yup';
45

56
export const isValidProvider = async (iexec: IExec) => {
67
const client = await iexec.config.resolveContractsClient();
@@ -53,3 +54,59 @@ export const positiveNumberSchema = () =>
5354

5455
export const booleanSchema = () =>
5556
boolean().strict().typeError('${path} should be a boolean');
57+
58+
const isPositiveIntegerStringTest = (value: string) => /^\d+$/.test(value);
59+
60+
const stringSchema = () =>
61+
string().strict().typeError('${path} should be a string');
62+
63+
const positiveIntegerStringSchema = () =>
64+
string().test(
65+
'is-positive-int',
66+
'${path} should be a positive integer',
67+
(value) => isUndefined(value) || isPositiveIntegerStringTest(value)
68+
);
69+
70+
const positiveStrictIntegerStringSchema = () =>
71+
string().test(
72+
'is-positive-strict-int',
73+
'${path} should be a strictly positive integer',
74+
(value) =>
75+
isUndefined(value) ||
76+
(value !== '0' && isPositiveIntegerStringTest(value))
77+
);
78+
79+
export const campaignRequestSchema = () =>
80+
object({
81+
app: addressSchema().required(),
82+
appmaxprice: positiveIntegerStringSchema().required(),
83+
workerpool: addressSchema().required(),
84+
workerpoolmaxprice: positiveIntegerStringSchema().required(),
85+
dataset: addressSchema().oneOf([NULL_ADDRESS]).required(),
86+
datasetmaxprice: positiveIntegerStringSchema().oneOf(['0']).required(),
87+
params: stringSchema()
88+
.test(
89+
'is-valid-bulk-params',
90+
'${path} should be a valid JSON string with bulk_cid field',
91+
(value) => {
92+
try {
93+
// eslint-disable-next-line @typescript-eslint/naming-convention
94+
const { bulk_cid } = JSON.parse(value);
95+
if (typeof bulk_cid === 'string') {
96+
return true;
97+
}
98+
} catch {}
99+
return false;
100+
}
101+
)
102+
.required(),
103+
requester: addressSchema().required(),
104+
beneficiary: addressSchema().required(),
105+
callback: addressSchema().required(),
106+
category: positiveIntegerStringSchema().required(),
107+
volume: positiveStrictIntegerStringSchema().required(),
108+
tag: stringSchema().required(),
109+
trust: positiveIntegerStringSchema().required(),
110+
salt: stringSchema().required(),
111+
sign: stringSchema().required(),
112+
}).typeError('${path} should be a BulkRequest object');

src/web3telegram/IExecWeb3telegram.ts

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,30 @@
11
import { AbstractProvider, AbstractSigner, Eip1193Provider } from 'ethers';
22
import { IExec } from 'iexec';
3+
import { IExecDataProtectorCore } from '@iexec/dataprotector';
34
import { GraphQLClient } from 'graphql-request';
45
import { fetchUserContacts } from './fetchUserContacts.js';
56
import { fetchMyContacts } from './fetchMyContacts.js';
67
import { sendTelegram } from './sendTelegram.js';
8+
import { sendTelegramCampaign } from './sendTelegramCampaign.js';
79
import {
810
Contact,
911
FetchUserContactsParams,
1012
SendTelegramParams,
1113
AddressOrENS,
1214
Web3TelegramConfigOptions,
13-
SendTelegramResponse,
1415
Web3SignerProvider,
1516
FetchMyContactsParams,
17+
SendTelegramResponse,
18+
PrepareTelegramCampaignResponse,
19+
PrepareTelegramCampaignParams,
20+
SendTelegramCampaignParams,
21+
SendTelegramCampaignResponse,
1622
} from './types.js';
1723
import { getChainDefaultConfig } from '../config/config.js';
1824
import { isValidProvider } from '../utils/validators.js';
1925
import { getChainIdFromProvider } from '../utils/getChainId.js';
2026
import { resolveDappAddressFromCompass } from '../utils/resolveDappAddressFromCompass.js';
27+
import { prepareTelegramCampaign } from './prepareTelegramCampaign.js';
2128

2229
type EthersCompatibleProvider =
2330
| AbstractProvider
@@ -34,6 +41,7 @@ interface Web3telegramResolvedConfig {
3441
ipfsGateway: string;
3542
defaultWorkerpool: string;
3643
iexec: IExec;
44+
dataProtector: IExecDataProtectorCore;
3745
}
3846

3947
export class IExecWeb3telegram {
@@ -51,6 +59,8 @@ export class IExecWeb3telegram {
5159

5260
private iexec!: IExec;
5361

62+
private dataProtector!: IExecDataProtectorCore;
63+
5464
private initPromise: Promise<void> | null = null;
5565

5666
private ethProvider: EthersCompatibleProvider;
@@ -75,6 +85,7 @@ export class IExecWeb3telegram {
7585
this.ipfsGateway = config.ipfsGateway;
7686
this.defaultWorkerpool = config.defaultWorkerpool;
7787
this.iexec = config.iexec;
88+
this.dataProtector = config.dataProtector;
7889
});
7990
}
8091
return this.initPromise;
@@ -121,6 +132,34 @@ export class IExecWeb3telegram {
121132
});
122133
}
123134

135+
async sendTelegramCampaign(
136+
args: SendTelegramCampaignParams
137+
): Promise<SendTelegramCampaignResponse> {
138+
await this.init();
139+
await isValidProvider(this.iexec);
140+
return sendTelegramCampaign({
141+
...args,
142+
workerpoolAddressOrEns:
143+
args.workerpoolAddressOrEns || this.defaultWorkerpool,
144+
dataProtector: this.dataProtector,
145+
});
146+
}
147+
148+
async prepareTelegramCampaign(
149+
args: PrepareTelegramCampaignParams
150+
): Promise<PrepareTelegramCampaignResponse> {
151+
await this.init();
152+
153+
return prepareTelegramCampaign({
154+
...args,
155+
iexec: this.iexec,
156+
dataProtector: this.dataProtector,
157+
ipfsNode: this.ipfsNode,
158+
ipfsGateway: this.ipfsGateway,
159+
dappAddressOrENS: this.dappAddressOrENS,
160+
});
161+
}
162+
124163
private async resolveConfig(): Promise<Web3telegramResolvedConfig> {
125164
const chainId = await getChainIdFromProvider(this.ethProvider);
126165
const chainDefaultConfig = getChainDefaultConfig(chainId, {
@@ -185,6 +224,17 @@ export class IExecWeb3telegram {
185224
throw new Error(`Failed to create GraphQLClient: ${error.message}`);
186225
}
187226

227+
const dataProtector = new IExecDataProtectorCore(this.ethProvider, {
228+
iexecOptions: {
229+
ipfsGatewayURL: ipfsGateway,
230+
...this.options?.iexecOptions,
231+
allowExperimentalNetworks: this.options.allowExperimentalNetworks,
232+
},
233+
ipfsGateway,
234+
ipfsNode,
235+
subgraphUrl,
236+
});
237+
188238
return {
189239
dappAddressOrENS,
190240
dappWhitelistAddress: dappWhitelistAddress.toLowerCase(),
@@ -193,6 +243,7 @@ export class IExecWeb3telegram {
193243
ipfsNode,
194244
ipfsGateway,
195245
iexec,
246+
dataProtector,
196247
};
197248
}
198249
}

src/web3telegram/fetchMyContacts.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export const fetchMyContacts = async ({
1616
dappAddressOrENS = throwIfMissing(),
1717
dappWhitelistAddress = throwIfMissing(),
1818
isUserStrict = false,
19+
bulkOnly = false,
1920
}: IExecConsumer &
2021
SubgraphConsumer &
2122
DappAddressConsumer &
@@ -33,5 +34,6 @@ export const fetchMyContacts = async ({
3334
dappWhitelistAddress,
3435
userAddress,
3536
isUserStrict: vIsUserStrict,
37+
bulkOnly,
3638
});
3739
};

src/web3telegram/fetchUserContacts.ts

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
isEnsTest,
1111
throwIfMissing,
1212
} from '../utils/validators.js';
13-
import { Contact, FetchUserContactsParams } from './types.js';
13+
import { Contact, FetchUserContactsParams, GrantedAccess } from './types.js';
1414
import { IExec } from 'iexec';
1515
import { PublishedDatasetorder } from 'iexec/IExecOrderbookModule';
1616
import {
@@ -27,6 +27,7 @@ export const fetchUserContacts = async ({
2727
dappWhitelistAddress = throwIfMissing(),
2828
userAddress,
2929
isUserStrict = false,
30+
bulkOnly = false,
3031
}: IExecConsumer &
3132
SubgraphConsumer &
3233
DappAddressConsumer &
@@ -48,19 +49,22 @@ export const fetchUserContacts = async ({
4849
const vIsUserStrict = booleanSchema()
4950
.label('isUserStrict')
5051
.validateSync(isUserStrict);
52+
const vBulkOnly = booleanSchema().label('bulkOnly').validateSync(bulkOnly);
5153

5254
const [dappOrders, whitelistOrders] = await Promise.all([
5355
fetchAllOrdersByApp({
5456
iexec,
5557
userAddress: vUserAddress,
5658
appAddress: vDappAddressOrENS,
5759
isUserStrict: vIsUserStrict,
60+
bulkOnly: vBulkOnly,
5861
}),
5962
fetchAllOrdersByApp({
6063
iexec,
6164
userAddress: vUserAddress,
6265
appAddress: vDappWhitelistAddress,
6366
isUserStrict: vIsUserStrict,
67+
bulkOnly: vBulkOnly,
6468
}),
6569
]);
6670
const orders = dappOrders.concat(whitelistOrders);
@@ -76,13 +80,25 @@ export const fetchUserContacts = async ({
7680
order.order.apprestrict.toLowerCase() ===
7781
vDappWhitelistAddress.toLowerCase()
7882
) {
79-
const contact = {
83+
const contact: Contact = {
8084
address: order.order.dataset.toLowerCase(),
8185
owner: order.signer.toLowerCase(),
8286
remainingAccess: order.remaining,
8387
accessPrice: order.order.datasetprice,
8488
accessGrantTimestamp: order.publicationTimestamp,
8589
isUserStrict: order.order.requesterrestrict !== ZeroAddress,
90+
grantedAccess: {
91+
dataset: order.order.dataset,
92+
datasetprice: order.order.datasetprice.toString(),
93+
volume: order.order.volume.toString(),
94+
tag: order.order.tag,
95+
apprestrict: order.order.apprestrict,
96+
workerpoolrestrict: order.order.workerpoolrestrict,
97+
requesterrestrict: order.order.requesterrestrict,
98+
salt: order.order.salt,
99+
sign: order.order.sign,
100+
remainingAccess: order.remaining,
101+
} as GrantedAccess,
86102
};
87103
myContacts.push(contact);
88104
}
@@ -105,23 +121,24 @@ async function fetchAllOrdersByApp({
105121
userAddress,
106122
appAddress,
107123
isUserStrict,
124+
bulkOnly,
108125
}: {
109126
iexec: IExec;
110127
userAddress: string;
111128
appAddress: string;
112129
isUserStrict: boolean;
130+
bulkOnly?: boolean;
113131
}): Promise<PublishedDatasetorder[]> {
114-
const ordersFirstPage = iexec.orderbook.fetchDatasetOrderbook(
115-
ANY_DATASET_ADDRESS,
116-
{
117-
app: appAddress,
118-
requester: userAddress,
119-
isAppStrict: true,
120-
isRequesterStrict: isUserStrict,
121-
// Use maxPageSize here to avoid too many round-trips (we want everything anyway)
122-
pageSize: 1000,
123-
}
124-
);
132+
const ordersFirstPage = iexec.orderbook.fetchDatasetOrderbook({
133+
dataset: ANY_DATASET_ADDRESS,
134+
app: appAddress,
135+
requester: userAddress,
136+
isAppStrict: true,
137+
isRequesterStrict: isUserStrict,
138+
bulkOnly,
139+
// Use maxPageSize here to avoid too many round-trips (we want everything anyway)
140+
pageSize: 1000,
141+
});
125142
const { orders: allOrders } = await autoPaginateRequest({
126143
request: ordersFirstPage,
127144
});

0 commit comments

Comments
 (0)