diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..ecd5b3a
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1 @@
+*.solx linguist-language=Solidity
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..d531853
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,5 @@
+{
+ "files.associations": {
+ "*.solx": "solidity"
+ }
+}
diff --git a/README.md b/README.md
index 2fa7622..6a64936 100644
--- a/README.md
+++ b/README.md
@@ -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** at the same time!
+
+
## 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
@@ -24,6 +43,43 @@ forge build
forge test
```
+**Experiment**
+
+> [!NOTE]
+> Try it out: [`test/Example.solx`](./test/Example.solx)
+
+
+
+Specify variables to sync between Solidity and TypeScript:
+
+```solidity
+uint256 a;
+// @typescript-start (uint256 a)
+a++;
+// @typescript-end ()
+assertEq(a, 1);
+```
+
+Specify variables to clone 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
diff --git a/bun.lockb b/bun.lockb
index f8cbfde..2757524 100755
Binary files a/bun.lockb and b/bun.lockb differ
diff --git a/demo.gif b/demo.gif
new file mode 100644
index 0000000..f30959d
Binary files /dev/null and b/demo.gif differ
diff --git a/foundry.toml b/foundry.toml
index 71624f9..d4284f5 100644
--- a/foundry.toml
+++ b/foundry.toml
@@ -1,5 +1,5 @@
[profile.default]
-src = "src"
+test = "out/solx"
out = "out"
libs = ["dependencies"]
verbosity = 2
diff --git a/package.json b/package.json
index f471dfe..b8b290f 100644
--- a/package.json
+++ b/package.json
@@ -1,5 +1,8 @@
{
"devDependencies": {
"@types/bun": "^1.1.6"
+ },
+ "dependencies": {
+ "ethers": "^6.13.2"
}
}
\ No newline at end of file
diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol
deleted file mode 100644
index 7d20403..0000000
--- a/script/Deploy.s.sol
+++ /dev/null
@@ -1,10 +0,0 @@
-// SPDX-License-Identifier: MIT OR Apache-2.0
-pragma solidity ^0.8.26;
-
-import "@forge-std/Script.sol";
-
-contract Delpoy is Script {
- function run() public {
- vm.broadcast(vm.promptSecretUint("Deployer private key"));
- }
-}
diff --git a/solx/hooks.sh b/solx/hooks.sh
new file mode 100644
index 0000000..9ab031e
--- /dev/null
+++ b/solx/hooks.sh
@@ -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
+}
diff --git a/solx/transpiler.ts b/solx/transpiler.ts
new file mode 100644
index 0000000..71156a9
--- /dev/null
+++ b/solx/transpiler.ts
@@ -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);
diff --git a/src/Contract.sol b/src/Contract.sol
deleted file mode 100644
index 26185e7..0000000
--- a/src/Contract.sol
+++ /dev/null
@@ -1,4 +0,0 @@
-// SPDX-License-Identifier: MIT OR Apache-2.0
-pragma solidity 0.8.26;
-
-contract Contract {}
diff --git a/test/Contract.t.sol b/test/Contract.t.sol
deleted file mode 100644
index 0419659..0000000
--- a/test/Contract.t.sol
+++ /dev/null
@@ -1,10 +0,0 @@
-// SPDX-License-Identifier: MIT OR Apache-2.0
-pragma solidity ^0.8.26;
-
-import "@forge-std/Test.sol";
-
-contract ContractTest is Test {
- function setUp() public {}
-
- function test() public {}
-}
diff --git a/test/Example.solx b/test/Example.solx
new file mode 100644
index 0000000..0de3ceb
--- /dev/null
+++ b/test/Example.solx
@@ -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? 😱
+ }
+}
\ No newline at end of file