Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
91ea715
feat: add grantedAccess property to Contact type for bulk processing …
abbesBenayache Oct 24, 2025
d7d866c
refactor: sendTelegram to use processProtectedData from IExecDataProt…
abbesBenayache Oct 27, 2025
e771eba
refactor: update fetchDatasetOrderbook, moove dataset parameter from …
abbesBenayache Oct 28, 2025
89fd0e9
feat: add bulk telegram message processing support
abbesBenayache Oct 28, 2025
a46ed8a
chore: update iexec dependency to exact version for bulk processing
abbesBenayache Oct 29, 2025
7a20180
refactor: update types for bulk processing
abbesBenayache Oct 29, 2025
596edb3
feat: add bulkOnly parameter to fetchMyContacts and fetchUserContacts
abbesBenayache Oct 29, 2025
a0d7095
refactor: refactor sendTelegram to use grantedAccess instead of bulkR…
abbesBenayache Oct 29, 2025
8fe349d
test: add unit tests for bulkOnly parameter in fetchMyContacts
abbesBenayache Oct 29, 2025
4f72d3d
test: refactor sendTelegram unit tests for grantedAccess
abbesBenayache Oct 29, 2025
f2fbc23
test: add e2e tests for bulkOnly parameter
abbesBenayache Oct 29, 2025
9e3931f
test: update sendTelegramBulk e2e test for grantedAccess API
abbesBenayache Oct 29, 2025
69f71c9
ci: allow nightly releases from any branch
abbesBenayache Nov 3, 2025
6459c41
chore: sync .github workflows from main
abbesBenayache Nov 3, 2025
fe889e7
ci: enable nightly tag promotion from any branch
abbesBenayache Nov 3, 2025
9b30233
ci: resolve merge conflict by integrating nightly release support int…
abbesBenayache Nov 3, 2025
ef292c0
Merge remote-tracking branch 'origin/main' into feat/sdk-bulk-process…
abbesBenayache Nov 3, 2025
f8c2c99
chore: update dependencies to official releases
abbesBenayache Nov 4, 2025
4f3512b
fix: update bulk processing for iexec SDK v8.22.0
abbesBenayache Nov 4, 2025
e57d35b
ci: trigger all tests
abbesBenayache Nov 4, 2025
d56cd36
test: update unit test to use bulkAccesses instead of bulkOrders
abbesBenayache Nov 4, 2025
b2e371c
refactor: SendTelegramResponse conditional typing
PierreJeanjacquot Nov 4, 2025
df46c8c
feat: add prepareTelegramCampaign WIP
PierreJeanjacquot Nov 5, 2025
f57ec89
fix: workerpoolAddressOrEns is optional
PierreJeanjacquot Nov 5, 2025
90edd26
test: patch iexec sdk for tests (enable testing on bellecour fork)
PierreJeanjacquot Nov 5, 2025
10592b2
test: prepareTelegramCampaign test
PierreJeanjacquot Nov 5, 2025
e8df98d
refactor: extract bulk processing to sendTelegramCampaign function
abbesBenayache Nov 5, 2025
9cb3b48
chore: trigger CI tests
abbesBenayache Nov 6, 2025
dfb8682
Merge remote-tracking branch 'origin/main' into feat/sdk-bulk-process…
abbesBenayache Nov 6, 2025
600573b
fix: resolve ENS names to addresses in createAndPublishAppOrders
abbesBenayache Nov 6, 2025
5b37ed5
chore: stop using deprecated function call
PierreJeanjacquot Nov 6, 2025
0cbdb8d
fix: improve protocol error handling in sendTelegram and fix test
abbesBenayache Nov 6, 2025
ee11dba
chore: restore npm publish ci
PierreJeanjacquot Nov 6, 2025
3fcb83b
refactor: simplify code
PierreJeanjacquot Nov 6, 2025
ec12e18
refactor: move types
PierreJeanjacquot Nov 6, 2025
9c76421
refactor: move types
PierreJeanjacquot Nov 6, 2025
86a1bc0
refactor: remove useless type
PierreJeanjacquot Nov 6, 2025
46bb6b8
refactor: clean types
PierreJeanjacquot Nov 6, 2025
4e3063a
refactor: fix typo in file name
PierreJeanjacquot Nov 6, 2025
93a32d5
feat: add campaignRequest validation
PierreJeanjacquot Nov 6, 2025
e86e320
test: update tests timeout
PierreJeanjacquot Nov 6, 2025
c08e432
chore: update market API for tests
PierreJeanjacquot Nov 6, 2025
a69a146
chore: remove useless apporder publication in test
PierreJeanjacquot Nov 6, 2025
bb795b8
style: format tests
PierreJeanjacquot Nov 6, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6,658 changes: 4,987 additions & 1,671 deletions package-lock.json

