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

Add all resolc dependencies to resolc_packed.js file #176

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
Open
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
1 change: 1 addition & 0 deletions .github/workflows/build-revive-wasm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ jobs:
path: |
${{ env.REVIVE_WASM_INSTALL_DIR }}/resolc.js
Copy link
Member

Choose a reason for hiding this comment

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

Do we still need to release it? Does it have any worth over resolc_packed.js? How would you use it?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

resolc.js makes sense for use with Node.js, where it is not dynamically downloaded, for example in js-revive

${{ env.REVIVE_WASM_INSTALL_DIR }}/resolc.wasm
${{ env.REVIVE_WASM_INSTALL_DIR }}/resolc_packed.js
Copy link
Member

Choose a reason for hiding this comment

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

Does the suffix packed make sense here? Given that this is not really packed but loads the wasm file (i.e not containing it).

Copy link
Collaborator Author

@smiasojed smiasojed Feb 3, 2025

Choose a reason for hiding this comment

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

It does not make sense. I am not sure if I will merge it—perhaps I should find a better solution.
Such a solution has a few issues:

  1. We need to store the WASM file on a public server with CSP enabled - it will not work with GH pages.
  2. During the release process, we would need to upload it to such a server and properly set RESOLC_WASM_URI = "http://127.0.0.1:8080/resolc.wasm, which is embedded in resolc.js.

retention-days: 1

test-revive-wasm:
Expand Down
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ install-npm:

install-wasm: install-npm
cargo build --target wasm32-unknown-emscripten -p revive-solidity --release --no-default-features
npm run build:package

install-llvm-builder:
cargo install --path crates/llvm-builder
Expand Down
53 changes: 53 additions & 0 deletions js/build.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
const fs = require("fs");
const path = require("path");
const { minify } = require("terser");

const SOLJSON_URI =
"https://binaries.soliditylang.org/wasm/soljson-v0.8.28+commit.7893614a.js";
Comment on lines +5 to +6
Copy link
Member

Choose a reason for hiding this comment

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

Are we planning to this dynamically inside remix in the future?

I think this should be done client side. In remix , the user can select the solc version there. Remix will download resolc and bundle it together. This way we can support all solc versions (since resolc is always compatible with any solc, the resolc version can be hardcoded to whatever is currently compatible with the node).

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

How it works in Remix: Remix lists the compiler files and loads the selected soljson-0.8.28+commit.xxxx. The file must follow this format because the soljson-0.8.28 part is used internally, for example, in pragma checking.

Steps to bundle it on the Remix side:
Modify the compiler tab to browse two lists of compilers.
Store this information in Remix's internal objects.
Update the web worker logic and API to accept two compilers.
Modify the non worker compilation flow in remix.
Modify the test logic, as it also expects a single compiler.

This change will affect many files in Remix. I wanted to implement it, but first, I need to run Remix's unit and E2E tests. A side effect of this change is that any future Remix updates will take more time to integrate.

I think I could remove soljson dep from revive and add it during compiler list generation on backend

Copy link
Member

Choose a reason for hiding this comment

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

We have two options here:

  1. Implement this properly in remix and don't bundle anything together.
  2. Bundle resolc + solc. The solc version bundled is the latest (supported) version. We declare support for other solc versions as out of scope. Honestly I don't think remix is being used for serious dApp development and deployments but rather as a playground for quick experimentation, demos and so forth.

The second option would also allow us to spend much less resources in optimizing the compiler for the in-browser environments. Honestly I think the second option would allow us to move forward fast and focus on more important issues.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We need to decide where to host the compilers if GitHub Pages does not support enabling CSP headers.
When creating the resolc_packed (bundle) release, we must upload the WASM file to a server that supports CSP.

Copy link
Collaborator Author

@smiasojed smiasojed Jan 30, 2025

Choose a reason for hiding this comment

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

I will put them in the Remix repo. No one else will use them, so it's safe to keep them there.

const RESOLC_WASM_URI = "http://127.0.0.1:8080/resolc.wasm";
const RESOLC_WASM_TARGET_DIR = path.join(
__dirname,
"../target/wasm32-unknown-emscripten/release",
);
const RESOLC_JS = path.join(RESOLC_WASM_TARGET_DIR, "resolc.js");
const RESOLC_JS_PACKED = path.join(RESOLC_WASM_TARGET_DIR, "resolc_packed.js");

