@iexec/iexec-oracle-factory-wrapper
Version:
A wrapper for creating API based oracles for ethereum on the top of iExec
386 lines • 16.1 kB
JavaScript
import CID from 'cids';
import { DEFAULT_IPFS_GATEWAY, getFactoryDefaults } from '../config/config.js';
import * as ipfs from '../services/ipfs/index.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, }) => {
let paramSet;
let paramsJson;
let isUploaded = false;
if (ipfs.isCid(paramSetOrCid)) {
const cid = new CID(paramSetOrCid).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, }) =>
// eslint-disable-next-line sonarjs/cognitive-complexity
new Observable((observer) => {
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
})
.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((resolve, reject) => {
iexec.task.obsTask(taskid, { dealid }).then((obs) => {
stopWatcher = obs.subscribe({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
next: (value) => {
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 };
//# sourceMappingURL=updateOracle.js.map