Large diffs are not rendered by default.

12 changes: 6 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,18 @@
"scripts": {
"build": "rm -rf dist && tsc --project tsconfig.build.json",
"check-types": "tsc --noEmit",
"test:prepare": "node tests/scripts/prepare-bellecour-fork-for-tests.js",
"test:prepare": "node tests/scripts/prepare-bellecour-fork-for-tests.js && node tests/scripts/prepare-iexec.js",
"test": "NODE_OPTIONS=--experimental-vm-modules jest --testMatch \"**/tests/**/*.test.ts\" --forceExit -b",
"test:coverage": "NODE_OPTIONS=--experimental-vm-modules jest --testMatch \"**/tests/**/*.test.ts\" --forceExit --coverage",
"test:unit": "NODE_OPTIONS=--experimental-vm-modules jest --testMatch \"**/tests/unit/**/*.test.ts\" -b",
"test:unit:coverage": "NODE_OPTIONS=--experimental-vm-modules jest --testMatch \"**/tests/unit/**/*.unit.ts\" --coverage",
"test:e2e": "NODE_OPTIONS=--experimental-vm-modules jest --testMatch \"**/tests/e2e/**/*.test.ts\" --forceExit -b",
"test:e2e:coverage": "NODE_OPTIONS=--experimental-vm-modules jest --testMatch \"**/tests/e2e/**/*.test.ts\" --coverage",
"lint": "eslint .",
"format": "prettier --write \"src/**/*.ts\"",
"check-format": "prettier --check \"src/**/*.ts\"",
"format": "prettier --write \"src/**/*.ts\" \"tests/**/*.ts\"",
"check-format": "prettier --check \"src/**/*.ts\" \"tests/**/*.ts\"",
"stop-test-stack": "cd tests && docker compose down --volumes --remove-orphans",
"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"
"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"
},
"repository": {
"type": "git",
Expand All @@ -49,15 +49,15 @@
"dependencies": {
"@ethersproject/bytes": "^5.7.0",
"@ethersproject/random": "^5.7.0",
"@iexec/dataprotector": "^2.0.0-beta.21",
"buffer": "^6.0.3",
"ethers": "^6.8.1",
"graphql-request": "^6.1.0",
"iexec": "^8.20.0",
"iexec": "^8.22.0",
"kubo-rpc-client": "^4.1.3",
"yup": "^1.1.1"
},
"devDependencies": {
"@iexec/dataprotector": "^2.0.0-beta.19",
"@jest/globals": "^29.7.0",
"@swc/core": "^1.3.96",
"@swc/jest": "^0.2.29",
Expand Down
31 changes: 17 additions & 14 deletions src/utils/subgraphQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,20 +71,23 @@ export const getValidContact = async (
);

// Convert protectedData[] into Contact[] using the map for constant time lookups
return protectedDataList.map(({ id, name }) => {
const contact = contactsMap.get(id);
if (contact) {
return {
address: id,
name: name,
remainingAccess: contact.remainingAccess,
accessPrice: contact.accessPrice,
owner: contact.owner,
accessGrantTimestamp: contact.accessGrantTimestamp,
isUserStrict: contact.isUserStrict,
};
}
});
return protectedDataList
.map(({ id, name }) => {
const contact = contactsMap.get(id);
if (contact) {
return {
address: id,
name: name,
remainingAccess: contact.remainingAccess,
accessPrice: contact.accessPrice,
owner: contact.owner,
accessGrantTimestamp: contact.accessGrantTimestamp,
isUserStrict: contact.isUserStrict,
grantedAccess: contact.grantedAccess,
};
}
})
.filter((contact) => !!contact);
} catch (error) {
throw new WorkflowError({
message: 'Failed to fetch subgraph',
Expand Down
59 changes: 58 additions & 1 deletion src/utils/validators.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { isAddress } from 'ethers';
import { IExec } from 'iexec';
import { ValidationError, boolean, number, string } from 'yup';
import { NULL_ADDRESS } from 'iexec/utils';
import { ValidationError, boolean, number, object, string } from 'yup';

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

export const booleanSchema = () =>
boolean().strict().typeError('${path} should be a boolean');

const isPositiveIntegerStringTest = (value: string) => /^\d+$/.test(value);

const stringSchema = () =>
string().strict().typeError('${path} should be a string');

const positiveIntegerStringSchema = () =>
string().test(
'is-positive-int',
'${path} should be a positive integer',
(value) => isUndefined(value) || isPositiveIntegerStringTest(value)
);

const positiveStrictIntegerStringSchema = () =>
string().test(
'is-positive-strict-int',
'${path} should be a strictly positive integer',
(value) =>
isUndefined(value) ||
(value !== '0' && isPositiveIntegerStringTest(value))
);

export const campaignRequestSchema = () =>
object({
app: addressSchema().required(),
appmaxprice: positiveIntegerStringSchema().required(),
workerpool: addressSchema().required(),
workerpoolmaxprice: positiveIntegerStringSchema().required(),
dataset: addressSchema().oneOf([NULL_ADDRESS]).required(),
datasetmaxprice: positiveIntegerStringSchema().oneOf(['0']).required(),
params: stringSchema()
.test(
'is-valid-bulk-params',
'${path} should be a valid JSON string with bulk_cid field',
(value) => {
try {
// eslint-disable-next-line @typescript-eslint/naming-convention
const { bulk_cid } = JSON.parse(value);
if (typeof bulk_cid === 'string') {
return true;
}
} catch {}
return false;
}
)
.required(),
requester: addressSchema().required(),
beneficiary: addressSchema().required(),
callback: addressSchema().required(),
category: positiveIntegerStringSchema().required(),
volume: positiveStrictIntegerStringSchema().required(),
tag: stringSchema().required(),
trust: positiveIntegerStringSchema().required(),
salt: stringSchema().required(),
sign: stringSchema().required(),
}).typeError('${path} should be a BulkRequest object');
53 changes: 52 additions & 1 deletion src/web3telegram/IExecWeb3telegram.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,30 @@
import { AbstractProvider, AbstractSigner, Eip1193Provider } from 'ethers';
import { IExec } from 'iexec';
import { IExecDataProtectorCore } from '@iexec/dataprotector';
import { GraphQLClient } from 'graphql-request';
import { fetchUserContacts } from './fetchUserContacts.js';
import { fetchMyContacts } from './fetchMyContacts.js';
import { sendTelegram } from './sendTelegram.js';
import { sendTelegramCampaign } from './sendTelegramCampaign.js';
import {
Contact,
FetchUserContactsParams,
SendTelegramParams,
AddressOrENS,
Web3TelegramConfigOptions,
SendTelegramResponse,
Web3SignerProvider,
FetchMyContactsParams,
SendTelegramResponse,
PrepareTelegramCampaignResponse,
PrepareTelegramCampaignParams,
SendTelegramCampaignParams,
SendTelegramCampaignResponse,
} from './types.js';
import { getChainDefaultConfig } from '../config/config.js';
import { isValidProvider } from '../utils/validators.js';
import { getChainIdFromProvider } from '../utils/getChainId.js';
import { resolveDappAddressFromCompass } from '../utils/resolveDappAddressFromCompass.js';
import { prepareTelegramCampaign } from './prepareTelegramCampaign.js';

type EthersCompatibleProvider =
| AbstractProvider
Expand All @@ -34,6 +41,7 @@
ipfsGateway: string;
defaultWorkerpool: string;
iexec: IExec;
dataProtector: IExecDataProtectorCore;
}

export class IExecWeb3telegram {
Expand All @@ -51,6 +59,8 @@

private iexec!: IExec;

private dataProtector!: IExecDataProtectorCore;

private initPromise: Promise<void> | null = null;

private ethProvider: EthersCompatibleProvider;
Expand All @@ -75,6 +85,7 @@
this.ipfsGateway = config.ipfsGateway;
this.defaultWorkerpool = config.defaultWorkerpool;
this.iexec = config.iexec;
this.dataProtector = config.dataProtector;
});
}
return this.initPromise;
Expand Down Expand Up @@ -121,6 +132,34 @@
});
}