const resolcJs = fs.readFileSync(RESOLC_JS, "utf-8");

const packedJsContent = `
if (typeof importScripts === "function") {
importScripts("${SOLJSON_URI}");

var moduleArgs = {
wasmBinary: (function () {
var xhr = new XMLHttpRequest();
xhr.open("GET", "${RESOLC_WASM_URI}", false);
xhr.responseType = "arraybuffer";
xhr.send(null);
return new Uint8Array(xhr.response);
})(),
soljson: Module
};
} else {
console.log("Not a WebWorker, skipping Soljson and WASM loading.");
}

${resolcJs}

createRevive = createRevive.bind(null, moduleArgs);
`;

minify(packedJsContent)
.then((minifiedJs) => {
if (minifiedJs.error) {
console.error("Error during minification:", minifiedJs.error);
process.exit(1);
}

fs.writeFileSync(RESOLC_JS_PACKED, minifiedJs.code, "utf-8");
console.log(`Combined script written to ${RESOLC_JS_PACKED}`);
})
.catch((err) => {
console.error("Minification failed:", err);
process.exit(1);
});
114 changes: 70 additions & 44 deletions js/e2e/web.test.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
const { test, expect } = require('@playwright/test');
const fs = require('fs');
const path = require('path');
const { test, expect } = require("@playwright/test");
const fs = require("fs");
const path = require("path");

function loadFixture(fixture) {
const fixturePath = path.resolve(__dirname, `../fixtures/${fixture}`);
return JSON.parse(fs.readFileSync(fixturePath, 'utf-8'));
return JSON.parse(fs.readFileSync(fixturePath, "utf-8"));
}

async function loadTestPage(page) {
await page.goto("http://127.0.0.1:8080");
const outputElement = page.locator("#output");
await outputElement.waitFor({ state: "visible" });
await page.setContent("");
}

