Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add .dryrun Command Support #191

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,13 @@ When running the console, you can type `dot` commands to instruct the console to
| `.editor` | This command opens a simple cli editor that you can type on multiple lines, and then you can type `.done` or `.cancel` to exist editor mode. |
| `.load` | This command allows you to load a lua source file from your local directory |
| `.load-blueprint [name]` | This command will grab a lua file from the blueprints directory and load it into your process. |
| `.dryrun {tags}` | This command will allow you to perform dryrun. |
| `.exit` | This command will exit you console, but you can also do `Ctrl-C` or `Ctrl-D` |

## License

The ao and aos codebases are offered under the BSL 1.1 license for the duration
of the testnet period. After the testnet phase is over, the code will be made
available under either a new
[evolutionary forking](https://arweave.medium.com/arweave-is-an-evolutionary-protocol-e072f5e69eaa)
[evolutionary forking](https://arweave.medium.com/arweave-is-an-evolutionary-protocol-e072f5e69eaa)s

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This s seem to be a typo?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, that's a typo.

license, or a traditional OSS license (GPLv3/v2, MIT, etc).
176 changes: 176 additions & 0 deletions src/commands/dryrun.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import { dryrun } from "@permaweb/aoconnect";
import chalk from "chalk";

export async function performDryRun(input, pid, data, owner, id, anchor) {
await processInput(input, pid, data, owner, id, anchor);
}

async function extractTags(input, pid) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code needs to be simplified. I suggest you reduce complexity by:

  • Removing side effects throwable stuff to the top
  • Generalize the parsing so it cant throw and then put it into a functor
  • Try to reduce conditions to lower complexity

This should give an indication:

async function extractTags(input, pid) {
  // Directly throw an error if input doesn't include "{", avoiding multiple checks.
  if (!input.includes("{")) throw new Error("Tags not specified");

  // Use optional chaining to simplify error handling.
  const content = input.match(/\{(.*?)\}/)?.[1];
  if (!content) throw new Error("Invalid Syntax Usage");

  // Streamline tag parsing and creation with map (the functor), removing redundant else block.
  const tags = content.split(",").map(pair => {
    if (pair.includes("=")) {
      let [key, value] = pair.split("=").map(item => item.trim().replace(/^['"]|['"]$/g, ""));
      // Simplify replacement for "ao.id" with direct approach.
      value = value === "ao.id" ? pid : value;
      return { name: key, value };
    } else {
      // Handle action tags directly within the map function.
      const action = pair.trim().replace(/^'|'$/g, "");
      return { name: "Action", value: action };
    }
  });

  // Check and add "Target" 
  if (!tags.some(tag => tag.name === "Target")) {
    tags.unshift({ name: "Target", value: pid });
  }

  return tags;
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P.S if target can be appended you can get rid of else al in all

if (!input.includes("{")) throw new Error("Tags not specified");

  const content = input.match(/\{(.*?)\}/)?.[1];
  if (!content) throw new Error("Invalid Syntax Usage");

  return content.split(",").map(pair => {
    const [key, rawValue] = pair.includes("=") ? pair.split("=") : ["Action", pair];
    const value = rawValue.trim().replace(/^['"](.*)['"]$/, "$1").replace(/^ao\.id$/, pid);
    return { name: key.trim(), value };
  }).concat({ name: "Target", value: pid });

let tags = [];

// Check if the input contains curly braces
if (!input.includes("{")) {
throw new Error("Tags not specified");
}

// Check if the input contains curly braces
if (input.includes("{")) {
try {
// Extracting content within curly braces
const content = input.match(/\{(.*?)\}/)[1];

const pairs = content.split(",");

// Iterating through each key-value pair
pairs.forEach((pair) => {
// Check if the pair contains an equal sign
if (pair.includes("=")) {
// Splitting each pair by "=" to get key and value
const [key, value] = pair.split("=").map((item) => item.trim());
// Normalize the value
let normalizedValue = value;
normalizedValue = normalizedValue.replace(/^['"](.*)['"]$/, "$1");

console.log("normalizedValue", normalizedValue);

// Replace "ao.id" with `pid`
if (normalizedValue === "ao.id") {
normalizedValue = pid;
}

//use extra quotes to use the "ao.id" word itself
if (normalizedValue === "'ao.id'" || normalizedValue === `ao.id"`) {
normalizedValue = "ao.id";
}

// Pushing key-value pair to tags array
tags.push({ name: key, value: normalizedValue });
} else {
const action = pair.trim().replace(/^'|'$/g, "");
tags.push({ name: "Action", value: action });
}
});
} catch (error) {
throw new Error("Invalid Syntax Usage");
}
} else {
// If no curly braces, assume single word input, set it as Action
const action = input.trim().replace(/^'|'$/g, "");
let normalizedAction = action;
normalizedAction = normalizedAction.replace(/^['"](.*)['"]$/, "$1");
console.log(normalizedAction);
tags.push({ name: "Action", value: normalizedAction });
}

// Adding default Target if not already present
let targetIncluded = tags.some((tag) => tag.name === "Target");
if (!targetIncluded) {
tags.unshift({ name: "Target", value: pid });
}

return tags;
}

async function callDryRun(input, pid, data, owner, id, anchor) {
try {
const tags = await extractTags(input, pid);
// check for Pid and data in params
const customProcessId = getCustomProcessId(input, pid) || pid;
const customData = getCustomData(input, pid) || data;

const dryrunParams = {
process: customProcessId,
tags,
...(customData && { data: customData }),
...(owner && { Owner: owner }),
...(id && { Id: id }),
...(anchor && { anchor: anchor }),
};

checkForVerboseFlag(input, dryrunParams);

const result = await dryrun(dryrunParams);
return result;
} catch (error) {
console.error(chalk.red("Error Performing DryRun:", error.message));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it not be right to propagate the error up in context? Why swallow it here.

}
}

function checkForVerboseFlag(input, dryrunParams) {
const regex = /(^|\s)-v($|\s)/; // Regular expression to match -v preceded and followed by whitespace or string boundary

if (regex.test(input)) {
console.log("dryrunParams:", dryrunParams);
}
}


const getCustomProcessId = (input, pid) => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Slightly cleaner.

const getCustomProcessId = (input, pid) => {
  const match = input.match(/-p=(["'])(.*?)\1/);
  if (!match) return null;
  const customProcessId = match[2] === "ao.id" ? pid : match[2];
  if (customProcessId.length !== 43) throw new Error("Invalid Process Id");
  return customProcessId;
};

const match = input.match(/-p=(["'])(.*?)\1/);
if (match) {
let customProcessParam = match[2];

// Replace "ao.id" with `pid`
if (customProcessParam === "ao.id") {
customProcessParam = pid;
}

if (customProcessParam.length !== 43) {
throw new Error("Invalid Process Id");
}
return customProcessParam;
}
return null;
};

const getCustomData = (input, pid) => {
try {
let match = input.match(/-d=(["'])(.*?)\1/);
if (match) {
if (match[2] === "ao.id") {
return pid;
}

if (match[2] === "'ao.id'" || match[2] === `"ao.id"`) {
return "ao.id";
}

return match[2];
}
} catch (error) {
throw new Error("Invalid Data Format");
}
return null;
};

const getSpecificResult = (obj, path, defaultValue = undefined) => {
const travel = (regexp) =>
String.prototype.split
.call(path, regexp)
.filter(Boolean)
.reduce(
(res, key) => (res !== null && res !== undefined ? res[key] : res),
obj,
);

const result = travel(/[,[\]]+?/) || travel(/[,[\].]+?/);
return result === undefined || result === obj ? defaultValue : result;
};

async function processInput(input, pid, data, owner, id, anchor) {
try {
const flagIndex = input.indexOf("-r");
const flag = flagIndex !== -1 ? getFlag(input, flagIndex) : null;
const result = await callDryRun(input, pid, data, owner, id, anchor);
console.log(chalk.green("result:"));
flag ? console.log(getSpecificResult(result, flag)) : console.log(result);
} catch (error) {
console.error(chalk.red("Error Processing Input:", error.message));
}
}

const getFlag = (input, flagIndex) => {
const flagStart = flagIndex + 3;
const flagEnd = input.indexOf(" ", flagStart);
return input.slice(flagStart, flagEnd !== -1 ? flagEnd : undefined).trim();
};
167 changes: 167 additions & 0 deletions src/commands/dryrun.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
# README for Command: .dryrun

### Syntax

.dryrun <flags> <{Tags}>

### Quick Examples

- Replacing the Send command with `.dryrun`:

.dryrun ({ Target = 'FgU-RiEaLuC__SHZnI9pSIa_ZI8o-8hUVG9nPJvs92k', Action = 'Balance' })

or

- Using single word Action:

.dryrun ({Balance}).

**Note:**
- While using single words, the word inside the braces will be considered as Action ('Action'='Word'), and it takes your Process ID `ao.id` as both the Target and Process ID by default.
- Tags must be enclosed with curly braces `{}` surrounding the circle brackets with parentheses `()` is optional.



## Flags:
These flags determine the output you get on your terminal.

### TheFlags List:
- `-p` for specifying Process ID.
- `-d` for specifying data.
- `-r` for accessing the Result Object.
- `-v` for viewing the DryRun Parameterss

### Usage:
- Flags can be used in combinations, and the order of flags can be interchangeable.
- If you don't specify any flags, it will default to the `result object` (`-r`).
- If you don't specify a `-p` Process ID, your Process ID will be used.
- If you don't specify any `-d`, no data will be passed.




## Tags:
The tags that will be passed to perform DryRun.

### Usage:
- Tags must be enclosed with curly braces `{}`. Surrounding the circle brackets with parentheses `()` is optional.
- Single word tags must also be enclosed within curly braces `{}`.
- By default, the single word will be considered as Action. It takes your Process ID `ao.id` as both the Target and Process ID by default.

### Using 'ao.id'
Using "ao.id" to mention Process ID will work fine. By default Process ID and Target ID will be 'ao.id' (your process id). so its better to leave eit without mentioning it. If you want to specify the "ao.id" word itself, then give it enclosed with a single quote enclosed with a double quote.

Samples :

# to get ao.id

.dryrun {'Balance'}

.dryrun ({ Target = ao.id, Action = 'Balance' })

.dryrun ({ Target = 'ao.id', Action = 'Balance' })

# to get 'ao.id'

.dryrun ({ Target = "'ao.id'", Action = 'Balance' })


## Examples:

- Replacing the Send command with `.dryrun` will give you the Result Object:

.dryrun ({ Target = 'FgU-RiEaLuC__SHZnI9pSIa_ZI8o-8hUVG9nPJvs92k', Action = 'Balance' })

- Using Single word Action:

.dryrun ({Balance})

This will use "Balance" as Action and your Process ID as Target & Process ID:

# .dryrun ({ Target = ao.id, Action = 'Balance' })

**Sample Result Object Output:**

{
Messages: [
{
Target: '1234',
Anchor: '00000000000000000000000000000917',
Tags: [Array]
}
],
Spawns: [],
Output: [],
GasUsed: 466333831
}

This will give you the Result Overview. (More details will be available in the Message property.)


- Accessing the keys of the Result Object:

.dryrun -r.Messages[0] ({Balance})

This will give the Message Object of index=0 in Messages Array in the result object.

Example Output:

{
Target: '1234',
Anchor: '00000000000000000000000000000917',
Tags: [
{ value: 'ao', name: 'Data-Protocol' },
{ value: 'ao.TN.1', name: 'Variant' },
{ value: 'Message', name: 'Type' },
{
value: 'FgU-RiEaLuC__SHZnI9pSIa_ZI8o-8hUVG9nPJvs92k',
name: 'From-Process'
},
{
value: '9afQ1PLf2mrshqCTZEzzJTR2gWaC9zNPnYgYEqg1Pt4',
name: 'From-Module'
},
{ value: '917', name: 'Ref_' },
{ value: '1e+14', name: 'Data' },
{ value: '1234', name: 'Target' },
{ value: 'PNTS', name: 'Ticker' },
{ value: '100000000000000', name: 'Balance' }
]
}


- Sending data to the DryRun:

.dryrun -d="test" ({Balance})

or

.dryrun -d="test" ({ Target = 'FgU-RiEaLuC__SHZnI9pSIa_ZI8o-8hUVG9nPJvs92k', Action = 'Balance' })

or sending as Tag **(recommended method)**:

.dryrun -d="test" ({ Target = 'FgU-RiEaLuC__SHZnI9pSIa_ZI8o-8hUVG9nPJvs92k', Action = 'Balance' , Data = 'test'})


- Passing Process ID:

.dryrun -p="FgU-RiEaLuC__SHZnI9pSIa_ZI8o-8hUVG9nPJvs92k" ({Balance})


- Viewing DryRun Params :

.dryrun -v {Message}

Example Output :

dryrunParams: {
process: 'FXYu2-c66N90yG2yqL0myafDgRTGONn0xn7xmZxEq7g',
tags: [
{
name: 'Target',
value: 'FXYu2-c66N90yG2yqL0myafDgRTGONn0xn7xmZxEq7g'
},
{ name: 'Action', value: 'Message' }
]
}

10 changes: 10 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { unmonitor } from './commands/unmonitor.js'
import { loadBlueprint } from './commands/blueprints.js'
import { help, replHelp } from './services/help.js'
import { list } from './services/list.js'
import { performDryRun } from './commands/dryrun.js'

const argv = minimist(process.argv.slice(2))
let luaData = ""
Expand Down Expand Up @@ -269,6 +270,15 @@ if (!argv['watch']) {
return;
}


if (!editorMode && line.startsWith('.dryrun')) {
let pid = id
let input = line
await performDryRun(input, pid)
rl.prompt(false);
return;
}

if (editorMode && line === ".done") {
line = editorData
editorData = ""
Expand Down
1 change: 1 addition & 0 deletions src/services/help.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ ${chalk.green('Commands:')}
${chalk.green('.monitor')} Starts monitoring cron messages for this Process
${chalk.green('.unmonitor')} Stops monitoring cron messages for this Process
${chalk.green('.editor')} Simple code editor for writing multi-line lua expressions
${chalk.green('.dryrun')} To Perform dryrun
${chalk.green('.help')} Print this help screen
${chalk.green('.exit')} Quit console
`)
Expand Down