diff --git a/.github/workflows/wasm.yaml b/.github/workflows/wasm.yaml new file mode 100644 index 0000000..ad3026b --- /dev/null +++ b/.github/workflows/wasm.yaml @@ -0,0 +1,34 @@ +name: WASM + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +env: + CARGO_TERM_COLOR: always + RUSTFLAGS: "-Dwarnings" + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Requirements + run: | + sudo apt-get update && sudo apt-get install binaryen + curl -fsSL https://rustwasm.github.io/wasm-pack/installer/init.sh | sh + curl -fsSL https://deno.land/x/install/install.sh | sh + echo "${HOME}/.deno/bin" >> "${GITHUB_PATH}" + - name: Versions + run: | + wasm-opt --version + wasm-pack --version + deno --version + - name: Tests + run: | + cd wasm + make test diff --git a/Cargo.lock b/Cargo.lock index 5df2874..599acb2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -57,12 +57,24 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bumpalo" +version = "3.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" + [[package]] name = "cc" version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + [[package]] name = "cfg-if" version = "1.0.0" @@ -143,6 +155,19 @@ dependencies = [ "tiny-keccak", ] +[[package]] +name = "deadbeef-wasm" +version = "0.1.0" +dependencies = [ + "deadbeef-core", + "getrandom", + "hex", + "serde", + "serde-wasm-bindgen", + "wasm-bindgen", + "wee_alloc", +] + [[package]] name = "errno" version = "0.3.1" @@ -170,9 +195,11 @@ version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] @@ -231,6 +258,15 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "js-sys" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" +dependencies = [ + "wasm-bindgen", +] + [[package]] name = "libc" version = "0.2.147" @@ -243,6 +279,18 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" +[[package]] +name = "log" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" + +[[package]] +name = "memory_units" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8452105ba047068f40ff7093dd1d9da90898e63dd61736462e9cdda6a90ad3c3" + [[package]] name = "num_cpus" version = "1.15.0" @@ -327,6 +375,37 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "serde" +version = "1.0.164" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e8c8cf938e98f769bc164923b06dce91cea1751522f46f8466461af04c9027d" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde-wasm-bindgen" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3b143e2833c57ab9ad3ea280d21fd34e285a42837aeb0ee301f4f41890fa00e" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "serde_derive" +version = "1.0.164" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9735b638ccc51c28bf6914d90a2e9725b377144fc612c49a611fddd1b631d68" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "strsim" version = "0.10.0" @@ -371,6 +450,94 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasm-bindgen" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" +dependencies = [ + "cfg-if 1.0.0", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" + +[[package]] +name = "wee_alloc" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb3b5a6b2bb17cb6ad44a2e68a43e8d2722c997da10e928665c72ec6c0a0b8e" +dependencies = [ + "cfg-if 0.1.10", + "libc", + "memory_units", + "winapi", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-sys" version = "0.48.0" diff --git a/Cargo.toml b/Cargo.toml index cbd245a..ff7a41a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,4 +2,5 @@ members = [ "cli", "core", + "wasm", ] diff --git a/cli/LICENSE b/cli/LICENSE new file mode 120000 index 0000000..ea5b606 --- /dev/null +++ b/cli/LICENSE @@ -0,0 +1 @@ +../LICENSE \ No newline at end of file diff --git a/cli/src/main.rs b/cli/src/main.rs index 7ff83c1..eac3b54 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -95,11 +95,11 @@ fn main() { let threads = (0..num_cpus::get()) .map(|_| { thread::spawn({ - let safe = Safe::new(contracts.clone(), args.owners.clone(), args.threshold); + let mut safe = Safe::new(contracts.clone(), args.owners.clone(), args.threshold); let prefix = args.prefix.0.clone(); let result = sender.clone(); move || { - let safe = deadbeef_core::search(safe, &prefix); + deadbeef_core::search(&mut safe, &prefix); let _ = result.send(safe); } }) @@ -107,20 +107,21 @@ fn main() { .collect::>(); let safe = receiver.recv().expect("missing result"); + let transaction = safe.transaction(); if args.quiet { - println!("0x{}", hex::encode(&safe.calldata)); + println!("0x{}", hex::encode(&transaction.calldata)); } else { - println!("address: {}", safe.creation_address); - println!("factory: {}", safe.factory); - println!("singleton: {}", safe.singleton); - println!("fallback: {}", safe.fallback_handler); - println!("owners: {}", safe.owners[0]); - for owner in &safe.owners[1..] { + println!("address: {}", safe.creation_address()); + println!("factory: {}", contracts.proxy_factory); + println!("singleton: {}", contracts.singleton); + println!("fallback: {}", contracts.fallback_handler); + println!("owners: {}", args.owners[0]); + for owner in &args.owners[1..] { println!(" {}", owner); } - println!("threshold: {}", safe.threshold); - println!("calldata: 0x{}", hex::encode(&safe.calldata)); + println!("threshold: {}", args.threshold); + println!("calldata: 0x{}", hex::encode(&transaction.calldata)); } let _ = threads; diff --git a/core/LICENSE b/core/LICENSE new file mode 120000 index 0000000..ea5b606 --- /dev/null +++ b/core/LICENSE @@ -0,0 +1 @@ +../LICENSE \ No newline at end of file diff --git a/core/src/lib.rs b/core/src/lib.rs index 2ee4aa0..35abc2c 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -10,11 +10,10 @@ pub use self::{ pub use hex_literal::hex; use rand::{rngs::SmallRng, Rng as _, SeedableRng as _}; -/// For the specified Safe parameters -pub fn search(mut safe: Safe, prefix: &[u8]) -> Transaction { +/// Search for a vanity address with the specified Safe parameters and prefix. +pub fn search(safe: &mut Safe, prefix: &[u8]) { let mut rng = SmallRng::from_entropy(); while !safe.creation_address().0.starts_with(prefix) { safe.update_salt_nonce(|n| rng.fill(n)); } - safe.transaction() } diff --git a/core/src/safe.rs b/core/src/safe.rs index 3bb68a9..5bef0e5 100644 --- a/core/src/safe.rs +++ b/core/src/safe.rs @@ -30,18 +30,8 @@ pub struct Contracts { /// Safe deployment transaction information. #[derive(Clone, Debug, Eq, PartialEq)] pub struct Transaction { - /// The final address that the safe will end up on. - pub creation_address: Address, - /// The address of the proxy factory for deploying the Safe. - pub factory: Address, - /// The address of the Safe singleton contract. - pub singleton: Address, - /// The owners of the safe. - pub owners: Vec
, - /// The signature threshold to use with the safe. - pub threshold: usize, - /// The address of the fallback handler that the safe was initialized with. - pub fallback_handler: Address, + /// The `to` address for the Ethereum transaction. + pub to: Address, /// The calldata to send to the proxy factory to create this Safe. pub calldata: Vec, } @@ -93,17 +83,12 @@ impl Safe { } /// Returns the transaction information for the current safe deployment. - pub fn transaction(self) -> Transaction { + pub fn transaction(&self) -> Transaction { let calldata = self.contracts .proxy_calldata(&self.owners, self.threshold, self.salt_nonce()); Transaction { - creation_address: self.creation_address(), - factory: self.contracts.proxy_factory, - singleton: self.contracts.singleton, - owners: self.owners, - threshold: self.threshold, - fallback_handler: self.contracts.fallback_handler, + to: self.contracts.proxy_factory, calldata, } } @@ -243,36 +228,27 @@ mod tests { assert_eq!( safe.transaction(), Transaction { - creation_address: address!("cDa7814460beF6D0BF5dc1b34AB29605b36c3bC4"), - factory: address!("1111111111111111111111111111111111111111"), - singleton: address!("2222222222222222222222222222222222222222"), - owners: vec![ - address!("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), - address!("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"), - address!("cccccccccccccccccccccccccccccccccccccccc"), - ], - threshold: 2, - fallback_handler: address!("3333333333333333333333333333333333333333"), + to: address!("1111111111111111111111111111111111111111"), calldata: hex!( "1688f0b9 - 0000000000000000000000002222222222222222222222222222222222222222 - 0000000000000000000000000000000000000000000000000000000000000060 - eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee - 00000000000000000000000000000000000000000000000000000000000001a4 - b63e800d00000000000000000000000000000000000000000000000000000000 - 0000010000000000000000000000000000000000000000000000000000000000 - 0000000200000000000000000000000000000000000000000000000000000000 - 0000000000000000000000000000000000000000000000000000000000000000 - 0000018000000000000000000000000033333333333333333333333333333333 - 3333333300000000000000000000000000000000000000000000000000000000 - 0000000000000000000000000000000000000000000000000000000000000000 - 0000000000000000000000000000000000000000000000000000000000000000 - 0000000000000000000000000000000000000000000000000000000000000000 - 00000003000000000000000000000000aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa - aaaaaaaa000000000000000000000000bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb - bbbbbbbb000000000000000000000000cccccccccccccccccccccccccccccccc - cccccccc00000000000000000000000000000000000000000000000000000000 - 0000000000000000000000000000000000000000000000000000000000000000" + 0000000000000000000000002222222222222222222222222222222222222222 + 0000000000000000000000000000000000000000000000000000000000000060 + eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee + 00000000000000000000000000000000000000000000000000000000000001a4 + b63e800d00000000000000000000000000000000000000000000000000000000 + 0000010000000000000000000000000000000000000000000000000000000000 + 0000000200000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000018000000000000000000000000033333333333333333333333333333333 + 3333333300000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000 + 00000003000000000000000000000000aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + aaaaaaaa000000000000000000000000bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb + bbbbbbbb000000000000000000000000cccccccccccccccccccccccccccccccc + cccccccc00000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000000" ) .to_vec(), } diff --git a/wasm/.gitignore b/wasm/.gitignore new file mode 100644 index 0000000..e13fdbc --- /dev/null +++ b/wasm/.gitignore @@ -0,0 +1,4 @@ +/dist +/lib/pkg +/node_modules +package-lock.json diff --git a/wasm/Cargo.toml b/wasm/Cargo.toml new file mode 100644 index 0000000..29d6644 --- /dev/null +++ b/wasm/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "deadbeef-wasm" +version = "0.1.0" +edition = "2021" +publish = false +license = "GPL-3.0-or-later" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +deadbeef-core = { version = "0.1.0", path = "../core" } +getrandom = { version = "0.2", features = ["js"] } +hex = "0.4" +serde = { version = "1", features = ["derive"] } +serde-wasm-bindgen = "0.5" +wasm-bindgen = "0.2" +wee_alloc = "0.4" diff --git a/wasm/LICENSE b/wasm/LICENSE new file mode 120000 index 0000000..ea5b606 --- /dev/null +++ b/wasm/LICENSE @@ -0,0 +1 @@ +../LICENSE \ No newline at end of file diff --git a/wasm/Makefile b/wasm/Makefile new file mode 100644 index 0000000..f5c3798 --- /dev/null +++ b/wasm/Makefile @@ -0,0 +1,20 @@ +.PHONY: dist +dist: lib/pkg + deno run --allow-all bundle.ts + +.PHONY: lib/pkg +lib/pkg: + wasm-pack build --target web --release --no-pack \ + --out-name deadbeef --out-dir lib/pkg + +.PHONY: test +test: dist + deno test test/index.ts + +.PHONY: host +host: dist + python3 -m http.server + +.PHONY: clean +clean: + rm -rf lib/pkg diff --git a/wasm/README.md b/wasm/README.md new file mode 100644 index 0000000..c8a0d9f --- /dev/null +++ b/wasm/README.md @@ -0,0 +1,35 @@ +# WebAssembly `0xdeadbeef` + +The `deadbeef` tool packaged for the Web using WebAssembly. + +## Requirements + +- [`wasm-opt` (Binaryen)](https://github.com/WebAssembly/binaryen) +- [`wasm-pack`](https://rustwasm.github.io/wasm-pack/installer/) +- [`deno`](https://deno.land/manual/getting_started/installation) + +## Building + +As the build process is non-trivial, a `Makefile` is provided. +To build the WebAssembly package, just run: + +```sh +make +``` + +## Testing + +Easy-peasy-lemon-squeezy: + +```sh +make test +``` + +## Example + +An example web page that computes a Safe creation transaction for a single owner is provided. +To check it out, navigate to [`http://localhost:8000/example`](http://localhost:8000/example) while running: + +```sh +make host +``` diff --git a/wasm/bundle.ts b/wasm/bundle.ts new file mode 100644 index 0000000..b1d31e8 --- /dev/null +++ b/wasm/bundle.ts @@ -0,0 +1,40 @@ +/** + * This script bundles the `DeadbeefWorker` into a single file. This is done in + * a two step process: + * 1. The `worker.js` and imports are bundled into a file with the `deadbeef` + * WASM blob embedded. + * 2. The `index.js` is bundled with the `worker.js` JavaScript, so we can + * do `deadbeef` vanity Safe address searching with a single file. + */ + +// TODO(nlordell): Figure out why this doesn't work... +// import { build } from "https://deno.land/x/esbuild@v0.18.10/mod.js"; + +import { build } from "npm:esbuild@0.18.10"; + +await build({ + entryPoints: ["lib/worker.js"], + bundle: true, + // TODO(nlordell): Figure out why minifying doesn't work. + // minify: true, + outdir: "dist", + format: "esm", + loader: { + [".wasm"]: "binary", + }, +}); + +const workerSrc = await Deno.readTextFile("dist/worker.js"); +const indexSrc = await Deno.readTextFile("lib/index.js"); + +const bundle = ` +const workerSource = new Blob([${JSON.stringify(workerSrc)}]); +const workerUrl = URL.createObjectURL(workerSource); + +${ + indexSrc + .replace('new URL("./worker.js", import.meta.url)', "workerUrl") +} +`; + +await Deno.writeTextFile("dist/index.js", `${bundle.trim()}\n`); diff --git a/wasm/example/index.html b/wasm/example/index.html new file mode 100644 index 0000000..57abf0e --- /dev/null +++ b/wasm/example/index.html @@ -0,0 +1,80 @@ + + + + + 0xdeadbeef + + + +
+