async sendTelegramCampaign(
args: SendTelegramCampaignParams
): Promise<SendTelegramCampaignResponse> {
await this.init();
await isValidProvider(this.iexec);
return sendTelegramCampaign({
...args,
workerpoolAddressOrEns:
args.workerpoolAddressOrEns || this.defaultWorkerpool,
dataProtector: this.dataProtector,
});
}

async prepareTelegramCampaign(
args: PrepareTelegramCampaignParams
): Promise<PrepareTelegramCampaignResponse> {
await this.init();

return prepareTelegramCampaign({
...args,
iexec: this.iexec,
dataProtector: this.dataProtector,
ipfsNode: this.ipfsNode,
ipfsGateway: this.ipfsGateway,
dappAddressOrENS: this.dappAddressOrENS,
});
}

private async resolveConfig(): Promise<Web3telegramResolvedConfig> {
const chainId = await getChainIdFromProvider(this.ethProvider);
const chainDefaultConfig = getChainDefaultConfig(chainId, {
Expand All @@ -141,7 +180,7 @@
allowExperimentalNetworks: this.options.allowExperimentalNetworks,
}
);
} catch (e: any) {

Check warning on line 183 in src/web3telegram/IExecWeb3telegram.ts

View workflow job for this annotation

GitHub Actions / check-code

Unexpected any. Specify a different type

Check warning on line 183 in src/web3telegram/IExecWeb3telegram.ts

View workflow job for this annotation

GitHub Actions / npm-publish-dry-run / publish-npm / build

Unexpected any. Specify a different type
throw new Error(`Unsupported ethProvider: ${e.message}`);
}

