diff --git a/bindings/cashu-js/Cargo.toml b/bindings/cashu-js/Cargo.toml index d73ee4a2..b0f8a96d 100644 --- a/bindings/cashu-js/Cargo.toml +++ b/bindings/cashu-js/Cargo.toml @@ -20,6 +20,9 @@ serde_json.workspace = true serde.workspace = true wasm-bindgen = { version = "0.2.87", features = ["serde-serialize"] } wasm-bindgen-futures = "0.4.37" +console_error_panic_hook = "0.1" + + [package.metadata.wasm-pack.profile.release] wasm-opt = true diff --git a/bindings/cashu-js/examples/amount.js b/bindings/cashu-js/examples/amount.js index 165a2071..57c1f189 100644 --- a/bindings/cashu-js/examples/amount.js +++ b/bindings/cashu-js/examples/amount.js @@ -1,5 +1,12 @@ -const Amount = require("../"); +const {Amount, loadWasmAsync, loadWasmSync } = require(".."); -let amount = Amount.fromSat(10); -console.log(amount) +function main() { + loadWasmSync(); + + let amount = Amount.fromSat(BigInt(10)); + + console.log(amount.toSat()) +} + +main(); \ No newline at end of file diff --git a/bindings/cashu-js/justfile b/bindings/cashu-js/justfile index fa614e35..b2741e5a 100644 --- a/bindings/cashu-js/justfile +++ b/bindings/cashu-js/justfile @@ -2,4 +2,4 @@ build: wasm-pack build pack: - wasm-pack pack + npm run package \ No newline at end of file diff --git a/bindings/cashu-js/package.json b/bindings/cashu-js/package.json index d2df5e22..9b70632b 100644 --- a/bindings/cashu-js/package.json +++ b/bindings/cashu-js/package.json @@ -37,5 +37,8 @@ "node": ">= 10" }, "scripts": { + "build": "WASM_PACK_ARGS=--release ./scripts/build.sh", + "build:dev": "WASM_PACK_ARGS=--dev ./scripts/build.sh", + "package": "npm run build && npm pack" } } diff --git a/bindings/cashu-js/scripts/LICENSE b/bindings/cashu-js/scripts/LICENSE new file mode 100644 index 00000000..2ff56442 --- /dev/null +++ b/bindings/cashu-js/scripts/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022-2023 Yuki Kishimoto + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/bindings/cashu-js/scripts/build.sh b/bindings/cashu-js/scripts/build.sh new file mode 100755 index 00000000..604950fe --- /dev/null +++ b/bindings/cashu-js/scripts/build.sh @@ -0,0 +1,35 @@ +#!/bin/bash +# +# Build the JavaScript modules +# +# This script is really a workaround for https://github.com/rustwasm/wasm-pack/issues/1074. +# +# Currently, the only reliable way to load WebAssembly in all the JS +# environments we want to target (web-via-webpack, web-via-browserify, jest) +# seems to be to pack the WASM into base64, and then unpack it and instantiate +# it at runtime. +# +# Hopefully one day, https://github.com/rustwasm/wasm-pack/issues/1074 will be +# fixed and this will be unnecessary. + +set -e + +cd $(dirname "$0")/.. + +WASM_BINDGEN_WEAKREF=1 wasm-pack build --target nodejs --scope rust-cashu --out-dir pkg "${WASM_PACK_ARGS[@]}" + +# Convert the Wasm into a JS file that exports the base64'ed Wasm. +echo "module.exports = \`$(base64 pkg/cashu_js_bg.wasm)\`;" > pkg/cashu_js_bg.wasm.js + +# In the JavaScript: +# 1. Strip out the lines that load the WASM, and our new epilogue. +# 2. Remove the imports of `TextDecoder` and `TextEncoder`. We rely on the global defaults. +{ + sed -e '/Text..coder.*= require(.util.)/d' \ + -e '/^const path = /,$d' pkg/cashu_js.js + cat scripts/epilogue.js +} > pkg/cashu_js.js.new +mv pkg/cashu_js.js.new pkg/cashu_js.js + +# also extend the typescript +cat scripts/epilogue.d.ts >> pkg/cashu_js.d.ts \ No newline at end of file diff --git a/bindings/cashu-js/scripts/epilogue.d.ts b/bindings/cashu-js/scripts/epilogue.d.ts new file mode 100644 index 00000000..db38bcde --- /dev/null +++ b/bindings/cashu-js/scripts/epilogue.d.ts @@ -0,0 +1,10 @@ +/** + * Load the WebAssembly module in the background, if it has not already been loaded. + * + * Returns a promise which will resolve once the other methods are ready. + * + * @returns {Promise} + */ + export function loadWasmAsync(): Promise; + + export function loadWasmSync(): void; \ No newline at end of file diff --git a/bindings/cashu-js/scripts/epilogue.js b/bindings/cashu-js/scripts/epilogue.js new file mode 100644 index 00000000..f588ecc6 --- /dev/null +++ b/bindings/cashu-js/scripts/epilogue.js @@ -0,0 +1,76 @@ +let inited = false; +module.exports.loadWasmSync = function () { + if (inited) { + return; + } + if (initPromise) { + throw new Error("Asynchronous initialisation already in progress: cannot initialise synchronously"); + } + const bytes = unbase64(require("./cashu_js_bg.wasm.js")); + const mod = new WebAssembly.Module(bytes); + const instance = new WebAssembly.Instance(mod, imports); + wasm = instance.exports; + wasm.__wbindgen_start(); + inited = true; +}; + +let initPromise = null; + +/** + * Load the WebAssembly module in the background, if it has not already been loaded. + * + * Returns a promise which will resolve once the other methods are ready. + * + * @returns {Promise} + */ +module.exports.loadWasmAsync = function () { + if (inited) { + return Promise.resolve(); + } + if (!initPromise) { + initPromise = Promise.resolve() + .then(() => require("./cashu_js_bg.wasm.js")) + .then((b64) => WebAssembly.instantiate(unbase64(b64), imports)) + .then((result) => { + wasm = result.instance.exports; + wasm.__wbindgen_start(); + inited = true; + }); + } + return initPromise; +}; + +const b64lookup = new Uint8Array([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 62, 0, 62, 0, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, + 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 0, 0, 0, 0, 63, 0, 26, 27, 28, 29, 30, 31, 32, + 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, +]); + +// base64 decoder, based on the code at https://developer.mozilla.org/en-US/docs/Glossary/Base64#solution_2_%E2%80%93_rewriting_atob_and_btoa_using_typedarrays_and_utf-8 +function unbase64(sBase64) { + const sB64Enc = sBase64.replace(/[^A-Za-z0-9+/]/g, ""); + const nInLen = sB64Enc.length; + const nOutLen = (nInLen * 3 + 1) >> 2; + const taBytes = new Uint8Array(nOutLen); + + let nMod3; + let nMod4; + let nUint24 = 0; + let nOutIdx = 0; + for (let nInIdx = 0; nInIdx < nInLen; nInIdx++) { + nMod4 = nInIdx & 3; + nUint24 |= b64lookup[sB64Enc.charCodeAt(nInIdx)] << (6 * (3 - nMod4)); + if (nMod4 === 3 || nInLen - nInIdx === 1) { + nMod3 = 0; + while (nMod3 < 3 && nOutIdx < nOutLen) { + taBytes[nOutIdx] = (nUint24 >>> ((16 >>> nMod3) & 24)) & 255; + nMod3++; + nOutIdx++; + } + nUint24 = 0; + } + } + + return taBytes; +} \ No newline at end of file diff --git a/bindings/cashu-js/src/lib.rs b/bindings/cashu-js/src/lib.rs index aa3c3f35..5c3a20d7 100644 --- a/bindings/cashu-js/src/lib.rs +++ b/bindings/cashu-js/src/lib.rs @@ -1,3 +1,12 @@ +use wasm_bindgen::prelude::*; + pub mod error; pub mod nuts; pub mod types; + +pub use types::JsAmount; + +#[wasm_bindgen(start)] +pub fn start() { + console_error_panic_hook::set_once(); +} diff --git a/bindings/cashu-js/src/types/amount.rs b/bindings/cashu-js/src/types/amount.rs index 44b3dda8..a518a872 100644 --- a/bindings/cashu-js/src/types/amount.rs +++ b/bindings/cashu-js/src/types/amount.rs @@ -26,9 +26,9 @@ impl From for JsAmount { #[wasm_bindgen(js_class = Amount)] impl JsAmount { #[wasm_bindgen(constructor)] - pub fn new(sats: u64) -> Self { + pub fn new(sats: u32) -> Self { Self { - inner: Amount::from_sat(sats), + inner: Amount::from_sat(sats as u64), } } diff --git a/bindings/cashu-js/tsconfig.json b/bindings/cashu-js/tsconfig.json index 84bc409f..6ccdd0cf 100644 --- a/bindings/cashu-js/tsconfig.json +++ b/bindings/cashu-js/tsconfig.json @@ -3,7 +3,7 @@ "strict": true }, "typedocOptions": { - "entryPoints": ["pkg/nostr_js.d.ts"], + "entryPoints": ["pkg/cashu_js.d.ts"], "readme": "README.md" } }