Skip to content

Commit 4b2de3a

Browse files
feat: add multichain configuration support
1 parent 34c8f93 commit 4b2de3a

File tree

5 files changed

+228
-64
lines changed

5 files changed

+228
-64
lines changed

src/config/config.ts

Lines changed: 61 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,66 @@
1-
export const WEB3_MAIL_DAPP_ADDRESS = 'web3mail.apps.iexec.eth';
2-
export const PROD_WORKERPOOL_ADDRESS = 'prod-v8-bellecour.main.pools.iexec.eth';
3-
export const DATAPROTECTOR_SUBGRAPH_ENDPOINT =
4-
'https://thegraph.iex.ec/subgraphs/name/bellecour/dataprotector-v2';
51
export const MAX_DESIRED_DATA_ORDER_PRICE = 0;
62
export const MAX_DESIRED_APP_ORDER_PRICE = 0;
73
export const MAX_DESIRED_WORKERPOOL_ORDER_PRICE = 0;
84
export const DEFAULT_CONTENT_TYPE = 'text/plain';
9-
export const IPFS_UPLOAD_URL = '/dns4/ipfs-upload.v8-bellecour.iex.ec/https';
10-
export const DEFAULT_IPFS_GATEWAY = 'https://ipfs-gateway.v8-bellecour.iex.ec';
11-
export const WHITELIST_SMART_CONTRACT_ADDRESS =
12-
'0x781482C39CcE25546583EaC4957Fb7Bf04C277D2';
135
export const ANY_DATASET_ADDRESS = 'any';
6+
7+
// Chain IDs
8+
export const CHAIN_IDS = {
9+
BELLECOUR: 134,
10+
AVALANCHE_FUJI: 43113,
11+
ARBITRUM_SEPOLIA: 421614,
12+
} as const;
13+
14+
export const DEFAULT_CHAIN_ID = CHAIN_IDS.BELLECOUR;
15+
16+
interface ChainConfig {
17+
name: string;
18+
dappAddress: string;
19+
prodWorkerpoolAddress: string;
20+
dataProtectorSubgraph: string;
21+
ipfsUploadUrl: string;
22+
ipfsGateway: string;
23+
whitelistSmartContract: string;
24+
}
25+
26+
export const CHAIN_CONFIG: Record<number, ChainConfig> = {
27+
[CHAIN_IDS.BELLECOUR]: {
28+
name: 'bellecour',
29+
// eslint-disable-next-line sonarjs/no-duplicate-string
30+
dappAddress: 'web3mail.apps.iexec.eth',
31+
prodWorkerpoolAddress: 'prod-v8-bellecour.main.pools.iexec.eth',
32+
dataProtectorSubgraph:
33+
'https://thegraph.iex.ec/subgraphs/name/bellecour/dataprotector-v2',
34+
ipfsUploadUrl: '/dns4/ipfs-upload.v8-bellecour.iex.ec/https',
35+
ipfsGateway: 'https://ipfs-gateway.v8-bellecour.iex.ec',
36+
whitelistSmartContract: '0x781482C39CcE25546583EaC4957Fb7Bf04C277D2',
37+
},
38+
[CHAIN_IDS.AVALANCHE_FUJI]: {
39+
name: 'avalanche',
40+
dappAddress: 'web3mail.apps.iexec.eth', // TODO: Update with actual Avalanche address
41+
prodWorkerpoolAddress: 'prod-v8-avalanche.main.pools.iexec.eth', // TODO: Update
42+
dataProtectorSubgraph:
43+
'https://thegraph.avalanche.iex.ec/subgraphs/name/avalanche/dataprotector-v2', // TODO: Update
44+
ipfsUploadUrl: '/dns4/ipfs-upload.v8-avalanche.iex.ec/https', // TODO: Update
45+
ipfsGateway: 'https://ipfs-gateway.v8-avalanche.iex.ec', // TODO: Update
46+
whitelistSmartContract: '0x781482C39CcE25546583EaC4957Fb7Bf04C277D2', // TODO: Update
47+
},
48+
[CHAIN_IDS.ARBITRUM_SEPOLIA]: {
49+
name: 'arbitrum',
50+
dappAddress: 'web3mail.apps.iexec.eth', // TODO: Update with actual Arbitrum address
51+
prodWorkerpoolAddress: 'prod-v8-arbitrum.main.pools.iexec.eth', // TODO: Update
52+
dataProtectorSubgraph:
53+
'https://thegraph.arbitrum.iex.ec/subgraphs/name/arbitrum/dataprotector-v2', // TODO: Update
54+
ipfsUploadUrl: '/dns4/ipfs-upload.v8-arbitrum.iex.ec/https', // TODO: Update
55+
ipfsGateway: 'https://ipfs-gateway.v8-arbitrum.iex.ec', // TODO: Update
56+
whitelistSmartContract: '0x781482C39CcE25546583EaC4957Fb7Bf04C277D2', // TODO: Update
57+
},
58+
};
59+
60+
export function getChainConfig(chainId: number): ChainConfig {
61+
const config = CHAIN_CONFIG[chainId];
62+
if (!config) {
63+
throw new Error(`Unsupported chain ID: ${chainId}`);
64+
}
65+
return config;
66+
}