Expand Down Expand Up @@ -181,10 +220,21 @@

try {
graphQLClient = new GraphQLClient(subgraphUrl);
} catch (error: any) {

Check warning on line 223 in src/web3telegram/IExecWeb3telegram.ts

View workflow job for this annotation

GitHub Actions / check-code

Unexpected any. Specify a different type

Check warning on line 223 in src/web3telegram/IExecWeb3telegram.ts

View workflow job for this annotation

GitHub Actions / npm-publish-dry-run / publish-npm / build

Unexpected any. Specify a different type
throw new Error(`Failed to create GraphQLClient: ${error.message}`);
}

const dataProtector = new IExecDataProtectorCore(this.ethProvider, {
iexecOptions: {
ipfsGatewayURL: ipfsGateway,
...this.options?.iexecOptions,
allowExperimentalNetworks: this.options.allowExperimentalNetworks,
},
ipfsGateway,
ipfsNode,
subgraphUrl,
});

return {
dappAddressOrENS,
dappWhitelistAddress: dappWhitelistAddress.toLowerCase(),
Expand All @@ -193,6 +243,7 @@
ipfsNode,
ipfsGateway,
iexec,
dataProtector,
};
}
}
2 changes: 2 additions & 0 deletions src/web3telegram/fetchMyContacts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export const fetchMyContacts = async ({
dappAddressOrENS = throwIfMissing(),
dappWhitelistAddress = throwIfMissing(),
isUserStrict = false,
bulkOnly = false,
}: IExecConsumer &
SubgraphConsumer &
DappAddressConsumer &
Expand All @@ -33,5 +34,6 @@ export const fetchMyContacts = async ({
dappWhitelistAddress,
userAddress,
isUserStrict: vIsUserStrict,
bulkOnly,
});
};
43 changes: 30 additions & 13 deletions src/web3telegram/fetchUserContacts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
isEnsTest,
throwIfMissing,
} from '../utils/validators.js';
import { Contact, FetchUserContactsParams } from './types.js';
import { Contact, FetchUserContactsParams, GrantedAccess } from './types.js';
import { IExec } from 'iexec';
import { PublishedDatasetorder } from 'iexec/IExecOrderbookModule';
import {
Expand All @@ -27,6 +27,7 @@ export const fetchUserContacts = async ({
dappWhitelistAddress = throwIfMissing(),
userAddress,
isUserStrict = false,
bulkOnly = false,
}: IExecConsumer &
SubgraphConsumer &
DappAddressConsumer &
Expand All @@ -48,19 +49,22 @@ export const fetchUserContacts = async ({
const vIsUserStrict = booleanSchema()
.label('isUserStrict')
.validateSync(isUserStrict);
const vBulkOnly = booleanSchema().label('bulkOnly').validateSync(bulkOnly);

const [dappOrders, whitelistOrders] = await Promise.all([
fetchAllOrdersByApp({
iexec,
userAddress: vUserAddress,
appAddress: vDappAddressOrENS,
isUserStrict: vIsUserStrict,
bulkOnly: vBulkOnly,
}),
fetchAllOrdersByApp({
iexec,
userAddress: vUserAddress,
appAddress: vDappWhitelistAddress,
isUserStrict: vIsUserStrict,
bulkOnly: vBulkOnly,
}),
]);
const orders = dappOrders.concat(whitelistOrders);
Expand All @@ -76,13 +80,25 @@ export const fetchUserContacts = async ({
order.order.apprestrict.toLowerCase() ===
vDappWhitelistAddress.toLowerCase()
) {
const contact = {
const contact: Contact = {
address: order.order.dataset.toLowerCase(),
owner: order.signer.toLowerCase(),
remainingAccess: order.remaining,
accessPrice: order.order.datasetprice,
accessGrantTimestamp: order.publicationTimestamp,
isUserStrict: order.order.requesterrestrict !== ZeroAddress,
grantedAccess: {
dataset: order.order.dataset,
datasetprice: order.order.datasetprice.toString(),
volume: order.order.volume.toString(),
tag: order.order.tag,
apprestrict: order.order.apprestrict,
workerpoolrestrict: order.order.workerpoolrestrict,
requesterrestrict: order.order.requesterrestrict,
salt: order.order.salt,
sign: order.order.sign,
remainingAccess: order.remaining,
} as GrantedAccess,
};
myContacts.push(contact);
}
Expand All @@ -105,23 +121,24 @@ async function fetchAllOrdersByApp({
userAddress,
appAddress,
isUserStrict,
bulkOnly,
}: {
iexec: IExec;
userAddress: string;
appAddress: string;
isUserStrict: boolean;
bulkOnly?: boolean;
}): Promise<PublishedDatasetorder[]> {
const ordersFirstPage = iexec.orderbook.fetchDatasetOrderbook(
ANY_DATASET_ADDRESS,
{
app: appAddress,
requester: userAddress,
isAppStrict: true,
isRequesterStrict: isUserStrict,
// Use maxPageSize here to avoid too many round-trips (we want everything anyway)
pageSize: 1000,
}
);
const ordersFirstPage = iexec.orderbook.fetchDatasetOrderbook({
dataset: ANY_DATASET_ADDRESS,
app: appAddress,
requester: userAddress,
isAppStrict: true,
isRequesterStrict: isUserStrict,
bulkOnly,
// Use maxPageSize here to avoid too many round-trips (we want everything anyway)
pageSize: 1000,
});
const { orders: allOrders } = await autoPaginateRequest({
request: ordersFirstPage,
});
Expand Down
Loading
Loading