-
Notifications
You must be signed in to change notification settings - Fork 18
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
Feature/wasm example #85
Open
riverKanies
wants to merge
5
commits into
bitcoindevkit:master
Choose a base branch
from
riverKanies:feature/wasm-example
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
# WASM Example | ||
|
||
Because rust can compile to WASM, it is possible to use BDK in the browser. However, there are a few limitations to keep in mind which will be highlighted in this example. That being said, there are perfectly viable work-arounds for these limitations that should suffice for most use cases. | ||
|
||
!!! warning | ||
There are several limitations to using BDK in WASM. Basically any functionality that requires OS access is not directly available in WASM and must therefore be handled in JavaScript. Some key limitations include: | ||
|
||
- No access to the file system | ||
- No access to the system time | ||
- Network access is limited to http(s) | ||
|
||
## WASM Considerations Overview | ||
|
||
### No access to the file system | ||
With no direct access to the file system, persistence cannot be handled by BDK directly. Instead, an in memory wallet must be used in the WASM environment, and the data must be exported through a binding to the JavaScript environment to be persisted. | ||
|
||
### No access to the system time | ||
Any function that requires system time, such as any sort of timestamp, must access system time through a wasm binding to the JavaScript environment. This means some BDK functions that are commonly used in rust won't work in WASM and instead an alternate rust function that takes a timestamp as an argument must be used (I.E. instead of `.apply_update()` we must use `.apply_update_at()`). | ||
|
||
### Network access is limited to http(s) | ||
This effectively means that the blockchain client must be an Esplora instance. Both RPC and Electrum clients require sockets and will not work for BDK in a WASM environment out of the box. | ||
|
||
## Troubleshooting | ||
WASM errors can be quite cryptic, so it's important to understand the limitations of the WASM environment. One common error you might see while running a BDK function through a WASM binding in the browser is `unreachable`. This error likely will not point you to the actual BDK function that is causing the error. Instead you need to be able to assess whether you are calling a function that uses a rust feature that is unsupported in the WASM environment. For example, if you do a scan and then try to use `.apply_update()` you will get an `unreachable` error. This is because `.apply_update()` requires system time, which is not available in the WASM environment. Instead you need to use `.apply_update_at()` which takes an explicit timestamp as an argument (see below). | ||
|
||
## WASM App Example | ||
|
||
In this example we will cover basic BDK functionality in a WASM environment. We will show code snippets for both the rust and JavaScript necessary to create a custom WASM package, and we will highlight the key differences from the plain rust examples (due to WASM limitations). | ||
|
||
!!! info | ||
The WASM example code is split into two project folders: a rust project that uses wasm-pack to compile rust code to WASM files, and a JavaScript project that pulls the WASM project as a dependency. The JS project represents the web app and the rust project is used to generate an npm module. | ||
|
||
### Initializing a Wallet | ||
|
||
From JS running in our browser, first we need our descriptors: | ||
|
||
```javascript | ||
--8<-- "examples/wasm/js/index.js:descriptors" | ||
``` | ||
|
||
Then we can initialize the wallet, we'll use some conditional logic here to either 1) create a new wallet and perform a full scan, or 2) load a wallet from stored data and sync it to get recent updates. | ||
|
||
```javascript | ||
--8<-- "examples/wasm/js/index.js:wallet" | ||
``` | ||
|
||
#### Network Consideration | ||
Notice we are including blockchain client details in wallet initialization (Signet, and the esplora url). This is because we are forced to use esplora, so we may as well initialize the client at the same time as the wallet. | ||
|
||
Here is the relevant rust code: | ||
|
||
```rust | ||
--8<-- "examples/wasm/rust/src/lib.rs:wallet" | ||
``` | ||
|
||
The first time you load the page in your browser, you should see info in the console confirming that a new wallet was created and a full scan was performed. If you then reload the page you should see that the wallet was loaded from the previously saved data and a sync was performed instead of a full scan. | ||
|
||
#### System Time Consideration | ||
Notice we are using a JS binding to access system time with `js_sys::Date::now()`, then passing that timestamp to the `apply_update_at()` function, rather than attempting to use the `.apply_update()` function which would throw an error. | ||
|
||
#### Persistence Consideration | ||
Also notice we are using an in-memory wallet with `.create_wallet_no_persist()`. If you try to use persistence through file or database you will get an error becuase those features require OS access. Instead we have to create a binding to pass the wallet data to the JavaScript environment where we can handle persistence. We have a method to grab the new updates to the wallet data, and a method to merge new updates with existing data. With this simple approach to persistence we must always merge existing data with the updates unless there is no existing data (i.e. after new wallet creation). The rust side methods to extract the wallet data are: | ||
|
||
```rust | ||
--8<-- "examples/wasm/rust/src/lib.rs:store" | ||
``` | ||
|
||
Notice we're converting the wallet data to a JSON string so that it plays nicely with WASM; and on the JS side we'll save our data string with a minimal custom browser store: | ||
|
||
```javascript | ||
--8<-- "examples/wasm/js/index.js:store" | ||
``` | ||
|
||
This is just to show an example of how the wallet data can be persisted. We're using local storage here, but in practice a wallet app would generally use cloud storage of some sort since browser local storage tends to be temporary. | ||
|
||
### Balance and Addresses | ||
|
||
We can now get the balance of our wallet and generate a new address. Here is the JS code: | ||
|
||
```javascript | ||
--8<-- "examples/wasm/js/index.js:utils" | ||
``` | ||
|
||
Here is the rust code that gets called: | ||
|
||
```rust | ||
--8<-- "examples/wasm/rust/src/lib.rs:utils" | ||
``` | ||
|
||
Notice we call `take_merged()` and `Store.save()` after generating a new address so our wallet keeps track of generated addresses (so we don't re-use them). If you reload the browser you can see the generated address value updated along with the index. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
node_modules | ||
|
||
package-lock.json |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
nodejs 20.9.0 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
# WASM App Example | ||
|
||
## Server app to test a WASM package | ||
|
||
Works when next to (in same parent folder as) the `rust` counter-part (pulls from `../rust` as a custom local npm package). | ||
|
||
run with `npm start` | ||
open browser to http://localhost:8080/ | ||
open browser console to see scan rusults |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
import { WalletWrapper, greet } from '../rust/pkg'; | ||
|
||
// --8<-- [start:store] | ||
// simple string storage example | ||
const Store = { | ||
save: data => { | ||
if (!data) { | ||
console.log("No data to save"); | ||
return; | ||
} | ||
localStorage.setItem("walletData", data); // data is already a JSON string | ||
}, | ||
load: () => { | ||
return localStorage.getItem("walletData"); // return the JSON string directly | ||
} | ||
} | ||
// --8<-- [end:store] | ||
|
||
// --8<-- [start:descriptors] | ||
const externalDescriptor = "tr([12071a7c/86'/1'/0']tpubDCaLkqfh67Qr7ZuRrUNrCYQ54sMjHfsJ4yQSGb3aBr1yqt3yXpamRBUwnGSnyNnxQYu7rqeBiPfw3mjBcFNX4ky2vhjj9bDrGstkfUbLB9T/0/*)#z3x5097m"; | ||
const internalDescriptor = "tr([12071a7c/86'/1'/0']tpubDCaLkqfh67Qr7ZuRrUNrCYQ54sMjHfsJ4yQSGb3aBr1yqt3yXpamRBUwnGSnyNnxQYu7rqeBiPfw3mjBcFNX4ky2vhjj9bDrGstkfUbLB9T/1/*)#n9r4jswr"; | ||
// --8<-- [end:descriptors] | ||
|
||
async function run() { | ||
console.log(greet()); // Should print "Hello, bdk-wasm!" | ||
|
||
// --8<-- [start:wallet] | ||
let walletDataString = Store.load(); | ||
console.log("Wallet data:", walletDataString); | ||
|
||
let wallet; | ||
if (!walletDataString) { | ||
console.log("Creating new wallet"); | ||
wallet = new WalletWrapper( | ||
"signet", | ||
externalDescriptor, | ||
internalDescriptor, | ||
"https://mutinynet.com/api" | ||
); | ||
|
||
console.log("Performing Full Scan..."); | ||
await wallet.scan(2); | ||
|
||
const stagedDataString = wallet.take_staged(); | ||
console.log("Staged:", stagedDataString); | ||
|
||
Store.save(stagedDataString); | ||
console.log("Wallet data saved to local storage"); | ||
walletDataString = stagedDataString; | ||
} else { | ||
console.log("Loading wallet"); | ||
wallet = WalletWrapper.load( | ||
walletDataString, | ||
"https://mutinynet.com/api", | ||
externalDescriptor, | ||
internalDescriptor | ||
); | ||
|
||
console.log("Syncing..."); | ||
await wallet.sync(2); | ||
|
||
const stagedDataString = wallet.take_staged(); | ||
console.log("Staged:", stagedDataString); | ||
|
||
Store.save(stagedDataString); | ||
console.log("Wallet data saved to local storage"); | ||
} | ||
// --8<-- [end:wallet] | ||
|
||
// --8<-- [start:utils] | ||
// Test balance | ||
console.log("Balance:", wallet.balance()); | ||
|
||
// Test address generation | ||
console.log("New address:", wallet.reveal_next_address()); | ||
|
||
// handle changeset merge on rust side | ||
const mergedDataString = wallet.take_merged(walletDataString); | ||
|
||
console.log("Merged:", mergedDataString); | ||
|
||
Store.save(mergedDataString); | ||
console.log("new address saved"); | ||
// --8<-- [end:utils] | ||
} | ||
|
||
run().catch(console.error); | ||
|
||
// to clear local storage: | ||
// localStorage.removeItem("walletData"); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
{ | ||
"name": "example-bdk-wasm", | ||
"version": "1.0.0", | ||
"description": "", | ||
"main": "index.js", | ||
"scripts": { | ||
"start": "webpack serve", | ||
"test": "echo \"Error: no test specified\" && exit 1" | ||
}, | ||
"keywords": [], | ||
"author": "", | ||
"license": "ISC", | ||
"devDependencies": { | ||
"webpack": "^5.96.1", | ||
"webpack-cli": "^5.1.4", | ||
"webpack-dev-server": "^5.1.0" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
<!DOCTYPE html> | ||
<html> | ||
<head> | ||
<meta charset="utf-8"> | ||
<title>BDK WASM Test</title> | ||
</head> | ||
<body> | ||
<script src="bundle.js"></script> | ||
</body> | ||
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
const path = require('path'); | ||
|
||
module.exports = { | ||
entry: './index.js', | ||
output: { | ||
path: path.resolve(__dirname, 'public'), | ||
filename: 'bundle.js', | ||
}, | ||
mode: 'development', | ||
experiments: { | ||
asyncWebAssembly: true, | ||
}, | ||
devServer: { | ||
static: { | ||
directory: path.join(__dirname, 'public'), | ||
}, | ||
compress: true, | ||
port: 8080, | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
install: | ||
- appveyor-retry appveyor DownloadFile https://win.rustup.rs/ -FileName rustup-init.exe | ||
- if not defined RUSTFLAGS rustup-init.exe -y --default-host x86_64-pc-windows-msvc --default-toolchain nightly | ||
- set PATH=%PATH%;C:\Users\appveyor\.cargo\bin | ||
- rustc -V | ||
- cargo -V | ||
|
||
build: false | ||
|
||
test_script: | ||
- cargo test --locked |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
[env] | ||
AR = "/opt/homebrew/opt/llvm/bin/llvm-ar" | ||
CC = "/opt/homebrew/opt/llvm/bin/clang" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
/target | ||
**/*.rs.bk | ||
Cargo.lock | ||
bin/ | ||
pkg/ | ||
wasm-pack.log | ||
tmp/ | ||
|
||
.env |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
language: rust | ||
sudo: false | ||
|
||
cache: cargo | ||
|
||
matrix: | ||
include: | ||
|
||
# Builds with wasm-pack. | ||
- rust: beta | ||
env: RUST_BACKTRACE=1 | ||
addons: | ||
firefox: latest | ||
chrome: stable | ||
before_script: | ||
- (test -x $HOME/.cargo/bin/cargo-install-update || cargo install cargo-update) | ||
- (test -x $HOME/.cargo/bin/cargo-generate || cargo install --vers "^0.2" cargo-generate) | ||
- cargo install-update -a | ||
- curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh -s -- -f | ||
script: | ||
- cargo generate --git . --name testing | ||
# Having a broken Cargo.toml (in that it has curlies in fields) anywhere | ||
# in any of our parent dirs is problematic. | ||
- mv Cargo.toml Cargo.toml.tmpl | ||
- cd testing | ||
- wasm-pack build | ||
- wasm-pack test --chrome --firefox --headless | ||
|
||
# Builds on nightly. | ||
- rust: nightly | ||
env: RUST_BACKTRACE=1 | ||
before_script: | ||
- (test -x $HOME/.cargo/bin/cargo-install-update || cargo install cargo-update) | ||
- (test -x $HOME/.cargo/bin/cargo-generate || cargo install --vers "^0.2" cargo-generate) | ||
- cargo install-update -a | ||
- rustup target add wasm32-unknown-unknown | ||
script: | ||
- cargo generate --git . --name testing | ||
- mv Cargo.toml Cargo.toml.tmpl | ||
- cd testing | ||
- cargo check | ||
- cargo check --target wasm32-unknown-unknown | ||
- cargo check --no-default-features | ||
- cargo check --target wasm32-unknown-unknown --no-default-features | ||
- cargo check --no-default-features --features console_error_panic_hook | ||
- cargo check --target wasm32-unknown-unknown --no-default-features --features console_error_panic_hook | ||
- cargo check --no-default-features --features "console_error_panic_hook wee_alloc" | ||
- cargo check --target wasm32-unknown-unknown --no-default-features --features "console_error_panic_hook wee_alloc" | ||
|
||
# Builds on beta. | ||
- rust: beta | ||
env: RUST_BACKTRACE=1 | ||
before_script: | ||
- (test -x $HOME/.cargo/bin/cargo-install-update || cargo install cargo-update) | ||
- (test -x $HOME/.cargo/bin/cargo-generate || cargo install --vers "^0.2" cargo-generate) | ||
- cargo install-update -a | ||
- rustup target add wasm32-unknown-unknown | ||
script: | ||
- cargo generate --git . --name testing | ||
- mv Cargo.toml Cargo.toml.tmpl | ||
- cd testing | ||
- cargo check | ||
- cargo check --target wasm32-unknown-unknown | ||
- cargo check --no-default-features | ||
- cargo check --target wasm32-unknown-unknown --no-default-features | ||
- cargo check --no-default-features --features console_error_panic_hook | ||
- cargo check --target wasm32-unknown-unknown --no-default-features --features console_error_panic_hook | ||
# Note: no enabling the `wee_alloc` feature here because it requires | ||
# nightly for now. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
{ | ||
"rust-analyzer.server.extraEnv": { | ||
"AR": "/opt/homebrew/opt/llvm/bin/llvm-ar", | ||
"CC": "/opt/homebrew/opt/llvm/bin/clang" | ||
}, | ||
"rust-analyzer.cargo.target": "wasm32-unknown-unknown" | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@notmandatory I understand very well this limitation for Electrum now but doesn't the Bitcoin node's RPC allow for HTTP? This works fine when I test with my Polar network, so why isn't the client working on the browser ?