Skip to content

Commit

Permalink
feat: poc
Browse files Browse the repository at this point in the history
  • Loading branch information
ZeroEkkusu committed Aug 19, 2024
1 parent 79dc4d5 commit f8d8c80
Show file tree
Hide file tree
Showing 13 changed files with 318 additions and 28 deletions.
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.solx linguist-language=Solidity
5 changes: 5 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"files.associations": {
"*.solx": "solidity"
}
}
62 changes: 59 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,36 @@
# Soldeer Minimal
# SOLX

**Minimal Forge, Soldeer, and Bun template.**
**Solidity × Any-Language Transpiler for Foundry.**

Soldeer Minimal uses Soldeer package manager instead of git modules and can run TypeScript out of box.
Write Solidity AND **any other programming language** <ins>at the same time</ins>!

<img src="./demo.gif"></img>

## Usage

> [!IMPORTANT]
> Proof of concept. For research purposes only.
**Clone**

```shell
git clone https://github.com/ZeroEkkusu/solx
```

**Install**

```shell
soldeer install & bun install
```

**Hook**

```
source solx/hooks.sh
```

This will hook SOLX transpiler to Forge.

**Build**

```shell
Expand All @@ -24,6 +43,43 @@ forge build
forge test
```

**Experiment**

> [!NOTE]
> Try it out: [`test/Example.solx`](./test/Example.solx)


Specify <ins>variables to sync</ins> between Solidity and TypeScript:

```solidity
uint256 a;
// @typescript-start (uint256 a)
a++;
// @typescript-end ()
assertEq(a, 1);
```

Specify <ins>variables to clone</ins> from TypeScript:

```solidity
uint256 a = 1;
// @typescript-start ()
const b = 1;
// @typescript-end (uint256 b)
assertEq(a, b);
```

Use `console.log` in TypeScript:

```solidity
// @typescript-start ()
console.log('👀');
// @typescript-end ()
```

Only TypeScript is supported currently.

## License
Licensed under either of
Expand Down
Binary file modified bun.lockb
Binary file not shown.
Binary file added demo.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion foundry.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[profile.default]
src = "src"
test = "out/solx"
out = "out"
libs = ["dependencies"]
verbosity = 2
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
{
"devDependencies": {
"@types/bun": "^1.1.6"
},
"dependencies": {
"ethers": "^6.13.2"
}
}
10 changes: 0 additions & 10 deletions script/Deploy.s.sol

This file was deleted.

10 changes: 10 additions & 0 deletions solx/hooks.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Override for 'forge build' and 'forge test'
forge() {
if [ "$1" = "build" ]; then
bun solx/transpiler.ts && command forge build "${@:2}"
elif [ "$1" = "test" ]; then
bun solx/transpiler.ts && command forge test "${@:2}"
else
command forge "$@"
fi
}
217 changes: 217 additions & 0 deletions solx/transpiler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
import * as fs from "fs";
import * as path from "path";

interface ProcessedContent {
solidity: string;
typescript: string;
}

function processSolidityFile(inputPath: string): void {
const content = fs.readFileSync(inputPath, "utf-8");
const { solidity, typescript } = extractTypeScriptBlock(content);

const outputDir = path.join(process.cwd(), "out", "solx");
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}

const outputSolPath = path.join(
outputDir,
path.basename(inputPath).replace(".solx", ".sol")
);
const outputTsPath = path.join(outputDir, "example.ts");

fs.writeFileSync(outputSolPath, solidity);
fs.writeFileSync(outputTsPath, typescript);

/*console.log(`Processed ${inputPath}`);
console.log(`Generated ${outputSolPath}`);
console.log(`Generated ${outputTsPath}`);*/
}

