Skip to content

Commit 4a9f66d

Browse files
Feature/add use voucher to processprotecteddata (#421)
2 parents b8b70d8 + 93decc8 commit 4a9f66d

File tree

11 files changed

+908
-584
lines changed

11 files changed

+908
-584
lines changed

packages/sdk/package-lock.json

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

packages/sdk/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@
6060
"debug": "^4.3.4",
6161
"ethers": "^6.13.2",
6262
"graphql-request": "^6.0.0",
63-
"iexec": "^8.13.1",
63+
"iexec": "^8.14.0",
6464
"jszip": "^3.7.1",
6565
"kubo-rpc-client": "^4.1.1",
6666
"magic-bytes.js": "^1.0.15",

packages/sdk/src/lib/dataProtectorCore/processProtectedData.ts

Lines changed: 54 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { pushRequesterSecret } from '../../utils/pushRequesterSecret.js';
1414
import {
1515
addressOrEnsSchema,
1616
addressSchema,
17+
booleanSchema,
1718
positiveNumberSchema,
1819
secretsSchema,
1920
stringSchema,
@@ -24,6 +25,7 @@ import {
2425
import { isERC734 } from '../../utils/whitelist.js';
2526
import { getResultFromCompletedTask } from './getResultFromCompletedTask.js';
2627
import {
28+
MatchOptions,
2729
OnStatusUpdateFn,
2830
ProcessProtectedDataParams,
2931
ProcessProtectedDataResponse,
@@ -32,6 +34,10 @@ import {
3234
import { IExecConsumer } from '../types/internalTypes.js';
3335
import { getWhitelistContract } from './smartContract/getWhitelistContract.js';
3436
import { isAddressInWhitelist } from './smartContract/whitelistContract.read.js';
37+
import {
38+
checkUserVoucher,
39+
filterWorkerpoolOrders,
40+
} from '../../utils/processProtectedData.models.js';
3541

3642
export type ProcessProtectedData = typeof processProtectedData;
3743

@@ -46,6 +52,8 @@ export const processProtectedData = async ({
4652
inputFiles,
4753
secrets,
4854
workerpool,
55+
useVoucher = false,
56+
voucherAddress,
4957
onStatusUpdate = () => {},
5058
}: IExecConsumer &
5159
ProcessProtectedDataParams): Promise<ProcessProtectedDataResponse> => {
@@ -73,7 +81,12 @@ export const processProtectedData = async ({
7381
.default(WORKERPOOL_ADDRESS) // Default workerpool if none is specified
7482
.label('workerpool')
7583
.validateSync(workerpool);
76-
84+
const vUseVoucher = booleanSchema()
85+
.label('useVoucher')
86+
.validateSync(useVoucher);
87+
const vVoucherAddress = addressOrEnsSchema()
88+
.label('voucherAddress')
89+
.validateSync(voucherAddress);
7790
try {
7891
const vOnStatusUpdate =
7992
validateOnStatusUpdateCallback<
@@ -106,6 +119,20 @@ export const processProtectedData = async ({
106119
requester = vUserWhitelist;
107120
}
108121
}
122+
let userVoucher;
123+
if (vUseVoucher) {
124+
try {
125+
userVoucher = await iexec.voucher.showUserVoucher(requester);
126+
checkUserVoucher({ userVoucher });
127+
} catch (err) {
128+
if (err?.message?.startsWith('No Voucher found for address')) {
129+
throw new Error(
130+
'Oops, it seems your wallet is not associated with any voucher. Check on https://builder.iex.ec/'
131+
);
132+
}
133+
throw err;
134+
}
135+
}
109136

110137
vOnStatusUpdate({
111138
title: 'FETCH_PROTECTED_DATA_ORDERBOOK',
@@ -148,18 +175,28 @@ export const processProtectedData = async ({
148175
workerpool: vWorkerpool === ethers.ZeroAddress ? 'any' : vWorkerpool, // if address zero was chosen use any workerpool
149176
app: vApp,
150177
dataset: vProtectedData,
178+
requester: requester, // public orders + user specific orders
179+
isRequesterStrict: useVoucher, // If voucher, we only want user specific orders
151180
minTag: SCONE_TAG,
152181
maxTag: SCONE_TAG,
182+
category: 0,
153183
});
154184
vOnStatusUpdate({
155185
title: 'FETCH_WORKERPOOL_ORDERBOOK',
156186
isDone: true,
157187
});
188+
const desiredPriceWorkerpoolOrder = filterWorkerpoolOrders({
189+
workerpoolOrders: [...workerpoolOrderbook.orders],
190+
useVoucher: vUseVoucher,
191+
userVoucher,
192+
});
193+
if (!desiredPriceWorkerpoolOrder) {
194+
throw new Error('No Workerpool order found.');
195+
}
158196

159197
const underMaxPriceOrders = fetchOrdersUnderMaxPrice(
160198
datasetOrderbook,
161199
appOrderbook,
162-
workerpoolOrderbook,
163200
vMaxPrice
164201
);
165202

@@ -179,24 +216,32 @@ export const processProtectedData = async ({
179216
});
180217
const requestorderToSign = await iexec.order.createRequestorder({
181218
app: vApp,
182-
category: underMaxPriceOrders.workerpoolorder.category,
219+
category: desiredPriceWorkerpoolOrder.category,
183220
dataset: vProtectedData,
184221
appmaxprice: underMaxPriceOrders.apporder.appprice,
185222
datasetmaxprice: underMaxPriceOrders.datasetorder.datasetprice,
186-
workerpoolmaxprice: underMaxPriceOrders.workerpoolorder.workerpoolprice,
223+
workerpoolmaxprice: desiredPriceWorkerpoolOrder.workerpoolprice,
187224
tag: SCONE_TAG,
188-
workerpool: underMaxPriceOrders.workerpoolorder.workerpool,
225+
workerpool: desiredPriceWorkerpoolOrder.workerpool,
189226
params: {
190227
iexec_input_files: vInputFiles,
191228
iexec_secrets: secretsId,
192229
iexec_args: vArgs,
193230
},
194231
});
195232
const requestorder = await iexec.order.signRequestorder(requestorderToSign);
196-
const { dealid, txHash } = await iexec.order.matchOrders({
197-
requestorder,
198-
...underMaxPriceOrders,
199-
});
233+
const matchOptions: MatchOptions = {
234+
useVoucher: vUseVoucher,
235+
...(vVoucherAddress ? { voucherAddress: vVoucherAddress } : {}),
236+
};
237+
const { dealid, txHash } = await iexec.order.matchOrders(
238+
{
239+
requestorder,
240+
workerpoolorder: desiredPriceWorkerpoolOrder,
241+
...underMaxPriceOrders,
242+
},
243+
matchOptions
244+
);
200245
const taskId = await iexec.deal.computeTaskId(dealid, 0);
201246

202247
vOnStatusUpdate({

packages/sdk/src/lib/types/commonTypes.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,3 +117,8 @@ export interface SearchableDataSchema
117117
| SearchableSchemaEntryType
118118
| SearchableSchemaEntryType[]
119119
> {}
120+
121+
export type MatchOptions = {
122+
useVoucher: boolean;
123+
voucherAddress?: string;
124+
};

packages/sdk/src/lib/types/coreTypes.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,16 @@ export type ProcessProtectedDataParams = {
334334
*/
335335
workerpool?: AddressOrENS;
336336

337+
/**
338+
* A boolean that indicates whether to use a voucher or no.
339+
*/
340+
useVoucher?: boolean;
341+
342+
/**
343+
* Override the voucher contract to use, must be combined with useVoucher: true the user must be authorized by the voucher's owner to use it.
344+
*/
345+
voucherAddress?: AddressOrENS;
346+
337347
/**
338348
* Callback function that will get called at each step of the process
339349
*/

packages/sdk/src/utils/fetchOrdersUnderMaxPrice.ts

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,12 @@ import {
22
PaginableOrders,
33
PublishedApporder,
44
PublishedDatasetorder,
5-
PublishedWorkerpoolorder,
65
} from 'iexec/IExecOrderbookModule';
76
import { DEFAULT_MAX_PRICE } from '../config/config.js';
87

98
export const fetchOrdersUnderMaxPrice = (
109
datasetOrderbook: PaginableOrders<PublishedDatasetorder>,
1110
appOrderbook: PaginableOrders<PublishedApporder>,
12-
workerpoolOrderbook: PaginableOrders<PublishedWorkerpoolorder>,
1311
vMaxPrice = DEFAULT_MAX_PRICE
1412
) => {
1513
const datasetorder = datasetOrderbook.orders[0]?.order;
@@ -20,21 +18,13 @@ export const fetchOrdersUnderMaxPrice = (
2018
if (!apporder) {
2119
throw new Error(`No app orders found`);
2220
}
23-
const workerpoolorder = workerpoolOrderbook.orders[0]?.order;
24-
if (!workerpoolorder) {
25-
throw new Error(`No workerpool orders found`);
26-
}
2721

28-
const totalPrice =
29-
datasetorder.datasetprice +
30-
apporder.appprice +
31-
workerpoolorder.workerpoolprice;
22+
const totalPrice = datasetorder.datasetprice + apporder.appprice;
3223

3324
if (totalPrice <= vMaxPrice) {
3425
return {
3526
datasetorder,
3627
apporder,
37-
workerpoolorder,
3828
};
3929
}
4030

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { Address, BN } from 'iexec';
2+
import { PublishedWorkerpoolorder } from 'iexec/IExecOrderbookModule';
3+
4+
type VoucherInfo = {
5+
owner: Address;
6+
address: Address;
7+
type: BN;
8+
balance: BN;
9+
expirationTimestamp: BN;
10+
sponsoredApps: Address[];
11+
sponsoredDatasets: Address[];
12+
sponsoredWorkerpools: Address[];
13+
allowanceAmount: BN;
14+
authorizedAccounts: Address[];
15+
};
16+
17+
function bnToNumber(bn: BN) {
18+
return Number(bn.toString());
19+
}
20+
21+
export function checkUserVoucher({
22+
userVoucher,
23+
}: {
24+
userVoucher: VoucherInfo;
25+
}) {
26+
if (bnToNumber(userVoucher.expirationTimestamp) < Date.now() / 1000) {
27+
throw new Error(
28+
'Oops, it seems your voucher has expired. You might want to ask for a top up. Check on https://builder.iex.ec/'
29+
);
30+
}
31+
32+
if (bnToNumber(userVoucher.balance) === 0) {
33+
throw new Error(
34+
'Oops, it seems your voucher is empty. You might want to ask for a top up. Check on https://builder.iex.ec/'
35+
);
36+
}
37+
}
38+
39+
export function filterWorkerpoolOrders({
40+
workerpoolOrders,
41+
useVoucher,
42+
userVoucher,
43+
}: {
44+
workerpoolOrders: PublishedWorkerpoolorder[];
45+
useVoucher: boolean;
46+
userVoucher?: VoucherInfo;
47+
}) {
48+
if (workerpoolOrders.length === 0) {
49+
return null;
50+
}
51+
52+
let eligibleWorkerpoolOrders = [...workerpoolOrders];
53+
let maxVoucherSponsoredAmount = 0; // may be safer to use bigint
54+
55+
if (useVoucher) {
56+
if (!userVoucher) {
57+
throw new Error(
58+
'useVoucher === true but userVoucher is undefined? Hum...'
59+
);
60+
}
61+
// only voucher sponsored workerpoolorders
62+
eligibleWorkerpoolOrders = eligibleWorkerpoolOrders.filter(({ order }) =>
63+
userVoucher.sponsoredWorkerpools.includes(order.workerpool)
64+
);
65+
if (eligibleWorkerpoolOrders.length === 0) {
66+
throw new Error(
67+
'Found some workerpool orders but none can be sponsored by your voucher.'
68+
);
69+
}
70+
maxVoucherSponsoredAmount = bnToNumber(userVoucher.balance);
71+
}
72+
73+
const [cheapestOrder] = eligibleWorkerpoolOrders.sort(
74+
(order1, order2) =>
75+
order1.order.workerpoolprice - order2.order.workerpoolprice
76+
);
77+
78+
if (
79+
!cheapestOrder ||
80+
cheapestOrder.order.workerpoolprice > maxVoucherSponsoredAmount
81+
) {
82+
return null;
83+
}
84+
return cheapestOrder.order;
85+
}

packages/sdk/tests/test-utils.ts

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -180,11 +180,37 @@ export const EMPTY_ORDER_BOOK: any = {
180180
};
181181

182182
export function resolveWithNoOrder() {
183-
return jest
184-
.fn<() => Promise<{ orders: []; count: 0 }>>()
185-
.mockResolvedValue(EMPTY_ORDER_BOOK);
183+
return EMPTY_ORDER_BOOK;
186184
}
187185

186+
export const mockWorkerpoolOrderbook = {
187+
orders: [
188+
{
189+
order: {
190+
workerpool: '0x0e7Bc972c99187c191A17f3CaE4A2711a4188c3F',
191+
workerpoolprice: 263157894,
192+
volume: 1000,
193+
tag: '0x0000000000000000000000000000000000000000000000000000000000000003',
194+
category: 0,
195+
trust: 0,
196+
apprestrict: '0x0000000000000000000000000000000000000000',
197+
datasetrestrict: '0x0000000000000000000000000000000000000000',
198+
requesterrestrict: '0xa1C2e8D384520c5D85Ab288598dC53a06db7dB5d',
199+
salt: '0xa6df3aca62cce93b407a5fe2b683e4fc4a5ff36d3e99731e642ad21f9b77e774',
200+
sign: '0xe2d0b978101b54e0bdce2fe08d44543114a01f994eff0f1ec8ec6ff4f0c5ccbf217271cde8b6d73019bec4486d1914a7087253f4bd3e583f1b60bab66f75de1a1c',
201+
},
202+
orderHash:
203+
'0x4dacfe7ed8883f9d3034d3367c7e6d8f5bc2f9434a58b2a60d480948e216f6d8',
204+
chainId: 134,
205+
publicationTimestamp: '2025-02-25T15:10:16.612Z',
206+
signer: '0x0c2e2F5c360cB58dC9A4813fA29656b56b546BF3',
207+
status: 'open',
208+
remaining: 828,
209+
},
210+
],
211+
count: 1,
212+
};
213+
188214
export function observableMockComplete() {
189215
const mockObservable: any = {
190216
subscribe: jest.fn(({ complete }) => {

packages/sdk/tests/unit/dataProtectorCore/grantAccess.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,9 @@ describe('dataProtectorCore.grantAccess()', () => {
255255
const authorizedAppAddress = getRandomAddress();
256256
const iexec = {
257257
orderbook: {
258-
fetchDatasetOrderbook: resolveWithNoOrder(), // Say that access does not yet exist
258+
fetchDatasetOrderbook: jest
259+
.fn<() => Promise<{ orders: []; count: 0 }>>()
260+
.mockResolvedValue(resolveWithNoOrder()), // Say that access does not yet exist
259261
},
260262
app: {
261263
checkDeployedApp: jest.fn().mockReturnValue(true),

0 commit comments

Comments
 (0)