src/utils/getChainId.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import {
2+
JsonRpcProvider,
3+
BrowserProvider,
4+
AbstractProvider,
5+
AbstractSigner,
6+
Eip1193Provider,
7+
} from 'ethers';
8+
import { DEFAULT_CHAIN_ID } from '../config/config.js';
9+
10+
type EthersCompatibleProvider =
11+
| string
12+
| AbstractProvider
13+
| AbstractSigner
14+
| Eip1193Provider;
15+
16+
export async function getChainIdFromProvider(
17+
ethProvider: EthersCompatibleProvider
18+
): Promise<number> {
19+
try {
20+
if (typeof ethProvider === 'string') {
21+
const provider = new JsonRpcProvider(ethProvider);
22+
const network = await provider.getNetwork();
23+
return Number(network.chainId);
24+
} else if (ethProvider instanceof AbstractProvider) {
25+
const network = await ethProvider.getNetwork();
26+
return Number(network.chainId);
27+
} else if (ethProvider instanceof AbstractSigner) {
28+
const { provider } = ethProvider;
29+
if (!provider) {
30+
throw Error('Signer is not connected to a provider');
31+
}
32+
const network = await provider.getNetwork();
33+
return Number(network.chainId);
34+
} else if ('request' in ethProvider) {
35+
const provider = new BrowserProvider(ethProvider as Eip1193Provider);
36+
const network = await provider.getNetwork();
37+
return Number(network.chainId);
38+
}
39+
} catch (e) {
40+
console.warn('Failed to detect chainId:', e);
41+
}
42+
return DEFAULT_CHAIN_ID;
43+
}

src/utils/ipfs-service.ts

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { create } from 'kubo-rpc-client';
2-
import { IPFS_UPLOAD_URL, DEFAULT_IPFS_GATEWAY } from '../config/config.js';
32

