@ethernity-cloud/runner
Version:
877 lines (789 loc) • 35.2 kB
JavaScript
import { ethers } from 'ethers';
import * as ipfsClient from './ipfs.js';
import {
delay,
formatDate,
generateRandomHexOfSize,
generateWallet,
isAddress,
isNullOrEmpty,
parseTransactionBytes
} from './utils.js';
import { decryptWithPrivateKey, encryptWithCertificate, sha256 } from './crypto.js';
import { resolveWalletContext } from './walletContext.js';
import EtnyContract from './contract/operation/etnyContract.js';
import EcldContract from './contract/operation/ecldContract.js';
import ImageRegistryContract from './contract/operation/imageRegistryContract.js';
import contractBloxberg from './contract/abi/etnyAbi.js';
import protocolContractPolygon from './contract/abi/polygonProtocolAbi.js';
import {
ECEvent,
ECStatus,
ECLog,
ECOrderTaskStatus,
ZERO_CHECKSUM,
ECAddress,
ECError,
ECRunner,
resolveNetworkConfig
} from './enums.js';
import PolygonProtocolContract from './contract/operation/polygonProtocolContract.js';
import BloxbergProtocolContract from './contract/operation/bloxbergProtocolContract.js';
import { Buffer } from 'buffer';
import util from 'util';
const LAST_BLOCKS = 20;
const VERSION = 'v3';
// Fallback IPFS endpoint used when the app did not call initializeStorage()
// before run(); prevents "Cannot read properties of null (reading 'add')".
// Plain HTTPS on the default port (443), no custom port.
const DEFAULT_IPFS_ADDRESS = 'https://ipfs.ethernity.cloud';
class EthernityCloudRunner extends EventTarget {
constructor(networkAddress = ECAddress.BLOXBERG.TESTNET_ADDRESS, walletOptions = {}, chainId = undefined) {
super();
this.networkAddress = networkAddress;
// Optional disambiguator for the ECLD-family testnets (IoTeX / Sepolia /
// LitVM) that share a token address. When omitted, it is resolved from the
// wallet provider's chainId in resolveNetworkContext() before the run.
this.chainId = chainId;
// Resolve the wallet once (raw privateKey / injected signer / provider, or
// MetaMask via window.ethereum when no options are given) and share it with
// every contract so they all talk to the same chain and wallet. We do NOT
// retain the raw walletOptions on the instance so the private key isn't
// exposed as an extra, easily-serialisable field on the runner.
this.walletContext = resolveWalletContext(networkAddress, walletOptions || {});
this.initializeContracts();
this.resetState();
// An explicit encryption public key (or one derived from a raw private key)
// lets us skip the MetaMask-only eth_getEncryptionPublicKey call.
if (this.walletContext.encryptionPublicKey) {
this.publicKey = this.walletContext.encryptionPublicKey;
}
}
initializeContracts() {
const wc = this.walletContext;
const cfg = resolveNetworkConfig(this.networkAddress, this.chainId);
if (!cfg) {
throw new Error('Invalid network address');
}
this.networkConfig = cfg;
if (cfg.family === 'bloxberg') {
this.tokenContract = new EtnyContract(this.networkAddress, wc);
this.protocolContract = new BloxbergProtocolContract(this.networkAddress, wc);
this.protocolAbi = contractBloxberg.abi;
} else {
// ECLD family: Polygon mainnet/Amoy + IoTeX / Sepolia / LitVM testnets.
this.tokenContract = new EcldContract(this.networkAddress, wc);
this.protocolAbi = protocolContractPolygon.abi;
// For the shared-token testnets the PoX address isn't known until a
// chainId is read (resolveNetworkContext); build the protocol contract
// now only when we already have the address.
if (cfg.protocolAddress) {
this.protocolContract = new PolygonProtocolContract(cfg.protocolAddress, wc);
}
}
}
/**
* Finalise the network descriptor once a provider is available. The ECLD
* testnets IoTeX / Sepolia / LitVM share a token address, so when no chainId
* hint was passed to the constructor we read it from the provider and rebuild
* the protocol contract against the correct PoX address. No-op for networks
* that were already unambiguously resolved by token address.
*/
async resolveNetworkContext() {
const provisional = this.networkConfig && this.networkConfig.provisional;
const provider = this.walletContext && this.walletContext.provider;
if (!provider || !provider.getNetwork) {
// Nothing to read the chainId from. If the config is already complete
// (bloxberg / polygon / an explicit chainId), we're fine; otherwise the
// shared-token testnet cannot be resolved.
if (provisional) {
throw new Error(
'This network shares a token address with other ECLD testnets. Pass the chainId to the EthernityCloudRunner constructor, or supply a { provider } / { rpcUrl } so it can be detected.'
);
}
return;
}
let chainId;
try {
const net = await provider.getNetwork();
chainId = net && net.chainId;
} catch (e) {
if (provisional) throw e;
return; // fall back to whatever initializeContracts() resolved
}
if (!chainId) {
if (provisional) throw new Error('Unable to detect chainId to resolve the ECLD testnet.');
return;
}
const cfg = resolveNetworkConfig(this.networkAddress, chainId);
if (!cfg || cfg.provisional) {
if (provisional) throw new Error(`Unsupported chainId ${chainId} for this token address.`);
return;
}
this.chainId = chainId;
// Build/rebuild the protocol contract when we just learned the PoX address
// (shared-token testnet) or it changed from what the constructor resolved.
if (cfg.family === 'ecld' &&
(!this.protocolContract || cfg.protocolAddress !== this.networkConfig.protocolAddress)) {
this.protocolContract = new PolygonProtocolContract(cfg.protocolAddress, this.walletContext);
}
this.networkConfig = cfg;
}
resetState() {
this.nodeAddress = '';
this.challengeHash = '';
this.publicKey = '';
this.orderId = -1;
this.order = null;
this.ordersOffset = -1;
this.doHash = null;
this.doRequest = -1;
this.scriptHash = '';
this.fileSetHash = '';
this.taskHasBeenPickedForApproval = false;
this.getResultFromOrderRepeats = 1;
this.secureLockEnclave = null;
this.trustedZoneImage = null;
this.resources = null;
this.enclaveImageIPFSHash = '';
this.enclavePublicKey = '';
this.enclaveDockerComposeIPFSHash = '';
this.imageRegistryContract = null;
this.status = ECStatus.DEFAULT;
this.progress = ECEvent.INIT;
this.lastError = null;
this.log = [];
this.result = null;
this.logLevel = ECLog.INFO;
this.running = false;
this.network = "Bloxberg_Testnet";
}
logAppend(message, logLevel = ECLog.INFO) {
const logLevelKey = Object.keys(ECLog).find((key) => ECLog[key] === logLevel);
const logEntry = `[${logLevelKey}] ${formatDate()} ${message}`;
if (this.logLevel >= logLevel) {
this.log.push(logEntry);
}
}
setLogLevel(logLevel) {
this.logLevel = logLevel;
}
async checkWalletBalance(taskPrice) {
this.dispatchECEvent('Checking wallet balance....');
const balance = await this.tokenContract.getBalance();
if (parseInt(balance, 10) < taskPrice) {
throw new Error(`Insufficient wallet balance (${balance}/${taskPrice})`);
}
}
async verifyNodeAddress(nodeAddress) {
this.dispatchECEvent('Verifying node address...');
if (!await this.isNodeOperatorAddress(nodeAddress)) {
throw new Error('Invalid node operator address');
}
this.nodeAddress = nodeAddress;
}
async initializeImageRegistry(secureLockEnclave) {
this.dispatchECEvent('Checking image registry...');
this.secureLockEnclave = secureLockEnclave;
// Resolve the Image Registry address from the network descriptor so the
// ECLD-family networks (Amoy/IoTeX/Sepolia/LitVM) hit their own registry
// rather than falling through the legacy 2-network switch. Bloxberg keeps
// the same address for pynithy/nodenithy, so PYNITHY is a safe default.
const registryAddress =
(this.networkConfig && this.networkConfig.imageRegistry &&
this.networkConfig.imageRegistry.PYNITHY) || undefined;
this.imageRegistryContract = new ImageRegistryContract(
this.networkAddress,
'etny-pynithy-testnet',
this.walletContext,
registryAddress
);
await this.getEnclaveDetails();
}
async initializeWeb3Connection() {
await this.tokenContract.initialize();
if (!await this.handleWeb3Connection()) {
throw new Error('Unable to connect to Web3');
}
}
async checkAllowance(taskPrice) {
// The ECLD family (Polygon + IoTeX / Sepolia / LitVM) pays via an ERC-20
// token and must set an allowance for the protocol contract before ordering.
// Bloxberg (ETNY) does not go through this path.
if (this.networkConfig && this.networkConfig.family === 'ecld') {
this.dispatchECEvent('Checking for the allowance on the current wallet...');
if (!await this.tokenContract.checkAndSetAllowance(
this.protocolContract.contractAddress(),
'100',
taskPrice.toString()
)) {
throw new Error('Unable to set allowance');
}
this.dispatchECEvent('Allowance checking completed.');
}
}
async processTask(code) {
this.challengeHash = generateRandomHexOfSize(20);
const imageMetadata = await this.getV3ImageMetadata(this.challengeHash);
const codeMetadata = await this.getV3CodeMetadata(code);
const inputMetadata = await this.getV3InputMedata();
await this.createDORequest(imageMetadata, codeMetadata, inputMetadata);
await this.findOrder();
if (!this.nodeAddress) {
await this.approveOrder();
}
await this.waitforTaskToBeProcessed();
await this.getOrderResult();
return this.result != null;
}
handleError(error) {
this.status = ECStatus.ERROR;
this.dispatchECEvent(error.message);
throw error;
}
// ... (other methods remain the same)
isMainnet = () => !!(this.networkConfig && this.networkConfig.isMainnet);
getLog = () => {
return this.log;
}
getStatus = () => {
return this.status;
}
dispatchECEvent = (message, log_level) => {
this.logAppend(message, log_level);
const status=this.status;
const progress=this.progress;
// Create a new custom event with a custom event name, and pass any data as the event detail
const customEvent = new CustomEvent(status, { detail: { message, status, progress } });
// Dispatch the custom event on the current instance of the class (or any DOM element)
this.dispatchEvent(customEvent);
};
// The trustedzone whose cert we fetch is the pynithy variant of the resolved
// network. Prefer an explicit setNetwork() value, then derive from the
// network descriptor, then fall back to the legacy Bloxberg testnet name.
resolveTrustedZoneImage() {
if (this.trustedZoneImage) return this.trustedZoneImage;
const key = this.networkConfig && this.networkConfig.networkKey;
if (key && ECRunner[key]) {
const suffix = this.networkConfig.isMainnet ? 'PYNITHY_RUNNER' : 'PYNITHY_RUNNER_TESTNET';
if (ECRunner[key][suffix]) return ECRunner[key][suffix];
}
return 'etny-pynithy-testnet';
}
async getEnclaveDetails() {
const details = await this.imageRegistryContract.getEnclaveDetailsV3(
this.secureLockEnclave,
VERSION,
this.resolveTrustedZoneImage()
);
if (details) {
[this.enclaveImageIPFSHash, this.enclavePublicKey, this.enclaveDockerComposeIPFSHash] = details;
this.dispatchECEvent(`ENCLAVE_IMAGE_IPFS_HASH:${this.enclaveImageIPFSHash}`, ECLog.DEBUG);
this.dispatchECEvent(`ENCLAVE_PUBLIC_KEY:${this.enclavePublicKey}`, ECLog.DEBUG);
this.dispatchECEvent(`ENCLAVE_DOCKER_COMPOSE_IPFS_HASH:${this.enclaveDockerComposeIPFSHash}`, ECLog.DEBUG);
}
}
getTokensFromFaucet = async () => {
const account = this.tokenContract.getCurrentWallet();
const balance = await this.tokenContract.getBalance(account);
if (parseInt(balance, 10) <= 100) {
const tx = await this.tokenContract.getFaucetTokens(account);
const transactionHash = tx.hash;
const isProcessed = await this.waitForTransactionToBeProcessed(this.tokenContract, transactionHash);
if (!isProcessed) {
return { success: false, message: 'Unable to create request, please check connectivity with Bloxberg node.' };
}
return { success: true };
}
return { success: true };
};
// eslint-disable-next-line class-methods-use-this
async getReason(contract, txHash) {
const tx = await contract.getProvider().getTransaction(txHash);
if (!tx) {
//console.log('tx not found');
return 'Transaction hash not found';
}
delete tx.gasPrice;
const code = await contract.getProvider().call(tx, tx.blockNumber);
const reason = ethers.utils.toUtf8String(`0x${code.substring(138)}`);
//console.log(reason);
return reason.trim();
}
// eslint-disable-next-line class-methods-use-this
async waitForTransactionToBeProcessed(tx, protocolEvent) {
while (true) {
try {
this.dispatchECEvent(`TX:` + util.inspect(tx, {depth: null}), ECLog.DEBUG);
const txReceipt = await tx.wait();
this.dispatchECEvent(`RECEIPT:` + util.inspect(txReceipt, {depth: null}), ECLog.DEBUG);
const events = txReceipt.events.find(event => event.event === protocolEvent);
this.dispatchECEvent(`EVENTS:` + util.inspect(events, {depth: null}), ECLog.DEBUG);
txReceipt.result = events.args;
return txReceipt;
}
catch (e) {
if (e.message.includes('transaction failed')) {
throw new Error(e.message);
}
this.dispatchECEvent('Transaction not confirmed yet: ' + e.message, ECLog.WARNING);
await delay(1000);
}
}
}
async handleWeb3Connection() {
try {
// eth_requestAccounts is a MetaMask connection prompt and is only
// meaningful for window.ethereum. With a raw key / injected signer the
// account is already known, and a public JsonRpcProvider rejects
// eth_requestAccounts, so skip it in that case.
if (!this.walletContext || this.walletContext.usesWindowEthereum) {
await this.tokenContract.getProvider().send('eth_requestAccounts', []);
}
const walletAddress = this.tokenContract.getCurrentWallet();
return walletAddress !== null && walletAddress !== undefined;
} catch (e) {
return false;
}
}
async approveOrder() {
while (true) {
try {
this.dispatchECEvent(`Approving task ${this.orderId}`);
const tx = await this.protocolContract.approveOrder(this.orderId);
this.dispatchECEvent(`${tx.hash} is pending...`)
await this.waitForTransactionToBeProcessed(tx, '_orderApprovedEV');
this.dispatchECEvent(`Task ${this.orderId} approved successfully!`);
break;
}
catch (e) {
this.dispatchECEvent(`Failed to approve task ${this.orderId}: ${e.message}`,ECLog.WARNING);
await delay(1000);
}
}
while (true) {
try {
const order = await this.protocolContract.getOrder(this.orderId);
this.nodeAddress = order.dproc;
break;
}
catch (e) {
this.dispatchECEvent(`Failed to get nodeAddress for task ${this.orderId}: ${e.message}`,ECLog.WARNING);
await delay(1000);
}
}
return true;
}
waitforTaskToBeProcessed = async () => {
this.progress = ECEvent.IN_PROGRESS
this.dispatchECEvent(`Operator ${this.nodeAddress} is processing task ${this.orderId}`);
while (true) {
try {
const order = await this.protocolContract.getOrder(this.orderId);
this.dispatchECEvent(`Order:` + util.inspect(order,{depth: null}), ECLog.DEBUG);
if (parseInt(order.status) == 1) {
this.dispatchECEvent(`Task ${this.orderId} is still processing...`, ECLog.DEBUG);
await delay(5000);
} else {
this.dispatchECEvent(`Task ${this.orderId} status is ${order.status}, continuing`, ECLog.DEBUG);
return true;
}
}
catch (e) {
this.dispatchECEvent(`Error while waiting for task to be processed: ${e.message}`, ECLog.WARNING);
await delay(1000);
}
}
}
getOrderResult = async () => {
this.dispatchECEvent(`Task ${this.orderId} was successfully processed.`);
const parsedOrderResult = await this.getResultFromOrder();
if (parsedOrderResult.success === false) {
this.status = ECStatus.ERROR;
this.progress = ECEvent.FINISHED;
this.dispatchECEvent(parsedOrderResult.message);
throw new Error(parsedOrderResult.message);
} else {
this.result = parsedOrderResult.result;
this.status = ECStatus.SUCCESS;
this.progress = ECEvent.FINISHED;
this.dispatchECEvent(`Task completed successfully.`);
}
}
async getWalletPublicKey() {
// Prefer an explicitly provided / derived X25519 encryption key (Web3Auth,
// raw key, or app-supplied) so we never need the MetaMask-only
// eth_getEncryptionPublicKey method.
if (this.walletContext && this.walletContext.encryptionPublicKey) {
return this.walletContext.encryptionPublicKey;
}
// eth_getEncryptionPublicKey only exists on window.ethereum (MetaMask). If we
// were initialised with a signer/provider (but no raw privateKey to derive
// from and no explicit encryptionPublicKey), we cannot obtain it. Fail with
// an actionable message instead of a cryptic "window.ethereum is undefined".
if (this.walletContext && !this.walletContext.usesWindowEthereum) {
throw new Error(
'Cannot obtain the wallet encryption public key: this wallet has no ' +
'eth_getEncryptionPublicKey. Pass { privateKey } (to derive it) or ' +
'{ encryptionPublicKey } to EthernityCloudRunner.'
);
}
await this.tokenContract.initialize();
const account = this.tokenContract.getCurrentWallet();
// Fall back to MetaMask only when running through window.ethereum.
const keyB64 = await window.ethereum.request({
method: 'eth_getEncryptionPublicKey',
params: [account]
});
return Buffer.from(keyB64, 'base64').toString('hex');
}
async setPublicKey(publicKey){
this.publicKey = publicKey;
}
async getV3ImageMetadata(challengeHash) {
// generating encrypted base64 hash of the challenge
const base64EncryptedChallenge = await encryptWithCertificate(challengeHash, this.enclavePublicKey);
// uploading to IPFS the base64 encrypted challenge
const challengeIPFSHash = await ipfsClient.uploadToIPFS(base64EncryptedChallenge);
// The challenge hash is a REQUIRED field of the DO-request metadata. If the
// IPFS upload failed we must NOT build a request with a null/empty hash: it
// serializes on-chain as the literal "null", the node cannot fetch it, the
// (already paid) order gets cancelled, and the task never completes. Abort
// here instead so no gas is spent on a doomed request.
if (!challengeIPFSHash || challengeIPFSHash === 'null') {
throw new Error(
'Failed to upload the challenge to IPFS (got no hash). Aborting before submitting the request. ' +
'Check IPFS connectivity.'
);
}
this.dispatchECEvent(`Uploaded challenge to IPFS: ${challengeIPFSHash}`);
const publicKey = this.publicKey ? this.publicKey : await this.getWalletPublicKey();
// image metadata for v3 format v3:image_ipfs_hash:image_name:docker_compose_ipfs_hash:client_challenge_ipfs_hash:public_key
return `${VERSION}:${this.enclaveImageIPFSHash}:etny-pynithy-testnet:${
this.enclaveDockerComposeIPFSHash
}:${challengeIPFSHash}:${publicKey}`;
}
async getV3CodeMetadata(code) {
// extracting code from all the code cells
let scriptChecksum = sha256(code);
// uploading all node js code to IPFS and received hash of transaction
const base64EncryptedScript = await encryptWithCertificate(code, this.enclavePublicKey);
this.scriptHash = await ipfsClient.uploadToIPFS(base64EncryptedScript);
// The payload (code) hash is REQUIRED -- it is the program the enclave
// executes. As with the challenge, refuse to build/submit a request if the
// upload produced no hash (see getV3ImageMetadata for the full rationale).
if (!this.scriptHash || this.scriptHash === 'null') {
throw new Error(
'Failed to upload the task code to IPFS (got no hash). Aborting before submitting the request. ' +
'Check IPFS connectivity.'
);
}
this.dispatchECEvent(`Uploaded encrypted code to IPFS: ${this.scriptHash}`);
// scriptChecksum = await this.tokenContract.signMessage(scriptChecksum);
// v3:code_ipfs_hash:code_checksum
return `${VERSION}:${this.scriptHash}:${scriptChecksum}`;
}
async getV3InputMedata() {
let fileSetChecksum = sha256(ZERO_CHECKSUM);
// fileSetChecksum = await this.tokenContract.signMessage(fileSetChecksum);
// v3::filesetchecksum
return `${VERSION}::${fileSetChecksum}`;
}
createDORequest = async (imageMetadata, codeMetadata, inputMetadata) => {
try {
// Belt-and-suspenders: never submit a (paid) request whose metadata carries
// a null/empty required IPFS hash. imageMetadata is
// v3:image:name:compose:challenge:pubkey (challenge required, index 4)
// codeMetadata is
// v3:code:checksum (code required, index 1)
// inputMetadata is v3::checksum -- the input hash is legitimately optional.
// A "null"/empty in a required slot means an upstream upload failed; abort
// now so the node never gets a request it cannot fulfill.
const _imgParts = String(imageMetadata).split(':');
const _codeParts = String(codeMetadata).split(':');
const _bad = (h) => !h || h === 'null';
if (_bad(_imgParts[4])) {
throw new Error(`Refusing to submit DO request: challenge IPFS hash is "${_imgParts[4]}" (upload failed).`);
}
if (_bad(_codeParts[1])) {
throw new Error(`Refusing to submit DO request: code IPFS hash is "${_codeParts[1]}" (upload failed).`);
}
this.ordersOffset = await this.protocolContract.getContract()._getOrdersCount();
this.progress = ECEvent.SENDING;
this.dispatchECEvent(`Submitting transaction for DO request`);
// add here call to SC(smart contract)
const tx = await this.protocolContract.addDORequest(
imageMetadata,
codeMetadata,
inputMetadata,
this.nodeAddress,
this.resources,
);
this.doHash = tx.hash;
this.dispatchECEvent(`${this.doHash} is pending...`);
const isProcessed = await this.waitForTransactionToBeProcessed(tx, '_addDORequestEV');
this.dispatchECEvent(`${this.doHash} confirmed!`);
const [ txFrom, requestId ] = isProcessed.result;
this.doRequest = requestId;
this.dispatchECEvent(`Request ${this.doRequest} was created successfully.`);
return true;
} catch (e) {
this.status = ECStatus.ERROR;
this.dispatchECEvent(`Transaction failed: ${e.message}`);
throw new Error(`Transaction failed: ${e.message}`);
}
}
findOrder = async () => {
this.progress = ECEvent.CREATED;
this.dispatchECEvent(`Waiting for Ethernity CLOUD network... `);
while (true) {
try {
const protocolContract = this.protocolContract.getContract();
const ordersCount = await protocolContract._getOrdersCount();
this.dispatchECEvent(`Orders count: ${ordersCount}`, ECLog.DEBUG);
for (let i = ordersCount - 1; i >= this.ordersOffset; i--) {
const order = await protocolContract._getOrder(i);
this.dispatchECEvent(`Checking order: ` + util.inspect(order, {depth: null}), ECLog.DEBUG);
this.dispatchECEvent(`Checking if: ${order.doRequest} == ${this.doRequest}`, ECLog.DEBUG);
if (parseInt(order.doRequest) === parseInt(this.doRequest)) {
this.dispatchECEvent(`Found order with orderId: ${i}`, ECLog.DEBUG);
this.orderId = i;
this.order = order;
this.progress = ECEvent.ORDER_PLACED;
this.dispatchECEvent(`Connected!`);
return true;
}
await delay(200);
}
await delay(1000);
continue;
}
catch(e){
this.dispatchECEvent(`Failed to find order: ` + e.message, ECLog.WARNING);
await delay(1000);
}
}
}
// eslint-disable-next-line class-methods-use-this
parseOrderResult = (result) => {
try {
const arr = result.split(':');
const tBytes = arr[1].startsWith('0x') ? arr[1] : `0x${arr[1]}`;
return {
version: arr[0],
transactionBytes: tBytes,
resultIPFSHash: arr[2]
};
} catch (e) {
throw new Error(ECError.PARSE_ERROR);
}
};
parseTransactionBytes(bytes) {
try {
const result = parseTransactionBytes(this.protocolAbi, bytes);
const arr = result.result.split(':');
return {
version: arr[0],
from: result.from,
taskCode: arr[1],
taskCodeString: ECOrderTaskStatus[arr[1]],
checksum: arr[2],
enclaveChallenge: arr[3]
};
} catch (e) {
throw new Error(ECError.PARSE_ERROR);
}
}
async getResultFromOrder() {
try {
// get the result of the order using the `etnyContract` object
this.progress = ECEvent.DOWNLOADING;
this.dispatchECEvent(`Downloading result...`);
const orderResult = await this.protocolContract.getResultFromOrder(this.orderId);
// parse the order result
const parsedOrderResult = this.parseOrderResult(orderResult);
if(parsedOrderResult.resultIPFSHash === undefined) {
return { success: false, message: 'Task processing failed, no IPFS hash returned' };
}
this.dispatchECEvent(`Downloading: ${parsedOrderResult.resultIPFSHash}`);
this.progress = ECEvent.VERIFYING;
// parse the transaction bytes of the order result
const transactionResult = this.parseTransactionBytes(parsedOrderResult.transactionBytes);
// generate a wallet address using the `challengeHash` and `transactionResult.enclaveChallenge`
const wallet = generateWallet(this.challengeHash, transactionResult.enclaveChallenge);
// check if the generated wallet address matches the `transactionResult.from` address
if (!wallet || wallet !== transactionResult.from) {
return { success: false, message: 'Integrity check failed, signer wallet address is wrong.' };
}
// get the result value from IPFS using the `parsedOrderResult.resultIPFSHash`
const ipfsResult = await ipfsClient.getFromIPFS(parsedOrderResult.resultIPFSHash);
// decrypt data
this.dispatchECEvent(`Validating proof...`);
const currentWalletAddress = this.tokenContract.getCurrentWallet();
const decryptedData = await decryptWithPrivateKey(
ipfsResult,
currentWalletAddress,
this.walletContext ? this.walletContext.privateKey : null
);
if (!decryptedData.success) {
return { success: false, message: 'Could not decrypt the order result.' };
}
// update the loading message to show the result value
//this.dispatchECEvent(`Result value: ${decryptedData.data}`);
// calculate the SHA-256 checksum of the result value
const ipfsResultChecksum = sha256(decryptedData.data);
// check if the calculated checksum matches the `transactionResult.checksum`
if (ipfsResultChecksum !== transactionResult.checksum) {
return { success: false, message: 'Integrity check failed, checksum of the order result is wrong.' };
}
return {
success: true,
contractAddress: this.tokenContract.contractAddress(),
inputTransactionHash: this.doHash,
orderId: this.orderId,
imageHash: `${this.enclaveImageIPFSHash}:${this.secureLockEnclave}`,
scriptHash: this.scriptHash,
fileSetHash: this.fileSetHash,
resultHash: parsedOrderResult.resultIPFSHash,
resultTaskCode: transactionResult.taskCodeString,
resultValue: ipfsResult,
result: decryptedData.data
};
} catch (ex) {
//console.log(ex);
if (ex.name === ECError.PARSE_ERROR) {
return { success: false, message: 'Ethernity parsing transaction error.' };
}
if (ex.name === ECError.IPFS_DOWNLOAD_ERROR) {
return { success: false, message: 'Ethernity IPFS download result error.' };
}
await delay(5000);
this.getResultFromOrderRepeats += 1;
// eslint-disable-next-line no-return-await
return await this.getResultFromOrder();
}
}
async getProofDetails(endBlockNumber = this.protocolContract.getProvider().getBlockNumber()) {
// get the original input transaction hash and the output transaction hash for the order
const transaction = await this.protocolContract.getProvider().getTransaction(this.doHash);
const startBlockNumber = await this.protocolContract.getProvider().getBlock(transaction.blockNumber);
const startblockTimestamp = startBlockNumber.timestamp;
let resultBlockNumber;
let resultTransactionHash;
let resultBlockTimestamp;
// eslint-disable-next-line no-plusplus
for (let i = endBlockNumber; i >= startBlockNumber; i--) {
/// eslint-disable-next-line no-await-in-loop
const block = await this.protocolContract.getProvider().getBlockWithTransactions(i);
// eslint-disable-next-line no-continue
if (!block || !block.transactions) continue;
// eslint-disable-next-line no-restricted-syntax
for (const transaction of block.transactions) {
if (transaction.to === this.protocolContract.contractAddress() && transaction.data) {
resultBlockNumber = transaction.blockNumber;
resultTransactionHash = transaction.hash;
resultBlockTimestamp = block.timestamp;
}
}
}
return {
inputTransactionHash: this.doHash,
inputTimestamp: startblockTimestamp,
inputBlockNumber: startblockNumber,
outputTransactionHash: outputTransactionHash,
outputBlockTimestamp: resultBlockTimestamp,
outputBlockNumber: resultBlockNumber,
}
}
async getResult() {
return this.result;
}
reset = () => {
this.orderId = -1;
this.doHash = null;
this.doRequest = -1;
this.scriptHash = '';
this.fileSetHash = '';
this.interval = null;
this.getResultFromOrderRepeats = 1;
this.taskHasBeenPickedForApproval = false;
};
cleanup = async () => {
this.reset();
const contract = this.protocolContract.getContract();
contract.removeAllListeners();
};
isNodeOperatorAddress = async (nodeAddress) => {
if (isNullOrEmpty(nodeAddress)) return true;
if (isAddress(nodeAddress)) {
const isNode = await this.protocolContract.isNodeOperator(nodeAddress);
if (!isNode) {
this.status = ECStatus.ERROR;
this.dispatchECEvent('Introduced address is not a valid node operator address.');
throw new Error('Introduced address is not a valid node operator address.');
}
return true;
}
this.status = ECStatus.ERROR;
this.dispatchECEvent('Introduced address is not a valid wallet address.');
throw new Error('Introduced address is not a valid wallet address.');
};
// eslint-disable-next-line class-methods-use-this
initializeStorage(ipfsAddress, protocol, port, token) {
ipfsClient.initialize(ipfsAddress, protocol, port, token);
}
// use this in order to reset the instance and have a new runner
static resetInstance() {
EthernityCloudRunner.instance = null;
}
async setNetwork(network, type) {
this.network = network.toLowerCase()+ "_" + type.toUpperCase()
this.trustedZoneImage = ECRunner[network.toUpperCase()]["PYNITHY_RUNNER_"+type.toUpperCase()]
}
async checkNetwork() {
try {
// checking network
const networkName = await this.tokenContract.getNetworkName();
const expected = this.networkConfig && this.networkConfig.networkName;
if (expected && networkName !== expected) {
this.status = ECStatus.ERROR;
const label = (this.networkConfig.networkKey || expected).toString();
this.dispatchECEvent(`Please switch Web3 network and use ${label}!`);
throw new Error(`Please switch Web3 network and use ${label}!`);
}
return networkName === expected;
} catch (e) {
this.status = ECStatus.ERROR;
this.dispatchECEvent(
`Error while connecting web3 client: ${e.message}`,
);
throw new Error(
`Error while connecting web3 client: ${e.message}`,
);
return false;
}
}
async run(resources, secureLockEnclave, code, nodeAddress = '', trustedZoneEnclave = 'etny-nodenithy-testnet') {
try {
this.resources = resources;
// If the app never configured storage, fall back to the default IPFS
// endpoint so the challenge/code upload doesn't fail with a null client.
// An explicit initializeStorage() call before run() takes precedence.
if (!ipfsClient.isInitialized()) {
this.initializeStorage(DEFAULT_IPFS_ADDRESS);
}
// Disambiguate shared-token ECLD testnets (IoTeX/Sepolia/LitVM) from the
// live provider before any contract call depends on the PoX address.
await this.resolveNetworkContext();
await this.checkWalletBalance(this.resources.taskPrice);
await this.verifyNodeAddress(nodeAddress);
await this.initializeImageRegistry(secureLockEnclave);
await this.initializeWeb3Connection();
await this.checkAllowance(this.resources.taskPrice);
await this.processTask(code);
} catch (error) {
this.handleError(error);
}
}
}
export default EthernityCloudRunner;