diff --git a/src/bin/toolkit.js b/src/bin/toolkit.js index 9c82e84..2bce9e2 100644 --- a/src/bin/toolkit.js +++ b/src/bin/toolkit.js @@ -10,503 +10,565 @@ const path = require('path') const readline = require('readline') const { exec } = require("child_process") -Promise.all([ - import('witnet-radon-js'), -]) -.then(([{ default: witnet_radon_js_1 }, ]) => { +const toolkit = require("../") - const { Radon } = witnet_radon_js_1; - - /// CONSTANTS ======================================================================================================= - - const version = '1.6.7' - const toolkitDownloadUrlBase = `https://github.com/witnet/witnet-rust/releases/download/${version}/` - const toolkitDownloadNames = { - win32: (arch) => `witnet_toolkit-${arch}-pc-windows-msvc.exe`, - linux: (arch) => `witnet_toolkit-${arch}-unknown-linux-gnu${arch.includes("arm") ? "eabihf" : ""}`, - darwin: (arch) => `witnet_toolkit-${arch}-apple-darwin`, - } - const toolkitFileNames = { - win32: (arch) => `witnet_toolkit-${version}-${arch}-pc-windows-msvc.exe`, - linux: (arch) => `witnet_toolkit-${version}-${arch}-unknown-linux-gnu${arch.includes("arm") ? "eabihf" : ""}`, - darwin: (arch) => `witnet_toolkit-${version}-${arch}-apple-darwin`, - } - const archsMap = { - arm64: 'x86_64', - x64: 'x86_64' - } - - - /// ENVIRONMENT ACQUISITION ========================================================================================= - - let args = process.argv - const binDir = __dirname - - const toolkitDirPath = path.resolve(binDir, '../../assets/') - const platform = guessPlatform() - const arch = guessArch() - const toolkitDownloadName = guessToolkitDownloadName(platform, arch) - const toolkitFileName = guessToolkitFileName(platform, arch) - const toolkitBinPath = guessToolkitBinPath(toolkitDirPath, platform, arch) - const toolkitIsDownloaded = checkToolkitIsDownloaded(toolkitBinPath); - - function guessPlatform () { - return os.platform() - } - function guessArch () { - const rawArch = os.arch() - return archsMap[rawArch] || rawArch - } - function guessDownloadUrl(toolkitFileName) { - return `${toolkitDownloadUrlBase}${toolkitFileName}` - } - function guessToolkitDownloadName (platform, arch) { - return (toolkitDownloadNames[platform] || toolkitDownloadNames['linux'])(arch) - } - function guessToolkitFileName (platform, arch) { - return (toolkitFileNames[platform] || toolkitFileNames['linux'])(arch) - } - function guessToolkitBinPath (toolkitDirPath, platform, arch) { - const fileName = guessToolkitFileName(platform, arch) - - return path.resolve(toolkitDirPath, fileName) - } - function checkToolkitIsDownloaded (toolkitBinPath) { - return fs.existsSync(toolkitBinPath) - } - - - /// HELPER FUNCTIONS ================================================================================================ - - async function prompt (question) { - const readlineInterface = readline.createInterface({ - input: process.stdin, - output: process.stdout - }) - - return new Promise((resolve, _) => { - readlineInterface.question(`${question} `, (response) => { - readlineInterface.close() - resolve(response.trim()) - }) - }) - } - - async function downloadToolkit (toolkitDownloadName, toolkitFileName, toolkitBinPath, platform, arch) { - const downloadUrl = guessDownloadUrl(toolkitDownloadName) - console.log('Downloading', downloadUrl, 'into', toolkitBinPath) - - const file = fs.createWriteStream(toolkitBinPath) - const req = axios({ - method: "get", - url: downloadUrl, - responseType: "stream" - }).then(function (response) { - response.data.pipe(file) - }); - - return new Promise((resolve, reject) => { - file.on('finish', () => { - file.close(() => { - if (file.bytesWritten > 1000000) { - fs.chmodSync(toolkitBinPath, 0o755) - resolve() - } else { - reject(`No suitable witnet_toolkit binary found. Maybe your OS (${platform}) or architecture \ - (${arch}) are not yet supported. Feel free to complain about it in the Witnet community on Discord: \ - https://discord.gg/2rTFYXHmPm `) - } - }) - }) - const errorHandler = (err) => { - fs.unlink(downloadUrl, () => { - reject(err) - }) - } - file.on('error', errorHandler) +/// CONSTANTS ======================================================================================================= + +const version = '1.6.7' +const toolkitDownloadUrlBase = `https://github.com/witnet/witnet-rust/releases/download/${version}/` +const toolkitDownloadNames = { + win32: (arch) => `witnet_toolkit-${arch}-pc-windows-msvc.exe`, + linux: (arch) => `witnet_toolkit-${arch}-unknown-linux-gnu${arch.includes("arm") ? "eabihf" : ""}`, + darwin: (arch) => `witnet_toolkit-${arch}-apple-darwin`, +} +const toolkitFileNames = { + win32: (arch) => `witnet_toolkit-${version}-${arch}-pc-windows-msvc.exe`, + linux: (arch) => `witnet_toolkit-${version}-${arch}-unknown-linux-gnu${arch.includes("arm") ? "eabihf" : ""}`, + darwin: (arch) => `witnet_toolkit-${version}-${arch}-apple-darwin`, +} +const archsMap = { + arm64: 'x86_64', + x64: 'x86_64' +} + + +/// ENVIRONMENT ACQUISITION ========================================================================================= + +let args = process.argv +const binDir = __dirname + +const toolkitDirPath = path.resolve(binDir, '../../assets/') +const platform = guessPlatform() +const arch = guessArch() +const toolkitDownloadName = guessToolkitDownloadName(platform, arch) +const toolkitFileName = guessToolkitFileName(platform, arch) +const toolkitBinPath = guessToolkitBinPath(toolkitDirPath, platform, arch) +const toolkitIsDownloaded = checkToolkitIsDownloaded(toolkitBinPath); + +function guessPlatform () { + return os.platform() +} +function guessArch () { + const rawArch = os.arch() + return archsMap[rawArch] || rawArch +} +function guessDownloadUrl(toolkitFileName) { + return `${toolkitDownloadUrlBase}${toolkitFileName}` +} +function guessToolkitDownloadName (platform, arch) { + return (toolkitDownloadNames[platform] || toolkitDownloadNames['linux'])(arch) +} +function guessToolkitFileName (platform, arch) { + return (toolkitFileNames[platform] || toolkitFileNames['linux'])(arch) +} +function guessToolkitBinPath (toolkitDirPath, platform, arch) { + const fileName = guessToolkitFileName(platform, arch) + + return path.resolve(toolkitDirPath, fileName) +} +function checkToolkitIsDownloaded (toolkitBinPath) { + return fs.existsSync(toolkitBinPath) +} + + +/// HELPER FUNCTIONS ================================================================================================ + +async function prompt (question) { + const readlineInterface = readline.createInterface({ + input: process.stdin, + output: process.stdout + }) + + return new Promise((resolve, _) => { + readlineInterface.question(`${question} `, (response) => { + readlineInterface.close() + resolve(response.trim()) }) - } - - async function toolkitRun(settings, args) { - const cmd = `${settings.paths.toolkitBinPath} ${args.join(' ')}` - if (settings.verbose) { - console.log('Running >', cmd) - } - - return new Promise((resolve, reject) => { - exec(cmd, { maxBuffer: 1024 * 1024 * 10 }, (error, stdout, stderr) => { - if (error) { - reject(error) + }) +} + +async function downloadToolkit (toolkitDownloadName, toolkitFileName, toolkitBinPath, platform, arch) { + const downloadUrl = guessDownloadUrl(toolkitDownloadName) + console.log('Downloading', downloadUrl, 'into', toolkitBinPath) + + const file = fs.createWriteStream(toolkitBinPath) + const req = axios({ + method: "get", + url: downloadUrl, + responseType: "stream" + }).then(function (response) { + response.data.pipe(file) + }); + + return new Promise((resolve, reject) => { + file.on('finish', () => { + file.close(() => { + if (file.bytesWritten > 1000000) { + fs.chmodSync(toolkitBinPath, 0o755) + resolve() + } else { + reject(`No suitable witnet_toolkit binary found. Maybe your OS (${platform}) or architecture \ +(${arch}) are not yet supported. Feel free to complain about it in the Witnet community on Discord: \ +https://discord.gg/2rTFYXHmPm `) } - if (stderr) { - if (settings.verbose) { - console.log('STDERR <', stderr) - } - reject(stderr) - } - if (settings.verbose) { - console.log('STDOUT <', stdout) - } - resolve(stdout) }) }) - } - - function formatRadonValue (call) { - const radonType = Object.keys(call)[0] - let value = JSON.stringify(call[radonType]) - - if (radonType === 'RadonInteger') { - value = parseInt(value.replace('\"', '')) - } else if (radonType === 'RadonBytes') { - value = JSON.parse(value).map(i => i.toString(16)).join("") - } else if (radonType === 'RadonError') { - value = red( - value - .replace(/.*Inner\:\s`Some\((?.*)\)`.*/g, '$') - .replace(/UnsupportedReducerInAT\s\{\soperator\:\s0\s\}/g, 'MissingReducer') - ) + const errorHandler = (err) => { + fs.unlink(downloadUrl, () => { + reject(err) + }) } - - return [radonType.replace('Radon', ''), value] - } - - function blue (string) { - return `\x1b[34m${string}\x1b[0m` - } - - function green (string) { - return `\x1b[32m${string}\x1b[0m` - } - - function red (string) { - return `\x1b[31m${string}\x1b[0m` - } - - function yellow (string) { - return `\x1b[33m${string}\x1b[0m` - } - - - /// COMMAND HANDLERS ================================================================================================ - - async function installCommand (settings) { - if (!settings.checks.toolkitIsDownloaded) { - // Skip confirmation if install is forced - if (!settings.force) { - console.log(`The witnet_toolkit ${version} native binary hasn't been downloaded yet (this is a requirement).`) - const will = await prompt("Do you want to download it now? (Y/n)") - - // Abort if not confirmed - if (!['', 'y'].includes(will.toLowerCase())) { - console.error('Aborted download of witnet_toolkit native binary.') - return - } + file.on('error', errorHandler) + }) +} + +async function toolkitRun(settings, args) { + const cmd = `${settings.paths.toolkitBinPath} ${args.join(' ')}` + return new Promise((resolve, reject) => { + exec(cmd, { maxBuffer: 1024 * 1024 * 10 }, (error, stdout, stderr) => { + if (error) { + reject(error) } - - return forcedInstallCommand(settings) - } - } - - async function forcedInstallCommand (settings) { - return downloadToolkit( - settings.paths.toolkitDownloadName, - settings.paths.toolkitFileName, - settings.paths.toolkitBinPath, - settings.system.platform, - settings.system.arch - ) - .catch((err) => { - console.error(`Error updating witnet_toolkit binary:`, err) - }) - } - - function decodeFilters (mir) { - return mir.map((filter) => { - if (filter.args.length > 0) { - const decodedArgs = cbor.decode(Buffer.from(filter.args)) - return {...filter, args: decodedArgs} - } else { - return filter + if (stderr) { + reject(stderr) } + resolve(stdout) }) - } - - function decodeScriptsAndArguments (mir) { - let decoded = mir.data_request - decoded.retrieve = decoded.retrieve.map((source) => { - const decodedScript = cbor.decode(Buffer.from(source.script)) - return {...source, script: decodedScript} - }) - decoded.aggregate.filters = decodeFilters(decoded.aggregate.filters) - decoded.tally.filters = decodeFilters(decoded.tally.filters) - - return decoded - } + }) +} - function tasksFromMatchingFiles (args, matcher) { - return fs.readdirSync(args[2]) - .filter((filename) => filename.match(matcher)) - .map((filename) => [args[0], args[1], path.join(args[2], filename)]) - } +function decodeUint8Arrays(_obj, key, value) { + return ['body', 'script', ].includes(key) ? JSON.stringify(value) : value +} - async function tasksFromArgs (args) { - // Ensure that no task contains arguments starting with `0x` - return [args.map(arg => arg.replace(/^0x/gm, ''))] - } - async function decodeQueryCommand (settings, args) { - const tasks = await tasksFromArgs(args) - const promises = Promise.all(tasks.map(async (task) => { - return fallbackCommand(settings, ['decode-query', ...task.slice(1)]) - .then(JSON.parse) - .then(decodeScriptsAndArguments) - .then((decoded) => JSON.stringify(decoded, null, 4)) - })) +/// COMMAND HANDLERS ================================================================================================ - return (await promises).join() - } +async function installCommand (settings) { + if (!settings.checks.toolkitIsDownloaded) { + // Skip confirmation if install is forced + if (!settings.force) { + console.log(`The witnet_toolkit ${version} native binary hasn't been downloaded yet (this is a requirement).`) + const will = await prompt("Do you want to download it now? (Y/n)") - async function traceQueryCommand (settings, args) { - let query, radon - const tasks = await tasksFromArgs(args) - - return Promise.all(tasks.map(async (task) => { - const queryJson = await fallbackCommand(settings, ['decode-query', ...task.slice(1)]) - const mir = JSON.parse(queryJson) - query = decodeScriptsAndArguments(mir) - radon = new Radon(query) - const output = await fallbackCommand(settings, ['try-query', ...task.slice(1)]) - let report; - try { - report = JSON.parse(output) - } catch { + // Abort if not confirmed + if (!['', 'y'].includes(will.toLowerCase())) { + console.error('Aborted download of witnet_toolkit native binary.') return } - const dataSourcesCount = report.retrieve.length - - const dataSourcesInterpolation = report.retrieve.map((source, sourceIndex, sources) => { - let executionTime - try { - executionTime = - (source.context.completion_time.nanos_since_epoch - source.context.start_time.nanos_since_epoch) / 1000000 - } catch (_) { - executionTime = 0 - } - - const cornerChar = sourceIndex < sources.length - 1 ? '├' : '└' - const sideChar = sourceIndex < sources.length - 1 ? '│' : ' ' + } - let traceInterpolation - try { - if ((source.partial_results || []).length === 0) { - source.partial_results = [source.result] + return forcedInstallCommand(settings) + } +} + +async function forcedInstallCommand (settings) { + return downloadToolkit( + settings.paths.toolkitDownloadName, + settings.paths.toolkitFileName, + settings.paths.toolkitBinPath, + settings.system.platform, + settings.system.arch + ) + .catch((err) => { + console.error(`Error updating witnet_toolkit binary:`, err) + }) +} + + +function tasksFromMatchingFiles (args, matcher) { + return fs.readdirSync(args[2]) + .filter((filename) => filename.match(matcher)) + .map((filename) => [args[0], args[1], path.join(args[2], filename)]) +} + +function tasksFromArgs (args) { + // Ensure that no task contains arguments starting with `0x` + return args.map(arg => arg.replace(/^0x/gm, '')) +} + +var cyan = (str) => `\x1b[36m${str}\x1b[0m` +var gray = (str) => `\x1b[90m${str}\x1b[0m` +var green = (str) => `\x1b[32m${str}\x1b[0m` +var lcyan = (str) => `\x1b[1;96m${str}\x1b[0m` +var lgreen = (str) => `\x1b[1;92m${str}\x1b[0m` +var lyellow = (str) => `\x1b[1;93m${str}\x1b[0m` +var lred = (str) => `\x1b[91m${str}\x1b[0m` +var red = (str) => `\x1b[31m${str}\x1b[0m` +var white = (str) => `\x1b[1;98m${str}\x1b[0m` +var yellow = (str) => `\x1b[33m${str}\x1b[0m` + +var commas = (number) => number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); +var extractTypeName = (str) => str ? str.split(/(?=[A-Z])/).slice(1).join("") : "Any" + +async function reportHeadline(request, headline) { + const trait = (str) => `${str}${" ".repeat(56 - str.length)}` + const indent = settings?.indent ? " ".repeat(indent) : "" + const resultDataType = `Result<${extractTypeName(request.retrieve[0]?.script?.constructor.name)}, Error>` + console.info(`${indent}╔══════════════════════════════════════════════════════════════════════════════╗`) + console.info(`${indent}║ ${white(headline)}${" ".repeat(77 - headline.length)}║`) + console.info(`${indent}╠══════════════════════════════════════════════════════════════════════════════╣`) + console.info(`${indent}║ ${white("RAD hash")}: ${lgreen(request.radHash())} ║`) + console.info(`${indent}║ > Bytes weight: ${white(trait(commas(request.weight())))} ║`) + console.info(`${indent}║ > Data sources: ${white(trait(commas(request.retrieve.length)))} ║`) + console.info(`${indent}║ > Radon operators: ${white(trait(commas(request.opsCount())))} ║`) + console.info(`${indent}║ > Result data type: ${yellow(trait(resultDataType))} ║`) + // console.info(`${indent}╠════════════════════════════════════════════════════════════════════════════╣`) + // console.info(`${indent}║ > Times solved: ${white(trait("{ values: 123, errors: 220 }"))} ║`) + // console.info(`${indent}║ > Times witnessed: ${white(trait("{ values: 2130, errors: 1326 }"))} ║`) + // console.info(`${indent}║ > Total fees: ${white(trait("15,234.123 Wits"))} ║`) + // console.info(`${indent}║ > Total slash: ${white(trait(" 56.123 Wits"))} ║`) + // console.info(`${indent}║ > Total burn: ${white(trait(" 0.789 Wits"))} ║`) + // if (verbose) { + // console.info(`${indent}╚══╤═════════════════════════════════════════════════════════════════════════╝`) + // } else { + // console.info(`${indent}╚════════════════════════════════════════════════════════════════════════════╝`) + // } +} + +async function decodeRadonRequestCommand (settings, args) { + const indent = settings?.indent ? " ".repeat(indent) : "" + const tasks = tasksFromArgs(args) + const promises = Promise.all(tasks.map(async (bytecode) => { + const request = toolkit.Utils.decodeRequest(bytecode) + if (settings?.json) { + console.info(JSON.stringify(settings?.verbose ? request.toJSON() : request.toProtobuf(), null, settings?.verbose && settings?.indent || 0)) + + } else { + reportHeadline(request, "WITNET DATA REQUEST DISASSEMBLE") + const trait = (str) => `${str}${" ".repeat(54 - str.length)}` + // console.info(`${indent}╠════════════════════════════════════════════════════════════════════════════╣`) + // console.info(`${indent}║ > Times solved: ${white(trait("{ values: 123, errors: 220 }"))} ║`) + // console.info(`${indent}║ > Times witnessed: ${white(trait("{ values: 2130, errors: 1326 }"))} ║`) + // console.info(`${indent}║ > Total fees: ${white(trait("15,234.123 Wits"))} ║`) + // console.info(`${indent}║ > Total slashed: ${white(trait(" 56.123 Wits"))} ║`) + // console.info(`${indent}║ > Total burnt: ${white(trait(" 0.789 Wits"))} ║`) + if (!settings.verbose) { + console.info(`${indent}╚══════════════════════════════════════════════════════════════════════════════╝`) + + } else { + console.info(`${indent}╚══╤═══════════════════════════════════════════════════════════════════════════╝`) + console.info(`${indent}┌──┴─────────────────┐`) + console.info(`${indent}│ ${white("RETRIEVE DATA")} │`) // ├ ┤ + console.info(`${indent}└──┬─┬───────────────┘`) + request.retrieve.forEach((source, sourceIndex) => { + // var subdomains = source.authority.toUpperCase().split(".") + // var authority = subdomains.length > 2 ? subdomains.slice(1).join(".") : subdomains.join(".") + var authority = source.authority?.toUpperCase() || (source.method === toolkit.RadonRetrievals.Methods.RNG ? "WIT/RNG" : "") + var corner = sourceIndex === request.retrieve.length - 1 ? "└" : "├" + var sep = sourceIndex === request.retrieve.length - 1 ? " " : "│" + console.info(`${indent} │ ${corner}─ ${lgreen("[ ")}${`Data Source #${sourceIndex + 1}`}: ${" ".repeat(3 - sourceIndex.toString().length)}${lcyan(authority)} ${lgreen("]")}`) + if (source.method !== toolkit.RadonRetrievals.Methods.RNG) { + console.info(`${indent} │ ${sep} > Method: ${lgreen(toolkit.RadonRetrievals.Methods[source.method])}`) + if (source?.schema) console.info(`${indent} │ ${sep} > URL schema: ${green(source.schema)}`) + if (source?.query) console.info(`${indent} │ ${sep} > URL query: ${green(source.query)}`) + if (source?.headers) console.info(`${indent} │ ${sep} > HTTP headers: ${green(JSON.stringify(source.headers))}`) + if (source?.body) console.info(`${indent} │ ${sep} > HTTP body: ${green(source.body)}`) + // if (source?.script) console.info(`${indent} │ ${sep} > Input data: ${lyellow("[ ")}${yellow(source.script.constructor.name)}${lyellow(" ]")}`) + if (source?.script) console.info(`${indent} │ ${sep} > Radon script: ${lcyan(source.script.toString())}`) + if (source?.script) console.info(`${indent} │ ${sep} > Output data: ${lyellow("[ ")}${yellow(source.script.constructor.name)}${lyellow(" ]")}`) } - traceInterpolation = source.partial_results.map((radonValue, callIndex) => { - const formattedRadonValue = formatRadonValue(radonValue) - - const operator = radon - ? (callIndex === 0 - ? blue(radon.retrieve[sourceIndex].kind) - : `.${blue(radon.retrieve[sourceIndex].script.operators[callIndex - 1].operatorInfo.name + '(')}${radon.retrieve[sourceIndex].script.operators[callIndex - 1].mirArguments.join(', ') + blue(')')}`) + ' ->' - : '' - - return ` │ ${sideChar} [${callIndex}] ${operator} ${yellow(formattedRadonValue[0])}: ${formattedRadonValue[1]}` - }).join('\n') - } catch (e) { - traceInterpolation = ` │ ${sideChar} ${red('[ERROR] Cannot decode execution trace information')}` - } - - let urlInterpolation = query ? ` - │ ${sideChar} Method: ${radon.retrieve[sourceIndex].kind} - │ ${sideChar} Complete URL: ${radon.retrieve[sourceIndex].url}` : '' - - // // TODO: take headers info from `radon` instead of `query` once POST is supported in `witnet-radon-js` - const headers = radon.retrieve[sourceIndex].headers;//query.retrieve[sourceIndex].headers - if (headers) { - const headersInterpolation = headers.map(([key, value]) => ` - │ ${sideChar} "${key}": "${value}"`).join() - urlInterpolation += ` - │ ${sideChar} Headers: ${headersInterpolation}` + if (sourceIndex < request.retrieve.length - 1) { + console.info(`${indent} │ │`) + } + }) + var stringifyFilter = (x) => `${lcyan(toolkit.RadonFilters.Opcodes[x.opcode])}(${x.args ? cyan(JSON.stringify(x.args)) : ""})` + var stringifyReducer = (x) => lcyan(`${toolkit.RadonReducers.Opcodes[x.opcode]}()`) + // console.info(` │`) + console.info(`${indent}┌──┴──────────────────┐`) + console.info(`${indent}│ ${white("AGGREGATE SOURCES")} │`) + console.info(`${indent}└──┬──────────────────┘`) // ┬ + request.aggregate?.filters.forEach(filter => + console.info(`${indent} │ > Radon filter: ${stringifyFilter(filter)}`)) + console.info(`${indent} │ > Radon reducer: ${stringifyReducer(request.aggregate)}`) + // console.info(` │`) + console.info(`${indent}┌──┴──────────────────┐`) + console.info(`${indent}│ ${white("WITNESSING TALLY")} │`) + console.info(`${indent}└─────────────────────┘`) // ┬ + request.tally?.filters.forEach(filter => + console.info(`${indent} > Radon filter: ${stringifyFilter(filter)}`)) + console.info(`${indent} > Radon reducer: ${stringifyReducer(request.tally)}`) + } + } + })) + return (await promises).join() +} + +async function dryrunRadonRequestCommand (settings, args) { + const indent = settings?.indent ? " ".repeat(indent) : "" + const tasks = tasksFromArgs(args) + const promises = Promise.all(tasks.map(async (bytecode) => { + const report = JSON.parse(await binaryFallbackCommand(settings, ['try-data-request', '--hex', bytecode])) + const result = report?.tally.result + const resultType = Object.keys(result)[0] + const resultValue = JSON.stringify(Object.values(result)[0]) + if (settings?.json) { + if (settings?.verbose) { + console.info(JSON.stringify(report, null, settings?.indent)) + } else { + result[resultType] = resultValue + console.info(JSON.stringify(result, null, settings?.indent)) + } + } else { + const request = toolkit.Utils.decodeRequest(bytecode) + reportHeadline(request, "WITNET DATA REQUEST DRY RUN REPORT", true) + console.info(`${indent}╚══╤═══════════════════════════════════════════════════════════════════════════╝`) + var execTimeMs = report.retrieve?.map(retrieval => + (retrieval?.running_time.secs || 0) + (retrieval?.running_time.nanos || 0) / 1000 + ).reduce( + (sum, secs) => sum + secs + ) + var execTimeMs = execTimeMs + " ms" + var flexbar = "─".repeat(execTimeMs.length); + var flexspc = " ".repeat(flexbar.length + 12) + console.info(`${indent}┌──┴─────────────────────────────${flexbar}──────┐`) + console.info(`${indent}│ ${white("Data providers")} ${flexspc} │`) // ├ ┤ + console.info(`${indent}├────────────────────────────────${flexbar}──────┤`) + console.info(`${indent}│ Execution time: ${green(execTimeMs)} ${flexspc} │`) + console.info(`${indent}└──┬─┬───────────────────────────${flexbar}──────┘`) + request.retrieve.forEach((source, sourceIndex) => { + var authority = source.authority?.toUpperCase() || (source.method === toolkit.RadonRetrievals.Methods.RNG ? "WIT/RNG" : "") + var corner = sourceIndex === request.retrieve.length - 1 ? "└" : "├" + var sep = sourceIndex === request.retrieve.length - 1 ? " " : "│" + console.info(`${indent} │ ${corner}─ [ ${lcyan(authority)} ]`) + if (source.method !== toolkit.RadonRetrievals.Methods.RNG) { + const result = report.retrieve[sourceIndex].result + const resultType = Object.keys(result)[0] + const resultValue = JSON.stringify(Object.values(result)[0]) + // console.info(`${indent} │ ${sep} > Method: ${lgreen(toolkit.RadonRetrievals.Methods[source.method])}`) + // if (source?.schema) console.info(`${indent} │ ${sep} > URL schema: ${green(source.schema)}`) + // if (source?.query) console.info(`${indent} │ ${sep} > URL query: ${green(source.query)}`) + // if (source?.headers) console.info(`${indent} │ ${sep} > HTTP headers: ${green(JSON.stringify(source.headers))}`) + // if (source?.body) console.info(`${indent} │ ${sep} > HTTP body: ${green(source.body)}`) + // if (source?.script) console.info(`${indent} │ ${sep} > Input data: ${lyellow("[ ")}${yellow(source.script.constructor.name)}${lyellow(" ]")}`) + // if (source?.script) console.info(`${indent} │ ${sep} > Radon script: ${lcyan(source.script.toString())}`) + // if (source?.script) console.info(`${indent} │ ${sep} > Output data: ${lyellow("[ ")}${yellow(source.script.constructor.name)}${lyellow(" ]")}`) + // console.info(`${indent} │ ${sep} ${yellow("[ ")}${yellow(resultType)}${yellow(" ]")} ${green(resultValue)}`) } - - // // TODO: take body info from `radon` instead of `query` once POST is supported in `witnet-radon-js` - const body = radon.retrieve[sourceIndex].body;//query.retrieve[sourceIndex].body - if (body) { - urlInterpolation += ` - │ ${sideChar} Body: ${Buffer.from(body)}` + if (settings?.verbose && sourceIndex < request.retrieve.length - 1) { + console.info(`${indent} │ │`) } - - const formattedRadonResult = formatRadonValue(source.result) - const resultInterpolation = `${yellow(formattedRadonResult[0])}: ${formattedRadonResult[1]}` - return ` - │ ${cornerChar}─${green('[')} Source #${sourceIndex + 1} ${ query.retrieve[sourceIndex].url ? `(${new URL(query.retrieve[sourceIndex].url).hostname})` : ''} ${green(']')}${urlInterpolation} - │ ${sideChar} Number of executed operators: ${source.context.call_index + 1 || 0} - │ ${sideChar} Execution time: ${executionTime > 0 ? executionTime + ' ms' : 'unknown'} - │ ${sideChar} Execution trace:\n${traceInterpolation} - │ ${sideChar} Execution result: ${resultInterpolation}` - }).join('\n │ │\n') - - let aggregationExecuted, aggregationExecutionTime, aggregationResult, tallyExecuted, tallyExecutionTime, tallyResult - - try { - aggregationExecuted = report.aggregate.context.completion_time !== null - aggregationExecutionTime = aggregationExecuted && - (report.aggregate.context.completion_time.nanos_since_epoch - report.aggregate.context.start_time.nanos_since_epoch) / 1000000 - aggregationResult = formatRadonValue(report.aggregate.result); - } catch (error) { - aggregationExecuted = false + }) + var flexbar = "─".repeat(16); + var flexspc = " ".repeat(28); + var extraWidth = 0 + if (['RadonMap', 'RadonArray', 'RadonError', 'RadonBytes', ].includes(resultType)) { + extraWidth = 31 + flexbar += "─".repeat(extraWidth) + flexspc += " ".repeat(extraWidth) } - - try { - tallyExecuted = report.tally.context.completion_time !== null - tallyExecutionTime = tallyExecuted && - (report.tally.context.completion_time.nanos_since_epoch - report.tally.context.start_time.nanos_since_epoch) / 1000000 - tallyResult = formatRadonValue(report.tally.result); - } catch (error) { - tallyExecuted = false + console.info(`${indent}┌──┴───────────────────────────${flexbar}─┐`) + console.info(`${indent}│ ${white("Aggregated result")}${flexspc} │`) // ├ ┤ + console.info(`${indent}├──────────────────────────────${flexbar}─┤`) + var printMapItem = (indent, width, key, value, indent2 = "") => { + // console.log(indent, width, key, value) + var key = `${indent2}"${key}": ` + var type = extractTypeName(Object.keys(value)[0]) + var value = Object.values(value)[0] + if (["Map", ].includes(type)) { + if (key.length > width - 12) { + console.info(`${indent}│ ${yellow("[ Map ]")} ${" ".repeat(width - 15)}${green("...")}│`) + } else { + console.info(`${indent}│ ${yellow("[ Map ]")} ${green(key)}${" ".repeat(width - 12 - key.length)}│`) + } + Object.entries(value).forEach(([ key, value ]) => printMapItem(indent, width, key, value, indent2 + " ")) + } else { + + if (key.length > width - 12) { + console.info(`${indent}│ ${yellow(type)} ${" ".repeat(width - 15)}${green("...")}│`) + } else { + if (["String", "Array", "Error", "Map"].includes(type)) { + value = JSON.stringify(value) + } + type = `[ ${type}${" ".repeat(7 - type.length)} ]` + var result = key + value + var spaces = width - 12 - result.length + if (result.length > width - 15) { + value = value.slice(0, width - 15 - key.length) + "..." + spaces = 0 + } + console.info(`${indent}│ ${yellow(type)} ${green(key)}${lgreen(value)}${" ".repeat(spaces)}│`) + } + } } - - let filenameInterpolation = '' - const retrievalInterpolation = `│ - │ ┌────────────────────────────────────────────────┐ - ├──┤ Retrieve stage │ - │ ├────────────────────────────────────────────────┤ - │ │ Number of retrieved data sources: ${dataSourcesCount}${` `.repeat(13 - dataSourcesCount.toString().length)}│ - │ └┬───────────────────────────────────────────────┘ - │ │${dataSourcesInterpolation}` - - const aggregationExecutionTimeInterpolation = aggregationExecuted ? ` - │ │ Execution time: ${aggregationExecutionTime} ms${` `.repeat(28 - aggregationExecutionTime.toString().length)}│` : '' - const aggregationInterpolation = `│ - │ ┌────────────────────────────────────────────────┐ - ├──┤ Aggregate stage │ - │ ├────────────────────────────────────────────────┤${aggregationExecutionTimeInterpolation} - │ │ Result is: ${yellow(aggregationResult[0])}: ${aggregationResult[1]}${` `.repeat(Math.max(0, (aggregationResult[0] === 'Error' ? 43 : 34) - aggregationResult[0].toString().length - aggregationResult[1].toString().length))}│ - │ └────────────────────────────────────────────────┘` - - const tallyExecutionTimeInterpolation = tallyExecuted ? ` - │ Execution time: ${tallyExecutionTime} ms${` `.repeat(28 - tallyExecutionTime.toString().length)}│` : '' - const tallyInterpolation = `│ - │ ┌────────────────────────────────────────────────┐ - └──┤ Tally stage │ - ├────────────────────────────────────────────────┤${tallyExecutionTimeInterpolation} - │ Result is: ${yellow(tallyResult[0])}: ${tallyResult[1]}${` `.repeat(Math.max(0, (tallyResult[0] === 'Error' ? 43 : 34) - tallyResult[0].toString().length - tallyResult[1].toString().length))}│ - └────────────────────────────────────────────────┘` - - return `╔═══════════════════════════════════════════════════╗ -║ Witnet data request local execution report ║${filenameInterpolation} -╚╤══════════════════════════════════════════════════╝ - ${retrievalInterpolation} - ${aggregationInterpolation} - ${tallyInterpolation}` - })).then((outputs) => outputs.join('\n')) - } - - async function fallbackCommand (settings, args) { - // For compatibility reasons, map query methods to data-request methods - if (args.length > 0) { - args = [args[0].replace('-query', '-data-request'), ...args.slice(1)] - return toolkitRun(settings, args) - .catch((err) => { - let errorMessage = err.message.split('\n').slice(1).join('\n').trim() - const errorRegex = /.*^error: (?.*)$.*/gm - const matched = errorRegex.exec(err.message) - if (matched) { - errorMessage = matched.groups.message + var printResult = (indent, width, resultType, resultValue) => { + resultType = extractTypeName(resultType) + // TODO: handle result arrays + if (resultType === "Map") { + console.info(`${indent}│ ${lyellow("[ Map ]")}${" ".repeat(width - 11)}│`) + var obj = JSON.parse(resultValue) + Object.entries(obj).forEach(([ key, value ]) => printMapItem(indent, width, key, value)) + } else { + if (resultType === "Bytes") { + resultValue = JSON.parse(resultValue).map(char => ('00' + char.toString(16)).slice(-2)).join("") } - console.error(errorMessage || err) - }) - } else { - console.info("USAGE:") - console.info(" npx witnet-toolkit ") - console.info("\nFLAGS:") - console.info(" --help Prints help information") - console.info(" --verbose Prints detailed information of the subcommands being run") - console.info(" --version Prints version information") - console.info("\nSUBCOMMANDS:") - console.info(" decode-query --hex Decodes some Witnet data query bytecode") - console.info(" trace-query --hex Resolves some Witnet data query bytecode locally, printing out step-by-step information") - console.info(" try-query --hex Resolves some Witnet data query bytecode locally, returning a detailed JSON report") - console.info() + var resultMaxWidth = width - resultType.length - 5 + if (resultValue.length > resultMaxWidth - 3) resultValue = resultValue.slice(0, resultMaxWidth - 3) + "..." + var spaces = width - resultType.length - resultValue.toString().length - 5 + var color = resultType.indexOf("Error") > -1 ? gray : lgreen + var typeText = resultType.indexOf("Error") > -1 ? `\x1b[1;98;41m Error \x1b[0m` : lyellow(`[ ${resultType} ]`) + console.info(`${indent}│ ${typeText} ${color(resultValue)}${" ".repeat(spaces)}│`) + } + } + printResult(indent, 46 + extraWidth, resultType, resultValue) + console.info(`${indent}└──────────────────────────────${flexbar}─┘`) + // TODO: Simulate witnesses from multiple regions } + })) + return (await promises).join() +} + +async function versionCommand (settings) { + return binaryFallbackCommand(settings, ['--version']) +} + +async function binaryFallbackCommand (settings, args) { + return toolkitRun(settings, args) + .catch((err) => { + let errorMessage = err.message.split('\n').slice(1).join('\n').trim() + const errorRegex = /.*^error: (?.*)$.*/gm + const matched = errorRegex.exec(err.message) + if (matched) { + errorMessage = matched.groups.message + } + console.error(errorMessage || err) + }) +} + + +/// PROCESS SETTINGS =============================================================================================== + +let force; +let forceIndex = args.indexOf('--force'); +if (forceIndex >= 2) { + // If the `--force` flag is found, process it, but remove it from args so it doesn't get passed down to the binary + force = args[forceIndex] + args.splice(forceIndex, 1) +} + +let json = false +if (args.includes('--json')) { + json = true + args.splice(args.indexOf('--json'), 1) +} + +let indent; +let indentIndex = args.indexOf('--indent') +if (indentIndex >= 2) { + if (args[indentIndex + 1] && !args[indentIndex + 1].startsWith('--')) { + indent = parseInt(args[indentIndex + 1]) + args.splice(indentIndex, 2) + } else { + args.splice(indentIndex) } - - - /// COMMAND ROUTER ================================================================================================== - - const router = { - 'decode-query': decodeQueryCommand, - 'fallback': fallbackCommand, - 'install': forcedInstallCommand, - 'trace-query': traceQueryCommand, - 'update': forcedInstallCommand, - } - - - /// PROCESS SETTINGS =============================================================================================== - - let force; - let forceIndex = args.indexOf('--force'); - if (forceIndex >= 2) { - // If the `--force` flag is found, process it, but remove it from args so it doesn't get passed down to the binary - force = args[forceIndex] - args.splice(forceIndex, 1) +} + +let verbose = false +if (args.includes('--verbose')) { + verbose = true + args.splice(args.indexOf('--verbose'), 1) +} + +const settings = { + paths: { + toolkitBinPath, + toolkitDirPath, + toolkitDownloadName, + toolkitFileName, + }, + checks: { + toolkitIsDownloaded, + }, + system: { + platform, + arch, + }, + force, json, indent, verbose +} + + +/// MAIN LOGIC ====================================================================================================== + +const mainRouter = { + '--': binaryFallbackCommand, + 'decodeRadonRequest': decodeRadonRequestCommand, + 'dryrunRadonRequest': dryrunRadonRequestCommand, + 'install': forcedInstallCommand, + 'network': networkFallbackCommand, + 'update': forcedInstallCommand, + 'version': versionCommand, +} + +const networkRouter = {} + +async function networkFallbackCommand (args) { + const networkCommand = networkRouter[args[0]]; + if (networkCommand) { + await networkCommand(settings, args.slice(1)) + } else { + console.info("\nUSAGE:") + console.info(` ${white("npx witnet network")} [ ...]`) + console.info("\nFLAGS:") + console.info(" --json Output data in JSON format") + console.info(" --indent <:nb> Number of white spaces used to prefix every output line") + console.info(" --verbose Report network detailed information") + console.info("\nNETWORK FLAGS:") + console.info(" --epoch <:nb> Extract data from or at specified epoch") + console.info(" --limit <:nb> Limit number of entries to fetch") + console.info(" --timeout <:secs> Limit seconds to wait for a result") + // console.info(" --help Explain commands requiring input params") + console.info("\nNETWORK COMMANDS:") + console.info(` address Public Witnet address corresponding to your ${yellow("WITNET_TOOLKIT_PRIVATE_KEY")}.`) + console.info(" blocks Lately consolidated blocks in the Witnet blockhain.") + console.info(" fees Lately consolidated transaction fees in the Witnet blockchain.") + console.info(" peers Search public P2P nodes in the Witnet network.") + console.info(" protocol Lorem ipsum.") + console.info(" providers Search public RPC providers in the Witnet network.") + console.info(" reporter Show Witnet network reporter's URL.") + console.info(" stakes Current staking entries in the Witnet network, ordered by power.") + console.info(" supply Current status of the Witnet network.") + console.info(" wips Lorem ipsum.") + console.info() + console.info(" getBalance Get balance of the specified Witnet address.") + console.info(" getUtxoInfo Get unspent transaction outputs of the specified Witnet address.") + console.info() + console.info(" getBlock Get details for the specified Witnet block.") + console.info(" getDataRequest Get current status or result of some unique data request transaction.") + console.info(" getTransaction Get details for specified Witnet transaction") + console.info() + console.info(" decodeRadonRequest Disassemble the Radon request given its network RAD hash.") + console.info(" dryrunRadonRequest Resolve the Radon request identified by the given network RAD hash, locally.") + console.info(" searchDataRequests Search data request transactions containing the given network RAD hash.") + console.info() + console.info(" sendDataRequest --unitary-fee <:nanoWits> --num-witnesses <:number>") + console.info(" sendValue <:nanoWits> --fee <:nanoWits> --to <:pkhAddress>") } +} - const settings = { - paths: { - toolkitBinPath, - toolkitDirPath, - toolkitDownloadName, - toolkitFileName, - }, - checks: { - toolkitIsDownloaded, - }, - system: { - platform, - arch, - }, - verbose: false, - force, +async function main () { + // Run installCommand before anything else, mainly to ensure that the witnet_toolkit binary + // has been downloaded, unless we're intentionally installing or updating the binary. + if (!args.includes('install') && !args.includes('update')) { + await installCommand(settings) } - - /// MAIN LOGIC ====================================================================================================== - - async function main () { - // Enter verbose mode if the --verbose flag is on - const verboseIndex = args.indexOf("--verbose") - if (verboseIndex >= 2) { - settings.verbose = true - args = [...args.slice(0, verboseIndex), ...args.slice(verboseIndex + 1)] - } - - // Find the right command using the commands router, or default to the fallback command - const commandName = args[2] - let command = router[commandName] || router['fallback'] - - // Run command before anything else, mainly to ensure that the witnet_toolkit binary - // has been downloaded. - // Skip if we are intentionally installing or updating the toolkit. - if (!['install', 'update'].includes(commandName)) { - await installCommand(settings) - } - - // Make sure that commands with --help are always passed through - if (args.includes("--help")) { - command = router['fallback'] - } - - // Run the invoked command, if any - if (command) { - const output = await command(settings, args.slice(2)) - if (output) { - console.log(output.trim()) - } - } + const command = mainRouter[args[2]]; args.splice(2, 1) + if (command) { + await command(settings, args.slice(2)) + } else { + console.info("\nUSAGE:") + console.info(` ${white("npx witnet")} [ ...]`) + console.info("\nFLAGS:") + console.info(" --json Output data in JSON format") + console.info(" --indent <:nb> Number of white spaces used to prefix every output line") + console.info(" --verbose Report step-by-step detailed information") + console.info("\nCOMMANDS:") + console.info(" decodeRadonRequest Disassemble hexified bytecode into a Radon request.") + console.info(" dryrunRadonRequest Resolve a Radon request given its hexified bytecode, locally.") + console.info() + console.info(" network Network commands suite for reading or interacting with the Witnet blockchain.") + console.info(" version Show version of the installed witnet_toolkit binary.") } +} - main() -}) +main() diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..0343772 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,28 @@ +import * as _RadonRetrievals from "./lib/radon/retrievals" +export const RadonRetrievals: typeof _RadonRetrievals = _RadonRetrievals; + +import * as _RadonReducers from "./lib/radon/reducers" +export const RadonReducers: typeof _RadonReducers = _RadonReducers; + +import * as _RadonFilters from "./lib/radon/filters" +export const RadonFilters: typeof _RadonFilters = _RadonFilters; + +import * as _RadonTypes from "./lib/radon/types" +export const RadonTypes: typeof _RadonTypes = _RadonTypes; + +export function RadonInnerScript(t: { new(): T; }): T { return new t(); } +export function RadonScript(): _RadonTypes.RadonString { return RadonInnerScript(_RadonTypes.RadonString); } + +export { RadonRequest, RadonRequestTemplate as RadonTemplate } from "./lib/radon/artifacts" + +export class RadonSLA { + public readonly numWitnesses: number; + public readonly unitaryFee: number; + constructor (numWitnesses: number, unitaryFee: number) { + this.numWitnesses = numWitnesses + this.unitaryFee = unitaryFee + } +} + +import * as _Utils from "./utils" +export const Utils: typeof _Utils = _Utils; diff --git a/src/lib/radon/artifacts.ts b/src/lib/radon/artifacts.ts index 99f1d83..6a718b7 100644 --- a/src/lib/radon/artifacts.ts +++ b/src/lib/radon/artifacts.ts @@ -1,54 +1,122 @@ -import { Class as Retrieval } from "./sources" -import { Class as Reducer, Mode } from "./reducers" +const helpers = require("./helpers") + +import { RadonRetrieval } from "./retrievals" +import { RadonReducer, Mode } from "./reducers" +import * as Utils from '../../utils' export type Args = string[] | string[][]; export interface Specs { - retrieve: Retrieval[]; - aggregate: Reducer; - tally: Reducer; - maxSize?: number; + retrieve: RadonRetrieval[]; + aggregate: RadonReducer; + tally: RadonReducer; } -export class Class { - public specs: Specs +class Class { + + public readonly retrieve: RadonRetrieval[]; + public readonly aggregate: RadonReducer; + public readonly tally: RadonReducer; + constructor(specs: Specs) { if (!specs.retrieve || !Array.isArray(specs.retrieve) || specs.retrieve.length == 0) { throw EvalError("\x1b[1;33mArtifact: cannot build if no sources are specified\x1b[0m") } specs.retrieve?.forEach((retrieval, index) => { if (retrieval === undefined) { - throw EvalError(`\x1b[1;31mArtifact: Retrieval #${index}\x1b[1;33m: undefined\x1b[0m`) - } else if (!(retrieval instanceof Retrieval)) { - throw EvalError(`\x1b[1;31mArtifact: Retrieval #${index}\x1b[1;33m: invalid type\x1b[0m`) + throw EvalError(`\x1b[1;31mArtifact: RadonRetrieval #${index}\x1b[1;33m: undefined\x1b[0m`) + } else if (!(retrieval instanceof RadonRetrieval)) { + throw EvalError(`\x1b[1;31mArtifact: RadonRetrieval #${index}\x1b[1;33m: invalid type\x1b[0m`) } }) - this.specs = specs - this.specs.maxSize = specs?.maxSize || 0 + this.retrieve = specs.retrieve; + this.aggregate = specs.aggregate; + this.tally = specs.tally; + } + + public opsCount(): number { + return (this.retrieve?.map(retrieval => retrieval.opsCount()).reduce((sum, a) => sum + a) || 0) + + this.aggregate?.opsCount() + + this.tally?.opsCount() } } -export class Template extends Class { - public argsCount: number; - public tests?: Map; +export class RadonRequest extends Class { + + public static from(hexString: string) { + return Utils.decodeRequest(hexString) + } + constructor(specs: { - retrieve: Retrieval | Retrieval[], - aggregate?: Reducer, - tally?: Reducer, - maxSize?: number, + retrieve: RadonRetrieval | RadonRetrieval[], + aggregate?: RadonReducer, + tally?: RadonReducer, + }) { + const retrieve = Array.isArray(specs.retrieve) ? specs.retrieve as RadonRetrieval[] : [ specs.retrieve ] + super({ + retrieve, + aggregate: specs?.aggregate || Mode(), + tally: specs?.tally || Mode(), + }) + let argsCount = retrieve.map(retrieval => retrieval.argsCount).reduce((prev, curr) => prev + curr) + if (argsCount > 0) { + throw EvalError("\x1b[1;33mRadonRequest: parameterized retrievals were passed\x1b[0m") + } + } + + public async execDryRun(): Promise { + return (await Utils.execDryRun(this.toBytecode(), '--json')).trim() + } + + public radHash(): string { + return Utils.sha256(helpers.encodeRequest(this.toProtobuf()))//.slice(0, 40) + } + + public toBytecode(): string { + return Utils.toHexString(helpers.encodeRequest(this.toProtobuf())) + } + + public toJSON(): any { + return { + retrieve: this.retrieve.map(retrieval => retrieval.toJSON()), + aggregate: this.aggregate.toJSON(), + tally: this.tally.toJSON(), + } + } + + public toProtobuf(): any { + return { + time_lock: 0, + retrieve: this.retrieve.map(retrieval => retrieval.toProtobuf()), + aggregate: this.aggregate.toProtobuf(), + tally: this.tally.toProtobuf(), + } + } + + public weight(): number { + return this.toBytecode().slice(2).length / 2; + } +} + +export class RadonRequestTemplate extends Class { + public readonly argsCount: number; + public readonly tests?: Map; + constructor(specs: { + retrieve: RadonRetrieval | RadonRetrieval[], + aggregate?: RadonReducer, + tally?: RadonReducer, }, tests?: Map ) { - const retrieve = Array.isArray(specs.retrieve) ? specs.retrieve as Retrieval[] : [ specs.retrieve ] + const retrieve = Array.isArray(specs.retrieve) ? specs.retrieve as RadonRetrieval[] : [ specs.retrieve ] super({ retrieve, aggregate: specs?.aggregate || Mode(), tally: specs?.tally || Mode(), - maxSize: specs?.maxSize || 32, }) this.argsCount = retrieve.map(retrieval => retrieval?.argsCount).reduce((prev, curr) => Math.max(prev, curr), 0) if (this.argsCount == 0) { - throw EvalError("\x1b[1;33mTemplate: cannot build w/ unparameterized sources\x1b[0m") + throw EvalError("\x1b[1;33mRadonRequestTemplate: no parameterized retrievals were passed\x1b[0m") } if (tests) { Object.keys(tests).forEach(test => { @@ -61,11 +129,11 @@ export class Template extends Class { Object(tests)[test] = Array(retrieve.length).fill(testArgs) testArgs = Object(tests)[test] } else if (testArgs?.length != retrieve.length) { - throw EvalError(`\x1b[1;33mTemplate: arguments mismatch in test \x1b[1;31m'${test}'\x1b[1;33m: ${testArgs?.length} tuples given vs. ${retrieve.length} expected\x1b[0m`) + throw EvalError(`\x1b[1;33mRadonRequestTemplate: arguments mismatch in test \x1b[1;31m'${test}'\x1b[1;33m: ${testArgs?.length} tuples given vs. ${retrieve.length} expected\x1b[0m`) } testArgs?.forEach((subargs, index)=> { if (subargs.length < retrieve[index].argsCount) { - throw EvalError(`\x1b[1;33mTemplate: arguments mismatch in test \x1b[1;31m'${test}'\x1b[1;33m: \x1b[1;37mRetrieval #${index}\x1b[1;33m: ${subargs?.length} parameters given vs. ${retrieve[index].argsCount} expected\x1b[0m`) + throw EvalError(`\x1b[1;33mRadonRequestTemplate: arguments mismatch in test \x1b[1;31m'${test}'\x1b[1;33m: \x1b[1;37mRetrieval #${index}\x1b[1;33m: ${subargs?.length} parameters given vs. ${retrieve[index].argsCount} expected\x1b[0m`) } }) } @@ -73,44 +141,37 @@ export class Template extends Class { this.tests = tests } } -} -export class Parameterized extends Class { - public args: string[][] - constructor(template: Template, args: Args) { - super(template.specs) - if (!args || !Array.isArray(args) || args.length == 0) { - throw EvalError(`\x1b[1;31mParameterized: no valid args were provided.\x1b[0m`); - } else if (!Array.isArray(args[0])) { - this.args = Array(this.specs.retrieve.length).fill(args); - } else { - this.args = args as string[][]; + public buildRequest(...args: string[][]): RadonRequest { + const retrieve: RadonRetrieval[] = [] + if (args.length !== this.retrieve.length) { + throw new EvalError(`\x1b[1;33mRadonRequest: mismatching args vectors (${args.length} != ${this.retrieve.length}): [${args}]}\x1b[0m`) } - this.specs.retrieve.map((retrieve, index) => { - if (args[index].length !== retrieve.argsCount) { - throw EvalError(`\x1b[1;31mParameterized: Retrieval #${index}\x1b[1;33m: parameters mismatch: ${args[index].length} given vs. ${retrieve.argsCount} required\x1b[0m`) + this.retrieve.forEach((retrieval, index) => { + if (retrieval.argsCount !== args[index].length) { + throw new EvalError(`\x1b[1;33mRadonRequest: mismatching args passed to retrieval #${index + 1} (${args[index].length} != ${retrieval.argsCount}): [${args[index]}]\x1b[0m`) } + retrieve.push(retrieval.foldArgs(...args[index])) + }) + return new RadonRequest({ + retrieve, + aggregate: this.aggregate, + tally: this.tally, }) } -} -export class Precompiled extends Class { - constructor(specs: { - retrieve: Retrieval | Retrieval[], - aggregate?: Reducer, - tally?: Reducer, - maxSize?: number, - }) { - const retrieve = Array.isArray(specs.retrieve) ? specs.retrieve as Retrieval[] : [ specs.retrieve ] - super({ + public buildRequestModal(...args: string[]): RadonRequest { + const retrieve: RadonRetrieval[] = [] + this.retrieve.forEach((retrieval, index) => { + if (retrieval.argsCount !== args.length) { + throw new EvalError(`\x1b[1;33mRadonRequest: mismatching args passed to retrieval #${index + 1} (${args.length} != ${retrieval.argsCount}): [${args}]\x1b[0m`) + } + retrieve.push(retrieval.foldArgs(...args)) + }) + return new RadonRequest({ retrieve, - aggregate: specs?.aggregate || Mode(), - tally: specs?.tally || Mode(), - maxSize: specs?.maxSize || 32, + aggregate: this.aggregate, + tally: this.tally, }) - let argsCount = retrieve.map(retrieval => retrieval.argsCount).reduce((prev, curr) => prev + curr) - if (argsCount > 0) { - throw EvalError("\x1b[1;33mPrecompiled: static requests cannot be built w/ parameterized sources\x1b[0m") - } } } diff --git a/src/lib/radon/filters.ts b/src/lib/radon/filters.ts index 69fe78f..0570cc8 100644 --- a/src/lib/radon/filters.ts +++ b/src/lib/radon/filters.ts @@ -1,11 +1,15 @@ -enum Opcodes { +const cbor = require("cbor") + +export enum Opcodes { Mode = 0x08, StandardDeviation = 0x05, } -export class Class { - public opcode: Opcodes; - public args?: any; +export class RadonFilter { + + readonly opcode: Opcodes; + readonly args?: any; + constructor(opcode: Opcodes, args?: any) { this.opcode = opcode this.args = args @@ -16,7 +20,27 @@ export class Class { } }}) } + + public toJSON(): any { + var json: any = { + op: Opcodes[this.opcode], + } + if (this.args) { + json.args = this.args + } + return json; + } + + public toProtobuf(): any { + var protobuf: any = { + op: this.opcode, + } + if (this.args) { + protobuf.args = cbor.encode(this.args) + } + return protobuf + } } -export function Mode () { return new Class(Opcodes.Mode); } -export function Stdev (stdev: number) { return new Class(Opcodes.StandardDeviation, stdev); } +export function Mode () { return new RadonFilter(Opcodes.Mode); } +export function Stdev (stdev: number) { return new RadonFilter(Opcodes.StandardDeviation, stdev); } diff --git a/src/lib/radon/helpers.js b/src/lib/radon/helpers.js new file mode 100644 index 0000000..cde98e5 --- /dev/null +++ b/src/lib/radon/helpers.js @@ -0,0 +1,130 @@ +var protoBuf = require("protobufjs") +var protoRoot = protoBuf.Root.fromJSON(require("../../../assets/witnet.proto.json")) +var RADRequest = protoRoot.lookupType("RADRequest") + +export function encodeRequest(payload) { + var errMsg = RADRequest.verify(payload) + if (errMsg) { + throw Error(errMsg); + } else { + var message = RADRequest.fromObject(payload); + return RADRequest.encode(message).finish() + } +} + +export function getWildcardsCountFromString(str) { + let maxArgsIndex = 0 + if (str) { + let match + const regexp = /\\\d\\/g + while ((match = regexp.exec(str)) !== null) { + let argsIndex = parseInt(match[0][1]) + 1 + if (argsIndex > maxArgsIndex) maxArgsIndex = argsIndex + } + } + return maxArgsIndex +} + +export function isHexString(str) { + return ( + !Number.isInteger(str) + && str.startsWith("0x") + && /^[a-fA-F0-9]+$/i.test(str.slice(2)) + ); +} + +export function isHexStringOfLength(str, max) { + return (isHexString(str) + && str.slice(2).length <= max * 2 + ); +} + +export function isWildcard(str) { + return str.length == 3 && /\\\d\\/g.test(str) +} + +export function parseURL(url) { + if (url && typeof url === 'string' && url.indexOf("://") > -1) { + const hostIndex = url.indexOf("://") + 3 + const schema = url.slice(0, hostIndex) + let host = url.slice(hostIndex) + let path = "" + let query = "" + const pathIndex = host.indexOf("/") + if (pathIndex > -1) { + path = host.slice(pathIndex + 1) + host = host.slice(0, pathIndex) + const queryIndex = path.indexOf("?") + if (queryIndex > -1) { + query = path.slice(queryIndex + 1) + path = path.slice(0, queryIndex) + } + } + return [schema, host, path, query]; + } else { + throw new EvalError(`Invalid URL was provided: ${url}`) + } +} + +export function replaceWildcards(obj, args) { + if (args.length > 10) args = args.slice(0, 10); + if (obj && typeof obj === "string") { + for (let argIndex = 0; argIndex < args.length; argIndex ++) { + const wildcard = `\\${argIndex}\\` + obj = obj.replaceAll(wildcard, args[argIndex]) + } + } else if (obj && Array.isArray(obj)) { + obj = obj.map(value => typeof value === "string" || Array.isArray(value) + ? replaceWildcards(value, args) + : value + ) + } + return obj; +} + +export function spliceWildcard(obj, argIndex, argValue, argsCount) { + if (obj && typeof obj === "string") { + const wildcard = `\\${argIndex}\\` + obj = obj.replaceAll(wildcard, argValue) + for (var j = argIndex + 1; j < argsCount; j++) { + obj = obj.replaceAll(`\\${j}\\`, `\\${j - 1}\\`) + } + } else if (obj && Array.isArray(obj)) { + obj = obj.map(value => typeof value === "string" || Array.isArray(value) + ? spliceWildcard(value, argIndex, argValue, argsCount) + : value + ) + } + return obj; +} + +export function toUtf8Array(str) { + var utf8 = []; + for (var i=0; i < str.length; i++) { + var charcode = str.charCodeAt(i); + if (charcode < 0x80) utf8.push(charcode); + else if (charcode < 0x800) { + utf8.push(0xc0 | (charcode >> 6), + 0x80 | (charcode & 0x3f)); + } + else if (charcode < 0xd800 || charcode >= 0xe000) { + utf8.push(0xe0 | (charcode >> 12), + 0x80 | ((charcode>>6) & 0x3f), + 0x80 | (charcode & 0x3f)); + } + // surrogate pair + else { + i++; + // UTF-16 encodes 0x10000-0x10FFFF by + // subtracting 0x10000 and splitting the + // 20 bits of 0x0-0xFFFFF into two halves + charcode = 0x10000 + (((charcode & 0x3ff)<<10) + | (str.charCodeAt(i) & 0x3ff)); + utf8.push(0xf0 | (charcode >>18), + 0x80 | ((charcode>>12) & 0x3f), + 0x80 | ((charcode>>6) & 0x3f), + 0x80 | (charcode & 0x3f)); + } + } + return utf8; +} diff --git a/src/lib/radon/index.ts b/src/lib/radon/index.ts index 89187f7..9fa72cd 100644 --- a/src/lib/radon/index.ts +++ b/src/lib/radon/index.ts @@ -1,93 +1,50 @@ import * as _Artifacts from "./artifacts" import * as _Filters from "./filters" import * as _Reducers from "./reducers" -import * as _Sources from "./sources" -import * as _RPC from "./ccdr" +import * as _Retrievals from "./retrievals" +import * as _RPCs from "./rpcs" import * as _Types from "./types" +import { RadonRequest, RadonRequestTemplate } from "./artifacts" +import { RadonReducer } from "./reducers" +import { RadonRetrieval } from "./retrievals" + /** - * Web3 deployable artifacts that can be declared within - * Witnet asset files: - * - data request templates, - * - parameterized requests, - * - precompiled requests. + * Constructors for Radon requests or templates. */ export const Artifacts: typeof _Artifacts = _Artifacts; /** - * Radon Filtering operators that can be used within both - * the Aggregate and Tally scripts of a Data Request. + * Set of Radon filters operators that can be used within both + * the `aggregate` and `tally` Radon reducers within + * a Radon request or template. */ export const Filters: typeof _Filters = _Filters; -/** - * Radon Reducing operators that can be used within both - * the Aggregate and Tally scripts of a Data Request. +/** + * Set of Radon reducers that can be applied to either + * data extracted from multiple data sources (i.e. `aggregate`), + * or results revealed from multiple witnessing nodes (i.e. `tally`). */ export const Reducers: typeof _Reducers = _Reducers; /** - * Supported data source types that can be added as part of a Data Request. + * Set of Radon retrievals that can be added as part of Radon requests or templates. */ -export const Sources: typeof _Sources = _Sources; +export const Retrievals: typeof _Retrievals = _Retrievals; /** - * Precompiled Remote Procedure Calls that can be included within - * Cross Chain Data Requests (i.e. `Witnet.Sources.CrossChainDataSource({ .. })`, - * grouped by JSON-RPC protocol: - * - JSON ETH/RPC endpoints - * - JSON WIT/RPC endpoints + * Set or Remote Procedure Calls that can be used within Cross-chain Radon retrievals. */ -export const CCDR: typeof _RPC = _RPC; +export const RPCs: typeof _RPCs = _RPCs; /** - * Data types handled by Radon scripts - * while processing response(s) from - * the source(s) of a Data Request. + * Set of data types that can be processed + * by Radon scripts when processing the result + * extracted from Radon retrievals. */ export const Types: typeof _Types = _Types; -/** - * Creates a proxy dictionary of Witnet Radon assets - * of the specified kind, where keys cannot - * be duplicated, and where items can be accessed - * by their name, no matter how deep they are placed - * within the given hierarchy. - * @param t Type of the contained Radon assets. - * @param dict Hierarchical object containing the assets. - * @returns - */ -export function Dictionary(t: { new(): T; }, dict: Object): Object { - return new Proxy(dict, proxyHandler(t)) -} - -function proxyHandler(t: { new(): T; }) { - return { - get(target: any, prop: string) { - let found = target[prop] ?? findKeyInObject(target, prop) - if (!found) { - throw EvalError(`\x1b[1;31m['${prop}']\x1b[1;33m was not found in dictionary\x1b[0m`) - } else if (!(found instanceof t)) { - throw EvalError(`\x1b[1;31m['${prop}']\x1b[1;33m was found with unexpected type!\x1b[0m`) - } - return found - } - } -} - -function findKeyInObject(dict: any, tag: string) { - for (const key in dict) { - if (typeof dict[key] === 'object') { - if (key === tag) { - return dict[key] - } else { - let found: any = findKeyInObject(dict[key], tag) - if (found) return found - } - } - } -} - /** * Creates a Radon script capable of processing the returned * string value from some remote data source (i.e. Radon Retrieval). @@ -108,19 +65,8 @@ export function InnerScript(t: { new(): T; }): T { r /// =================================================================================================================== /// --- Request and Template factory methods -------------------------------------------------------------------------- -export function PriceTickerRequest (dict: any, tags: Map) { - return RequestFromDictionary({ - retrieve: { - dict, - tags, - }, - aggregate: _Reducers.PriceAggregate(), - tally: _Reducers.PriceTally() - }) -}; - -export function PriceTickerTemplate (specs: { retrieve: _Sources.Class[], tests?: Map }) { - return new _Artifacts.Template( +export function PriceTickerTemplate (specs: { retrieve: RadonRetrieval[], tests?: Map }) { + return new RadonRequestTemplate( { retrieve: specs.retrieve, aggregate: _Reducers.PriceAggregate(), @@ -129,68 +75,28 @@ export function PriceTickerTemplate (specs: { retrieve: _Sources.Class[], tests? specs?.tests ); }; - -export function RequestFromDictionary (specs: { - retrieve: { dict: any, tags: Map, }, - aggregate?: _Reducers.Class, - tally?: _Reducers.Class -}) { - const sources: _Sources.Class[] = [] - const args: string[][] = [] - Object.keys(specs.retrieve.tags).forEach(key => { - const retrieval: _Sources.Class = specs.retrieve.dict[key] - const value: any = (specs.retrieve.tags as any)[key] - if (typeof value === 'string') { - if (retrieval?.tuples) { - args.push((retrieval.tuples as any)[value] || []) - } else { - throw EvalError(`\x1b[1;33mRequestFromDictionary: No tuple \x1b[1;31m'${value}'\x1b[1;33m was declared for retrieval \x1b[1;37m['${key}']\x1b[0m`) - } - } else { - args.push(value || []) - } - sources.push(retrieval) - }) - return new _Artifacts.Parameterized( - new _Artifacts.Template({ retrieve: sources, aggregate: specs.aggregate, tally: specs.tally }), - args - ) -}; -export function RequestFromTemplate (template: _Artifacts.Template, args: string[] | string[][]) { - return new _Artifacts.Parameterized(template, args); +export function Request (specs: { + retrieve: RadonRetrieval[], + aggregate?: RadonReducer, + tally?: RadonReducer +}): RadonRequest { + return new RadonRequest(specs) }; + export function RequestTemplate (specs: { - retrieve: _Sources.Class[], - aggregate?: _Reducers.Class, - tally?: _Reducers.Class, + retrieve: RadonRetrieval[], + aggregate?: RadonReducer, + tally?: RadonReducer, tests?: Map, -}) { - return new _Artifacts.Template( +}): RadonRequestTemplate { + return new RadonRequestTemplate( { retrieve: specs.retrieve, aggregate: specs?.aggregate, tally: specs?.tally - }, specs.tests - ); -}; - -export function RequestTemplateSingleSource (retrieval: _Sources.Class, tests?: Map) { - return new _Artifacts.Template( - { - retrieve: [ retrieval ], - aggregate: _Reducers.Mode(), - tally: _Reducers.Mode(Filters.Mode()), }, - tests + specs.tests ); }; - -export function StaticRequest (specs: { - retrieve: _Sources.Class[], - aggregate?: _Reducers.Class, - tally?: _Reducers.Class -}) { - return new _Artifacts.Precompiled(specs) -}; diff --git a/src/lib/radon/reducers.ts b/src/lib/radon/reducers.ts index b571776..2a5d0e5 100644 --- a/src/lib/radon/reducers.ts +++ b/src/lib/radon/reducers.ts @@ -1,5 +1,5 @@ import { RadonType as Script } from "./types" -import { Class as Filter, Stdev as StdevFilter } from "./filters" +import { RadonFilter, Stdev as StdevFilter } from "./filters" export enum Opcodes { Mode = 0x02, @@ -10,18 +10,18 @@ export enum Opcodes { } export interface Specs { - filters?: Filter[], + filters?: RadonFilter[], script?: Script, } -export class Class { - opcode: Opcodes - filters?: Filter[] - // TODO: script?: Script - constructor(opcode: Opcodes, filters?: Filter[]) { +export class RadonReducer { + + readonly opcode: Opcodes + readonly filters?: RadonFilter[] + + constructor(opcode: Opcodes, filters?: RadonFilter[]) { this.opcode = opcode this.filters = filters - // TODO: this.script = specs?.filters Object.defineProperty(this, "toString", { value: () => { let filters = "" this.filters?.map(filter => { filters = filter.toString() + `${filters ? "." + filters : ""}` }) @@ -35,13 +35,37 @@ export class Class { } }}) } + + public toJSON(): any { + var json: any = { + reducer: Opcodes[this.opcode], + } + if (this.filters && this.filters.length > 0) { + json.filter = this.filters.map(filter => filter.toJSON()) + } + return json + } + + public toProtobuf(): any { + var protobuf: any = { + reducer: this.opcode, + } + if (this.filters && this.filters.length > 0) { + protobuf.filters = this.filters.map(filter => filter.toProtobuf()) + } + return protobuf + } + + public opsCount(): number { + return 1 + (this.filters?.length || 0) + } } -export function Mode (...filters: Filter[]) { return new Class(Opcodes.Mode, filters); } -export function Mean (...filters: Filter[]) { return new Class(Opcodes.MeanAverage, filters); } -export function Median (...filters: Filter[]) { return new Class(Opcodes.MedianAverage, filters); } -export function Stdev (...filters: Filter[]) { return new Class(Opcodes.StandardDeviation, filters); } -export function ConcatHash (...filters: Filter[]) { return new Class(Opcodes.ConcatenateAndHash, filters); } +export function Mode (...filters: RadonFilter[]) { return new RadonReducer(Opcodes.Mode, filters); } +export function Mean (...filters: RadonFilter[]) { return new RadonReducer(Opcodes.MeanAverage, filters); } +export function Median (...filters: RadonFilter[]) { return new RadonReducer(Opcodes.MedianAverage, filters); } +export function Stdev (...filters: RadonFilter[]) { return new RadonReducer(Opcodes.StandardDeviation, filters); } +export function ConcatHash (...filters: RadonFilter[]) { return new RadonReducer(Opcodes.ConcatenateAndHash, filters); } -export function PriceAggregate () { return new Class(Opcodes.MeanAverage, [ StdevFilter(1.4) ]); } -export function PriceTally () { return new Class(Opcodes.MeanAverage, [ StdevFilter(2.5) ]); } +export function PriceAggregate () { return new RadonReducer(Opcodes.MeanAverage, [ StdevFilter(1.4) ]); } +export function PriceTally () { return new RadonReducer(Opcodes.MeanAverage, [ StdevFilter(2.5) ]); } diff --git a/src/lib/radon/retrievals.ts b/src/lib/radon/retrievals.ts new file mode 100644 index 0000000..9bd88d6 --- /dev/null +++ b/src/lib/radon/retrievals.ts @@ -0,0 +1,307 @@ +const cbor = require("cbor") +const helpers = require("./helpers") + +import * as _RPCS from "./rpcs" + +import graphQlCompress from "graphql-query-compress" +import { RadonOperators, RadonType as Script, RadonString as ScriptDefault} from "./types" +import { JsonRPC } from "./rpcs" + +/** + * Precompiled Remote Procedure Calls that can be included within + * Cross Chain Data Requests (i.e. `Witnet.Retrievals.CCDR({ .. })`, + * grouped by supported JSON-RPC protocol: E + * - JSON ETH-RPC + * - JSON WIT-RPC + */ +export const RPCs: typeof _RPCS = _RPCS; + +export enum Methods { + None = 0x0, + HttpGet = 0x01, + HttpHead = 0x04, + HttpPost = 0x03, + RNG = 0x02, +} + +export interface Specs { + url?: string, + headers?: Map, + body?: string, + script?: Script, +} + +export class RadonRetrieval { + public readonly argsCount: number; + public readonly method: Methods; + public readonly authority?: string; + public readonly body?: string; + public readonly headers?: string[][]; + public readonly path?: string; + public readonly query?: string; + public readonly schema?: string; + public readonly script?: Script; + public readonly url?: string; + + constructor(method: Methods, specs?: Specs) { + if (method === Methods.RNG && (specs?.url || specs?.headers?.size || specs?.body)) { + console.log(specs) + throw EvalError("\x1b[1;33mRadonRetrieval: badly specified RNG\x1b[0m"); + } else if (!specs?.url && (method == Methods.HttpPost || method == Methods.HttpGet)) { + throw EvalError("\x1b[1;33mRadonRetrieval: URL must be specified\x1b[0m"); + } else if (specs?.body && method == Methods.HttpGet) { + throw EvalError("\x1b[1;33mRadonRetrieval: body cannot be specified in HTTP-GET queries\x1b[0m") + } + this.method = method + if (specs?.headers) { + this.headers = [] + if (specs.headers instanceof Map) { + specs.headers.forEach((value: string, key: string) => this.headers?.push([key, value])) + } else { + this.headers = specs.headers + // Object.entries(specs.headers).forEach((entry: any) => this.headers?.push(entry)) + } + } + this.body = specs?.body + this.script = specs?.script + if (specs?.url) { + this.url = specs.url + if (!helpers.isWildcard(specs.url)) { + let parts = helpers.parseURL(specs.url) + this.schema = parts[0] + if (parts[1] !== "") this.authority = parts[1] + if (parts[2] !== "") this.path = parts[2] + if (parts[3] !== "") this.query = parts[3] + } + } + this.argsCount = Math.max( + helpers.getWildcardsCountFromString(specs?.url), + helpers.getWildcardsCountFromString(specs?.body), + ...this.headers?.map(header => helpers.getWildcardsCountFromString(header[1])) ?? [], + specs?.script?._countWildcards() || 0, + ) + } + + public isParameterized(): boolean { + return this.argsCount > 0 + } + /** + * Creates a new Radon Retrieval by orderly replacing indexed wildcards with given parameters. + * Fails if not parameterized, of if passing too many parameters. + */ + public foldArgs(...args: string[]): RadonRetrieval { + if (this.argsCount === 0) { + throw new EvalError(`\x1b[1;33mRadonRetrieval: cannot fold unparameterized retrieval\x1b[0m`) + } else if (args.length > this.argsCount) { + throw new EvalError(`\x1b[1;33mRadonRetrieval: too may args passed: ${args.length} > ${this.argsCount}\x1b[0m`) + } + let headers: Map = new Map(); + if (this.headers) { + this.headers.forEach(header => { + headers.set( + helpers.replaceWildcards(header[0], args), + helpers.replaceWildcards(header[1], args), + ) + }) + } + return new RadonRetrieval(this.method, { + url: helpers.replaceWildcards(this.url, args), + body: helpers.replaceWildcards(this.body, args), + headers, + script: this.script?._replaceWildcards(args), + }) + } + /** + * Creates one or more clones of this retrieval, in which the index-0 wildcard + * will be replaced by the given values. Fails if the retrieval is not parameterized. + */ + public spawnRetrievals(...values: string[]): RadonRetrieval[] { + const _spawned: RadonRetrieval[] = [] + if (this.argsCount == 0) { + throw new EvalError(`\x1b[1;33mRadonRetrieval: cannot spawn from unparameterized retrieval\x1b[0m`); + } + values.forEach(value => { + let headers: Map = new Map() + if (this.headers) { + this.headers.forEach(header => { + headers.set( + helpers.spliceWildcard(header[0], 0, value, this.argsCount), + helpers.spliceWildcard(header[1], 0, value, this.argsCount), + ) + }) + } + _spawned.push(new RadonRetrieval(this.method, { + url: helpers.spliceWildcard(this.url, 0, value, this.argsCount), + body: helpers.spliceWildcard(this.body, 0, value, this.argsCount), + headers, + script: this.script?._spliceWildcard(0, value) + })) + }) + return _spawned + } + + public toJSON(): any { + let json: any = { + kind: Methods[this.method], + } + if (this.url) json.url = this.url + if (this.headers && this.headers.length > 0) { + json.headers = this.headers.map(header => { var obj: any = {}; obj[header[0]] = header[1]; return obj; }) + } + if (this.body) json.body = this.body + if (this.script) json.script = this.script.toString() + return json + } + + public toProtobuf(): any { + let protobuf: any = { + kind: this.method, + } + if (this.url) protobuf.url = this.url + if (this.headers && this.headers.length > 0) { + protobuf.headers = this.headers.map(header => { return { left: header[0], right: header[1] }}) + } + if (this.body) { + var utf8Array = helpers.toUtf8Array(this.body) + protobuf.body = utf8Array + } + protobuf.script = Object.values(Uint8Array.from(cbor.encode(this.script?._encodeArray()))) + return protobuf + } + + public opsCount(): any { + return countOps(this.script?._encodeArray() || []) + } +} + +function countOps(items: any[]): number { + return items.length > 0 ? items.map(item => Array.isArray(item) ? countOps(item) : 1).reduce((sum, a) => sum + a) : 0 +} + +/** + * Creates a Witnet randomness Radon RadonRetrieval. + * @param script (Optional) Radon Script to apply to the random seed proposed by every single witness, + * before aggregation. + */ +export function RNG (script?: Script) { + if (script) { + if (!stringifyFirstOpcode(script._encodeArray())?.startsWith("Bytes")) { + throw new EvalError("Input Radon script should be a RadonBytes value") + } + } + return new RadonRetrieval(Methods.RNG, { script }); +}; + +function stringifyFirstOpcode(item: any) { + if (Array.isArray(item)) return stringifyFirstOpcode(item[0]); + else if (typeof item === 'number') return RadonOperators[item]; + else return undefined +} + +/** + * Creates a Witnet HTTP/GET Radon RadonRetrieval. + * @param specs RadonRetrieval parameters: URL, http headers (optional), Radon script (optional), + * pre-set tuples (optional to parameterized sources, only). + */ +export function HttpGet (specs: { + url: string, + headers?: Map, + script?: Script, + tuples?: Map +}) { + return new RadonRetrieval( + Methods.HttpGet, { + url: specs.url, + headers: specs.headers, + script: specs.script, + // tuples: specs.tuples + } + ); +}; + +/** + * Creates a Witnet HTTP/HEAD Radon RadonRetrieval. + * @param specs RadonRetrieval parameters: URL, http headers (optional), Radon script (optional), + * pre-set tuples (optional to parameterized sources, only). + */ +export function HttpHead (specs: { + url: string, + headers?: Map, + script?: Script, + tuples?: Map +}) { + return new RadonRetrieval( + Methods.HttpHead, { + url: specs.url, + headers: specs.headers, + script: specs.script, + // tuples: specs.tuples + } + ); +}; + +/** + * Creates a Witnet HTTP/POST Radon RadonRetrieval. + * @param specs RadonRetrieval parameters: URL, HTTP body (optional), HTTP headers (optional), Radon Script (optional), + * pre-set tuples (optional to parameterized sources, only). + */ +export function HttpPost (specs?: { + url: string, + body: string, + headers?: Map, + script?: Script, + tuples?: Map +}) { + return new RadonRetrieval( + Methods.HttpPost, { + url: specs?.url, + headers: specs?.headers, + body: specs?.body, + script: specs?.script, + // tuples: specs?.tuples + } + ); +}; + +/** + * Creates a Witnet GraphQL Radon RadonRetrieval (built on top of an HTTP/POST request). + * @param specs RadonRetrieval parameters: URL, GraphQL query string, Radon Script (optional), + * pre-set tuples (optional to parameterized sources, only). + */ +export function GraphQLQuery (specs: { + url: string, + query: string, + script?: Script, + tuples?: Map, +}) { + return new RadonRetrieval(Methods.HttpPost, { + url: specs.url, + body: `{\"query\":\"${graphQlCompress(specs.query).replaceAll('"', '\\"')}\"}`, + headers: new Map().set("Content-Type", "application/json;charset=UTF-8"), + script: specs?.script || new ScriptDefault(), + // tuples: specs?.tuples + }); +}; + +/** + * Creates a Cross Chain RPC retrieval on top of a HTTP/POST request. + * @param specs rpc: JsonRPC object encapsulating method and parameters, + * script?: RadonScript to apply to returned value + * presets?: Map containing preset parameters (only on parameterized retrievals). + */ +export function CrossChainRPC (specs: { + rpc: JsonRPC, + script?: Script +}) { + return new RadonRetrieval(Methods.HttpPost, { + url: "\\0\\", + body: JSON.stringify({ + jsonrpc: "2.0", + method: specs.rpc.method, + params: specs.rpc?.params, + id: 1, + }).replaceAll('\\\\', '\\'), + headers: new Map().set("Content-Type", "application/json;charset=UTF-8"), + script: specs?.script || new ScriptDefault(), + }); +}; diff --git a/src/lib/radon/ccdr/eth.ts b/src/lib/radon/rpcs/eth.ts similarity index 68% rename from src/lib/radon/ccdr/eth.ts rename to src/lib/radon/rpcs/eth.ts index 2fea1c0..3499651 100644 --- a/src/lib/radon/ccdr/eth.ts +++ b/src/lib/radon/rpcs/eth.ts @@ -1,7 +1,7 @@ -const utils = require("../utils") +const helpers = require("../helpers") import { - Call, + JsonRPC, BlockNumber, Bytes, Bytes32, @@ -17,15 +17,15 @@ function _isBlockHead(block: EthBlockHead): boolean { return ( block === "latest" || block === "earliest" || block === "finalized" || block === "pending" || typeof block === 'number' - || utils.isHexStringOfLength(block, 32) - || utils.isWildcard(block) + || helpers.isHexStringOfLength(block, 32) + || helpers.isWildcard(block) ); } /** * Retrieve the number of most recent block. */ -export const blockNumber = () => new Call("eth_blockNumber"); +export const blockNumber = () => new JsonRPC("eth_blockNumber"); /** * Invoke message call immediately without creating a transaction @@ -41,22 +41,22 @@ export const call = (tx: { value?: number | HexString, data?: HexString }) => { - if (tx?.from && !utils.isHexStringOfLength(tx?.from, 20) && !utils.isWildcard(tx?.from)) { + if (tx?.from && !helpers.isHexStringOfLength(tx?.from, 20) && !helpers.isWildcard(tx?.from)) { throw new EvalError("CCDR: EthCall: invalid 'from' address"); } - if (tx?.gas && !Number.isInteger(tx.gas) && !utils.isHexStringOfLength(tx.gas, 32) && !utils.isWildcard(tx.gas)) { + if (tx?.gas && !Number.isInteger(tx.gas) && !helpers.isHexStringOfLength(tx.gas, 32) && !helpers.isWildcard(tx.gas)) { throw new EvalError("CCDR: EthCall: invalid 'gas' value") } - if (tx?.gasPrice && !Number.isInteger(tx.gasPrice) && !utils.isHexStringOfLength(tx.gasPrice, 32) && !utils.isWildcard(tx.gasPrice)) { + if (tx?.gasPrice && !Number.isInteger(tx.gasPrice) && !helpers.isHexStringOfLength(tx.gasPrice, 32) && !helpers.isWildcard(tx.gasPrice)) { throw new EvalError("CCDR: EthCall: invalid 'gasPrice' value") } - if (tx?.value && !Number.isInteger(tx.value) && !utils.isHexStringOfLength(tx.value, 32) && !utils.isWildcard(tx.value)) { + if (tx?.value && !Number.isInteger(tx.value) && !helpers.isHexStringOfLength(tx.value, 32) && !helpers.isWildcard(tx.value)) { throw new EvalError("CCDR: EthCall: invalid transaction 'value'") } - if (tx?.data && !utils.isHexString(tx.data) && !utils.isWildcard(tx.data)) { + if (tx?.data && !helpers.isHexString(tx.data) && !helpers.isWildcard(tx.data)) { throw new EvalError("CCDR: EthCall: invalid transaction 'data'") } - return new Call("eth_call", [ tx ]); + return new JsonRPC("eth_call", [ tx ]); }; /** @@ -75,22 +75,22 @@ export const estimateGas = (tx: { value?: number | HexString, data?: HexString }) => { - if (tx?.from && !utils.isHexStringOfLength(tx?.from, 20) && !utils.isWildcard(tx?.from)) { + if (tx?.from && !helpers.isHexStringOfLength(tx?.from, 20) && !helpers.isWildcard(tx?.from)) { throw new EvalError("CCDR: EthEstimateGas: invalid 'from' address"); } - if (tx?.gas && !Number.isInteger(tx.gas) && !utils.isHexStringOfLength(tx.gas, 32) && !utils.isWildcard(tx.gas)) { + if (tx?.gas && !Number.isInteger(tx.gas) && !helpers.isHexStringOfLength(tx.gas, 32) && !helpers.isWildcard(tx.gas)) { throw new EvalError("CCDR: EthEstimateGas: invalid 'gas' value") } - if (tx?.gasPrice && !Number.isInteger(tx.gasPrice) && !utils.isHexStringOfLength(tx.gasPrice, 32) && !utils.isWildcard(tx.gasPrice)) { + if (tx?.gasPrice && !Number.isInteger(tx.gasPrice) && !helpers.isHexStringOfLength(tx.gasPrice, 32) && !helpers.isWildcard(tx.gasPrice)) { throw new EvalError("CCDR: EthEstimateGas: invalid 'gasPrice' value") } - if (tx?.value && !Number.isInteger(tx.value) && !utils.isHexStringOfLength(tx.value, 32) && !utils.isWildcard(tx.value)) { + if (tx?.value && !Number.isInteger(tx.value) && !helpers.isHexStringOfLength(tx.value, 32) && !helpers.isWildcard(tx.value)) { throw new EvalError("CCDR: EthEstimateGas: invalid transaction 'value'") } - if (tx?.data && !utils.isHexString(tx.data) && !utils.isWildcard(tx.data)) { + if (tx?.data && !helpers.isHexString(tx.data) && !helpers.isWildcard(tx.data)) { throw new EvalError("CCDR: EthEstimateGas: invalid transaction 'data'") } - return new Call("eth_estimateGas", [ tx ]); + return new JsonRPC("eth_estimateGas", [ tx ]); }; /** @@ -98,10 +98,10 @@ export const estimateGas = (tx: { * @param address Web3 address on remote EVM chain. */ export const getBalance = (address: EthAddress, block?: EthBlockHead) => { - if (!utils.isHexStringOfLength(address, 20) && !utils.isWildcard(address)) { + if (!helpers.isHexStringOfLength(address, 20) && !helpers.isWildcard(address)) { throw new EvalError("CCDR: EthGetBalance: invalid Web3 address format"); } else { - return new Call("eth_getBalance", [ address, block ]); + return new JsonRPC("eth_getBalance", [ address, block ]); } }; @@ -110,10 +110,10 @@ export const getBalance = (address: EthAddress, block?: EthBlockHead) => { * @param address EthAddress from where to get the code. */ export const getCode = (address: EthAddress) => { - if (!utils.isHexStringOfLength(address, 20) && !utils.isWildcard(address)) { + if (!helpers.isHexStringOfLength(address, 20) && !helpers.isWildcard(address)) { throw new EvalError("CCDR: EthGetCode: invalid Web3 address format"); } else { - return new Call("eth_getCode", [ address ]); + return new JsonRPC("eth_getCode", [ address ]); } }; @@ -145,23 +145,23 @@ export const getLogs = (filter: { filter.toBlock = `0x${(filter?.toBlock as number).toString(16)}` as EthBlockHead } } - if (filter?.blockHash && !utils.isHexStringOfLength(filter.blockHash, 32) && !utils.isWildcard(filter.blockHash)) { + if (filter?.blockHash && !helpers.isHexStringOfLength(filter.blockHash, 32) && !helpers.isWildcard(filter.blockHash)) { throw new EvalError("CCDR: EthGetLogs: invalid 'blockHash' value"); } if (filter?.topics) { filter.topics.map((value: Bytes32, index: number) => { - if (!utils.isHexStringOfLength(value, 32) && !utils.isWildcard(value)) { + if (!helpers.isHexStringOfLength(value, 32) && !helpers.isWildcard(value)) { throw new EvalError(`CCDR: EthGetLogs: topic #${index}: invalid hash`) } }) } - return new Call("eth_getLogs", [ filter ]); + return new JsonRPC("eth_getLogs", [ filter ]); }; /** * Retrieve an estimate of the current price per gas in wei. */ -export const gasPrice = () => new Call("eth_gasPrice"); +export const gasPrice = () => new JsonRPC("eth_gasPrice"); /** * Retrieve the value from a storage position at a given address. @@ -169,13 +169,13 @@ export const gasPrice = () => new Call("eth_gasPrice"); * @param offset Offset within storage address. */ export const getStorageAt = (address: EthAddress, offset: Bytes32) => { - if (!utils.isHexStringOfLength(address, 20) && !utils.isWildcard(address)) { + if (!helpers.isHexStringOfLength(address, 20) && !helpers.isWildcard(address)) { throw new EvalError("CCDR: EthGetStorageAt: invalid Web3 address format"); } - if (!utils.isHexStringOfLength(offset, 32) && !utils.isWildcard(offset)) { + if (!helpers.isHexStringOfLength(offset, 32) && !helpers.isWildcard(offset)) { throw new EvalError("CCDR: EthGetStorageAt: invalid storage offset value"); } - return new Call("eth_getStorageAt", [ address, offset ]); + return new JsonRPC("eth_getStorageAt", [ address, offset ]); }; /** @@ -183,13 +183,13 @@ export const getStorageAt = (address: EthAddress, offset: Bytes32) => { * @param txHash Hash of the remote transaction. */ export const getTransactionByBlockHashAndIndex = (blockHash: Bytes32, txIndex: number | Bytes32) => { - if (!utils.isHexStringOfLength(blockHash, 32) && !utils.isWildcard(blockHash)) { + if (!helpers.isHexStringOfLength(blockHash, 32) && !helpers.isWildcard(blockHash)) { throw new EvalError("CCDR: EthGetTransactionByBlockHashAndIndex: invalid block hash value"); } - if (!Number.isInteger(txIndex) && !utils.isHexStringOfLength(txIndex, 32) && !utils.isWildcard(txIndex)) { + if (!Number.isInteger(txIndex) && !helpers.isHexStringOfLength(txIndex, 32) && !helpers.isWildcard(txIndex)) { throw new EvalError("CCDR: EthGetTransactionByBlockHashAndIndex: invalid transaction index value") } - return new Call("eth_getTransactionByBlockHashAndIndex", [ blockHash, txIndex ]); + return new JsonRPC("eth_getTransactionByBlockHashAndIndex", [ blockHash, txIndex ]); }; /** @@ -207,10 +207,10 @@ export const getTransactionByBlockNumberAndIndex = ( blockNumber = `0x${(blockNumber as number).toString(16)}` as EthBlockHead } } - if (!Number.isInteger(txIndex) && !utils.isHexStringOfLength(txIndex, 32) && !utils.isWildcard(txIndex)) { + if (!Number.isInteger(txIndex) && !helpers.isHexStringOfLength(txIndex, 32) && !helpers.isWildcard(txIndex)) { throw new EvalError("CCDR: EthGetTransactionByBlockNumberAndIndex: invalid transaction index value") } - return new Call("eth_getTransactionByBlockHashAndIndex", [ blockNumber, txIndex ]); + return new JsonRPC("eth_getTransactionByBlockHashAndIndex", [ blockNumber, txIndex ]); }; /** @@ -218,10 +218,10 @@ export const getTransactionByBlockNumberAndIndex = ( * @param txHash Hash of the remote transaction. */ export const getTransactionByHash = (txHash: Bytes32) => { - if (!utils.isHexStringOfLength(txHash, 32) && !utils.isWildcard(txHash)) { + if (!helpers.isHexStringOfLength(txHash, 32) && !helpers.isWildcard(txHash)) { throw new EvalError("CCDR: EthGetTransactionByHash: invalid transaction hash value"); } else { - return new Call("eth_getTransactionByHash", [ txHash ]); + return new JsonRPC("eth_getTransactionByHash", [ txHash ]); } }; @@ -230,10 +230,10 @@ export const getTransactionByHash = (txHash: Bytes32) => { * @param address EthAddress from where to get transaction count. */ export const getTransactionCount = (address: EthAddress) => { - if (!utils.isHexStringOfLength(address, 20) && !utils.isWildcard(address)) { + if (!helpers.isHexStringOfLength(address, 20) && !helpers.isWildcard(address)) { throw new EvalError("CCDR: EthGetTransactionCount: invalid Web3 address format"); } else { - return new Call("eth_getTransactionCount", [ address ]); + return new JsonRPC("eth_getTransactionCount", [ address ]); } }; @@ -242,10 +242,10 @@ export const getTransactionCount = (address: EthAddress) => { * @param txHash Hash of the remote transaction. */ export const getTransactionReceipt = (txHash: Bytes32) => { - if (!utils.isHexStringOfLength(txHash, 32) && !utils.isWildcard(txHash)) { + if (!helpers.isHexStringOfLength(txHash, 32) && !helpers.isWildcard(txHash)) { throw new EvalError("CCDR: EthGetTransactionReceipt: invalid transaction hash value"); } else { - return new Call("eth_getTransactionReceipt", [ txHash ]); + return new JsonRPC("eth_getTransactionReceipt", [ txHash ]); } }; @@ -254,9 +254,9 @@ export const getTransactionReceipt = (txHash: Bytes32) => { * @param data The signed transaction data. */ export const sendRawTransaction = (data: Bytes) => { - if (!utils.isHexString(data) && !utils.isWildcard(data)) { + if (!helpers.isHexString(data) && !helpers.isWildcard(data)) { throw new EvalError("CCDR: EthSendRawTransaction: invalid signed transaction data"); } else { - return new Call("eth_sendRawTransaction", [ data ]); + return new JsonRPC("eth_sendRawTransaction", [ data ]); } }; diff --git a/src/lib/radon/ccdr/index.ts b/src/lib/radon/rpcs/index.ts similarity index 91% rename from src/lib/radon/ccdr/index.ts rename to src/lib/radon/rpcs/index.ts index aa46575..71e5fed 100644 --- a/src/lib/radon/ccdr/index.ts +++ b/src/lib/radon/rpcs/index.ts @@ -25,9 +25,9 @@ export type Bytes = HexString; export type BlockNumber = number | Bytes32; /** - * Base container class for Web3 Remote Procedure Calls. + * Base container class for JSON Remote Procedure Calls. */ -export class Call { +export class JsonRPC { method: string; params?: any; /** diff --git a/src/lib/radon/ccdr/wit.ts b/src/lib/radon/rpcs/wit.ts similarity index 73% rename from src/lib/radon/ccdr/wit.ts rename to src/lib/radon/rpcs/wit.ts index 1f599ef..fdd3116 100644 --- a/src/lib/radon/ccdr/wit.ts +++ b/src/lib/radon/rpcs/wit.ts @@ -1,8 +1,8 @@ -const utils = require("../utils") +const helpers = require("../helpers") import { Bytes32, - Call, + JsonRPC, } from "."; export type WitAddress = string & { @@ -15,13 +15,13 @@ export type WitAddress = string & { */ export const getBalance = (address: WitAddress, simple?: boolean) => { if ( - !utils.isWildcard(address) && ( + !helpers.isWildcard(address) && ( !address || typeof address !== "string" || address.length != 43 || !address.startsWith("wit") ) ) { throw new EvalError("CCDR: WitGetBalance: invalid Witnet address"); } else { - return new Call("getBalance", [ address, simple ]); + return new JsonRPC("getBalance", [ address, simple ]); } }; @@ -30,10 +30,10 @@ export const getBalance = (address: WitAddress, simple?: boolean) => { * @param blockHash The hash of the block to retrieve. */ export const getBlockByHash = (blockHash: Bytes32) => { - if (!utils.isHexStringOfLength(blockHash, 32) && !utils.isWildcard(blockHash)) { + if (!helpers.isHexStringOfLength(blockHash, 32) && !helpers.isWildcard(blockHash)) { throw new EvalError("CCDR: WitGetBlockByHash: invalid block hash value"); } else { - return new Call("getBlock", [ blockHash ]) + return new JsonRPC("getBlock", [ blockHash ]) } } @@ -42,7 +42,7 @@ export const getBlockByHash = (blockHash: Bytes32) => { * @param txHash Hash of the remote transaction. */ export const getSupplyInfo = () => { - return new Call("getSupplyInfo"); + return new JsonRPC("getSupplyInfo"); } /** @@ -50,10 +50,10 @@ export const getSupplyInfo = () => { * @param txHash The hash of the transaction to retrieve. */ export const getTransactionByHash = (txHash: Bytes32) => { - if (!utils.isHexStringOfLength(txHash, 32) && !utils.isWildcard(txHash)) { + if (!helpers.isHexStringOfLength(txHash, 32) && !helpers.isWildcard(txHash)) { throw new EvalError("CCDR: WitGetTransactionByHash: invalid transaction hash value"); } else { - return new Call("getTransaction", [ txHash ]) + return new JsonRPC("getTransaction", [ txHash ]) } } @@ -61,5 +61,5 @@ export const getTransactionByHash = (txHash: Bytes32) => { * Get Witnet node syncrhonization status. */ export const syncStatus = () => { - return new Call("syncStatus"); + return new JsonRPC("syncStatus"); } diff --git a/src/lib/radon/sources.ts b/src/lib/radon/sources.ts deleted file mode 100644 index b5ff069..0000000 --- a/src/lib/radon/sources.ts +++ /dev/null @@ -1,223 +0,0 @@ -const utils = require("./utils") - -import graphQlCompress from "graphql-query-compress" -import { RadonType as Script, RadonString as DefaultScript} from "./types" -import { Call as RPC } from "./ccdr" - -enum Methods { - None = 0x0, - HttpGet = 0x01, - HttpHead = 0x04, - HttpPost = 0x03, - RNG = 0x02, -} - -export interface Specs { - url?: string, - headers?: Map, - body?: string, - script?: Script, - tuples?: Map, -} - -export class Class { - public argsCount: number; - public authority?: string; - public body?: string; - public headers?: string[][]; - public method: Methods; - public path?: string; - public query?: string; - public schema?: string; - public script?: Script; - public url?: string; - public tuples?: Map; - constructor(method: Methods, specs?: Specs) { - if (method === Methods.RNG && (specs?.url || specs?.headers || specs?.body)) { - throw EvalError("\x1b[1;33mRetrieval: badly specified RNG\x1b[0m"); - } else if (!specs?.url && (method == Methods.HttpPost || method == Methods.HttpGet)) { - throw EvalError("\x1b[1;33mRetrieval: URL must be specified\x1b[0m"); - } else if (specs?.body && method == Methods.HttpGet) { - throw EvalError("\x1b[1;33mWitnet.Sources: body cannot be specified here\x1b[0m") - } - this.method = method - this.headers = [] - if (specs?.headers) { - if (specs.headers instanceof Map) { - specs.headers.forEach((value: string, key: string) => this.headers?.push([key, value])) - } else { - Object.entries(specs.headers).forEach((entry: any) => this.headers?.push(entry)) - } - } - this.body = specs?.body - this.script = specs?.script - if (specs?.url) { - this.url = specs.url - if (!utils.isWildcard(specs.url)) { - let parts = utils.parseURL(specs.url) - this.schema = parts[0] - if (parts[1] !== "") this.authority = parts[1] - if (parts[2] !== "") this.path = parts[2] - if (parts[3] !== "") this.query = parts[3] - } - } - this.argsCount = Math.max( - utils.getMaxArgsIndexFromString(specs?.url), - utils.getMaxArgsIndexFromString(specs?.body), - ...this.headers.map(header => utils.getMaxArgsIndexFromString(header[1])), - specs?.script?._countArgs() || 0, - ) - this.tuples = specs?.tuples - } - /** - * Creates clones of this retrieval where all occurences of the specified parameter - * are replaced by the given values. Fails if the retrieval refers no parameters, - * or if the specified index is not referred by it. - * @param argIndex Index of the parameter upon which the new instances will be created. - * @param values Values used for the creation of the new sources. - * @returns An array with as many sources as spawning values were specified. - */ - public spawn(argIndex: number, values: string[]): Class[] { - const spawned: Class[] = [] - if (this.argsCount == 0) { - throw new EvalError(`\x1b[1;33mRetrieval: cannot spawn over unparameterized retrieval\x1b[0m`); - } else if (argIndex > this.argsCount) { - throw new EvalError(`\x1b[1;33mRetrieval: spawning parameter index out of range: ${argIndex} > ${this.argsCount}\x1b[0m`); - } - values.forEach(value => { - let headers: Map = new Map() - if (this.headers) { - this.headers.forEach(header => { - headers.set( - utils.spliceWildcards(header[0], argIndex, value, this.argsCount), - utils.spliceWildcards(header[1], argIndex, value, this.argsCount), - ) - }) - } - const script: Script | undefined = this.script?._spliceWildcards(argIndex, value); - spawned.push(new Class(this.method, { - url: utils.spliceWildcards(this.url, argIndex, value, this.argsCount), - body: utils.spliceWildcards(this.body, argIndex, value, this.argsCount), - headers, script, - })) - }) - return spawned - } -} - -/** - * Creates a Witnet randomness Radon Retrieval. - * @param script (Optional) Radon Script to apply to the random seed proposed by every single witness, - * before aggregation. - */ -export function RNG (script?: any) { return new Class(Methods.RNG, { script }); }; - -/** - * Creates a Witnet HTTP/GET Radon Retrieval. - * @param specs Retrieval parameters: URL, http headers (optional), Radon script (optional), - * pre-set tuples (optional to parameterized sources, only). - */ -export function HttpGet (specs: { - url: string, - headers?: Map, - script?: Script, - tuples?: Map -}) { - return new Class( - Methods.HttpGet, { - url: specs.url, - headers: specs.headers, - script: specs.script, - tuples: specs.tuples - } - ); -}; - -/** - * Creates a Witnet HTTP/HEAD Radon Retrieval. - * @param specs Retrieval parameters: URL, http headers (optional), Radon script (optional), - * pre-set tuples (optional to parameterized sources, only). - */ -export function HttpHead (specs: { - url: string, - headers?: Map, - script?: Script, - tuples?: Map -}) { - return new Class( - Methods.HttpHead, { - url: specs.url, - headers: specs.headers, - script: specs.script, - tuples: specs.tuples - } - ); -}; - -/** - * Creates a Witnet HTTP/POST Radon Retrieval. - * @param specs Retrieval parameters: URL, HTTP body (optional), HTTP headers (optional), Radon Script (optional), - * pre-set tuples (optional to parameterized sources, only). - */ -export function HttpPost (specs?: { - url: string, - body: string, - headers?: Map, - script?: Script, - tuples?: Map -}) { - return new Class( - Methods.HttpPost, { - url: specs?.url, - headers: specs?.headers, - body: specs?.body, - script: specs?.script, - tuples: specs?.tuples - } - ); -}; - -/** - * Creates a Witnet GraphQL Radon Retrieval (built on top of an HTTP/POST request). - * @param specs Retrieval parameters: URL, GraphQL query string, Radon Script (optional), - * pre-set tuples (optional to parameterized sources, only). - */ -export function GraphQLQuery (specs: { - url: string, - query: string, - script?: Script, - tuples?: Map, -}) { - return new Class(Methods.HttpPost, { - url: specs.url, - body: `{\"query\":\"${graphQlCompress(specs.query).replaceAll('"', '\\"')}\"}`, - headers: new Map().set("Content-Type", "application/json"), - script: specs?.script || new DefaultScript(), - tuples: specs?.tuples - }); -}; - -/** - * Creates a Cross Chain Data Source built on top of a HTTP/POST request. - * @param specs Retrieval parameters: RPC provider URL, RPC object encapsulating method and parameters, - * Radon Script (optional) to apply to returned value, and pre-set tuples (optional to parameterized sources, only). - */ -export function CrossChainDataSource (specs: { - url: string, - rpc: RPC, - script?: Script, - tuples?: Map -}) { - return new Class(Methods.HttpPost, { - url: specs.url, - body: JSON.stringify({ - jsonrpc: "2.0", - method: specs.rpc.method, - params: specs.rpc?.params, - id: 1, - }).replaceAll('\\\\', '\\'), - headers: new Map().set("Content-Type", "application/json"), - script: specs?.script || new DefaultScript(), - tuples: specs?.tuples - }); -}; diff --git a/src/lib/radon/types.ts b/src/lib/radon/types.ts index a04f434..7f6fa9a 100644 --- a/src/lib/radon/types.ts +++ b/src/lib/radon/types.ts @@ -1,13 +1,89 @@ -const utils = require('./utils') +const cbor = require("cbor") +const helpers = require('./helpers') -import * as reducers from './reducers' +import { Opcodes as RedonReducerOpcodes } from './reducers' +import * as Utils from '../../utils' -export enum RadonBytesEncoding { +enum RadonBytesEncodings { HexString = 0, Base64 = 1, -}; +} + +export enum RadonOperators { + ArrayLength = 0x10, + ArrayFilter = 0x11, + ArrayJoin = 0x12, + ArrayGetArray = 0x13, + ArrayGetBoolean = 0x14, + ArrayGetBytes = 0x15, + ArrayGetFloat = 0x16, + ArrayGetInteger = 0x17, + ArrayGetMap = 0x18, + ArrayGetString = 0x19, + ArrayMap = 0x1a, + ArrayReduce = 0x1b, + ArraySort = 0x1d, + ArrayPick = 0x1e, + BooleanStringify = 0x20, + BooleanNegate = 0x22, + BytesStringify = 0x30, + BytesHash = 0x31, + BytesAsInteger = 0x32, + BytesLength = 0x34, + BytesSlice = 0x3c, + FloatAbsolute = 0x50, + FloatStringify = 0x51, + FloatCeiling = 0x52, + FloatGreaterThan = 0x53, + FloatFloor = 0x54, + FloatLessThan = 0x55, + FloatModule = 0x56, + FloatMultiply = 0x57, + FloatNegate = 0x58, + FloatPower = 0x59, + FloatRound = 0x5b, + FloatTruncate = 0x5d, + IntegerAbsolute = 0x40, + IntegerToFloat = 0x41, + IntegerStringify = 0x42, + IntegerGreaterThan = 0x43, + IntegerLessThan = 0x44, + IntegerModulo = 0x46, + IntegerMultiply = 0x47, + IntegerNegate = 0x48, + IntegerPower = 0x49, + IntegerToBytes = 0x4a, + MapStringify = 0x60, + MapGetArray = 0x61, + MapGetBoolean = 0x62, + MapGetFloat = 0x64, + MapGetInteger = 0x65, + MapGetMap = 0x66, + MapGetString = 0x67, + MapKeys = 0x68, + MapValued = 0x69, + MapAlter = 0x6b, + MapPick = 0x6e, + StringAsBoolean = 0x70, + StringAsBytes = 0x71, + StringAsFloat = 0x72, + StringLength = 0x74, + StringMatch = 0x75, + StringParseJSONArray = 0x76, + StringParseJSONMap = 0x77, + StringParseXMLMap = 0x78, + StringToLowerCase = 0x79, + StringToUppserCase = 0x7a, + StringParseReplace = 0x7b, + StringParseSlice = 0x7c, + StringParseSplit = 0x7d, +} export class RadonType { + public static from(hexString: string) { + return Utils.decodeScript(hexString) + } + protected _bytecode?: any; protected _key?: string; protected _prev?: RadonType; @@ -16,6 +92,21 @@ export class RadonType { constructor (prev?: RadonType, key?: string) { this._key = key this._prev = prev + Object.defineProperty(this, "argsCount", { value: () => { + return Math.max( + helpers.getWildcardsCountFromString(key), + prev?._countWildcards() || 0 + ) + }}) + Object.defineProperty(this, "toArray", { value: () => { + let _result = this._bytecode ? [ this._bytecode ] : [] + if (this._prev !== undefined) _result = Object(this._prev).toArray().concat(_result) + return _result + }}) + Object.defineProperty(this, "toBytecode", { value: () => { + let _array = Object(this).toArray() + return Utils.toHexString(Object.values(Uint8Array.from(cbor.encode(_array)))) + }}) Object.defineProperty(this, "toString", { value: () => { let _result if (this._method) _result = `${this._method}(${this._params !== undefined ? this._params : ""})` @@ -32,10 +123,10 @@ export class RadonType { * (Compilation time only) Returns the maximum index from all wildcarded arguments refered at any step of the script, plus 1. * @returns 0 if the script refers no wildcarded argument at all. */ - public _countArgs(): number { + public _countWildcards(): number { return Math.max( - utils.getMaxArgsIndexFromString(this?._key), - this._prev?._countArgs() || 0 + helpers.getWildcardsCountFromString(this?._key), + this._prev?._countWildcards() || 0 ); } /** @@ -47,13 +138,37 @@ export class RadonType { return _result } /** - * (Compilation time only) Clone the script where all occurences of the specified argument - * will be replaced by the given value, and all wildcarded arguments with a higher - * index will be decreased by one. - * @param argIndex Index of the wildcarded argument whose value is to be replaced. - * @param argValue Value used to replace given argument. + * (Compilation time only) Clone the script and replace indexed wildcards + * with given parameters. Fails if less values than indexed wildcards are provided. + */ + public _replaceWildcards(args: string[]): RadonType { + const RadonClass = [ + RadonArray, + RadonBoolean, + RadonBytes, + RadonFloat, + RadonInteger, + RadonMap, + RadonString + ].find(RadonClass => this instanceof RadonClass) || RadonType; + if (args.length < this._countWildcards()) { + throw EvalError(`\x1b[1;33m${RadonClass}: insufficient args were provided (${args.length} < ${this._countWildcards()})\x1b[0m`) + } + const spliced = new RadonClass(this._prev?._replaceWildcards(args), this._key); + spliced._set( + helpers.replaceWildcards(this._bytecode, args), + this._method, + helpers.replaceWildcards(this._params, args), + ) + return spliced + } + + /** + * (Compilation time only) Clone the script and replace all occurences of the + * specified argument index by the given value. All wildcards with a higher + * index will be decreased by one in the new script. */ - public _spliceWildcards(argIndex: number, argValue: string): RadonType { + _spliceWildcard(argIndex: number, argValue: string): RadonType { const RadonClass = [ RadonArray, RadonBoolean, @@ -63,12 +178,12 @@ export class RadonType { RadonMap, RadonString ].find(RadonClass => this instanceof RadonClass) || RadonType; - const argsCount: number = this._countArgs() - const spliced = new RadonClass(this._prev?._spliceWildcards(argIndex, argValue), this._key); + const argsCount: number = this._countWildcards() + const spliced = new RadonClass(this._prev?._spliceWildcard(argIndex, argValue), this._key); spliced._set( - utils.spliceWildcards(this._bytecode, argIndex, argValue, argsCount), + helpers.spliceWildcard(this._bytecode, argIndex, argValue, argsCount), this._method, - utils.spliceWildcards(this._params, argIndex, argValue, argsCount) + helpers.spliceWildcard(this._params, argIndex, argValue, argsCount) ) return spliced } @@ -206,10 +321,10 @@ export class RadonArray extends RadonType { * convertable into float values. * @returns A `RadonFloat` object. */ - public reduce(reductor: reducers.Opcodes) { + public reduce(reductor: RedonReducerOpcodes) { this._bytecode = [ 0x1B, reductor ] this._method = "reduce" - this._params = reducers.Opcodes[reductor] + this._params = RedonReducerOpcodes[reductor] return new RadonFloat(this) } /** @@ -237,11 +352,11 @@ export class RadonArray extends RadonType { * @param indexes Indexes of the input items to take into the output array. * @return A `RadonArray` object. */ - public pick(indexes: number | number[]) { - if (!indexes || Array(indexes).length == 0) { + public pick(...indexes: number[]) { + if (Array(indexes).length == 0) { throw new EvalError(`\x1b[1;33mRadonArray::pick: a non-empty array of numbers must be provided\x1b[0m`) } - this._bytecode = [ 0x1e, typeof indexes === 'number' ? indexes : indexes as number[] ] + this._bytecode = [ 0x1e, ...indexes ] this._params = JSON.stringify(indexes) this._method = "pick" return new RadonArray(this) @@ -270,6 +385,7 @@ export class RadonBoolean extends RadonType { } export class RadonBytes extends RadonType { + static readonly Encodings = RadonBytesEncodings; /** * Convert buffer into (big-endian) integer. * @returns A `RadonBytes` object. @@ -323,10 +439,10 @@ export class RadonBytes extends RadonType { * 1 -> Base64 string * @returns A `RadonString` object. */ - public stringify(encoding?: RadonBytesEncoding) { + public stringify(encoding?: RadonBytesEncodings) { if (encoding) { this._bytecode = [ 0x30, encoding ] - this._params = `${RadonBytesEncoding[encoding]}` + this._params = `${RadonBytesEncodings[encoding]}` } else { this._bytecode = 0x30 } @@ -567,7 +683,7 @@ export class RadonMap extends RadonType { * @returns The same RadonMap upon which this operator is executed, with the specified item(s) altered * by the given `innerScript`. */ - public alter(keys: string | string[], innerScript: RadonType) { + public alter(innerScript: RadonType, ...keys: string[]) { const RadonClass = [ RadonArray, RadonBoolean, @@ -575,14 +691,15 @@ export class RadonMap extends RadonType { RadonFloat, RadonInteger, RadonMap, - RadonString - ].find(RadonClass => innerScript instanceof RadonClass) || RadonType; - if (RadonClass instanceof RadonType) { - throw new EvalError(`\x1b[1;33mRadonMap::alter: inner script returns undetermined RadonType\x1b[0m`) + RadonString, + RadonType, + ].find(RadonClass => innerScript instanceof RadonClass) || undefined; + if (!RadonClass || RadonClass instanceof RadonType) { + throw new EvalError(`\x1b[1;33mRadonMap::alter: passed inner script is not valid\x1b[0m`) } - this._bytecode = [ 0x6b, keys, innerScript._encodeArray() ] + this._bytecode = [ 0x6b, innerScript._encodeArray(), ...keys ] this._method = "alter" - this._params = `${JSON.stringify(keys)}, { ${innerScript.toString()} }` + this._params = `{ ${innerScript.toString()}${keys.length > 0 ? `, ${JSON.stringify(keys).slice(1, -1)}` : ""} }` return new RadonMap(this) } /** @@ -665,11 +782,11 @@ export class RadonMap extends RadonType { * @param keys Key string of the input items to take into the output map. * @return A `RadonMap` object. */ - public pick(keys: string | string[]) { - if (!keys || Array(keys).length == 0) { + public pick(...keys: string[]) { + if (Array(keys).length == 0) { throw new EvalError(`\x1b[1;33mRadonMap::pick: a non-empty array of key strings must be provided\x1b[0m`) } - this._bytecode = [ 0x6e, keys ] + this._bytecode = [ 0x6e, ...keys ] this._params = JSON.stringify(keys) this._method = "pick" return new RadonMap(this) @@ -711,10 +828,10 @@ export class RadonString extends RadonType { * 1 -> Base64 string * @returns A `RadonBytes` object. */ - public asBytes(encoding?: RadonBytesEncoding) { + public asBytes(encoding?: RadonBytesEncodings) { if (encoding !== undefined) { this._bytecode = [ 0x71, encoding ] - this._params = `${RadonBytesEncoding[encoding]}` + this._params = `${RadonBytesEncodings[encoding]}` } else { this._bytecode = 0x71 } @@ -768,9 +885,9 @@ export class RadonString extends RadonType { * @param jsonPaths (optional) Array of JSON paths within input `RadonString` from where to fetch items that will be appended to the output `RadonArray`. * @returns A `RadonArray` object. */ - public parseJSONArray(jsonPaths?: string | string[]) { - if (jsonPaths) { - this._bytecode = [ 0x76, jsonPaths ] + public parseJSONArray(...jsonPaths: string[]) { + if (jsonPaths.length > 0) { + this._bytecode = [ 0x76, ...jsonPaths ] this._params = `${jsonPaths}` } else { this._bytecode = 0x76 @@ -858,7 +975,7 @@ export class RadonString extends RadonType { return new RadonString(this) } /** - * Upser case all characters. + * Upper case all characters. * @returns A `RadonString` object. */ public toUpperCase() { diff --git a/src/lib/radon/utils.js b/src/lib/radon/utils.js deleted file mode 100644 index 91a7333..0000000 --- a/src/lib/radon/utils.js +++ /dev/null @@ -1,69 +0,0 @@ -export function getMaxArgsIndexFromString(str) { - let maxArgsIndex = 0 - if (str) { - let match - const regexp = /\\\d\\/g - while ((match = regexp.exec(str)) !== null) { - let argsIndex = parseInt(match[0][1]) + 1 - if (argsIndex > maxArgsIndex) maxArgsIndex = argsIndex - } - } - return maxArgsIndex -} - -export function isHexString(str) { - return ( - !Number.isInteger(str) - && str.startsWith("0x") - && /^[a-fA-F0-9]+$/i.test(str.slice(2)) - ); -} - -export function isHexStringOfLength(str, max) { - return (isHexString(str) - && str.slice(2).length <= max * 2 - ); -} - -export function isWildcard(str) { - return str.length == 3 && /\\\d\\/g.test(str) -} - -export function parseURL(url) { - if (url && typeof url === 'string' && url.indexOf("://") > -1) { - const hostIndex = url.indexOf("://") + 3 - const schema = url.slice(0, hostIndex) - let host = url.slice(hostIndex) - let path = "" - let query = "" - const pathIndex = host.indexOf("/") - if (pathIndex > -1) { - path = host.slice(pathIndex + 1) - host = host.slice(0, pathIndex) - const queryIndex = path.indexOf("?") - if (queryIndex > -1) { - query = path.slice(queryIndex + 1) - path = path.slice(0, queryIndex) - } - } - return [schema, host, path, query]; - } else { - throw new EvalError(`Invalid URL was provided: ${url}`) - } -} - -export function spliceWildcards(obj, argIndex, argValue, argsCount) { - if (obj && typeof obj === "string") { - const wildcard = `\\${argIndex}\\` - obj = obj.replaceAll(wildcard, argValue) - for (var j = argIndex + 1; j < argsCount; j++) { - obj = obj.replaceAll(`\\${j}\\`, `\\${j - 1}\\`) - } - } else if (obj && Array.isArray(obj)) { - obj = obj.map(value => typeof value === "string" || Array.isArray(value) - ? spliceWildcards(value, argIndex, argValue, argsCount) - : value - ) - } - return obj; -} diff --git a/src/utils.js b/src/utils.js index b40f9c7..6061107 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,170 +1,188 @@ -const { execSync } = require("child_process") - -module.exports = { - dryRunBytecode, - dryRunBytecodeVerbose, - fromAscii, - getRequestMethodString, - getRequestResultDataTypeString, - getMaxArgsIndexFromString, - isHexString, - isHexStringOfLength, - isWildcard, - mapObjectRecursively, - padLeft, - parseURL, - spliceWildcards, - splitSelectionFromProcessArgv, -} - -async function dryRunBytecode(bytecode) { - return (await execSync(`npx witnet-toolkit try-query --hex ${bytecode}`)).toString() +const cbor = require("cbor") +const os = require("os") +const crypto = require("crypto") +const { exec } = require("child_process") + +var protoBuf = require("protobufjs") +var protoRoot = protoBuf.Root.fromJSON(require("../assets/witnet.proto.json")) +var RADRequest = protoRoot.lookupType("RADRequest") + +import { RadonRequest } from "./lib/radon/artifacts" +import { RadonRetrieval } from "./lib/radon/retrievals" +import { RadonReducer } from "./lib/radon/reducers" +import { RadonFilter } from "./lib/radon/filters" +import * as RadonTypes from "./lib/radon/types" + +export function decodeRequest(hexString) { + const buffer = fromHexString(hexString) + const obj = RADRequest.decode(buffer) + const retrieve = obj.retrieve.map(retrieval => { + const specs = {} + if (retrieval?.url) { specs.url = retrieval.url } + if (retrieval?.headers) { + specs.headers = retrieval.headers.map(stringPair => [ + stringPair.left, + stringPair.right + ]) + } + if (retrieval?.body && retrieval.body.length > 0) { + specs.body = utf8ArrayToStr(Object.values(retrieval.body)) + } + if (retrieval?.script) specs.script = decodeScript(toHexString(retrieval.script)) + return new RadonRetrieval(retrieval.kind, specs) + }) + var decodeFilter = (f) => { + if (f?.args && f.args.length > 0) return new RadonFilter(f.op, cbor.decode(f.args)) + else return new RadonFilter(f.op); + } + return new RadonRequest({ + retrieve, + aggregate: new RadonReducer(obj.aggregate.reducer, obj.aggregate.filters?.map(decodeFilter)), + tally: new RadonReducer(obj.tally.reducer, obj.tally.filters?.map(decodeFilter)) + }) } -async function dryRunBytecodeVerbose(bytecode) { - return (await execSync(`npx witnet-toolkit trace-query --hex ${bytecode}`)).toString() +export function decodeScript(hexString) { + const buffer = fromHexString(hexString) + const array = cbor.decode(buffer) + return parseScript(array) } -function fromAscii(str) { - const arr1 = [] - for (let n = 0, l = str.length; n < l; n++) { - const hex = Number(str.charCodeAt(n)).toString(16) - arr1.push(hex) +export async function execDryRun(bytecode, ...flags) { + if (!isHexString(bytecode)) { + throw EvalError("Witnet.Utils.execDryRun: invalid bytecode") + } else { + const npx = os.type() === "Windows_NT" ? "npx.cmd" : "npx" + return cmd(npx, "witnet-toolkit", "dryrunRadonRequest", bytecode, ...flags) + .catch((err) => { + let errorMessage = err.message.split('\n').slice(1).join('\n').trim() + const errorRegex = /.*^error: (?.*)$.*/gm + const matched = errorRegex.exec(err.message) + if (matched) { + errorMessage = matched.groups.message + } + console.error(errorMessage || err) + }) } - return "0x" + arr1.join("") } -function getMaxArgsIndexFromString(str) { - let maxArgsIndex = 0 - if (str) { - let match - const regexp = /\\\d\\/g - while ((match = regexp.exec(str)) !== null) { - let argsIndex = parseInt(match[0][1]) + 1 - if (argsIndex > maxArgsIndex) maxArgsIndex = argsIndex - } - } - return maxArgsIndex +export function fromHexString(hexString) { + if (hexString.startsWith("0x")) hexString = hexString.slice(2) + return Uint8Array.from(hexString.match(/.{1,2}/g).map((byte) => parseInt(byte, 16))) } -function getRequestMethodString(method) { - if (!method) { - return "HTTP-GET"; - } else { - const methodNameMap = { - 0: "UNKNOWN", - 1: "HTTP-GET", - 2: "RNG", - 3: "HTTP-POST", - 4: "HTTP-HEAD", - }; - return methodNameMap[method] || method.toString(); - } +export function sha256(buffer) { + const hash = crypto.createHash('sha256') + hash.update(buffer) + return hash.digest('hex') } -function getRequestResultDataTypeString(type) { - const typeMap = { - 1: "Array", - 2: "Bool", - 3: "Bytes", - 4: "Integer", - 5: "Float", - 6: "Map", - 7: "String", - }; - return typeMap[type] || "(Undetermined)"; +export function toHexString(buffer) { + return "0x" + Array.prototype.map.call(buffer, x => ('00' + x.toString(16)).slice(-2)) + .join('') + .match(/[a-fA-F0-9]{2}/g) + .join('') } +// internal helper methods ------------------------------------------------------------------------ + +function cmd (...command) { + return new Promise((resolve, reject) => { + exec(command.join(" "), { maxBuffer: 1024 * 1024 * 10 }, (error, stdout, stderr) => { + if (error) { + reject(error) + } + if (stderr) { + reject(stderr) + } + resolve(stdout) + }) + }) +}; + function isHexString(str) { + if (str.startsWith("0x")) str = str.slice(2) return ( !Number.isInteger(str) - && str.startsWith("0x") && /^[a-fA-F0-9]+$/i.test(str.slice(2)) ); } -function isHexStringOfLength(str, max) { - return (isHexString(str) - && str.slice(2).length <= max * 2 - ); -} - -function isWildcard(str) { - return str.length == 3 && /\\\d\\/g.test(str) -} - -function mapObjectRecursively(obj, callback) { - let newObj = {} - for (let key in obj) { - if (obj.hasOwnProperty(key)) { - if (typeof obj[key] === "object") { - newObj[key] = mapObjectRecursively(obj[key], callback); +function parseScript(array, script) { + if (Array.isArray(array)) { + array.forEach(item => { + if (Array.isArray(item)) { + script = parseScriptOperator(script, item[0], item.slice(1)) } else { - newObj[key] = callback(key, obj[key]); + script = parseScriptOperator(script, item) } - } - } - return newObj; -} - -function padLeft(str, char, size) { - if (str.length < size) { - return char.repeat((size - str.length) / char.length) + str + }) + return script } else { - return str + return parseScriptOperator(script, array) } } -function parseURL(url) { - if (url && typeof url === 'string' && url.indexOf("://") > -1) { - const hostIndex = url.indexOf("://") + 3 - const schema = url.slice(0, hostIndex) - let host = url.slice(hostIndex) - let path = "" - let query = "" - const pathIndex = host.indexOf("/") - if (pathIndex > -1) { - path = host.slice(pathIndex + 1) - host = host.slice(0, pathIndex) - const queryIndex = path.indexOf("?") - if (queryIndex > -1) { - query = path.slice(queryIndex + 1) - path = path.slice(0, queryIndex) - } +function parseScriptOperator(script, opcode, args) { + if (!script) { + const found = Object.entries({ + "10": RadonTypes.RadonArray, + "20": RadonTypes.RadonBoolean, + "30": RadonTypes.RadonBytes, + "40": RadonTypes.RadonInteger, + "50": RadonTypes.RadonFloat, + "60": RadonTypes.RadonMap, + "70": RadonTypes.RadonString, + }).find(entry => entry[0] === (parseInt(opcode) & 0xf0).toString(16)) + const RadonClass = found ? found[1] : RadonTypes.RadonType; + script = new RadonClass() + } + if (opcode) { + var operator = RadonTypes.RadonOperators[opcode].split(/(?=[A-Z])/).slice(1).join("") + operator = operator.charAt(0).toLowerCase() + operator.slice(1) + switch (operator) { + case "filter": case "map": case "sort": + return script[operator](parseScript(args[0]), args.slice(1)); + + case "alter": + return script[operator](args[0], parseScript(args[1], ...args.slice(2))); + + default: + return script[operator](args) } - return [schema, host, path, query]; - } else { - throw new EvalError(`Invalid URL was provided: ${url}`) } } -function spliceWildcards(obj, argIndex, argValue, argsCount) { - if (obj && typeof obj === "string") { - const wildcard = `\\${argIndex}\\` - obj = obj.replaceAll(wildcard, argValue) - for (var j = argIndex + 1; j < argsCount; j++) { - obj = obj.replaceAll(`\\${j}\\`, `\\${j - 1}\\`) +function utf8ArrayToStr(array) { + var out, i, len, c; + var char2, char3; + + out = ""; + len = array.length; + i = 0; + while(i < len) { + c = array[i++]; + switch(c >> 4) + { + case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7: + // 0xxxxxxx + out += String.fromCharCode(c); + break; + case 12: case 13: + // 110x xxxx 10xx xxxx + char2 = array[i++]; + out += String.fromCharCode(((c & 0x1F) << 6) | (char2 & 0x3F)); + break; + case 14: + // 1110 xxxx 10xx xxxx 10xx xxxx + char2 = array[i++]; + char3 = array[i++]; + out += String.fromCharCode(((c & 0x0F) << 12) | + ((char2 & 0x3F) << 6) | + ((char3 & 0x3F) << 0)); + break; } - } else if (obj && Array.isArray(obj)) { - obj = obj.map(value => typeof value === "string" || Array.isArray(value) - ? spliceWildcards(value, argIndex, argValue, argsCount) - : value - ) } - return obj; -} -function splitSelectionFromProcessArgv(operand) { - let selection = [] - if (process.argv.includes(operand)) { - process.argv.map((argv, index, args) => { - if (argv === operand) { - if (index < process.argv.length - 1 && !args[index + 1].startsWith("--")) { - selection = args[index + 1].replaceAll(":", ".").split(",") - } - } - }) - } - return selection + return out; } - diff --git a/tsconfig.json b/tsconfig.json index 4d8f47b..a3599aa 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -36,8 +36,10 @@ ], /* List of folders to include type definitions from. */ }, "files": [ + "src/index.ts", + "src/bin/toolkit.js", "src/lib/radon/index.ts", - "src/lib/radon/utils.js", + "src/lib/radon/helpers.js", ], "include": [ "src/**/*.d.ts",