43
interface GetOptions {
54
ipfsGateway?: string;
@@ -8,10 +7,7 @@ interface AddOptions extends GetOptions {
87
ipfsNode?: string;
98
}
109

11-
const get = async (
12-
cid,
13-
{ ipfsGateway = DEFAULT_IPFS_GATEWAY }: GetOptions = {}
14-
) => {
10+
const get = async (cid, { ipfsGateway }: GetOptions = {}) => {
1511
const multiaddr = `/ipfs/${cid.toString()}`;
1612
const publicUrl = `${ipfsGateway}${multiaddr}`;
1713
const res = await fetch(publicUrl);
@@ -22,13 +18,7 @@ const get = async (
2218
return new Uint8Array(arrayBuffer);
2319
};
2420

25-
const add = async (
26-
content,
27-
{
28-
ipfsNode = IPFS_UPLOAD_URL,
29-
ipfsGateway = DEFAULT_IPFS_GATEWAY,
30-
}: AddOptions = {}
31-
) => {
21+
const add = async (content, { ipfsNode, ipfsGateway }: AddOptions = {}) => {
3222
const ipfsClient = create(ipfsNode);
3323
const { cid } = await ipfsClient.add(content);
3424
await get(cid.toString(), { ipfsGateway });

src/web3mail/IExecWeb3mail.ts

Lines changed: 121 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { AbstractProvider, AbstractSigner, Eip1193Provider } from 'ethers';
22
import { IExec } from 'iexec';
3+
import { GraphQLClient } from 'graphql-request';
34
import { fetchUserContacts } from './fetchUserContacts.js';
45
import { fetchMyContacts } from './fetchMyContacts.js';
56
import { sendEmail } from './sendEmail.js';
@@ -13,66 +14,75 @@ import {
1314
Web3SignerProvider,
1415
FetchMyContactsParams,
1516
} from './types.js';
16-
import { GraphQLClient } from 'graphql-request';
17-
import {
18-
WEB3_MAIL_DAPP_ADDRESS,
19-
IPFS_UPLOAD_URL,
20-
DEFAULT_IPFS_GATEWAY,
21-
DATAPROTECTOR_SUBGRAPH_ENDPOINT,
22-
WHITELIST_SMART_CONTRACT_ADDRESS,
23-
} from '../config/config.js';
17+
import { CHAIN_CONFIG } from '../config/config.js';
2418
import { isValidProvider } from '../utils/validators.js';
19+
import { getChainIdFromProvider } from '../utils/getChainId.js';
20+
21+
type EthersCompatibleProvider =
22+
| AbstractProvider
23+
| AbstractSigner
24+
| Eip1193Provider
25+
| Web3SignerProvider
26+
| string;
27+
28+
interface Web3mailResolvedConfig {
29+
dappAddressOrENS: AddressOrENS;
30+
dappWhitelistAddress: AddressOrENS;
31+
graphQLClient: GraphQLClient;
32+
ipfsNode: string;
33+
ipfsGateway: string;
34+
defaultWorkerpool: string;
35+
iexec: IExec;
36+
}
2537

2638
export class IExecWeb3mail {
27-
private iexec: IExec;
39+
private dappAddressOrENS!: AddressOrENS;
40+
41+
private dappWhitelistAddress!: AddressOrENS;
42+
43+
private graphQLClient!: GraphQLClient;
44+
45+
private ipfsNode!: string;
2846

29-
private ipfsNode: string;
47+
private ipfsGateway!: string;
3048

31-
private ipfsGateway: string;
49+
private defaultWorkerpool!: string;
3250

33-
private dataProtectorSubgraph: string;
51+
private iexec!: IExec;
3452

35-
private dappAddressOrENS: AddressOrENS;
53+
private initPromise: Promise<void> | null = null;
3654

37-
private dappWhitelistAddress: AddressOrENS;
55+
private ethProvider: EthersCompatibleProvider;
3856

39-
private graphQLClient: GraphQLClient;
57+
private options: Web3MailConfigOptions;
4058

4159
constructor(
42-
ethProvider?:
43-
| Eip1193Provider
44-
| AbstractProvider
45-
| AbstractSigner
46-
| Web3SignerProvider
47-
| string,
60+
ethProvider?: EthersCompatibleProvider,
4861
options?: Web3MailConfigOptions
4962
) {
50-
try {
51-
this.iexec = new IExec(
52-
{ ethProvider: ethProvider || 'bellecour' },
53-
options?.iexecOptions
54-
);
55-
} catch (e) {
56-
throw Error('Unsupported ethProvider');
57-
}
63+
this.ethProvider = ethProvider || 'bellecour';
64+
this.options = options || {};
65+
}
5866

59-
try {
60-
this.dataProtectorSubgraph =
61-
options?.dataProtectorSubgraph || DATAPROTECTOR_SUBGRAPH_ENDPOINT;
62-
this.graphQLClient = new GraphQLClient(this.dataProtectorSubgraph);
63-
} catch (e) {
64-
throw Error('Impossible to create GraphQLClient');
67+
async init(): Promise<void> {
68+
if (!this.initPromise) {
69+
this.initPromise = this.resolveConfig().then((config) => {
70+
this.dappAddressOrENS = config.dappAddressOrENS;
71+
this.dappWhitelistAddress = config.dappWhitelistAddress;
72+
this.graphQLClient = config.graphQLClient;
73+
this.ipfsNode = config.ipfsNode;
74+
this.ipfsGateway = config.ipfsGateway;
75+
this.defaultWorkerpool = config.defaultWorkerpool;
76+
this.iexec = config.iexec;
77+
});
6578
}
66-
67-
this.dappAddressOrENS = options?.dappAddressOrENS || WEB3_MAIL_DAPP_ADDRESS;
68-
this.ipfsNode = options?.ipfsNode || IPFS_UPLOAD_URL;
69-
this.ipfsGateway = options?.ipfsGateway || DEFAULT_IPFS_GATEWAY;
70-
this.dappWhitelistAddress =
71-
options?.dappWhitelistAddress || WHITELIST_SMART_CONTRACT_ADDRESS;
79+
return this.initPromise;
7280
}
7381

7482
async fetchMyContacts(args?: FetchMyContactsParams): Promise<Contact[]> {
83+
await this.init();
7584
await isValidProvider(this.iexec);
85+
7686
return fetchMyContacts({
7787
...args,
7888
iexec: this.iexec,
@@ -82,7 +92,9 @@ export class IExecWeb3mail {
8292
});
8393
}
8494

85-
fetchUserContacts(args: FetchUserContactsParams): Promise<Contact[]> {
95+
async fetchUserContacts(args: FetchUserContactsParams): Promise<Contact[]> {
96+
await this.init();
97+
8698
return fetchUserContacts({
8799
...args,
88100
iexec: this.iexec,
@@ -93,9 +105,11 @@ export class IExecWeb3mail {
93105
}
94106

95107
async sendEmail(args: SendEmailParams): Promise<SendEmailResponse> {
108+
await this.init();
96109
await isValidProvider(this.iexec);
97110
return sendEmail({
98111
...args,
112+
workerpoolAddressOrEns: this.defaultWorkerpool,
99113
iexec: this.iexec,
100114
ipfsNode: this.ipfsNode,
101115
ipfsGateway: this.ipfsGateway,
@@ -105,4 +119,69 @@ export class IExecWeb3mail {
105119
useVoucher: args?.useVoucher,
106120
});
107121
}
122+
123+
private async resolveConfig(): Promise<Web3mailResolvedConfig> {
124+
const chainId = await getChainIdFromProvider(this.ethProvider);
125+
const chainDefaultConfig = CHAIN_CONFIG[chainId];
126+
127+
const subgraphUrl =
128+
this.options?.dataProtectorSubgraph ||
129+
chainDefaultConfig?.dataProtectorSubgraph;
130+
const dappAddressOrENS =
131+
this.options?.dappAddressOrENS || chainDefaultConfig?.dappAddress;
132+
const dappWhitelistAddress =
133+
this.options?.dappWhitelistAddress ||
134+
chainDefaultConfig?.whitelistSmartContract;
135+
const ipfsGateway =
136+
this.options?.ipfsGateway || chainDefaultConfig?.ipfsGateway;
137+
const defaultWorkerpool = chainDefaultConfig?.prodWorkerpoolAddress;
138+
const ipfsNode =
139+
this.options?.ipfsNode || chainDefaultConfig?.ipfsUploadUrl;
140+
141+
const missing = [];
142+
if (!subgraphUrl) missing.push('dataProtectorSubgraph');
143+
if (!dappAddressOrENS) missing.push('dappAddress');
144+
if (!dappWhitelistAddress) missing.push('whitelistSmartContract');
145+
if (!ipfsGateway) missing.push('ipfsGateway');
146+
if (!defaultWorkerpool) missing.push('prodWorkerpoolAddress');
147+
if (!ipfsNode) missing.push('ipfsUploadUrl');
148+
149+
if (missing.length > 0) {
150+
throw new Error(
151+
`Missing required configuration for chainId ${chainId}: ${missing.join(
152+
', '
153+
)}`
154+
);
155+
}
156+
157+
let iexec: IExec, graphQLClient: GraphQLClient;
158+
159+
try {
160+
iexec = new IExec(
161+
{ ethProvider: this.ethProvider },
162+
{
163+
ipfsGatewayURL: ipfsGateway,
164+
...this.options?.iexecOptions,
165+
}
166+
);
167+
} catch (e: any) {
168+
throw new Error(`Unsupported ethProvider: ${e.message}`);
169+
}
170+
171+
try {
172+
graphQLClient = new GraphQLClient(subgraphUrl);
173+
} catch (error: any) {
174+
throw new Error(`Failed to create GraphQLClient: ${error.message}`);
175+
}
176+
177+
return {
178+
dappAddressOrENS,
179+
dappWhitelistAddress: dappWhitelistAddress.toLowerCase(),
180+
defaultWorkerpool,
181+
graphQLClient,
182+
ipfsNode,
183+
ipfsGateway,
184+
iexec,
185+
};
186+
}
108187
}

src/web3mail/sendEmail.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import {
44
MAX_DESIRED_APP_ORDER_PRICE,
55
MAX_DESIRED_DATA_ORDER_PRICE,
66
MAX_DESIRED_WORKERPOOL_ORDER_PRICE,
7-
PROD_WORKERPOOL_ADDRESS,
87
} from '../config/config.js';
98
import { handleIfProtocolError, WorkflowError } from '../utils/errors.js';
109
import { generateSecureUniqueId } from '../utils/generateUniqueId.js';
@@ -42,7 +41,7 @@ export type SendEmail = typeof sendEmail;
4241
export const sendEmail = async ({
4342
graphQLClient = throwIfMissing(),
4443
iexec = throwIfMissing(),
45-
workerpoolAddressOrEns = PROD_WORKERPOOL_ADDRESS,
44+
workerpoolAddressOrEns,
4645
dappAddressOrENS,
4746
dappWhitelistAddress,
4847
ipfsNode,

0 commit comments

Comments
 (0)