Skip to content

Commit df1b88c

Browse files
committed
feat: use serde_json for serialization
1 parent 1ad389c commit df1b88c

File tree

4 files changed

+44
-53
lines changed

4 files changed

+44
-53
lines changed

docs/cookbook/wasm.md

+4-4
Original file line numberDiff line numberDiff line change
@@ -53,25 +53,25 @@ Here is the relevant rust code:
5353
--8<-- "examples/wasm/rust/src/lib.rs:wallet"
5454
```
5555

56-
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 reload the page you should see that the wallet was loaded from the previously saved data and a sync was performed instead of a scan.
56+
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.
5757

5858
#### System Time Consideration
5959
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.
6060

6161
#### Persistence Consideration
62-
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. The rust side methods to extract the wallet data are:
62+
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:
6363

6464
```rust
6565
--8<-- "examples/wasm/rust/src/lib.rs:store"
6666
```
6767

68-
And they are called from our minimal custom browser store:
68+
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:
6969

7070
```javascript
7171
--8<-- "examples/wasm/js/index.js:store"
7272
```
7373

74-
This is just to show an example of how the wallet data can be persisted. We're using local storage here and stringifying the data (which takes some special logic to handle the Map values in the data). In practice a wallet app should probably use cloud storage of some sort since browser local storage is relatively temporary.
74+
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.
7575

7676
### Balance and Addresses
7777

examples/wasm/js/index.js

+20-35
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,17 @@
11
import { WalletWrapper, greet } from '../rust/pkg';
22

33
// --8<-- [start:store]
4-
// needed to handle js Map serialization
4+
// simple string storage example
55
const Store = {
66
save: data => {
77
if (!data) {
88
console.log("No data to save");
99
return;
1010
}
11-
const serializedStaged = JSON.stringify(data, (key, value) => {
12-
if (value instanceof Map) {
13-
return {
14-
dataType: 'Map',
15-
value: Array.from(value.entries())
16-
};
17-
}
18-
return value;
19-
});
20-
localStorage.setItem("walletData", serializedStaged);
11+
localStorage.setItem("walletData", data); // data is already a JSON string
2112
},
2213
load: () => {
23-
const walletDataString = localStorage.getItem("walletData");
24-
// Convert serialized Maps back to Map objects when loading
25-
const walletData = JSON.parse(walletDataString, (key, value) => {
26-
if (value?.dataType === 'Map') {
27-
return new Map(value.value);
28-
}
29-
return value;
30-
});
31-
return walletData;
14+
return localStorage.getItem("walletData"); // return the JSON string directly
3215
}
3316
}
3417
// --8<-- [end:store]
@@ -42,11 +25,11 @@ async function run() {
4225
console.log(greet()); // Should print "Hello, bdk-wasm!"
4326

4427
// --8<-- [start:wallet]
45-
let walletData = Store.load();
46-
console.log("Wallet data:", walletData);
28+
let walletDataString = Store.load();
29+
console.log("Wallet data:", walletDataString);
4730

4831
let wallet;
49-
if (!walletData) {
32+
if (!walletDataString) {
5033
console.log("Creating new wallet");
5134
wallet = new WalletWrapper(
5235
"signet",
@@ -58,16 +41,16 @@ async function run() {
5841
console.log("Performing Full Scan...");
5942
await wallet.scan(2);
6043

61-
const stagedData = wallet.take_staged();
62-
console.log("Staged:", stagedData);
44+
const stagedDataString = wallet.take_staged();
45+
console.log("Staged:", stagedDataString);
6346

64-
Store.save(stagedData);
47+
Store.save(stagedDataString);
6548
console.log("Wallet data saved to local storage");
66-
walletData = stagedData;
49+
walletDataString = stagedDataString;
6750
} else {
6851
console.log("Loading wallet");
6952
wallet = WalletWrapper.load(
70-
walletData,
53+
walletDataString,
7154
"https://mutinynet.com/api",
7255
externalDescriptor,
7356
internalDescriptor
@@ -76,10 +59,10 @@ async function run() {
7659
console.log("Syncing...");
7760
await wallet.sync(2);
7861

79-
const stagedData = wallet.take_staged();
80-
console.log("Staged:", stagedData);
62+
const stagedDataString = wallet.take_staged();
63+
console.log("Staged:", stagedDataString);
8164

82-
Store.save(stagedData);
65+
Store.save(stagedDataString);
8366
console.log("Wallet data saved to local storage");
8467
}
8568
// --8<-- [end:wallet]
@@ -89,12 +72,14 @@ async function run() {
8972
console.log("Balance:", wallet.balance());
9073

9174
// Test address generation
92-
console.log("New address:", wallet.get_new_address());
75+
console.log("New address:", wallet.reveal_next_address());
9376

94-
const mergedData = wallet.take_merged(walletData);
95-
console.log("Merged:", mergedData);
77+
// handle changeset merge on rust side
78+
const mergedDataString = wallet.take_merged(walletDataString);
79+
80+
console.log("Merged:", mergedDataString);
9681

97-
Store.save(mergedData);
82+
Store.save(mergedDataString);
9883
console.log("new address saved");
9984
// --8<-- [end:utils]
10085
}

examples/wasm/rust/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ wasm-bindgen-futures = "0.4.45"
1616
js-sys = "0.3.72"
1717
web-sys = { version = "0.3.72", features = ["console"] }
1818
serde-wasm-bindgen = "0.6.5"
19+
serde_json = "1.0"
1920

2021
# The `console_error_panic_hook` crate provides better debugging of panics by
2122
# logging them with `console.error`. This is great for development, but requires

examples/wasm/rust/src/lib.rs

+19-14
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ use bdk_esplora::{
55
use bdk_wallet::{chain::Merge, bitcoin::Network, ChangeSet, KeychainKind, Wallet};
66
use js_sys::Date;
77
use wasm_bindgen::prelude::*;
8-
use serde_wasm_bindgen::{from_value, to_value};
8+
// use serde_wasm_bindgen::{from_value, to_value};
9+
use serde_json::{self, Value};
910

1011
const PARALLEL_REQUESTS: usize = 1;
1112

@@ -71,15 +72,16 @@ impl WalletWrapper {
7172
})
7273
}
7374

74-
pub fn load(changeset: JsValue, url: &str, external_descriptor: &str, internal_descriptor: &str) -> JsResult<WalletWrapper> {
75-
let changeset = from_value(changeset)?;
75+
pub fn load(changeset_str: &str, url: &str, external_descriptor: &str, internal_descriptor: &str) -> JsResult<WalletWrapper> {
76+
let changeset_value: Value = serde_json::from_str(changeset_str)?;
77+
let changeset: ChangeSet = serde_json::from_value(changeset_value)?;
78+
7679
let wallet_opt = Wallet::load()
7780
.descriptor(KeychainKind::External, Some(external_descriptor.to_string()))
7881
.descriptor(KeychainKind::Internal, Some(internal_descriptor.to_string()))
7982
.extract_keys()
8083
.load_wallet_no_persist(changeset)?;
8184

82-
8385
let wallet = match wallet_opt {
8486
Some(wallet) => wallet,
8587
None => return Err(JsError::new("Failed to load wallet, check the changeset")),
@@ -126,7 +128,7 @@ impl WalletWrapper {
126128
balance.total().to_sat()
127129
}
128130

129-
pub fn get_new_address(&mut self) -> String {
131+
pub fn reveal_next_address(&mut self) -> String {
130132
let address = self
131133
.wallet
132134
.reveal_next_address(KeychainKind::External);
@@ -144,24 +146,27 @@ impl WalletWrapper {
144146
}
145147

146148
// --8<-- [start:store]
147-
pub fn take_staged(&mut self) -> JsResult<JsValue> {
149+
pub fn take_staged(&mut self) -> JsResult<String> {
148150
match self.wallet.take_staged() {
149151
Some(changeset) => {
150-
Ok(to_value(&changeset)?)
152+
let value = serde_json::to_value(&changeset)?;
153+
Ok(serde_json::to_string(&value)?)
151154
}
152-
None => Ok(JsValue::null()),
155+
None => Ok("null".to_string()),
153156
}
154157
}
155158

156-
pub fn take_merged(&mut self, previous: JsValue) -> JsResult<JsValue> {
159+
pub fn take_merged(&mut self, previous: String) -> JsResult<String> {
157160
match self.wallet.take_staged() {
158161
Some(curr_changeset) => {
159-
let mut changeset: ChangeSet = from_value(previous)?;
160-
changeset.merge(curr_changeset);
161-
Ok(to_value(&changeset)?)
162+
let previous_value: Value = serde_json::from_str(&previous)?;
163+
let mut previous_changeset: ChangeSet = serde_json::from_value(previous_value)?;
164+
previous_changeset.merge(curr_changeset);
165+
let final_value = serde_json::to_value(&previous_changeset)?;
166+
Ok(serde_json::to_string(&final_value)?)
162167
}
163-
None => Ok(JsValue::null()),
168+
None => Ok("null".to_string()),
164169
}
165170
}
166171
// --8<-- [end:store]
167-
}
172+
}

0 commit comments

Comments
 (0)