diff --git a/bindings/nodejs-old/src/message_handler.rs b/bindings/nodejs-old/src/message_handler.rs index b32f2b0d5b..6a4dc0aac2 100644 --- a/bindings/nodejs-old/src/message_handler.rs +++ b/bindings/nodejs-old/src/message_handler.rs @@ -138,7 +138,7 @@ pub fn send_message(mut cx: FunctionContext) -> JsResult { Ok(()) }); } else { - panic!("Message handler got destroyed") + panic!("Message handler was destroyed") } }); @@ -169,7 +169,7 @@ pub fn listen(mut cx: FunctionContext) -> JsResult { }) .await; } else { - panic!("Message handler got destroyed") + panic!("Message handler was destroyed") } }); diff --git a/bindings/nodejs/examples/how_tos/alias_wallet/request-funds.ts b/bindings/nodejs/examples/how_tos/alias_wallet/request-funds.ts index 534f723ce8..07f490c3e4 100644 --- a/bindings/nodejs/examples/how_tos/alias_wallet/request-funds.ts +++ b/bindings/nodejs/examples/how_tos/alias_wallet/request-funds.ts @@ -43,11 +43,11 @@ async function run() { // Get Alias address const aliasAddress = Utils.aliasIdToBech32( aliasId, - await (await wallet.getClient()).getBech32Hrp(), + await wallet.getClient().getBech32Hrp(), ); - const faucetResponse = await ( - await wallet.getClient() - ).requestFundsFromFaucet(faucetUrl, aliasAddress); + const faucetResponse = await wallet + .getClient() + .requestFundsFromFaucet(faucetUrl, aliasAddress); console.log(faucetResponse); await new Promise((resolve) => setTimeout(resolve, 10000)); diff --git a/bindings/nodejs/examples/how_tos/alias_wallet/transaction.ts b/bindings/nodejs/examples/how_tos/alias_wallet/transaction.ts index 0ffe4ef766..65980068ba 100644 --- a/bindings/nodejs/examples/how_tos/alias_wallet/transaction.ts +++ b/bindings/nodejs/examples/how_tos/alias_wallet/transaction.ts @@ -50,7 +50,7 @@ async function run() { // Get Alias address const aliasAddress = Utils.aliasIdToBech32( aliasId, - await (await wallet.getClient()).getBech32Hrp(), + await wallet.getClient().getBech32Hrp(), ); // Find first output unlockable by the alias address @@ -59,9 +59,8 @@ async function run() { address: aliasAddress, }, ]; - const input = ( - await (await wallet.getClient()).basicOutputIds(queryParameters) - ).items[0]; + const input = (await wallet.getClient().basicOutputIds(queryParameters)) + .items[0]; const params = [ { diff --git a/bindings/nodejs/examples/how_tos/nft_collection/01_mint_collection_nft.ts b/bindings/nodejs/examples/how_tos/nft_collection/01_mint_collection_nft.ts index c78aeba3b7..ae5711a889 100644 --- a/bindings/nodejs/examples/how_tos/nft_collection/01_mint_collection_nft.ts +++ b/bindings/nodejs/examples/how_tos/nft_collection/01_mint_collection_nft.ts @@ -41,7 +41,7 @@ async function run() { // Get the id we generated with `00_mint_issuer_nft` const issuerNftId: NftId = process.argv[2]; - const bech32Hrp = await (await wallet.getClient()).getBech32Hrp(); + const bech32Hrp = await wallet.getClient().getBech32Hrp(); const issuer = Utils.nftIdToBech32(issuerNftId, bech32Hrp); const nftMintParams = []; diff --git a/bindings/nodejs/examples/how_tos/nfts/mint_nft.ts b/bindings/nodejs/examples/how_tos/nfts/mint_nft.ts index d8853a5dca..154507ecec 100644 --- a/bindings/nodejs/examples/how_tos/nfts/mint_nft.ts +++ b/bindings/nodejs/examples/how_tos/nfts/mint_nft.ts @@ -79,7 +79,7 @@ async function run() { console.log('Minted NFT 1'); // Build an NFT manually by using the `NftOutputBuilder` - const client = await wallet.getClient(); + const client = wallet.getClient(); const hexAddress = Utils.bech32ToHex(senderAddress); const output = await client.buildNftOutput({ diff --git a/bindings/nodejs/examples/how_tos/simple_transaction/request-funds.ts b/bindings/nodejs/examples/how_tos/simple_transaction/request-funds.ts index 2a86b934ef..f1f680fc88 100644 --- a/bindings/nodejs/examples/how_tos/simple_transaction/request-funds.ts +++ b/bindings/nodejs/examples/how_tos/simple_transaction/request-funds.ts @@ -29,9 +29,9 @@ async function run() { const address = (await account.addresses())[0].address; console.log(address); - const faucetResponse = await ( - await wallet.getClient() - ).requestFundsFromFaucet(faucetUrl, address); + const faucetResponse = await wallet + .getClient() + .requestFundsFromFaucet(faucetUrl, address); console.log(faucetResponse); } catch (error) { console.error('Error: ', error); diff --git a/bindings/nodejs/lib/bindings.ts b/bindings/nodejs/lib/bindings.ts index 55b5dbba1f..6dac30f6b6 100644 --- a/bindings/nodejs/lib/bindings.ts +++ b/bindings/nodejs/lib/bindings.ts @@ -35,7 +35,7 @@ const callClientMethodAsync = ( handler: ClientMethodHandler, ): Promise => new Promise((resolve, reject) => { - callClientMethod(method, handler, (error: Error, result: string) => { + callClientMethod(method, handler, (error: any, result: string) => { if (error) { reject(error); } else { @@ -52,7 +52,7 @@ const callSecretManagerMethodAsync = ( callSecretManagerMethod( method, handler, - (error: Error, result: string) => { + (error: any, result: string) => { if (error) { reject(error); } else { @@ -76,15 +76,21 @@ const listenWalletAsync = ( callback: (error: Error, event: Event) => void, handler: WalletMethodHandler, ): Promise => { - listenWallet( - eventTypes, - function (err: any, data: string) { - const parsed = JSON.parse(data); - callback(err, new Event(parsed.accountIndex, parsed.event)); - }, - handler, - ); - return Promise.resolve(); + return new Promise((resolve) => { + listenWallet( + eventTypes, + function (err: any, data: string) { + const parsed = JSON.parse(data); + callback( + // Send back raw error instead of parsing + err, + new Event(parsed.accountIndex, parsed.event), + ); + }, + handler, + ); + resolve(); + }); }; const callWalletMethodAsync = ( @@ -92,7 +98,7 @@ const callWalletMethodAsync = ( handler: WalletMethodHandler, ): Promise => new Promise((resolve, reject) => { - callWalletMethod(method, handler, (error: Error, result: string) => { + callWalletMethod(method, handler, (error: any, result: string) => { if (error) { reject(error); } else { @@ -111,8 +117,8 @@ export { callSecretManagerMethodAsync, callUtilsMethod, callWalletMethodAsync, - destroyWallet, listenWalletAsync, + destroyWallet, getClientFromWallet, getSecretManagerFromWallet, listenMqtt, diff --git a/bindings/nodejs/lib/client/client-method-handler.ts b/bindings/nodejs/lib/client/client-method-handler.ts index fb28a4a006..5fc500873b 100644 --- a/bindings/nodejs/lib/client/client-method-handler.ts +++ b/bindings/nodejs/lib/client/client-method-handler.ts @@ -1,10 +1,11 @@ // Copyright 2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +import { errorHandle } from '..'; import { callClientMethodAsync, createClient, - listenMqtt, + listenMqtt as listenMqttRust, destroyClient, } from '../bindings'; import type { IClientOptions, __ClientMethods__ } from '../types/client'; @@ -19,16 +20,24 @@ export class ClientMethodHandler { * @param options client options or a client method handler. */ constructor(options: IClientOptions | ClientMethodHandler) { - // The rust client object is not extensible - if (Object.isExtensible(options)) { - this.methodHandler = createClient(JSON.stringify(options)); - } else { - this.methodHandler = options as ClientMethodHandler; + try { + // The rust client object is not extensible + if (Object.isExtensible(options)) { + this.methodHandler = createClient(JSON.stringify(options)); + } else { + this.methodHandler = options as ClientMethodHandler; + } + } catch (error: any) { + throw errorHandle(error); } } - async destroy() { - return destroyClient(this.methodHandler); + destroy(): void { + try { + destroyClient(this.methodHandler); + } catch (error: any) { + throw errorHandle(error); + } } /** @@ -39,9 +48,18 @@ export class ClientMethodHandler { */ async callMethod(method: __ClientMethods__): Promise { return callClientMethodAsync( - JSON.stringify(method), + // mapToObject is required to convert maps to array since they otherwise get serialized as `[{}]` even if not empty + JSON.stringify(method, function mapToObject(_key, value) { + if (value instanceof Map) { + return Object.fromEntries(value); + } else { + return value; + } + }), this.methodHandler, - ); + ).catch((error: any) => { + throw errorHandle(error); + }); } /** @@ -50,10 +68,14 @@ export class ClientMethodHandler { * @param topics The topics to listen to. * @param callback The callback to be called when an MQTT event is received. */ - async listen( + listenMqtt( topics: string[], callback: (error: Error, result: string) => void, - ): Promise { - return listenMqtt(topics, callback, this.methodHandler); + ): void { + try { + listenMqttRust(topics, callback, this.methodHandler); + } catch (error: any) { + throw errorHandle(error); + } } } diff --git a/bindings/nodejs/lib/client/client.ts b/bindings/nodejs/lib/client/client.ts index bc6f7610ed..b298b7aac3 100644 --- a/bindings/nodejs/lib/client/client.ts +++ b/bindings/nodejs/lib/client/client.ts @@ -69,8 +69,8 @@ export class Client { this.methodHandler = new ClientMethodHandler(options); } - async destroy() { - return this.methodHandler.destroy(); + destroy() { + this.methodHandler.destroy(); } /** @@ -1163,11 +1163,11 @@ export class Client { * * @param topics An array of MQTT topics to listen to. */ - async listenMqtt( + listenMqtt( topics: string[], callback: (error: Error, result: string) => void, - ): Promise { - return this.methodHandler.listen(topics, callback); + ): void { + return this.methodHandler.listenMqtt(topics, callback); } /** diff --git a/bindings/nodejs/lib/index.ts b/bindings/nodejs/lib/index.ts index 59d996af3c..70dc25992f 100644 --- a/bindings/nodejs/lib/index.ts +++ b/bindings/nodejs/lib/index.ts @@ -39,3 +39,44 @@ export * from './types'; export * from './utils'; export * from './wallet'; export * from './logger'; + +// For future reference to see what we return from rust as a serialized string +export type Result = { + // "error" | "panic" or other binding method response name + type: string; + payload: { + // All method names from types/bridge/__name__.name + // Or all variants of rust sdk Error type + type: string; + // If "ok", json payload + payload?: string; + // If !"ok", error + error?: string; + }; +}; + +function errorHandle(error: any): Error { + if (error instanceof TypeError) { + // neon or other bindings lib related error + throw error; + } else if (error instanceof Error) { + try { + const err: Result = JSON.parse(error.message); + if (err.type == 'panic') { + return Error(err.payload.toString()); + } else { + return Error(err.payload.error); + } + } catch (err: any) { + // json error, SyntaxError, we must have send a bad error + return error; + } + } else { + // Something bad happened! Make sure we dont double parse + //Possible scenarios: + // - "json to string error: x" + return TypeError(error); + } +} + +export { errorHandle }; diff --git a/bindings/nodejs/lib/secret_manager/secret-manager-method-handler.ts b/bindings/nodejs/lib/secret_manager/secret-manager-method-handler.ts index 639aa6a036..7e502f4b22 100644 --- a/bindings/nodejs/lib/secret_manager/secret-manager-method-handler.ts +++ b/bindings/nodejs/lib/secret_manager/secret-manager-method-handler.ts @@ -1,6 +1,7 @@ // Copyright 2021-2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +import { errorHandle } from '..'; import { callSecretManagerMethodAsync, createSecretManager, @@ -19,11 +20,17 @@ export class SecretManagerMethodHandler { * @param options A secret manager type or a secret manager method handler. */ constructor(options: SecretManagerType | SecretManagerMethodHandler) { - // The rust secret manager object is not extensible - if (Object.isExtensible(options)) { - this.methodHandler = createSecretManager(JSON.stringify(options)); - } else { - this.methodHandler = options as SecretManagerMethodHandler; + try { + // The rust secret manager object is not extensible + if (Object.isExtensible(options)) { + this.methodHandler = createSecretManager( + JSON.stringify(options), + ); + } else { + this.methodHandler = options as SecretManagerMethodHandler; + } + } catch (error: any) { + throw errorHandle(error); } } @@ -35,9 +42,18 @@ export class SecretManagerMethodHandler { */ async callMethod(method: __SecretManagerMethods__): Promise { return callSecretManagerMethodAsync( - JSON.stringify(method), + // mapToObject is required to convert maps to array since they otherwise get serialized as `[{}]` even if not empty + JSON.stringify(method, function mapToObject(_key, value) { + if (value instanceof Map) { + return Object.fromEntries(value); + } else { + return value; + } + }), this.methodHandler, - ); + ).catch((error: any) => { + throw errorHandle(error); + }); } } diff --git a/bindings/nodejs/lib/wallet/wallet-method-handler.ts b/bindings/nodejs/lib/wallet/wallet-method-handler.ts index 7c18c80b52..073a42dc4c 100644 --- a/bindings/nodejs/lib/wallet/wallet-method-handler.ts +++ b/bindings/nodejs/lib/wallet/wallet-method-handler.ts @@ -19,9 +19,11 @@ import type { } from '../types/wallet'; import { Client } from '../client'; import { SecretManager } from '../secret_manager'; +import { errorHandle } from '..'; // The WalletMethodHandler class interacts with methods with the rust bindings. export class WalletMethodHandler { + // External rust object methodHandler: any; /** @@ -35,13 +37,26 @@ export class WalletMethodHandler { secretManager: options?.secretManager, }; - this.methodHandler = createWallet(JSON.stringify(walletOptions)); + try { + this.methodHandler = createWallet(JSON.stringify(walletOptions)); + } catch (error: any) { + throw errorHandle(error); + } + } + + destroy(): void { + try { + destroyWallet(this.methodHandler); + } catch (error: any) { + throw errorHandle(error); + } } /** * Call a wallet method on the Rust backend. * * @param method The wallet method to call. + * @returns A promise that resolves to a JSON string response holding the result of the wallet method. */ async callMethod(method: __Method__): Promise { return callWalletMethodAsync( @@ -54,17 +69,8 @@ export class WalletMethodHandler { } }), this.methodHandler, - ).catch((error: Error) => { - try { - if (error.message !== undefined) { - error = JSON.parse(error.message).payload; - } else { - error = JSON.parse(error.toString()).payload; - } - } catch (e) { - console.error(e); - } - return Promise.reject(error); + ).catch((error: any) => { + throw errorHandle(error); }); } @@ -97,42 +103,36 @@ export class WalletMethodHandler { eventTypes: WalletEventType[], callback: (error: Error, event: Event) => void, ): Promise { - return listenWalletAsync(eventTypes, callback, this.methodHandler); - } - - async destroy(): Promise { - return destroyWallet(this.methodHandler); + return listenWalletAsync( + eventTypes, + callback, + this.methodHandler, + ).catch((error: any) => { + throw errorHandle(error); + }); } /** * Get the client associated with the wallet. */ - async getClient(): Promise { - return new Promise((resolve, reject) => { - getClientFromWallet(this.methodHandler).then((result: any) => { - if (result.message !== undefined) { - reject(JSON.parse(result.message).payload); - } else { - resolve(new Client(result)); - } - }); - }); + getClient(): Client { + try { + const result = getClientFromWallet(this.methodHandler); + return new Client(result); + } catch (error: any) { + throw errorHandle(error); + } } /** * Get the secret manager associated with the wallet. */ - async getSecretManager(): Promise { - return new Promise((resolve, reject) => { - getSecretManagerFromWallet(this.methodHandler).then( - (result: any) => { - if (result.message !== undefined) { - reject(JSON.parse(result.message).payload); - } else { - resolve(new SecretManager(result)); - } - }, - ); - }); + getSecretManager(): SecretManager { + try { + const result = getSecretManagerFromWallet(this.methodHandler); + return new SecretManager(result); + } catch (error: any) { + throw errorHandle(error); + } } } diff --git a/bindings/nodejs/lib/wallet/wallet.ts b/bindings/nodejs/lib/wallet/wallet.ts index 18028faa91..ea4d3c6e9e 100644 --- a/bindings/nodejs/lib/wallet/wallet.ts +++ b/bindings/nodejs/lib/wallet/wallet.ts @@ -81,7 +81,7 @@ export class Wallet { /** * Destroy the Wallet and drop its database connection. */ - async destroy(): Promise { + destroy(): void { return this.methodHandler.destroy(); } @@ -144,7 +144,7 @@ export class Wallet { /** * Get client. */ - async getClient(): Promise { + getClient(): Client { return this.methodHandler.getClient(); } @@ -162,7 +162,7 @@ export class Wallet { /** * Get secret manager. */ - async getSecretManager(): Promise { + getSecretManager(): SecretManager { return this.methodHandler.getSecretManager(); } diff --git a/bindings/nodejs/src/client.rs b/bindings/nodejs/src/client.rs index da62dca605..0f4e3696d1 100644 --- a/bindings/nodejs/src/client.rs +++ b/bindings/nodejs/src/client.rs @@ -1,7 +1,7 @@ // Copyright 2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -use std::sync::Arc; +use std::sync::{Arc, RwLock}; use iota_sdk_bindings_core::{ call_client_method as rust_call_client_method, @@ -9,20 +9,18 @@ use iota_sdk_bindings_core::{ listen_mqtt as rust_listen_mqtt, ClientMethod, Response, Result, }; use neon::prelude::*; -use tokio::sync::RwLock; type JsCallback = Root>; -// Wrapper so we can destroy the ClientMethodHandler -pub type ClientMethodHandlerWrapperInner = Arc>>; -// Wrapper because we can't impl Finalize on ClientMethodHandlerWrapperInner -pub struct ClientMethodHandlerWrapper(pub ClientMethodHandlerWrapperInner); +pub type SharedClientMethodHandler = Arc>>; + +#[derive(Clone)] pub struct ClientMethodHandler { channel: Channel, client: Client, } -impl Finalize for ClientMethodHandlerWrapper {} +impl Finalize for ClientMethodHandler {} impl ClientMethodHandler { pub fn new(channel: Channel, options: String) -> Result { @@ -52,104 +50,119 @@ impl ClientMethodHandler { (msg, is_err) } - Err(e) => { - log::error!("{:?}", e); - (format!("Couldn't parse to method with error - {e:?}"), true) - } + Err(e) => ( + serde_json::to_string(&Response::Error(e.into())).expect("json to string error"), + true, + ), } } } -pub fn create_client(mut cx: FunctionContext) -> JsResult> { +pub fn create_client(mut cx: FunctionContext) -> JsResult> { let options = cx.argument::(0)?; let options = options.value(&mut cx); let channel = cx.channel(); let method_handler = ClientMethodHandler::new(channel, options) .or_else(|e| cx.throw_error(serde_json::to_string(&Response::Error(e)).expect("json to string error")))?; - Ok(cx.boxed(ClientMethodHandlerWrapper(Arc::new(RwLock::new(Some(method_handler)))))) + Ok(cx.boxed(Arc::new(RwLock::new(Some(method_handler))))) } -pub fn destroy_client(mut cx: FunctionContext) -> JsResult { - let method_handler = Arc::clone(&cx.argument::>(0)?.0); - let channel = cx.channel(); - let (deferred, promise) = cx.promise(); - crate::RUNTIME.spawn(async move { - *method_handler.write().await = None; - deferred.settle_with(&channel, move |mut cx| Ok(cx.undefined())); - }); - Ok(promise) +pub fn destroy_client(mut cx: FunctionContext) -> JsResult { + match cx.argument::>(0)?.write() { + Ok(mut lock) => *lock = None, + Err(e) => { + return cx + .throw_error(serde_json::to_string(&Response::Panic(e.to_string())).expect("json to string error")); + } + } + Ok(cx.undefined()) } pub fn call_client_method(mut cx: FunctionContext) -> JsResult { - let method = cx.argument::(0)?; - let method = method.value(&mut cx); - let method_handler = Arc::clone(&cx.argument::>(1)?.0); - let callback = cx.argument::(2)?.root(&mut cx); - - crate::RUNTIME.spawn(async move { - if let Some(method_handler) = &*method_handler.read().await { - let (response, is_error) = method_handler.call_method(method).await; - method_handler.channel.send(move |mut cx| { - let cb = callback.into_inner(&mut cx); - let this = cx.undefined(); - - let args = [ - if is_error { - cx.string(response.clone()).upcast::() - } else { - cx.undefined().upcast::() - }, - cx.string(response).upcast::(), - ]; - - cb.call(&mut cx, this, args)?; - - Ok(()) - }); - } else { - panic!("Client got destroyed") + match cx.argument::>(1)?.read() { + Ok(lock) => { + let method_handler = lock.clone(); + let method = cx.argument::(0)?; + let method = method.value(&mut cx); + let callback = cx.argument::(2)?.root(&mut cx); + if let Some(method_handler) = method_handler { + crate::RUNTIME.spawn(async move { + let (response, is_error) = method_handler.call_method(method).await; + method_handler.channel.send(move |mut cx| { + let cb = callback.into_inner(&mut cx); + let this = cx.undefined(); + + let args = [ + if is_error { + cx.error(response.clone())?.upcast::() + } else { + cx.undefined().upcast::() + }, + cx.string(response).upcast::(), + ]; + + cb.call(&mut cx, this, args)?; + Ok(()) + }); + }); + + Ok(cx.undefined()) + } else { + // Notify that the client was destroyed + cx.throw_error( + serde_json::to_string(&Response::Panic("Client was destroyed".to_string())) + .expect("json to string error"), + ) + } } - }); - - Ok(cx.undefined()) + Err(e) => cx.throw_error(serde_json::to_string(&Response::Panic(e.to_string())).expect("json to string error")), + } } // MQTT -pub fn listen_mqtt(mut cx: FunctionContext) -> JsResult { +pub fn listen_mqtt(mut cx: FunctionContext) -> JsResult { let js_arr_handle: Handle = cx.argument(0)?; let vec: Vec> = js_arr_handle.to_vec(&mut cx)?; let mut topics = Vec::with_capacity(vec.len()); for topic_string in vec { let topic = topic_string.downcast::(&mut cx).unwrap(); - topics.push(Topic::new(topic.value(&mut cx).as_str()).expect("invalid MQTT topic")); + topics.push(Topic::new(topic.value(&mut cx).as_str()).or_else(|e| { + cx.throw_error(serde_json::to_string(&Response::Error(e.into())).expect("json to string error")) + })?); } let callback = Arc::new(cx.argument::(1)?.root(&mut cx)); - let method_handler = Arc::clone(&cx.argument::>(2)?.0); - let (deferred, promise) = cx.promise(); - - crate::RUNTIME.spawn(async move { - if let Some(method_handler) = &*method_handler.read().await { - let channel0 = method_handler.channel.clone(); - let channel1 = method_handler.channel.clone(); - rust_listen_mqtt(&method_handler.client, topics, move |event_data| { - call_event_callback(&channel0, event_data, callback.clone()) - }) - .await; - - deferred.settle_with(&channel1, move |mut cx| Ok(cx.undefined())); - } else { - panic!("Client got destroyed") - } - }); - Ok(promise) + match cx.argument::>(2)?.read() { + Ok(lock) => { + let method_handler = lock.clone(); + if let Some(method_handler) = method_handler { + crate::RUNTIME.spawn(async move { + rust_listen_mqtt(&method_handler.client, topics, move |event_data| { + call_event_callback(&method_handler.channel, event_data, callback.clone()) + }) + .await; + }); + Ok(cx.undefined()) + } else { + // Notify that the client was destroyed + cx.throw_error( + serde_json::to_string(&Response::Panic("Client was destroyed".to_string())) + .expect("json to string error"), + ) + } + } + Err(e) => cx.throw_error(serde_json::to_string(&Response::Panic(e.to_string())).expect("json to string error")), + } } fn call_event_callback(channel: &neon::event::Channel, event_data: String, callback: Arc) { channel.send(move |mut cx| { let cb = (*callback).to_inner(&mut cx); + // instance of class we call on, its a global fn so "undefined" let this = cx.undefined(); + + // callback is fn(err, event) let args = [ cx.undefined().upcast::(), cx.string(event_data).upcast::(), diff --git a/bindings/nodejs/src/secret_manager.rs b/bindings/nodejs/src/secret_manager.rs index 7c5531638f..072cb452fa 100644 --- a/bindings/nodejs/src/secret_manager.rs +++ b/bindings/nodejs/src/secret_manager.rs @@ -88,7 +88,7 @@ pub fn call_secret_manager_method(mut cx: FunctionContext) -> JsResult() + cx.error(response.clone())?.upcast::() } else { cx.undefined().upcast::() }, diff --git a/bindings/nodejs/src/wallet.rs b/bindings/nodejs/src/wallet.rs index 7a02d091f5..1690a845a3 100644 --- a/bindings/nodejs/src/wallet.rs +++ b/bindings/nodejs/src/wallet.rs @@ -1,7 +1,7 @@ // Copyright 2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -use std::sync::Arc; +use std::sync::{Arc, RwLock}; use iota_sdk_bindings_core::{ call_wallet_method as rust_call_wallet_method, @@ -13,24 +13,22 @@ use iota_sdk_bindings_core::{ Response, Result, WalletMethod, WalletOptions, }; use neon::prelude::*; -use tokio::sync::RwLock; use crate::{ - client::{ClientMethodHandler, ClientMethodHandlerWrapper}, + client::{ClientMethodHandler, SharedClientMethodHandler}, secret_manager::SecretManagerMethodHandler, }; -// Wrapper so we can destroy the WalletMethodHandler -pub type WalletMethodHandlerWrapperInner = Arc>>; -// Wrapper because we can't impl Finalize on WalletMethodHandlerWrapperInner -pub struct WalletMethodHandlerWrapper(pub WalletMethodHandlerWrapperInner); -impl Finalize for WalletMethodHandlerWrapper {} +pub type SharedWalletMethodHandler = Arc>>; +#[derive(Clone)] pub struct WalletMethodHandler { channel: Channel, wallet: Wallet, } +impl Finalize for WalletMethodHandler {} + type JsCallback = Root>; impl WalletMethodHandler { @@ -58,19 +56,14 @@ impl WalletMethodHandler { (msg, is_err) } - Err(e) => { - log::error!("{:?}", e); - ( - serde_json::to_string(&Response::Error(e.into())).expect("json to string error"), - true, - ) - } + Err(e) => ( + serde_json::to_string(&Response::Error(e.into())).expect("json to string error"), + true, + ), } } } -impl Finalize for WalletMethodHandler {} - fn call_event_callback(channel: &neon::event::Channel, event_data: Event, callback: Arc) { channel.send(move |mut cx| { let cb = (*callback).to_inner(&mut cx); @@ -87,48 +80,56 @@ fn call_event_callback(channel: &neon::event::Channel, event_data: Event, callba }); } -pub fn create_wallet(mut cx: FunctionContext) -> JsResult> { +pub fn create_wallet(mut cx: FunctionContext) -> JsResult> { let options = cx.argument::(0)?; let options = options.value(&mut cx); let channel = cx.channel(); let method_handler = WalletMethodHandler::new(channel, options) .or_else(|e| cx.throw_error(serde_json::to_string(&Response::Error(e)).expect("json to string error")))?; - Ok(cx.boxed(WalletMethodHandlerWrapper(Arc::new(RwLock::new(Some(method_handler)))))) + Ok(cx.boxed(Arc::new(RwLock::new(Some(method_handler))))) } pub fn call_wallet_method(mut cx: FunctionContext) -> JsResult { - let method = cx.argument::(0)?; - let method = method.value(&mut cx); - let method_handler = Arc::clone(&cx.argument::>(1)?.0); - let callback = cx.argument::(2)?.root(&mut cx); - - crate::RUNTIME.spawn(async move { - if let Some(method_handler) = &*method_handler.read().await { - let (response, is_error) = method_handler.call_method(method).await; - method_handler.channel.send(move |mut cx| { - let cb = callback.into_inner(&mut cx); - let this = cx.undefined(); - - let args = [ - if is_error { - cx.string(response.clone()).upcast::() - } else { - cx.undefined().upcast::() - }, - cx.string(response).upcast::(), - ]; - - cb.call(&mut cx, this, args)?; - - Ok(()) - }); - } else { - panic!("Wallet got destroyed") + match cx.argument::>(1)?.read() { + Ok(lock) => { + let method_handler = lock.clone(); + let method = cx.argument::(0)?; + let method = method.value(&mut cx); + let callback = cx.argument::(2)?.root(&mut cx); + if let Some(method_handler) = method_handler { + crate::RUNTIME.spawn(async move { + let (response, is_error) = method_handler.call_method(method).await; + method_handler.channel.send(move |mut cx| { + let cb = callback.into_inner(&mut cx); + let this = cx.undefined(); + + let args = [ + if is_error { + cx.error(response.clone())?.upcast::() + } else { + cx.undefined().upcast::() + }, + cx.string(response).upcast::(), + ]; + + cb.call(&mut cx, this, args)?; + + Ok(()) + }); + }); + + Ok(cx.undefined()) + } else { + // Notify that the wallet was destroyed + cx.throw_error( + serde_json::to_string(&Response::Panic("Wallet was destroyed".to_string())) + .expect("json to string error"), + ) + } } - }); - - Ok(cx.undefined()) + Err(e) => cx.throw_error(serde_json::to_string(&Response::Panic(e.to_string())).expect("json to string error")), + } } pub fn listen_wallet(mut cx: FunctionContext) -> JsResult { @@ -141,88 +142,88 @@ pub fn listen_wallet(mut cx: FunctionContext) -> JsResult { WalletEventType::try_from(event_type.value(&mut cx) as u8).or_else(|e| cx.throw_error(e))?; event_types.push(wallet_event_type); } - let callback = Arc::new(cx.argument::(1)?.root(&mut cx)); - let method_handler = Arc::clone(&cx.argument::>(2)?.0); - crate::RUNTIME.spawn(async move { - if let Some(method_handler) = &*method_handler.read().await { - let channel = method_handler.channel.clone(); - method_handler - .wallet - .listen(event_types, move |event_data| { - call_event_callback(&channel, event_data.clone(), callback.clone()) - }) - .await; - } else { - panic!("Wallet got destroyed") + match cx.argument::>(2)?.read() { + Ok(lock) => { + let method_handler = lock.clone(); + if let Some(method_handler) = method_handler { + crate::RUNTIME.spawn(async move { + method_handler + .wallet + .listen(event_types, move |event_data| { + call_event_callback(&method_handler.channel, event_data.clone(), callback.clone()) + }) + .await; + }); + + Ok(cx.undefined()) + } else { + // Notify that the wallet was destroyed + cx.throw_error( + serde_json::to_string(&Response::Panic("Wallet was destroyed".to_string())) + .expect("json to string error"), + ) + } } - }); - - Ok(cx.undefined()) + Err(e) => cx.throw_error(serde_json::to_string(&Response::Panic(e.to_string())).expect("json to string error")), + } } -pub fn destroy_wallet(mut cx: FunctionContext) -> JsResult { - let method_handler = Arc::clone(&cx.argument::>(0)?.0); - let channel = cx.channel(); - let (deferred, promise) = cx.promise(); - crate::RUNTIME.spawn(async move { - *method_handler.write().await = None; - deferred.settle_with(&channel, move |mut cx| Ok(cx.undefined())); - }); - Ok(promise) +pub fn destroy_wallet(mut cx: FunctionContext) -> JsResult { + match cx.argument::>(0)?.write() { + Ok(mut lock) => *lock = None, + Err(e) => { + return cx + .throw_error(serde_json::to_string(&Response::Panic(e.to_string())).expect("json to string error")); + } + } + Ok(cx.undefined()) } -pub fn get_client(mut cx: FunctionContext) -> JsResult { - let method_handler = Arc::clone(&cx.argument::>(0)?.0); - let channel = cx.channel(); - - let (deferred, promise) = cx.promise(); - crate::RUNTIME.spawn(async move { - if let Some(method_handler) = &*method_handler.read().await { - let client_method_handler = - ClientMethodHandler::new_with_client(channel.clone(), method_handler.wallet.client().clone()); - deferred.settle_with(&channel, move |mut cx| { - Ok(cx.boxed(ClientMethodHandlerWrapper(Arc::new(RwLock::new(Some( - client_method_handler, - )))))) - }); - } else { - deferred.settle_with(&channel, move |mut cx| { - cx.error( - serde_json::to_string(&Response::Panic("Wallet got destroyed".to_string())) +pub fn get_client(mut cx: FunctionContext) -> JsResult> { + match cx.argument::>(0)?.read() { + Ok(lock) => { + if let Some(method_handler) = &*lock { + let client_method_handler = + ClientMethodHandler::new_with_client(cx.channel(), method_handler.wallet.client().clone()); + + Ok(cx.boxed(Arc::new(RwLock::new(Some(client_method_handler))))) + } else { + // Notify that the wallet was destroyed + cx.throw_error( + serde_json::to_string(&Response::Panic("Wallet was destroyed".to_string())) .expect("json to string error"), ) - }); + } } - }); - - Ok(promise) + Err(e) => cx.throw_error(serde_json::to_string(&Response::Panic(e.to_string())).expect("json to string error")), + } } -pub fn get_secret_manager(mut cx: FunctionContext) -> JsResult { - let method_handler = Arc::clone(&cx.argument::>(0)?.0); - let channel = cx.channel(); - - let (deferred, promise) = cx.promise(); - crate::RUNTIME.spawn(async move { - if let Some(method_handler) = &*method_handler.read().await { - let secret_manager_method_handler = SecretManagerMethodHandler::new_with_secret_manager( - channel.clone(), - method_handler.wallet.get_secret_manager().clone(), - ); - deferred.settle_with(&channel, move |mut cx| Ok(cx.boxed(secret_manager_method_handler))); - } else { - deferred.settle_with(&channel, move |mut cx| { - cx.error( - serde_json::to_string(&Response::Panic("Wallet got destroyed".to_string())) +pub fn get_secret_manager(mut cx: FunctionContext) -> JsResult>> { + match cx.argument::>(0)?.read() { + Ok(lock) => { + if let Some(method_handler) = &*lock { + let secret_manager_method_handler = SecretManagerMethodHandler::new_with_secret_manager( + cx.channel(), + method_handler.wallet.get_secret_manager().clone(), + ); + + Ok(cx.boxed(secret_manager_method_handler)) + } else { + // Notify that the wallet was destroyed + cx.throw_error( + serde_json::to_string(&Response::Panic("Wallet was destroyed".to_string())) .expect("json to string error"), ) - }); + } } - }); - - Ok(promise) + Err(e) => { + return cx + .throw_error(serde_json::to_string(&Response::Panic(e.to_string())).expect("json to string error")); + } + } } pub fn migrate_db_chrysalis_to_stardust(mut cx: FunctionContext) -> JsResult { diff --git a/bindings/nodejs/tests/client/examples.spec.ts b/bindings/nodejs/tests/client/examples.spec.ts index 2a135c1a3d..07ddad1db7 100644 --- a/bindings/nodejs/tests/client/examples.spec.ts +++ b/bindings/nodejs/tests/client/examples.spec.ts @@ -185,4 +185,24 @@ describe.skip('Main examples', () => { expect(blockIdAndBlock[0]).toBeValidBlockId(); }); + + it('destroy', async () => { + const client = new Client({ + nodes: [ + { + url: process.env.NODE_URL || 'http://localhost:14265', + }, + ], + localPow: true, + }); + + client.destroy(); + + try { + const _info = await client.getInfo(); + throw 'Should return an error because the client was destroyed'; + } catch (err: any) { + expect(err.message).toContain('Client was destroyed'); + } + }) }); diff --git a/bindings/nodejs/tests/wallet/wallet.spec.ts b/bindings/nodejs/tests/wallet/wallet.spec.ts index 9d514e213c..5459721925 100644 --- a/bindings/nodejs/tests/wallet/wallet.spec.ts +++ b/bindings/nodejs/tests/wallet/wallet.spec.ts @@ -36,8 +36,8 @@ describe('Wallet', () => { expect(account.getMetadata().index).toStrictEqual(0); - await wallet.destroy() - removeDir(storagePath) + wallet.destroy(); + removeDir(storagePath); }, 8000); it('generate address', async () => { @@ -85,8 +85,8 @@ describe('Wallet', () => { 'tst1qzp37j45rkfmqn05fapq66vyw0vkmz5zqhmeuey5fked0wt4ry43jeqp2wv', ); - await wallet.destroy() - removeDir(storagePath) + wallet.destroy(); + removeDir(storagePath); }, 8000); it('recreate wallet', async () => { @@ -118,20 +118,67 @@ describe('Wallet', () => { expect(account.getMetadata().index).toStrictEqual(0); - const client = await wallet.getClient(); + const client = wallet.getClient(); const localPoW = await client.getLocalPow(); expect(localPoW).toBeTruthy(); - await wallet.destroy(); + wallet.destroy(); const recreatedWallet = new Wallet(walletOptions); const accounts = await recreatedWallet.getAccounts(); expect(accounts.length).toStrictEqual(1); - await recreatedWallet.destroy() + recreatedWallet.destroy(); removeDir(storagePath) }, 20000); + + it('error after destroy', async () => { + let storagePath = 'test-error-after-destroy'; + removeDir(storagePath); + + const walletOptions = { + storagePath, + clientOptions: { + nodes: ['https://api.testnet.shimmer.network'], + }, + coinType: CoinType.Shimmer, + secretManager: { + stronghold: { + snapshotPath: `./${storagePath}/wallet.stronghold`, + password: `A12345678*`, + }, + }, + }; + + const wallet = new Wallet(walletOptions); + await wallet.storeMnemonic( + 'vital give early extra blind skin eight discover scissors there globe deal goat fat load robot return rate fragile recycle select live ordinary claim', + ); + + const account = await wallet.createAccount({ + alias: 'Alice', + }); + + expect(account.getMetadata().index).toStrictEqual(0); + + wallet.destroy(); + + try { + const _accounts = await wallet.getAccounts(); + throw 'Should return an error because the wallet was destroyed'; + } catch (err: any) { + expect(err.message).toContain('Wallet was destroyed'); + } + + try { + const _client = wallet.getClient(); + throw 'Should return an error because the wallet was destroyed'; + } catch (err: any) { + expect(err.message).toContain('Wallet was destroyed'); + } + removeDir(storagePath); + }, 35000); }) function removeDir(storagePath: string) { diff --git a/bindings/python/src/wallet.rs b/bindings/python/src/wallet.rs index 3a5e1ed57e..2dd2f39733 100644 --- a/bindings/python/src/wallet.rs +++ b/bindings/python/src/wallet.rs @@ -49,7 +49,7 @@ pub fn call_wallet_method(wallet: &Wallet, method: String) -> Result { let response = crate::block_on(async { match wallet.wallet.read().await.as_ref() { Some(wallet) => rust_call_wallet_method(wallet, method).await, - None => Response::Panic("wallet got destroyed".into()), + None => Response::Panic("wallet was destroyed".into()), } }); @@ -77,7 +77,7 @@ pub fn listen_wallet(wallet: &Wallet, events: Vec, handler: PyObject) { .read() .await .as_ref() - .expect("wallet got destroyed") + .expect("wallet was destroyed") .listen(rust_events, move |event| { let event_string = serde_json::to_string(&event).expect("json to string error"); Python::with_gil(|py| { @@ -101,7 +101,7 @@ pub fn get_client_from_wallet(wallet: &Wallet) -> Result { .map(|w| w.client().clone()) .ok_or_else(|| { Error::from( - serde_json::to_string(&Response::Panic("wallet got destroyed".into())) + serde_json::to_string(&Response::Panic("wallet was destroyed".into())) .expect("json to string error") .as_str(), ) @@ -123,7 +123,7 @@ pub fn get_secret_manager_from_wallet(wallet: &Wallet) -> Result .map(|w| w.get_secret_manager().clone()) .ok_or_else(|| { Error::from( - serde_json::to_string(&Response::Panic("wallet got destroyed".into())) + serde_json::to_string(&Response::Panic("wallet was destroyed".into())) .expect("json to string error") .as_str(), ) diff --git a/bindings/wasm/lib/bindings.ts b/bindings/wasm/lib/bindings.ts index 295583d6fc..8f49dae865 100644 --- a/bindings/wasm/lib/bindings.ts +++ b/bindings/wasm/lib/bindings.ts @@ -9,7 +9,8 @@ import { __UtilsMethods__ } from './utils'; // Import needs to be in a single line, otherwise it breaks // prettier-ignore // @ts-ignore: path is set to match runtime transpiled js path when bundled. -import { initLogger, createClient, destroyClient, createSecretManager, createWallet, callClientMethodAsync, callSecretManagerMethodAsync, callUtilsMethodRust, callWalletMethodAsync, destroyWallet, listenWalletAsync, getClientFromWallet, getSecretManagerFromWallet, listenMqtt, migrateStrongholdSnapshotV2ToV3, migrateDbChrysalisToStardust } from '../wasm/iota_sdk_wasm'; +import { initLogger, createClient, destroyClient, createSecretManager, createWallet, callClientMethodAsync, callSecretManagerMethodAsync, callUtilsMethodRust, callWalletMethodAsync, destroyWallet, listenWalletAsync, getClientFromWallet, getSecretManagerFromWallet as getSecretManagerFromWalletRust, listenMqtt, migrateStrongholdSnapshotV2ToV3, migrateDbChrysalisToStardust } from '../wasm/iota_sdk_wasm'; +import { SecretManagerMethodHandler, WalletMethodHandler } from '.'; const callUtilsMethod = (method: __UtilsMethods__): any => { const response = JSON.parse(callUtilsMethodRust(JSON.stringify(method))); @@ -20,6 +21,15 @@ const callUtilsMethod = (method: __UtilsMethods__): any => { } }; +const getSecretManagerFromWallet = ( + method: WalletMethodHandler, +): SecretManagerMethodHandler => { + // TODO figure out why this one is extensible but client isnt + const res = getSecretManagerFromWalletRust(method); + Object.preventExtensions(res); + return res; +}; + export { initLogger, createClient, diff --git a/bindings/wasm/package.json b/bindings/wasm/package.json index 02860cd5c3..2c5e208dcc 100644 --- a/bindings/wasm/package.json +++ b/bindings/wasm/package.json @@ -57,7 +57,7 @@ "typedoc": "^0.23.9", "typedoc-plugin-markdown": "^3.13.4", "typescript": "^4.7.4", - "wasm-opt": "^1.3.0" + "wasm-opt": "^1.4.0" }, "resolutions": { "decode-uri-component": "^0.2.1", diff --git a/bindings/wasm/src/client.rs b/bindings/wasm/src/client.rs index 5abf7d79fc..f341c29480 100644 --- a/bindings/wasm/src/client.rs +++ b/bindings/wasm/src/client.rs @@ -1,45 +1,87 @@ // Copyright 2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +use std::sync::{Arc, RwLock}; + use iota_sdk_bindings_core::{ call_client_method, iota_sdk::client::{Client, ClientBuilder}, ClientMethod, Response, }; -use wasm_bindgen::{prelude::wasm_bindgen, JsCast, JsValue}; -use wasm_bindgen_futures::future_to_promise; +use wasm_bindgen::{prelude::wasm_bindgen, JsError}; -use crate::{ArrayString, PromiseString}; +use crate::ArrayString; /// The Client method handler. #[wasm_bindgen(js_name = ClientMethodHandler)] pub struct ClientMethodHandler { - pub(crate) client: Client, + pub(crate) client: Arc>>, +} + +impl ClientMethodHandler { + pub(crate) fn new(client: Client) -> Self { + Self { + client: Arc::new(RwLock::new(Some(client))), + } + } +} + +macro_rules! client_pre { + ($method_handler:ident) => { + match $method_handler.client.read() { + Ok(handler) => { + if let Some(client) = handler.clone() { + Ok(client) + } else { + // Notify that the client was destroyed + Err(JsError::new( + &serde_json::to_string(&Response::Panic("Client was destroyed".to_string())) + .expect("json to string error"), + )) + } + } + Err(e) => Err(JsError::new( + &serde_json::to_string(&Response::Panic(e.to_string())).expect("json to string error"), + )), + } + }; } /// Creates a method handler with the given client options. #[wasm_bindgen(js_name = createClient)] #[allow(non_snake_case)] -pub fn create_client(clientOptions: String) -> Result { - let runtime = tokio::runtime::Builder::new_current_thread() - .build() - .map_err(|err| err.to_string())?; +pub fn create_client(clientOptions: String) -> Result { + let runtime = tokio::runtime::Builder::new_current_thread().build().map_err(|err| { + JsError::new(&serde_json::to_string(&Response::Panic(err.to_string())).expect("json to string error")) + })?; let client = runtime.block_on(async move { ClientBuilder::new() .from_json(&clientOptions) - .map_err(|err| err.to_string())? + .map_err(|err| { + JsError::new(&serde_json::to_string(&Response::Error(err.into())).expect("json to string error")) + })? .finish() .await - .map_err(|err| err.to_string()) + .map_err(|err| { + JsError::new(&serde_json::to_string(&Response::Error(err.into())).expect("json to string error")) + }) })?; - Ok(ClientMethodHandler { client }) + Ok(ClientMethodHandler::new(client)) } /// Necessary for compatibility with the node.js bindings. #[wasm_bindgen(js_name = destroyClient)] -pub async fn destroy_client(_client_method_handler: &ClientMethodHandler) -> Result<(), JsValue> { +pub fn destroy_client(client_method_handler: &ClientMethodHandler) -> Result<(), JsError> { + match client_method_handler.client.write() { + Ok(mut lock) => *lock = None, + Err(e) => { + return Err(JsError::new( + &serde_json::to_string(&Response::Panic(e.to_string())).expect("json to string error"), + )); + } + }; Ok(()) } @@ -48,22 +90,19 @@ pub async fn destroy_client(_client_method_handler: &ClientMethodHandler) -> Res /// Returns an error if the response itself is an error or panic. #[wasm_bindgen(js_name = callClientMethodAsync)] #[allow(non_snake_case)] -pub fn call_client_method_async(method: String, methodHandler: &ClientMethodHandler) -> Result { - let client: Client = methodHandler.client.clone(); - - let promise: js_sys::Promise = future_to_promise(async move { - let method: ClientMethod = serde_json::from_str(&method).map_err(|err| err.to_string())?; +pub async fn call_client_method_async(method: String, methodHandler: &ClientMethodHandler) -> Result { + let client = client_pre!(methodHandler)?; - let response = call_client_method(&client, method).await; - let ser = JsValue::from(serde_json::to_string(&response).map_err(|err| err.to_string())?); - match response { - Response::Error(_) | Response::Panic(_) => Err(ser), - _ => Ok(ser), - } - }); + let method: ClientMethod = serde_json::from_str(&method).map_err(|err| { + JsError::new(&serde_json::to_string(&Response::Error(err.into())).expect("json to string error")) + })?; - // WARNING: this does not validate the return type. Check carefully. - Ok(promise.unchecked_into()) + let response = call_client_method(&client, method).await; + let ser = serde_json::to_string(&response).expect("json to string error"); + match response { + Response::Error(_) | Response::Panic(_) => Err(JsError::new(&ser)), + _ => Ok(ser), + } } /// MQTT is not supported for WebAssembly bindings. @@ -71,8 +110,11 @@ pub fn call_client_method_async(method: String, methodHandler: &ClientMethodHand /// Throws an error if called, only included for compatibility /// with the Node.js bindings TypeScript definitions. #[wasm_bindgen(js_name = listenMqtt)] -pub fn listen_mqtt(_topics: ArrayString, _callback: &js_sys::Function) -> Result<(), JsValue> { - let js_error = js_sys::Error::new("Client MQTT not supported for WebAssembly"); - - Err(JsValue::from(js_error)) +pub fn listen_mqtt(_topics: ArrayString, _callback: &js_sys::Function) -> Result<(), JsError> { + Err(JsError::new( + &serde_json::to_string(&Response::Panic( + "Client MQTT not supported for WebAssembly".to_string(), + )) + .expect("json to string error"), + )) } diff --git a/bindings/wasm/src/lib.rs b/bindings/wasm/src/lib.rs index cf5528046a..066808b9ec 100644 --- a/bindings/wasm/src/lib.rs +++ b/bindings/wasm/src/lib.rs @@ -8,12 +8,12 @@ pub mod secret_manager; pub mod utils; pub mod wallet; -use wasm_bindgen::{prelude::wasm_bindgen, JsValue}; +use wasm_bindgen::{prelude::wasm_bindgen, JsError}; /// Initializes the console error panic hook for better panic messages. /// Gets automatically called when using wasm #[wasm_bindgen(start)] -pub fn start() -> Result<(), JsValue> { +pub fn start() -> Result<(), JsError> { console_error_panic_hook::set_once(); Ok(()) } @@ -22,7 +22,7 @@ pub fn start() -> Result<(), JsValue> { /// /// Calling this will enable all rust logs to be show #[wasm_bindgen(js_name = initLogger)] -pub async fn init_logger(_config: String) -> Result<(), JsValue> { +pub async fn init_logger(_config: String) -> Result<(), JsError> { wasm_logger::init(wasm_logger::Config::default()); Ok(()) } @@ -31,7 +31,4 @@ pub async fn init_logger(_config: String) -> Result<(), JsValue> { extern "C" { #[wasm_bindgen(typescript_type = "string[]")] pub type ArrayString; - - #[wasm_bindgen(typescript_type = "Promise")] - pub type PromiseString; } diff --git a/bindings/wasm/src/secret_manager.rs b/bindings/wasm/src/secret_manager.rs index ab08a2b2df..e3b2ef978d 100644 --- a/bindings/wasm/src/secret_manager.rs +++ b/bindings/wasm/src/secret_manager.rs @@ -9,10 +9,7 @@ use iota_sdk_bindings_core::{ Response, SecretManagerMethod, }; use tokio::sync::RwLock; -use wasm_bindgen::{prelude::wasm_bindgen, JsCast, JsValue}; -use wasm_bindgen_futures::future_to_promise; - -use crate::PromiseString; +use wasm_bindgen::{prelude::wasm_bindgen, JsError, JsValue}; /// The SecretManager method handler. #[wasm_bindgen(js_name = SecretManagerMethodHandler)] @@ -20,12 +17,22 @@ pub struct SecretManagerMethodHandler { pub(crate) secret_manager: Arc>, } +impl SecretManagerMethodHandler { + pub(crate) fn new(secret_manager: Arc>) -> Self { + Self { secret_manager } + } +} + /// Creates a method handler with the given secret_manager options. #[wasm_bindgen(js_name = createSecretManager)] #[allow(non_snake_case)] pub fn create_secret_manager(options: String) -> Result { - let secret_manager_dto = serde_json::from_str::(&options).map_err(|err| err.to_string())?; - let secret_manager = SecretManager::try_from(secret_manager_dto).map_err(|err| err.to_string())?; + let secret_manager_dto = serde_json::from_str::(&options).map_err(|err| { + JsError::new(&serde_json::to_string(&Response::Error(err.into())).expect("json to string error")) + })?; + let secret_manager = SecretManager::try_from(secret_manager_dto).map_err(|err| { + JsError::new(&serde_json::to_string(&Response::Error(err.into())).expect("json to string error")) + })?; Ok(SecretManagerMethodHandler { secret_manager: Arc::new(RwLock::new(secret_manager)), @@ -37,24 +44,21 @@ pub fn create_secret_manager(options: String) -> Result Result { - let secret_manager = methodHandler.secret_manager.clone(); - let promise: js_sys::Promise = future_to_promise(async move { - let method: SecretManagerMethod = serde_json::from_str(&method).map_err(|err| err.to_string())?; +) -> Result { + let secret_manager = &methodHandler.secret_manager; + let method: SecretManagerMethod = serde_json::from_str(&method).map_err(|err| { + JsError::new(&serde_json::to_string(&Response::Error(err.into())).expect("json to string error")) + })?; - let response = call_secret_manager_method(&secret_manager, method).await; - let ser = JsValue::from(serde_json::to_string(&response).map_err(|err| err.to_string())?); - match response { - Response::Error(_) | Response::Panic(_) => Err(ser), - _ => Ok(ser), - } - }); - - // WARNING: this does not validate the return type. Check carefully. - Ok(promise.unchecked_into()) + let response = call_secret_manager_method(secret_manager, method).await; + let ser = serde_json::to_string(&response).expect("json to string error"); + match response { + Response::Error(_) | Response::Panic(_) => Err(JsError::new(&ser)), + _ => Ok(ser), + } } /// Stronghold snapshot migration is not supported for WebAssembly bindings. @@ -69,8 +73,11 @@ pub fn migrate_stronghold_snapshot_v2_to_v3( _rounds: u32, _new_path: Option, _new_password: Option, -) -> Result<(), JsValue> { - let js_error = js_sys::Error::new("Stronghold snapshot migration is not supported for WebAssembly"); - - Err(JsValue::from(js_error)) +) -> Result<(), JsError> { + Err(JsError::new( + &serde_json::to_string(&Response::Panic( + "Stronghold snapshot migration is not supported for WebAssembly".to_string(), + )) + .expect("json to string error"), + )) } diff --git a/bindings/wasm/src/wallet.rs b/bindings/wasm/src/wallet.rs index 0f4890af73..719c0642d5 100644 --- a/bindings/wasm/src/wallet.rs +++ b/bindings/wasm/src/wallet.rs @@ -1,7 +1,7 @@ // Copyright 2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -use std::sync::Arc; +use std::sync::{Arc, RwLock}; use iota_sdk_bindings_core::{ call_wallet_method, @@ -11,82 +11,104 @@ use iota_sdk_bindings_core::{ }, Response, WalletMethod, WalletOptions, }; -use tokio::sync::{ - mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}, - Mutex, -}; -use wasm_bindgen::{prelude::wasm_bindgen, JsValue}; +use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}; +use wasm_bindgen::{prelude::wasm_bindgen, JsError, JsValue}; use crate::{client::ClientMethodHandler, secret_manager::SecretManagerMethodHandler}; /// The Wallet method handler. #[wasm_bindgen(js_name = WalletMethodHandler)] pub struct WalletMethodHandler { - wallet: Arc>>, + wallet: Arc>>, +} + +macro_rules! wallet_pre { + ($method_handler:ident) => { + match $method_handler.wallet.read() { + Ok(handler) => { + if let Some(wallet) = handler.clone() { + Ok(wallet) + } else { + // Notify that the wallet was destroyed + Err(JsError::new( + &serde_json::to_string(&Response::Panic("Wallet was destroyed".to_string())) + .expect("json to string error"), + )) + } + } + Err(e) => Err(JsError::new( + &serde_json::to_string(&Response::Panic(e.to_string())).expect("json to string error"), + )), + } + }; } /// Creates a method handler with the given options. #[wasm_bindgen(js_name = createWallet)] #[allow(non_snake_case)] -pub fn create_wallet(options: String) -> Result { - let wallet_options = serde_json::from_str::(&options).map_err(|e| e.to_string())?; +pub fn create_wallet(options: String) -> Result { + let wallet_options = serde_json::from_str::(&options).map_err(|e| { + JsError::new(&serde_json::to_string(&Response::Panic(e.to_string())).expect("json to string error")) + })?; let wallet_method_handler = tokio::runtime::Builder::new_current_thread() .build() .unwrap() .block_on(async move { wallet_options.build().await }) - .map_err(|e| e.to_string())?; + .map_err(|e| { + JsError::new(&serde_json::to_string(&Response::Panic(e.to_string())).expect("json to string error")) + })?; Ok(WalletMethodHandler { - wallet: Arc::new(Mutex::new(Some(wallet_method_handler))), + wallet: Arc::new(RwLock::new(Some(wallet_method_handler))), }) } #[wasm_bindgen(js_name = destroyWallet)] -pub async fn destroy_wallet(method_handler: &WalletMethodHandler) -> Result<(), JsValue> { - *method_handler.wallet.lock().await = None; +pub fn destroy_wallet(method_handler: &WalletMethodHandler) -> Result<(), JsError> { + match method_handler.wallet.write() { + Ok(mut lock) => *lock = None, + Err(e) => { + return Err(JsError::new( + &serde_json::to_string(&Response::Panic(e.to_string())).expect("json to string error"), + )); + } + }; Ok(()) } #[wasm_bindgen(js_name = getClientFromWallet)] -pub async fn get_client(method_handler: &WalletMethodHandler) -> Result { - let wallet = method_handler.wallet.lock().await; +pub fn get_client(method_handler: &WalletMethodHandler) -> Result { + let wallet = wallet_pre!(method_handler)?; - let client = wallet - .as_ref() - .ok_or_else(|| "wallet got destroyed".to_string())? - .client() - .clone(); + let client = wallet.client().clone(); - Ok(ClientMethodHandler { client }) + Ok(ClientMethodHandler::new(client)) } #[wasm_bindgen(js_name = getSecretManagerFromWallet)] -pub async fn get_secret_manager(method_handler: &WalletMethodHandler) -> Result { - let wallet = method_handler.wallet.lock().await; - - let secret_manager = wallet - .as_ref() - .ok_or_else(|| "wallet got destroyed".to_string())? - .get_secret_manager() - .clone(); +pub fn get_secret_manager(method_handler: &WalletMethodHandler) -> Result { + let wallet = wallet_pre!(method_handler)?; + let mngr = wallet.get_secret_manager().clone(); - Ok(SecretManagerMethodHandler { secret_manager }) + Ok(SecretManagerMethodHandler::new(mngr)) } /// Handles a method, returns the response as a JSON-encoded string. /// /// Returns an error if the response itself is an error or panic. #[wasm_bindgen(js_name = callWalletMethodAsync)] -pub async fn call_wallet_method_async(method: String, method_handler: &WalletMethodHandler) -> Result { - let wallet = method_handler.wallet.lock().await; - let method: WalletMethod = serde_json::from_str(&method).map_err(|err| err.to_string())?; +pub async fn call_wallet_method_async(method: String, method_handler: &WalletMethodHandler) -> Result { + let wallet = wallet_pre!(method_handler)?; + let method: WalletMethod = serde_json::from_str(&method).map_err(|err| JsError::new(&err.to_string()))?; - let response = call_wallet_method(wallet.as_ref().expect("wallet got destroyed"), method).await; + let response = call_wallet_method(&wallet, method).await; match response { - Response::Error(e) => Err(e.to_string().into()), - Response::Panic(p) => Err(p.into()), - _ => Ok(serde_json::to_string(&response).map_err(|e| e.to_string())?), + Response::Error(e) => Err(JsError::new( + &serde_json::to_string(&Response::Panic(e.to_string())).expect("json to string error"), + )), + Response::Panic(p) => Err(JsError::new(&p)), + _ => Ok(serde_json::to_string(&response).map_err(|e| JsError::new(&e.to_string()))?), } } @@ -103,23 +125,21 @@ pub async fn listen_wallet( vec: js_sys::Array, callback: js_sys::Function, method_handler: &WalletMethodHandler, -) -> Result { +) -> Result { let mut event_types = Vec::with_capacity(vec.length() as _); for event_type in vec.keys() { // We know the built-in iterator for set elements won't throw // exceptions, so just unwrap the element. let event_type = event_type.unwrap().as_f64().unwrap() as u8; - let wallet_event_type = WalletEventType::try_from(event_type).map_err(JsValue::from)?; + let wallet_event_type = WalletEventType::try_from(event_type).map_err(|e| { + JsError::new(&serde_json::to_string(&Response::Panic(e.to_string())).expect("json to string error")) + })?; event_types.push(wallet_event_type); } let (tx, mut rx): (UnboundedSender, UnboundedReceiver) = unbounded_channel(); - method_handler - .wallet - .lock() - .await - .as_ref() - .expect("wallet not initialised") + let wallet = wallet_pre!(method_handler)?; + wallet .listen(event_types, move |wallet_event| { tx.send(wallet_event.clone()).unwrap(); }) @@ -128,13 +148,16 @@ pub async fn listen_wallet( // Spawn on the same thread a continuous loop to check the channel wasm_bindgen_futures::spawn_local(async move { while let Some(wallet_event) = rx.recv().await { - callback - .call1( - &JsValue::NULL, - &JsValue::from(serde_json::to_string(&wallet_event).unwrap()), - ) - // Safe to unwrap, our callback has no return - .unwrap(); + let res = callback.call2( + &JsValue::NULL, + &JsValue::UNDEFINED, + &JsValue::from(serde_json::to_string(&wallet_event).unwrap()), + ); + // Call callback again with the error this time, to prevent wasm crashing. + // This does mean the callback is called a second time instead of once. + if let Err(e) = res { + callback.call2(&JsValue::NULL, &e, &JsValue::UNDEFINED).unwrap(); + } } // No more links to the unbounded_channel, exit loop });