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

Feature/example app #35

Closed
wants to merge 2 commits into from
Closed
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
3 changes: 3 additions & 0 deletions .cargo/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[env]
Copy link
Contributor

Choose a reason for hiding this comment

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

hi!
This file should be gitignore as it is environment specific.

AR = "/opt/homebrew/opt/llvm/bin/llvm-ar"
CC = "/opt/homebrew/opt/llvm/bin/clang"
2 changes: 0 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
/target
**/*.rs.bk
/.vscode
Copy link
Contributor

Choose a reason for hiding this comment

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

Same comment. .vscode and .cargo should not be part of the repo.

.idea
*.swp
.cargo
.npmrc

bin/
Expand Down
7 changes: 7 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
Copy link
Contributor

Choose a reason for hiding this comment

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

Same here.

"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"
}
27 changes: 25 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<img src="./static/bdk.png" width="220" />

<p>
<strong>The Bitcoin Dev Kit for Browsers and Node</strong>
<strong>The Bitcoin Dev Kit for Browsers, Node, and React Native</strong>
</p>

<p>
Expand Down Expand Up @@ -42,9 +42,32 @@ For a lightweight library providing stateless utility functions, see [`bitcoinjs
## Browser Usage

```sh
yarn add bdk
yarn add bitcoindevkit
```

## Notes on WASM Specific Considerations

> ⚠️ **Important:** 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. However, there are viable workarounds documented below. 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 using `wallet.take_staged()`. This will export the changeset for the updates to the wallet state, which must then be merged with current wallet state in JS (will depend on your persistence strategy). When you restart your app, you can feed your persisted wallet data to `wallet.load()` to recover the wallet state.

### 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. However, in the case of `wallet.apply_update()`, this is being handled behind the scenes. If it weren't you'd have to use `wallet.apply_update_at()` instead. But it's worth keeping this limitation in mind in case you do hit an error related to system time access limitations.

### 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 indicates you're trying to access OS level functionality through rust that isn't available in WASM.


## Development Environment

### Requirements
Expand Down
3 changes: 3 additions & 0 deletions examples/browser/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules

package-lock.json
1 change: 1 addition & 0 deletions examples/browser/.tool-versions
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
nodejs 20.9.0
27 changes: 27 additions & 0 deletions examples/browser/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# WASM App Example

## Server app to test a WASM package

`npm install`
`npm start`

open browser to http://localhost:8080/
open browser console to see scan rusults


## Build WASM package

By default the example will pull the bitcoindevkit package from npm.
However, if you want to pull from the local package (say for development) modify the dependency in the index.js file, and build:

From parent folder (the wasm-package):
`wasm-pack build --features esplora`

### Mac Users

Note: to build successfully on mac required installing llvm with homebrew (even though there's a default version) https://github.com/bitcoindevkit/bdk/issues/1671#issuecomment-2456858895
And properly pointing to it in which is being done in .cargo and .vscode folders

### Non-Mac Users

You may need to delete the .cargo and .vscode folders, our point them to the appropriate llvm version.
93 changes: 93 additions & 0 deletions examples/browser/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { Wallet, EsploraClient, ChangeSet } from 'bitcoindevkit';// pull from npm
// import { Wallet, EsploraClient, ChangeSet } from 'bdk';//pulling from local package

// 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
}
}

const externalDescriptor = "tr([12071a7c/86'/1'/0']tpubDCaLkqfh67Qr7ZuRrUNrCYQ54sMjHfsJ4yQSGb3aBr1yqt3yXpamRBUwnGSnyNnxQYu7rqeBiPfw3mjBcFNX4ky2vhjj9bDrGstkfUbLB9T/0/*)#z3x5097m";
const internalDescriptor = "tr([12071a7c/86'/1'/0']tpubDCaLkqfh67Qr7ZuRrUNrCYQ54sMjHfsJ4yQSGb3aBr1yqt3yXpamRBUwnGSnyNnxQYu7rqeBiPfw3mjBcFNX4ky2vhjj9bDrGstkfUbLB9T/1/*)#n9r4jswr";

async function run() {
let walletDataString = Store.load();
console.log("Wallet data:", walletDataString);

let wallet;
let client = new EsploraClient("https://mutinynet.com/api");
if (!walletDataString) {
console.log("Creating new wallet");
wallet = Wallet.create(
Copy link
Contributor

Choose a reason for hiding this comment

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

this if would constitute an example test showcasing create

"signet",
externalDescriptor,
internalDescriptor
);

console.log("Performing Full Scan...");
let full_scan_request = wallet.start_full_scan();
Copy link
Contributor

Choose a reason for hiding this comment

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

example to showcase full_scan using Esplora.

let update = await client.full_scan(full_scan_request, 1);
wallet.apply_update(update);

const stagedDataString = wallet.take_staged().to_json();
console.log("Staged:", stagedDataString);

Store.save(stagedDataString);
console.log("Wallet data saved to local storage");
walletDataString = stagedDataString;
} else {
console.log("Loading wallet");
let changeSet = ChangeSet.from_json(walletDataString);
Copy link
Contributor

Choose a reason for hiding this comment

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

this else would constitute an example test showcasing load

wallet = Wallet.load(
changeSet,
externalDescriptor,
internalDescriptor
);

console.log("Syncing...");
let sync_request = wallet.start_sync_with_revealed_spks();
let update = await client.sync(sync_request, 1);
Copy link
Contributor

Choose a reason for hiding this comment

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

example to showcase sync using Esplora.

wallet.apply_update(update);

const updateChangeSet = wallet.take_staged();
if (updateChangeSet) {
console.log("Update:", updateChangeSet.to_json());
let currentChangeSet = ChangeSet.from_json(walletDataString);
console.log("Current:", currentChangeSet.to_json());
currentChangeSet.merge(updateChangeSet);
console.log("Merged:", currentChangeSet.to_json());
Store.save(currentChangeSet.to_json());
}
}

// Test balance
console.log("Balance:", wallet.balance().confirmed.to_sat());

// Test address generation
console.log("New address:", wallet.reveal_next_address().address);


// handle merging
walletDataString = Store.load();
const updateChangeSet = wallet.take_staged();
console.log("Update:", updateChangeSet.to_json());
let currentChangeSet = ChangeSet.from_json(walletDataString);
console.log("Current:", currentChangeSet.to_json());
currentChangeSet.merge(updateChangeSet);
Copy link
Contributor

Choose a reason for hiding this comment

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

example of merge.

console.log("Merged:", currentChangeSet.to_json());
Store.save(currentChangeSet.to_json());
Copy link
Contributor

Choose a reason for hiding this comment

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

we can drop the Store IMO but that could be an example of storage, even though I don't see the need for it since the users will get a ChangeSet an can do with it what they like.

console.log("new address saved");
}

run().catch(console.error);

// to clear local storage:
// localStorage.removeItem("walletData");
23 changes: 23 additions & 0 deletions examples/browser/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "example-bdk-wasm",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "npx webpack serve --mode development",
"build": "npx webpack --mode production",
"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"
},
"dependencies": {
"bdk": "file:../../pkg",
"bitcoindevkit": "^0.1.3"
}
}
12 changes: 12 additions & 0 deletions examples/browser/public/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>BDK WASM Test</title>
</head>
<body>
<h1>BDK-WASM Test</h1>
<p>Open the console to see the output.</p>
<script src="bundle.js"></script>
</body>
</html>
20 changes: 20 additions & 0 deletions examples/browser/webpack.config.js
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,
},
};