async function runWorker(page, input) {
return await page.evaluate((input) => {
return new Promise((resolve, reject) => {
const worker = new Worker('worker.js');
const worker = new Worker("worker.js");
worker.postMessage(JSON.stringify(input));

worker.onmessage = (event) => {
Expand All @@ -26,62 +33,81 @@ async function runWorker(page, input) {
}, input);
}

test('should successfully compile valid Solidity code in browser', async ({ page }) => {
await page.goto("http://127.0.0.1:8080");
await page.setContent("");
const standardInput = loadFixture('storage.json')
test("should successfully compile valid Solidity code in browser", async ({
page,
}) => {
await loadTestPage(page);
const standardInput = loadFixture("storage.json");
const result = await runWorker(page, standardInput);
expect(typeof result).toBe('string');

expect(typeof result).toBe("string");
let output = JSON.parse(result);
expect(output).toHaveProperty('contracts');
expect(output.contracts['fixtures/storage.sol']).toHaveProperty('Storage');
expect(output.contracts['fixtures/storage.sol'].Storage).toHaveProperty('abi');
expect(output.contracts['fixtures/storage.sol'].Storage).toHaveProperty('evm');
expect(output.contracts['fixtures/storage.sol'].Storage.evm).toHaveProperty('bytecode');
expect(output).toHaveProperty("contracts");
expect(output.contracts["fixtures/storage.sol"]).toHaveProperty("Storage");
expect(output.contracts["fixtures/storage.sol"].Storage).toHaveProperty(
"abi",
);
expect(output.contracts["fixtures/storage.sol"].Storage).toHaveProperty(
"evm",
);
expect(output.contracts["fixtures/storage.sol"].Storage.evm).toHaveProperty(
"bytecode",
);
});

test('should successfully compile large valid Solidity code in browser', async ({ page }) => {
await page.goto("http://127.0.0.1:8080");
await page.setContent("");
const standardInput = loadFixture('token.json')
test("should successfully compile large valid Solidity code in browser", async ({
page,
browserName,
}) => {
if (browserName === "firefox") {
// Skipping tests with large contracts on Firefox due to out-of-memory issues.
test.skip();
}
await loadTestPage(page);
const standardInput = loadFixture("token.json");
const result = await runWorker(page, standardInput);
expect(typeof result).toBe('string');

expect(typeof result).toBe("string");
let output = JSON.parse(result);
expect(output).toHaveProperty('contracts');
expect(output.contracts['fixtures/token.sol']).toHaveProperty('MyToken');
expect(output.contracts['fixtures/token.sol'].MyToken).toHaveProperty('abi');
expect(output.contracts['fixtures/token.sol'].MyToken).toHaveProperty('evm');
expect(output.contracts['fixtures/token.sol'].MyToken.evm).toHaveProperty('bytecode');
expect(output).toHaveProperty("contracts");
expect(output.contracts["fixtures/token.sol"]).toHaveProperty("MyToken");
expect(output.contracts["fixtures/token.sol"].MyToken).toHaveProperty("abi");
expect(output.contracts["fixtures/token.sol"].MyToken).toHaveProperty("evm");
expect(output.contracts["fixtures/token.sol"].MyToken.evm).toHaveProperty(
"bytecode",
);
});

test('should throw an error for invalid Solidity code in browser', async ({ page }) => {
await page.goto("http://127.0.0.1:8080");
await page.setContent("");
const standardInput = loadFixture('invalid_contract_content.json')
test("should throw an error for invalid Solidity code in browser", async ({
page,
}) => {
await loadTestPage(page);
const standardInput = loadFixture("invalid_contract_content.json");
const result = await runWorker(page, standardInput);

expect(typeof result).toBe('string');
expect(typeof result).toBe("string");
let output = JSON.parse(result);
expect(output).toHaveProperty('errors');
expect(output).toHaveProperty("errors");
expect(Array.isArray(output.errors)).toBeTruthy(); // Check if it's an array
expect(output.errors.length).toBeGreaterThan(0);
expect(output.errors[0]).toHaveProperty('type');
expect(output.errors[0].type).toContain('ParserError');
expect(output.errors[0]).toHaveProperty("type");
expect(output.errors[0].type).toContain("ParserError");
});

test('should return not found error for missing imports in browser', async ({page}) => {
await page.goto("http://127.0.0.1:8080");
await page.setContent("");
const standardInput = loadFixture('missing_import.json')
test("should return not found error for missing imports in browser", async ({
page,
}) => {
await loadTestPage(page);
const standardInput = loadFixture("missing_import.json");
const result = await runWorker(page, standardInput);
expect(typeof result).toBe('string');

expect(typeof result).toBe("string");
let output = JSON.parse(result);
expect(output).toHaveProperty('errors');
expect(output).toHaveProperty("errors");
expect(Array.isArray(output.errors)).toBeTruthy(); // Check if it's an array
expect(output.errors.length).toBeGreaterThan(0);
expect(output.errors[0]).toHaveProperty('message');
expect(output.errors[0].message).toContain('Source "nonexistent/console.sol" not found');
expect(output.errors[0]).toHaveProperty("message");
expect(output.errors[0].message).toContain(
'Source "nonexistent/console.sol" not found',
);
});
97 changes: 50 additions & 47 deletions js/embed/pre.js
Original file line number Diff line number Diff line change
@@ -1,54 +1,57 @@
var Module = {
stdinData: null,
stdinDataPosition: 0,
stdoutData: [],
stderrData: [],
Module.stdinData = null;
Module.stdinDataPosition = 0;
Module.stdoutData = [];
Module.stderrData = [];

// Function to read and return all collected stdout data as a string
readFromStdout: function() {
if (!this.stdoutData.length) return "";
const decoder = new TextDecoder('utf-8');
const data = decoder.decode(new Uint8Array(this.stdoutData));
this.stdoutData = [];
return data;
},
// Method to read all collected stdout data
Module.readFromStdout = function () {
if (!Module.stdoutData.length) return "";
const decoder = new TextDecoder("utf-8");
const data = decoder.decode(new Uint8Array(Module.stdoutData));
Module.stdoutData = [];
return data;
};

// Function to read and return all collected stderr data as a string
readFromStderr: function() {
if (!this.stderrData.length) return "";
const decoder = new TextDecoder('utf-8');
const data = decoder.decode(new Uint8Array(this.stderrData));
this.stderrData = [];
return data;
},
// Method to read all collected stderr data
Module.readFromStderr = function () {
if (!Module.stderrData.length) return "";
const decoder = new TextDecoder("utf-8");
const data = decoder.decode(new Uint8Array(Module.stderrData));
Module.stderrData = [];
return data;
};

// Function to set input data for stdin
writeToStdin: function(data) {
const encoder = new TextEncoder();
this.stdinData = encoder.encode(data);
this.stdinDataPosition = 0;
},
// Method to write data to stdin
Module.writeToStdin = function (data) {
const encoder = new TextEncoder();
Module.stdinData = encoder.encode(data);
Module.stdinDataPosition = 0;
};

// `preRun` is called before the program starts running
preRun: function() {
// Define a custom stdin function
function customStdin() {
if (!Module.stdinData || Module.stdinDataPosition >= Module.stdinData.length) {
return null; // End of input (EOF)
}
return Module.stdinData[Module.stdinDataPosition++];
}
// Override the `preRun` method to customize file system initialization
Module.preRun = Module.preRun || [];
Module.preRun.push(function () {
// Custom stdin function
function customStdin() {
if (
!Module.stdinData ||
Module.stdinDataPosition >= Module.stdinData.length
) {
return null; // End of input (EOF)
}
return Module.stdinData[Module.stdinDataPosition++];
}

// Define a custom stdout function
function customStdout(char) {
Module.stdoutData.push(char);
}
// Custom stdout function
function customStdout(char) {
Module.stdoutData.push(char);
}

// Define a custom stderr function
function customStderr(char) {
Module.stderrData.push(char);
}
// Custom stderr function
function customStderr(char) {
Module.stderrData.push(char);
}

FS.init(customStdin, customStdout, customStderr);
},
};
// Initialize the FS (File System) with custom handlers
FS.init(customStdin, customStdout, customStderr);
});
55 changes: 30 additions & 25 deletions js/embed/soljson_interface.js
Original file line number Diff line number Diff line change
@@ -1,29 +1,34 @@
mergeInto(LibraryManager.library, {
soljson_compile: function(inputPtr, inputLen) {
const inputJson = UTF8ToString(inputPtr, inputLen);
const output = Module.soljson.cwrap('solidity_compile', 'string', ['string'])(inputJson);
return stringToNewUTF8(output);
},
soljson_version: function() {
const version = Module.soljson.cwrap("solidity_version", "string", [])();
return stringToNewUTF8(version);
},
resolc_compile: function(inputPtr, inputLen) {
const inputJson = UTF8ToString(inputPtr, inputLen);
var revive = createRevive();
revive.writeToStdin(inputJson);
soljson_compile: function (inputPtr, inputLen) {
const inputJson = UTF8ToString(inputPtr, inputLen);
const output = Module.soljson.cwrap("solidity_compile", "string", [
"string",
])(inputJson);
return stringToNewUTF8(output);
},
soljson_version: function () {
const version = Module.soljson.cwrap("solidity_version", "string", [])();
return stringToNewUTF8(version);
},
resolc_compile: function (inputPtr, inputLen) {
const inputJson = UTF8ToString(inputPtr, inputLen);
var revive = createRevive();
revive.writeToStdin(inputJson);

// Call main on the new instance
const result = revive.callMain(['--recursive-process']);
// Call main on the new instance
const result = revive.callMain(["--recursive-process"]);

if (result) {
const stderrString = revive.readFromStderr();
const error = JSON.stringify({ type: 'error', message: stderrString || "Unknown error" });
return stringToNewUTF8(error);
} else {
const stdoutString = revive.readFromStdout();
const json = JSON.stringify({ type: 'success', data: stdoutString });
return stringToNewUTF8(json);
}
},
if (result) {
const stderrString = revive.readFromStderr();
const error = JSON.stringify({
type: "error",
message: stderrString || "Unknown error",
});
return stringToNewUTF8(error);
} else {
const stdoutString = revive.readFromStdout();
const json = JSON.stringify({ type: "success", data: stdoutString });
return stringToNewUTF8(json);
}
},
});
Loading
Loading