function extractTypeScriptBlock(content: string): ProcessedContent {
const tsBlockRegex =
/\/\/ @typescript-start\s*\((.*?)\)([\s\S]*?)\/\/ @typescript-end\s*\((.*?)\)/;
const match = tsBlockRegex.exec(content);

if (!match) {
throw new Error("No TypeScript block found in the Solidity file.");
}

const [fullMatch, inputVars, tsCode, outputVars] = match;
const inputVarList = inputVars
? inputVars.split(",").map((v) => v.trim())
: [];
const outputVarList = outputVars
? outputVars.split(",").map((v) => v.trim())
: [];

const allOutputVars = [...new Set([...outputVarList, ...inputVarList])];
const newOutputVars = outputVarList.filter((v) => !inputVarList.includes(v));
const existingOutputVars = allOutputVars.filter(
(v) => !newOutputVars.includes(v)
);
const hasConsoleLog = tsCode.includes("console.log");

let solidity = content.replace(
fullMatch,
`
string[] memory cmd = new string[](${inputVarList.length > 0 ? "3" : "2"});
cmd[0] = "bun";
cmd[1] = "./out/solx/example.ts";
${
inputVarList.length > 0
? `cmd[2] = vm.toString(abi.encode(${inputVarList
.map((v) => v.split(" ")[1])
.join(", ")}));`
: ""
}
${
allOutputVars.length > 0 || hasConsoleLog
? "bytes memory solx_decoded = vm.ffi(cmd);"
: "vm.ffi(cmd);"
}
${
allOutputVars.length > 0 || hasConsoleLog
? `
(${newOutputVars
.map((v) => {
const [type, name] = v.split(" ");
return `${addMemoryKeyword(type)} ${name}`;
})
.join(", ")}${
existingOutputVars.length > 0
? (newOutputVars.length > 0 ? ", " : "") +
existingOutputVars
.map((v, i) => {
const [type, name] = v.split(" ");
return `${addMemoryKeyword(type)} solx_temp_${i}`;
})
.join(", ")
: ""
}${allOutputVars.length > 0 && hasConsoleLog ? ", " : ""}${
hasConsoleLog ? "string memory solx_logs" : ""
}) = abi.decode(solx_decoded, (${allOutputVars
.map((v) => v.split(" ")[0])
.join(", ")}${hasConsoleLog ? ", string" : ""}));
${existingOutputVars
.map((v, i) => `${v.split(" ")[1]} = solx_temp_${i};`)
.join("\n ")}`
: ""
}
${hasConsoleLog ? "console.log(solx_logs);" : ""}
`
);

let typescript = `
import { ethers } from "ethers";
${
hasConsoleLog
? `
let solx_logs = "";
let isFirstLog = true;
const originalConsoleLog = console.log;
console.log = (...args) => {
if (isFirstLog) {
solx_logs += args.join(' ');
isFirstLog = false;
} else {
solx_logs += "\\n " + args.join(' ');
}
};
`
: ""
}
const inputData = Bun.argv[2];
${
inputVarList.length > 0
? `
if (!inputData) {
console.error("No input data provided");
process.exit(1);
}
`
: ""
}
async function main() {
try {
const abi = new ethers.AbiCoder();
${
inputVarList.length > 0
? `
// Decode input based on the types specified in the Solidity FFI call
let [${inputVarList
.map((v) => v.split(" ")[1])
.join(", ")}] = abi.decode([${inputVarList
.map((v) => `"${v.split(" ")[0]}"`)
.join(", ")}], inputData);
`
: ""
}
// User's TypeScript code
${tsCode}
${
hasConsoleLog
? `
// Restore original console.log
console.log = originalConsoleLog;
`
: ""
}
${
allOutputVars.length > 0 || hasConsoleLog
? `
// Encode output including logs and all input variables
const encodedOutput = abi.encode(
[...${JSON.stringify(allOutputVars.map((v) => v.split(" ")[0]))}${
hasConsoleLog ? ', "string"' : ""
}],
[${allOutputVars.map((v) => v.split(" ")[1]).join(", ")}${
hasConsoleLog ? ", solx_logs" : ""
}]
);
console.log(encodedOutput);
`
: ""
}
} catch (error) {
console.error("An error occurred:", error);
process.exit(1);
}
}
main();
`;

return { solidity, typescript };
}

function addMemoryKeyword(type: string): string {
// Always add 'memory' for array types
if (type.includes("[]")) {
return `${type} memory`;
}

// For non-array types, add 'memory' if it's not a value type
if (
!/^(uint\d*|int\d*|bool|address|bytes([1-9]|[12][0-9]|3[0-2]))(\[\])?$/.test(
type
)
) {
return `${type} memory`;
}

return type;
}

// Main execution
const inputFilePath = path.join(process.cwd(), "test", "Example.solx");
processSolidityFile(inputFilePath);
4 changes: 0 additions & 4 deletions src/Contract.sol

This file was deleted.

10 changes: 0 additions & 10 deletions test/Contract.t.sol

This file was deleted.

22 changes: 22 additions & 0 deletions test/Example.solx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.26;

import "forge-std/Test.sol";

contract PikachuTest is Test {
// Let's catch Pikachu ⚡️ in .solx!

function test_catchPikachu() public {
string memory myPokemon;
uint256 randomness = vm.randomUint(0, 49);
// @typescript-start (string myPokemon, uint256 randomness)
console.log("Wow, I can write TypeScript inside Solidity!");
const response = await fetch("https://dummyapi.online/api/pokemon");
const json = await response.json();
myPokemon = json[randomness].pokemon;
const isPikachu: boolean = myPokemon === "Pikachu";
// @typescript-end (bool isPikachu)
console.log("I've caught", string.concat(myPokemon, isPikachu ? unicode" 🥹" : "."));
// Did you notice that isPikachu was not defined? 😱
}
}

0 comments on commit f8d8c80

Please sign in to comment.