@iexec/iexec-oracle-factory-wrapper
Version:
A wrapper for creating API based oracles for ethereum on the top of iExec
461 lines (445 loc) • 15.2 kB
text/typescript
import CID from 'cids';
import { DEFAULT_IPFS_GATEWAY, getFactoryDefaults } from '../config/config.js';
import * as ipfs from '../services/ipfs/index.js';
import { ParamSet } from '../types/common.js';
import { IExecConsumer } from '../types/internal.js';
import {
TaskExecutionMessage,
UpdateOracleMessage,
UpdateOracleOptions,
UpdateOracleParams,
} from '../types/updateOracle.js';
import {
ValidationError,
WorkflowError,
handleIfProtocolError,
updateErrorMessage,
} from '../utils/errors.js';
import { formatParamsJson } from '../utils/format.js';
import { Observable, SafeObserver } from '../utils/reactive.js';
import {
jsonParamSetSchema,
paramSetSchema,
updateTargetBlockchainsSchema,
} from '../utils/validators.js';
/**
* Retrieves parameter set information from IPFS.
* @param paramSetOrCid Parameter set or CID.
* @param ipfsGateway IPFS gateway URL.
* @returns JSON Object containing the parameter set.
* @throws {ValidationError} If there is a validation error.
* @throws {WorkflowError} If there is an unexpected workflow error.
*/
const getParamSet = async ({
paramSetOrCid,
ipfsGateway = DEFAULT_IPFS_GATEWAY,
}: {
paramSetOrCid: ParamSet | string;
ipfsGateway?: string;
}): Promise<{
paramSet: ParamSet;
paramsJson: string;
isUploaded: boolean;
}> => {
let paramSet;
let paramsJson;
let isUploaded = false;
if (ipfs.isCid(paramSetOrCid)) {
const cid = new CID(paramSetOrCid as string).toString();
const contentBuffer = await ipfs.get(cid, { ipfsGateway }).catch(() => {
throw Error(`Failed to load paramSetSet from CID ${cid}`);
});
const contentText = contentBuffer.toString();
try {
paramsJson = await jsonParamSetSchema().validate(contentText);
paramSet = JSON.parse(paramsJson);
isUploaded = true;
} catch (e) {
throw Error(`Content associated to CID ${cid} is not a valid paramSet`);
}
} else {
paramSet = await paramSetSchema().validate(paramSetOrCid);
paramsJson = await jsonParamSetSchema().validate(
formatParamsJson(paramSet)
);
}
return { paramSet, paramsJson, isUploaded };
};
/**
* Updates an oracle with new parameters.
* @param paramSetOrCid Parameter set or CID.
* @param iexec iExec SDK instance.
* @param targetBlockchains Chain ID of target blockchains for cross-chain update
* @param useVoucher Whether to use a voucher for payment (default: false)
* @param oracleApp Oracle application address.
* @param workerpool Workerpool address.
* @param ipfsGateway IPFS gateway URL.
* @param ipfsNode IPFS upload node URL.
* @param oracleContract Oracle contract address.
* @returns An observable for tracking the update process.
*/
const updateOracle = ({
paramSetOrCid,
targetBlockchains,
useVoucher,
iexec,
oracleApp,
oracleAppWhitelist,
ipfsGateway,
ipfsNode,
workerpool,
oracleContract,
}: UpdateOracleParams &
UpdateOracleOptions &
IExecConsumer): Observable<UpdateOracleMessage> =>
// eslint-disable-next-line sonarjs/cognitive-complexity
new Observable((observer: SafeObserver<UpdateOracleMessage>) => {
let abort = false;
let stopWatcher;
const safeObserver = new SafeObserver(observer);
const start = async () => {
try {
const targetBlockchainsArray =
await updateTargetBlockchainsSchema().validate(targetBlockchains);
const { chainId } = await iexec.network.getNetwork();
if (abort) return;
const appAddress =
oracleApp || getFactoryDefaults(chainId).ORACLE_APP_ADDRESS;
const appWhitelistAddress =
oracleAppWhitelist ||
getFactoryDefaults(chainId).ORACLE_APP_WHITELIST_ADDRESS;
const oracleAddress =
oracleContract || getFactoryDefaults(chainId).ORACLE_CONTRACT_ADDRESS;
const workerpoolAddress =
workerpool || getFactoryDefaults(chainId).WORKERPOOL_ADDRESS;
let cid;
safeObserver.next({
message: 'ENSURE_PARAMS',
});
const { isUploaded, paramSet, paramsJson } = await getParamSet({
paramSetOrCid,
ipfsGateway,
}).catch((e) => {
if (e instanceof ValidationError) {
throw e;
} else {
throw new WorkflowError({
message: 'Failed to load paramSet',
errorCause: e,
});
}
});
if (abort) return;
if (isUploaded) {
cid = paramSetOrCid;
} else {
safeObserver.next({
message: 'ENSURE_PARAMS_UPLOAD',
});
cid = await ipfs
.add(paramsJson, { ipfsGateway, ipfsNode })
.catch((e) => {
throw new WorkflowError({
message: 'Failed to upload paramSet',
errorCause: e,
});
});
if (abort) return;
}
safeObserver.next({
message: 'ENSURE_PARAMS_SUCCESS',
paramSet,
cid,
});
// Fetch app order
safeObserver.next({
message: 'FETCH_APP_ORDER',
});
const datasetAddress = paramSet.dataset;
const apporderbook = await iexec.orderbook.fetchAppOrderbook(
appAddress,
{
minTag: ['tee', 'scone'],
maxTag: ['tee', 'scone'],
requester: await iexec.wallet.getAddress(),
workerpool: workerpoolAddress,
dataset: datasetAddress,
}
);
if (abort) return;
const apporder =
apporderbook &&
apporderbook.orders[0] &&
apporderbook.orders[0].order;
if (!apporder) {
throw new WorkflowError({
message: updateErrorMessage,
errorCause: Error('No app order published'),
});
}
safeObserver.next({
message: 'FETCH_APP_ORDER_SUCCESS',
order: apporder,
});
let datasetorder;
if (
datasetAddress &&
datasetAddress !== '0x0000000000000000000000000000000000000000'
) {
// Fetch dataset order
safeObserver.next({
message: 'FETCH_DATASET_ORDER',
});
const [datasetorderForApp, datasetorderForWhitelist] =
await Promise.all([
iexec.orderbook
.fetchDatasetOrderbook(datasetAddress, {
minTag: ['tee', 'scone'],
maxTag: ['tee', 'scone'],
requester: await iexec.wallet.getAddress(),
workerpool: workerpoolAddress,
app: appAddress,
})
.then(
(orderbook) =>
orderbook &&
orderbook.orders[0] &&
orderbook.orders[0].order
),
iexec.orderbook
.fetchDatasetOrderbook(datasetAddress, {
minTag: ['tee', 'scone'],
maxTag: ['tee', 'scone'],
requester: await iexec.wallet.getAddress(),
workerpool: workerpoolAddress,
app: appWhitelistAddress,
})
.then(
(orderbook) =>
orderbook &&
orderbook.orders[0] &&
orderbook.orders[0].order
),
]);
if (abort) return;
if (datasetorderForApp && datasetorderForWhitelist) {
// get cheapest order
datasetorder =
datasetorderForApp.datasetprice <
datasetorderForWhitelist.datasetprice
? datasetorderForApp
: datasetorderForWhitelist;
} else {
datasetorder = datasetorderForApp || datasetorderForWhitelist;
}
if (!datasetorder) {
throw new WorkflowError({
message: updateErrorMessage,
errorCause: Error('No dataset order published'),
});
}
safeObserver.next({
message: 'FETCH_DATASET_ORDER_SUCCESS',
order: datasetorder,
});
}
// Fetch workerpool order
safeObserver.next({
message: 'FETCH_WORKERPOOL_ORDER',
});
const [workerpoolorderForApp, workerpoolorderForWhitelist] =
await Promise.all([
iexec.orderbook
.fetchWorkerpoolOrderbook({
minTag: ['tee', 'scone'],
requester: await iexec.wallet.getAddress(),
isRequesterStrict: useVoucher,
workerpool: workerpoolAddress,
app: appAddress,
dataset: datasetAddress,
})
.then(
(orderbook) =>
orderbook && orderbook.orders[0] && orderbook.orders[0].order
),
iexec.orderbook
.fetchWorkerpoolOrderbook({
minTag: ['tee', 'scone'],
requester: await iexec.wallet.getAddress(),
isRequesterStrict: useVoucher,
workerpool: workerpoolAddress,
app: appWhitelistAddress,
dataset: datasetAddress,
})
.then(
(orderbook) =>
orderbook && orderbook.orders[0] && orderbook.orders[0].order
),
]);
if (abort) return;
let workerpoolorder;
if (workerpoolorderForApp && workerpoolorderForWhitelist) {
// get cheapest order
workerpoolorder =
workerpoolorderForApp.workerpoolprice <
workerpoolorderForWhitelist.workerpoolprice
? workerpoolorderForApp
: workerpoolorderForWhitelist;
} else {
workerpoolorder =
workerpoolorderForApp || workerpoolorderForWhitelist;
}
if (!workerpoolorder) {
throw new WorkflowError({
message: updateErrorMessage,
errorCause: Error('No workerpool order published'),
});
}
safeObserver.next({
message: 'FETCH_WORKERPOOL_ORDER_SUCCESS',
order: workerpoolorder,
});
// Create request order
const requestorderToSign = await iexec.order
.createRequestorder({
app: appAddress,
category: workerpoolorder.category,
dataset: datasetAddress,
workerpool: workerpoolAddress,
callback: oracleAddress,
appmaxprice: apporder.appprice,
datasetmaxprice: datasetorder && datasetorder.datasetprice,
workerpoolmaxprice: workerpoolorder.workerpoolprice,
tag: ['tee', 'scone'],
params: {
iexec_args: targetBlockchainsArray.join(','),
iexec_input_files: [`${ipfsGateway}/ipfs/${cid}`],
iexec_developer_logger: true,
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any)
.catch((e) => {
throw new WorkflowError({
message: 'Failed to create request order',
errorCause: e,
});
});
if (abort) return;
safeObserver.next({
message: 'REQUEST_ORDER_SIGNATURE_SIGN_REQUEST',
order: requestorderToSign,
});
// Sign and publish request order
const requestorder = await iexec.order
.signRequestorder(requestorderToSign, { preflightCheck: false })
.catch((e) => {
throw new WorkflowError({
message: 'Failed to sign requestorder',
errorCause: e,
});
});
if (abort) return;
safeObserver.next({
message: 'REQUEST_ORDER_SIGNATURE_SUCCESS',
order: requestorder,
});
// Match orders
safeObserver.next({
message: 'MATCH_ORDERS_SIGN_TX_REQUEST',
apporder,
datasetorder,
workerpoolorder,
requestorder,
});
const { dealid, txHash } = await iexec.order.matchOrders(
{
apporder,
datasetorder,
workerpoolorder,
requestorder,
},
{ preflightCheck: false, useVoucher }
);
if (abort) return;
safeObserver.next({
message: 'MATCH_ORDERS_SUCCESS',
dealid,
txHash,
});
// Compute task ID
const taskid = await iexec.deal.computeTaskId(dealid, 0);
if (abort) return;
// Watch task execution
const watchExecution = () =>
new Promise<TaskExecutionMessage | void>((resolve, reject) => {
iexec.task.obsTask(taskid, { dealid }).then((obs) => {
stopWatcher = obs.subscribe({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
next: (value: any) => {
const { message } = value;
if (message === 'TASK_TIMEDOUT') {
reject(
new WorkflowError({
message: 'Oracle update task timed out, update failed',
errorCause: Error(
`Task ${taskid} from deal ${dealid} timed out`
),
})
);
}
if (message === 'TASK_COMPLETED') {
resolve();
}
if (message === 'TASK_UPDATED') {
safeObserver.next({
message: 'TASK_UPDATED',
dealid,
taskid,
status: value.task && value.task.statusName,
});
}
},
error: (e) => {
handleIfProtocolError(e, safeObserver);
reject(
new WorkflowError({
message: 'Failed to monitor oracle update task',
errorCause: e,
})
);
},
complete: () => {},
});
});
});
await watchExecution();
safeObserver.next({
message: 'UPDATE_TASK_COMPLETED',
});
safeObserver.complete();
} catch (e) {
if (abort) return;
handleIfProtocolError(e, safeObserver);
if (e instanceof WorkflowError || e instanceof ValidationError) {
safeObserver.error(e);
} else {
safeObserver.error(
new WorkflowError({
message: 'Failed to update oracle',
errorCause: e,
})
);
}
}
};
safeObserver.unsub = () => {
// teardown callback
abort = true;
if (typeof stopWatcher === 'function') {
stopWatcher();
}
};
start();
return safeObserver.unsubscribe.bind(safeObserver);
});
export { updateOracle };