+

+
+ + +
+
+ + +
+ + + +
+

+ +

+ Creation Address:
+ Salt Nonce:
+ To:
+ Calldata: +

+
+ + + + diff --git a/wasm/lib/index.d.ts b/wasm/lib/index.d.ts new file mode 100644 index 0000000..6c1ae17 --- /dev/null +++ b/wasm/lib/index.d.ts @@ -0,0 +1,41 @@ +export type Address = string; +export type Bytes = string; + +/** + * Safe parameters for searching for vanity addresses. + */ +export interface Safe { + proxyFactory: Address; + proxyInitCode: Bytes; + singleton: Address; + fallbackHandler: Address; + owners: Address[]; + threshold: number; +} + +/** + * Vanity Safe creation data. + */ +export interface Creation { + creationAddress: Address; + saltNonce: Bytes; + transaction: Transaction; +} + +/** + * Vanity Safe transaction data. + */ +export interface Transaction { + to: Address; + calldata: Bytes; +} + +/** + * A worker for searching for a vanity Safe address for the specified + * parameters and prefix. + */ +export declare class DeadbeefWorker { + constructor(safe: Safe, prefix: Bytes); + wait(): Promise; + cancel(err?: Error): void; +} diff --git a/wasm/lib/index.js b/wasm/lib/index.js new file mode 100644 index 0000000..2c17e8c --- /dev/null +++ b/wasm/lib/index.js @@ -0,0 +1,41 @@ +export class DeadbeefWorker { + #promise; + #terminate; + + constructor(safe, prefix) { + const worker = new Worker( + new URL("./worker.js", import.meta.url), + { type: "module" }, + ); + + this.#promise = new Promise((resolve, reject) => { + this.#terminate = (err) => { + worker.terminate(); + reject(err); + }; + + worker.addEventListener("message", (message) => { + this.#terminate = undefined; + + const { creation, err } = message.data ?? {}; + if (typeof creation === "object" && creation !== null) { + resolve(creation); + } else { + reject(err ?? new Error("unknown error")); + } + }); + }); + + worker.postMessage({ safe, prefix }); + } + + wait() { + return this.#promise; + } + + cancel(err) { + if (this.#terminate !== undefined) { + this.#terminate(err ?? new Error("cancelled")); + } + } +} diff --git a/wasm/lib/worker.js b/wasm/lib/worker.js new file mode 100644 index 0000000..21326cc --- /dev/null +++ b/wasm/lib/worker.js @@ -0,0 +1,15 @@ +import wasm from "./pkg/deadbeef_bg.wasm"; +import init, { search } from "./pkg/deadbeef.js"; + +self.onmessage = async (message) => { + const { safe, prefix } = message.data; + try { + await init(wasm); + const creation = search(safe, prefix); + self.postMessage({ creation }); + } catch (message) { + self.postMessage({ creation: null, err: new Error(message) }); + } finally { + self.close(); + } +}; diff --git a/wasm/package.json b/wasm/package.json new file mode 100644 index 0000000..dc07361 --- /dev/null +++ b/wasm/package.json @@ -0,0 +1,15 @@ +{ + "name": "deadbeef", + "version": "0.1.0", + "description": "compute vanity Safe addresses", + "keywords": ["wasm", "safe", "multisig", "vanity", "address"], + "homepage": "https://github.com/nlordell/deadbeef/tree/main/wasm", + "license": "GPL-3.0-or-later", + "files": ["dist/", "lib/"], + "main": "dist/index.js", + "browser": "dist/index.js", + "module": "lib/index.js", + "types": "lib/index.d.ts", + "repository": "github:nlordell/deadbeef", + "author": "Nicholas Rodrigues Lordello" +} diff --git a/wasm/src/lib.rs b/wasm/src/lib.rs new file mode 100644 index 0000000..d502d54 --- /dev/null +++ b/wasm/src/lib.rs @@ -0,0 +1,92 @@ +use deadbeef_core::{Contracts, Safe}; +use hex::FromHexError; +use std::error::Error; +use wasm_bindgen::prelude::*; +use wee_alloc::WeeAlloc; + +#[global_allocator] +static ALLOC: WeeAlloc = WeeAlloc::INIT; + +mod js { + use serde::{Deserialize, Serialize}; + + #[derive(Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct Safe { + pub proxy_factory: String, + pub proxy_init_code: String, + pub singleton: String, + pub fallback_handler: String, + pub owners: Vec, + pub threshold: usize, + } + + #[derive(Serialize)] + #[serde(rename_all = "camelCase")] + pub struct Creation { + pub creation_address: String, + pub salt_nonce: String, + pub transaction: Transaction, + } + + #[derive(Serialize)] + #[serde(rename_all = "camelCase")] + pub struct Transaction { + pub to: String, + pub calldata: String, + } +} + +#[wasm_bindgen] +pub fn search(safe: JsValue, prefix: &str) -> Result { + let result = inner(safe, prefix); + + // TODO(nlordell): Ideally, we would just return `Result` + // and be done with it. However, it looks like `wasm-pack`/`wasm-bindgen` is + // not marshalling the `JsError` type correctly, so work around it by using + // a `String` error and emitting an actual `Error` on the JS side. + result.map_err(|err| err.to_string()) +} + +fn inner(safe: JsValue, prefix: &str) -> Result> { + let safe = serde_wasm_bindgen::from_value::(safe)?; + + let contracts = Contracts { + proxy_factory: safe.proxy_factory.parse()?, + proxy_init_code: hex_decode(&safe.proxy_init_code)?, + singleton: safe.singleton.parse()?, + fallback_handler: safe.fallback_handler.parse()?, + }; + + let owners = safe + .owners + .iter() + .map(|owner| owner.parse()) + .collect::, _>>()?; + let threshold = safe.threshold; + let prefix = hex_decode(prefix)?; + + let mut safe = Safe::new(contracts, owners, threshold); + deadbeef_core::search(&mut safe, &prefix); + + let transaction = safe.transaction(); + + let creation = serde_wasm_bindgen::to_value(&js::Creation { + creation_address: safe.creation_address().to_string(), + salt_nonce: hex_encode(&safe.salt_nonce()), + transaction: js::Transaction { + to: transaction.to.to_string(), + calldata: hex_encode(&transaction.calldata), + }, + })?; + + Ok(creation) +} + +fn hex_decode(s: &str) -> Result, FromHexError> { + hex::decode(s.strip_prefix("0x").unwrap_or(s)) +} + +fn hex_encode(b: &[u8]) -> String { + format!("0x{}", hex::encode(b)) +} diff --git a/wasm/test/index.ts b/wasm/test/index.ts new file mode 100644 index 0000000..2aa7752 --- /dev/null +++ b/wasm/test/index.ts @@ -0,0 +1,58 @@ +// @deno-types="../lib/index.d.ts" +import { DeadbeefWorker } from "../dist/index.js"; + +const safe = { + proxyFactory: "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67", + proxyInitCode: + "0x608060405234801561001057600080fd5b506040516101e63803806101e68339818101604052602081101561003357600080fd5b8101908080519060200190929190505050600073ffffffffffffffffffffffffffffffffffffffff168173ffffffffffffffffffffffffffffffffffffffff1614156100ca576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260228152602001806101c46022913960400191505060405180910390fd5b806000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055505060ab806101196000396000f3fe608060405273ffffffffffffffffffffffffffffffffffffffff600054167fa619486e0000000000000000000000000000000000000000000000000000000060003514156050578060005260206000f35b3660008037600080366000845af43d6000803e60008114156070573d6000fd5b3d6000f3fea264697066735822122003d1488ee65e08fa41e58e888a9865554c535f2c77126a82cb4c0f917f31441364736f6c63430007060033496e76616c69642073696e676c65746f6e20616464726573732070726f7669646564", + singleton: "0x41675C099F32341bf84BFc5382aF534df5C7461a", + fallbackHandler: "0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99", + owners: [ + "0x1111111111111111111111111111111111111111", + "0x2222222222222222222222222222222222222222", + "0x3333333333333333333333333333333333333333", + ], + threshold: 2, +}; + +function assert(condition: boolean, message: string) { + if (!condition) { + throw new Error(message); + } +} + +function delay(duration: number): Promise { + return new Promise((resolve) => setTimeout(resolve, duration)); +} + +Deno.test("computes Safe creation", async () => { + const prefix = "0xabcd"; + + const worker = new DeadbeefWorker(safe, prefix); + const { creationAddress } = await worker.wait(); + + assert( + creationAddress.toLowerCase().startsWith(prefix), + "Safe creation address does not start with prefix", + ); +}); + +Deno.test("cancel resolves to error", async () => { + const longPrefix = "0x00112233445566778899aabbccddeeff"; + const worker = new DeadbeefWorker(safe, longPrefix); + + const creation = worker.wait(); + await Promise.race([creation, delay(100)]); + + worker.cancel(); + + let errored; + try { + await creation; + errored = false; + } catch { + errored = true; + } + + assert(errored, "Safe worker promise did not reject when cancelled"); +});