diff --git a/bindings/cashu-sdk-js/.gitignore b/bindings/cashu-sdk-js/.gitignore new file mode 100644 index 00000000..bed4e246 --- /dev/null +++ b/bindings/cashu-sdk-js/.gitignore @@ -0,0 +1,7 @@ +/target +**/*.rs.bk +Cargo.lock +bin/ +pkg/ +wasm-pack.log +*.tgz diff --git a/bindings/cashu-sdk-js/examples/amount.js b/bindings/cashu-sdk-js/examples/amount.js new file mode 100644 index 00000000..57c1f189 --- /dev/null +++ b/bindings/cashu-sdk-js/examples/amount.js @@ -0,0 +1,12 @@ +const {Amount, loadWasmAsync, loadWasmSync } = require(".."); + + +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-sdk-js/examples/wallet.js b/bindings/cashu-sdk-js/examples/wallet.js new file mode 100644 index 00000000..3d3e9642 --- /dev/null +++ b/bindings/cashu-sdk-js/examples/wallet.js @@ -0,0 +1,19 @@ +const {Amount, loadWasmAsync, Wallet, Client } = require(".."); + + +async function main() { + await loadWasmAsync(); + + let client = new Client("https://mutinynet-cashu.thesimpekid.space"); + + let keys = await client.getKeys(); + + let wallet = new Wallet(client, keys); + + let amount = Amount.fromSat(BigInt(10)); + let pr = await wallet.requestMint(amount); + + console.log(pr); +} + +main(); \ No newline at end of file diff --git a/bindings/cashu-sdk-js/justfile b/bindings/cashu-sdk-js/justfile index 119e1e58..b2741e5a 100644 --- a/bindings/cashu-sdk-js/justfile +++ b/bindings/cashu-sdk-js/justfile @@ -2,5 +2,4 @@ build: wasm-pack build pack: - wasm-pack pack - + npm run package \ No newline at end of file diff --git a/bindings/cashu-sdk-js/package.json b/bindings/cashu-sdk-js/package.json new file mode 100644 index 00000000..a371d7f9 --- /dev/null +++ b/bindings/cashu-sdk-js/package.json @@ -0,0 +1,44 @@ +{ + "name": "@rust-cashu/cashu-sdk", + "version": "0.0.1", + "description": "Cashu protocol implementation, for JavaScript", + "keywords": [ + "cashu", + "protocol", + "rust", + "bindings" + ], + "license": "BSD-3-Clause", + "homepage": "https://github.com/thesimplekid/cashu-crab", + "repository": { + "type": "git", + "url": "git+https://github.com/thesimplekid/cashu-crab" + }, + "bugs": { + "url": "https://github.com/thesimplekid/cashu-crab/issues" + }, + "author": { + "name": "thesimplekid", + "email": "tsk@thesimplekid.com", + "url": "https://github.com/thesimplekid" + }, + "main": "pkg/cashu_sdk_js.js", + "types": "pkg/cashu_sdk_js.d.ts", + "files": [ + "pkg/cashu_sdk_js_bg.wasm.js", + "pkg/cashu_sdk_js_bg.wasm.d.ts", + "pkg/cashu_sdk_js.js", + "pkg/cashu_sdk_js.d.ts" + ], + "devDependencies": { + "wasm-pack": "^0.10.2" + }, + "engines": { + "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-sdk-js/scripts/LICENSE b/bindings/cashu-sdk-js/scripts/LICENSE new file mode 100644 index 00000000..2ff56442 --- /dev/null +++ b/bindings/cashu-sdk-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-sdk-js/scripts/build.sh b/bindings/cashu-sdk-js/scripts/build.sh new file mode 100755 index 00000000..c432032c --- /dev/null +++ b/bindings/cashu-sdk-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_sdk_js_bg.wasm)\`;" > pkg/cashu_sdk_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_sdk_js.js + cat scripts/epilogue.js +} > pkg/cashu_sdk_js.js.new +mv pkg/cashu_sdk_js.js.new pkg/cashu_sdk_js.js + +# also extend the typescript +cat scripts/epilogue.d.ts >> pkg/cashu_sdk_js.d.ts \ No newline at end of file diff --git a/bindings/cashu-sdk-js/scripts/epilogue.d.ts b/bindings/cashu-sdk-js/scripts/epilogue.d.ts new file mode 100644 index 00000000..db38bcde --- /dev/null +++ b/bindings/cashu-sdk-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-sdk-js/scripts/epilogue.js b/bindings/cashu-sdk-js/scripts/epilogue.js new file mode 100644 index 00000000..da895f66 --- /dev/null +++ b/bindings/cashu-sdk-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_sdk_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_sdk_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